1212import os
1313import fnmatch
1414import toml
15+ import yaml
1516import logging
1617import pathlib
1718
@@ -78,9 +79,71 @@ def git_info(repository: str) -> dict[str, typing.Any]:
7879 return {}
7980
8081
82+ def _conda_dependency_parse (dependency : str ) -> tuple [str , str ] | None :
83+ """Parse a dependency definition into module-version."""
84+ if dependency .startswith ("::" ):
85+ logger .warning (
86+ f"Skipping Conda specific channel definition '{ dependency } ' in Python environment metadata."
87+ )
88+ return None
89+ elif ">=" in dependency :
90+ module , version = dependency .split (">=" )
91+ logger .warning (
92+ f"Ignoring '>=' constraint in Python package version, naively storing '{ module } =={ version } ', "
93+ "for a more accurate record use 'conda env export > environment.yml'"
94+ )
95+ elif "~=" in dependency :
96+ module , version = dependency .split ("~=" )
97+ logger .warning (
98+ f"Ignoring '~=' constraint in Python package version, naively storing '{ module } =={ version } ', "
99+ "for a more accurate record use 'conda env export > environment.yml'"
100+ )
101+ elif dependency .startswith ("-e" ):
102+ _ , version = dependency .split ("-e" )
103+ version = version .strip ()
104+ module = pathlib .Path (version ).name
105+ elif dependency .startswith ("file://" ):
106+ _ , version = dependency .split ("file://" )
107+ module = pathlib .Path (version ).stem
108+ elif dependency .startswith ("git+" ):
109+ _ , version = dependency .split ("git+" )
110+ if "#egg=" in version :
111+ repo , module = version .split ("#egg=" )
112+ module = repo .split ("/" )[- 1 ].replace (".git" , "" )
113+ else :
114+ module = version .split ("/" )[- 1 ].replace (".git" , "" )
115+ elif "==" not in dependency :
116+ logger .warning (
117+ f"Ignoring '{ dependency } ' in Python environment record as no version constraint specified."
118+ )
119+ return None
120+ else :
121+ module , version = dependency .split ("==" )
122+
123+ return module , version
124+
125+
126+ def _conda_env (environment_file : pathlib .Path ) -> dict [str , str ]:
127+ """Parse/interpret a Conda environment file."""
128+ content = yaml .load (environment_file .open (), Loader = yaml .SafeLoader )
129+ python_environment : dict [str , str ] = {}
130+ pip_dependencies : list [str ] = []
131+ for dependency in content .get ("dependencies" , []):
132+ if isinstance (dependency , dict ) and dependency .get ("pip" ):
133+ pip_dependencies = dependency ["pip" ]
134+ break
135+
136+ for dependency in pip_dependencies :
137+ if not (parsed := _conda_dependency_parse (dependency )):
138+ continue
139+ module , version = parsed
140+ python_environment [module .strip ().replace ("-" , "_" )] = version .strip ()
141+ return python_environment
142+
143+
81144def _python_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
82145 """Retrieve a dictionary of Python dependencies if lock file is available"""
83- python_meta : dict [str , str ] = {}
146+ python_meta : dict [str , dict ] = {}
84147
85148 if (pyproject_file := pathlib .Path (repository ).joinpath ("pyproject.toml" )).exists ():
86149 content = toml .load (pyproject_file )
@@ -105,22 +168,37 @@ def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]:
105168 python_meta ["environment" ] = {
106169 package ["name" ]: package ["version" ] for package in content
107170 }
171+ # Handle Conda case, albeit naively given the user may or may not have used 'conda env'
172+ # to dump their exact dependency versions
173+ elif (
174+ environment_file := pathlib .Path (repository ).joinpath ("environment.yml" )
175+ ).exists ():
176+ python_meta ["environment" ] = _conda_env (environment_file )
108177 else :
109178 with contextlib .suppress ((KeyError , ImportError )):
110179 from pip ._internal .operations .freeze import freeze
111180
112- python_meta ["environment" ] = {
113- entry [0 ]: entry [- 1 ]
114- for line in freeze (local_only = True )
115- if (entry := line .split ("==" ))
116- }
181+ # Conda supports having file names with @ as entries
182+ # in the requirements.txt file as opposed to ==
183+ python_meta ["environment" ] = {}
184+
185+ for line in freeze (local_only = True ):
186+ if line .startswith ("-e" ):
187+ python_meta ["environment" ]["local_install" ] = line .split (" " )[- 1 ]
188+ continue
189+ if "@" in line :
190+ entry = line .split ("@" )
191+ python_meta ["environment" ][entry [0 ].strip ()] = entry [- 1 ].strip ()
192+ elif "==" in line :
193+ entry = line .split ("==" )
194+ python_meta ["environment" ][entry [0 ].strip ()] = entry [- 1 ].strip ()
117195
118196 return python_meta
119197
120198
121199def _rust_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
122200 """Retrieve a dictionary of Rust dependencies if lock file available"""
123- rust_meta : dict [str , str ] = {}
201+ rust_meta : dict [str , dict ] = {}
124202
125203 if (cargo_file := pathlib .Path (repository ).joinpath ("Cargo.toml" )).exists ():
126204 content = toml .load (cargo_file ).get ("package" , {})
@@ -136,15 +214,15 @@ def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]:
136214 cargo_dat = toml .load (cargo_lock )
137215 rust_meta ["environment" ] = {
138216 dependency ["name" ]: dependency ["version" ]
139- for dependency in cargo_dat .get ("package" )
217+ for dependency in cargo_dat .get ("package" , [] )
140218 }
141219
142220 return rust_meta
143221
144222
145223def _julia_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
146224 """Retrieve a dictionary of Julia dependencies if a project file is available"""
147- julia_meta : dict [str , str ] = {}
225+ julia_meta : dict [str , dict ] = {}
148226 if (project_file := pathlib .Path (repository ).joinpath ("Project.toml" )).exists ():
149227 content = toml .load (project_file )
150228 julia_meta ["project" ] = {
@@ -157,7 +235,7 @@ def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]:
157235
158236
159237def _node_js_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
160- js_meta : dict [str , str ] = {}
238+ js_meta : dict [str , dict ] = {}
161239 if (
162240 project_file := pathlib .Path (repository ).joinpath ("package-lock.json" )
163241 ).exists ():
0 commit comments