1010import typing
1111import json
1212import toml
13+ import yaml
1314import logging
1415import pathlib
1516
@@ -76,9 +77,71 @@ def git_info(repository: str) -> dict[str, typing.Any]:
7677 return {}
7778
7879
80+ def _conda_dependency_parse (dependency : str ) -> tuple [str , str ] | None :
81+ """Parse a dependency definition into module-version."""
82+ if dependency .startswith ("::" ):
83+ logger .warning (
84+ f"Skipping Conda specific channel definition '{ dependency } ' in Python environment metadata."
85+ )
86+ return None
87+ elif ">=" in dependency :
88+ module , version = dependency .split (">=" )
89+ logger .warning (
90+ f"Ignoring '>=' constraint in Python package version, naively storing '{ module } =={ version } ', "
91+ "for a more accurate record use 'conda env export > environment.yml'"
92+ )
93+ elif "~=" in dependency :
94+ module , version = dependency .split ("~=" )
95+ logger .warning (
96+ f"Ignoring '~=' constraint in Python package version, naively storing '{ module } =={ version } ', "
97+ "for a more accurate record use 'conda env export > environment.yml'"
98+ )
99+ elif dependency .startswith ("-e" ):
100+ _ , version = dependency .split ("-e" )
101+ version = version .strip ()
102+ module = pathlib .Path (version ).name
103+ elif dependency .startswith ("file://" ):
104+ _ , version = dependency .split ("file://" )
105+ module = pathlib .Path (version ).stem
106+ elif dependency .startswith ("git+" ):
107+ _ , version = dependency .split ("git+" )
108+ if "#egg=" in version :
109+ repo , module = version .split ("#egg=" )
110+ module = repo .split ("/" )[- 1 ].replace (".git" , "" )
111+ else :
112+ module = version .split ("/" )[- 1 ].replace (".git" , "" )
113+ elif "==" not in dependency :
114+ logger .warning (
115+ f"Ignoring '{ dependency } ' in Python environment record as no version constraint specified."
116+ )
117+ return None
118+ else :
119+ module , version = dependency .split ("==" )
120+
121+ return module , version
122+
123+
124+ def _conda_env (environment_file : pathlib .Path ) -> dict [str , str ]:
125+ """Parse/interpret a Conda environment file."""
126+ content = yaml .load (environment_file .open (), Loader = yaml .SafeLoader )
127+ python_environment : dict [str , str ] = {}
128+ pip_dependencies : list [str ] = []
129+ for dependency in content .get ("dependencies" , []):
130+ if isinstance (dependency , dict ) and dependency .get ("pip" ):
131+ pip_dependencies = dependency ["pip" ]
132+ break
133+
134+ for dependency in pip_dependencies :
135+ if not (parsed := _conda_dependency_parse (dependency )):
136+ continue
137+ module , version = parsed
138+ python_environment [module .strip ().replace ("-" , "_" )] = version .strip ()
139+ return python_environment
140+
141+
79142def _python_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
80143 """Retrieve a dictionary of Python dependencies if lock file is available"""
81- python_meta : dict [str , str ] = {}
144+ python_meta : dict [str , dict ] = {}
82145
83146 if (pyproject_file := pathlib .Path (repository ).joinpath ("pyproject.toml" )).exists ():
84147 content = toml .load (pyproject_file )
@@ -103,22 +166,37 @@ def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]:
103166 python_meta ["environment" ] = {
104167 package ["name" ]: package ["version" ] for package in content
105168 }
169+ # Handle Conda case, albeit naively given the user may or may not have used 'conda env'
170+ # to dump their exact dependency versions
171+ elif (
172+ environment_file := pathlib .Path (repository ).joinpath ("environment.yml" )
173+ ).exists ():
174+ python_meta ["environment" ] = _conda_env (environment_file )
106175 else :
107176 with contextlib .suppress ((KeyError , ImportError )):
108177 from pip ._internal .operations .freeze import freeze
109178
110- python_meta ["environment" ] = {
111- entry [0 ]: entry [- 1 ]
112- for line in freeze (local_only = True )
113- if (entry := line .split ("==" ))
114- }
179+ # Conda supports having file names with @ as entries
180+ # in the requirements.txt file as opposed to ==
181+ python_meta ["environment" ] = {}
182+
183+ for line in freeze (local_only = True ):
184+ if line .startswith ("-e" ):
185+ python_meta ["environment" ]["local_install" ] = line .split (" " )[- 1 ]
186+ continue
187+ if "@" in line :
188+ entry = line .split ("@" )
189+ python_meta ["environment" ][entry [0 ].strip ()] = entry [- 1 ].strip ()
190+ elif "==" in line :
191+ entry = line .split ("==" )
192+ python_meta ["environment" ][entry [0 ].strip ()] = entry [- 1 ].strip ()
115193
116194 return python_meta
117195
118196
119197def _rust_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
120198 """Retrieve a dictionary of Rust dependencies if lock file available"""
121- rust_meta : dict [str , str ] = {}
199+ rust_meta : dict [str , dict ] = {}
122200
123201 if (cargo_file := pathlib .Path (repository ).joinpath ("Cargo.toml" )).exists ():
124202 content = toml .load (cargo_file ).get ("package" , {})
@@ -134,15 +212,15 @@ def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]:
134212 cargo_dat = toml .load (cargo_lock )
135213 rust_meta ["environment" ] = {
136214 dependency ["name" ]: dependency ["version" ]
137- for dependency in cargo_dat .get ("package" )
215+ for dependency in cargo_dat .get ("package" , [] )
138216 }
139217
140218 return rust_meta
141219
142220
143221def _julia_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
144222 """Retrieve a dictionary of Julia dependencies if a project file is available"""
145- julia_meta : dict [str , str ] = {}
223+ julia_meta : dict [str , dict ] = {}
146224 if (project_file := pathlib .Path (repository ).joinpath ("Project.toml" )).exists ():
147225 content = toml .load (project_file )
148226 julia_meta ["project" ] = {
@@ -155,7 +233,7 @@ def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]:
155233
156234
157235def _node_js_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
158- js_meta : dict [str , str ] = {}
236+ js_meta : dict [str , dict ] = {}
159237 if (
160238 project_file := pathlib .Path (repository ).joinpath ("package-lock.json" )
161239 ).exists ():
0 commit comments