12
12
import os
13
13
import fnmatch
14
14
import toml
15
+ import yaml
15
16
import logging
16
17
import pathlib
17
18
@@ -78,9 +79,71 @@ def git_info(repository: str) -> dict[str, typing.Any]:
78
79
return {}
79
80
80
81
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
+
81
144
def _python_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
82
145
"""Retrieve a dictionary of Python dependencies if lock file is available"""
83
- python_meta : dict [str , str ] = {}
146
+ python_meta : dict [str , dict ] = {}
84
147
85
148
if (pyproject_file := pathlib .Path (repository ).joinpath ("pyproject.toml" )).exists ():
86
149
content = toml .load (pyproject_file )
@@ -105,22 +168,37 @@ def _python_env(repository: pathlib.Path) -> dict[str, typing.Any]:
105
168
python_meta ["environment" ] = {
106
169
package ["name" ]: package ["version" ] for package in content
107
170
}
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 )
108
177
else :
109
178
with contextlib .suppress ((KeyError , ImportError )):
110
179
from pip ._internal .operations .freeze import freeze
111
180
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 ()
117
195
118
196
return python_meta
119
197
120
198
121
199
def _rust_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
122
200
"""Retrieve a dictionary of Rust dependencies if lock file available"""
123
- rust_meta : dict [str , str ] = {}
201
+ rust_meta : dict [str , dict ] = {}
124
202
125
203
if (cargo_file := pathlib .Path (repository ).joinpath ("Cargo.toml" )).exists ():
126
204
content = toml .load (cargo_file ).get ("package" , {})
@@ -136,15 +214,15 @@ def _rust_env(repository: pathlib.Path) -> dict[str, typing.Any]:
136
214
cargo_dat = toml .load (cargo_lock )
137
215
rust_meta ["environment" ] = {
138
216
dependency ["name" ]: dependency ["version" ]
139
- for dependency in cargo_dat .get ("package" )
217
+ for dependency in cargo_dat .get ("package" , [] )
140
218
}
141
219
142
220
return rust_meta
143
221
144
222
145
223
def _julia_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
146
224
"""Retrieve a dictionary of Julia dependencies if a project file is available"""
147
- julia_meta : dict [str , str ] = {}
225
+ julia_meta : dict [str , dict ] = {}
148
226
if (project_file := pathlib .Path (repository ).joinpath ("Project.toml" )).exists ():
149
227
content = toml .load (project_file )
150
228
julia_meta ["project" ] = {
@@ -157,7 +235,7 @@ def _julia_env(repository: pathlib.Path) -> dict[str, typing.Any]:
157
235
158
236
159
237
def _node_js_env (repository : pathlib .Path ) -> dict [str , typing .Any ]:
160
- js_meta : dict [str , str ] = {}
238
+ js_meta : dict [str , dict ] = {}
161
239
if (
162
240
project_file := pathlib .Path (repository ).joinpath ("package-lock.json" )
163
241
).exists ():
0 commit comments