14
14
15
15
from packagedcode import models
16
16
from packagedcode .pypi import BaseDependencyFileHandler
17
+ from dparse2 .parser import parse_requirement_line
17
18
18
19
"""
19
20
Handle Conda manifests and metadata, see https://docs.conda.io/en/latest/
23
24
"""
24
25
25
26
# TODO: there are likely other package data files for Conda
26
- # TODO: report platform
27
-
28
27
29
28
class CondaYamlHandler (BaseDependencyFileHandler ):
30
- # TODO: there are several other manifests worth adding
31
29
datasource_id = 'conda_yaml'
32
- path_patterns = ('*conda.yaml' , '*conda.yml' , )
33
- default_package_type = 'pypi '
30
+ path_patterns = ('*conda* .yaml' , '*env*.yaml' , '*environment*.yaml' )
31
+ default_package_type = 'conda '
34
32
default_primary_language = 'Python'
35
33
description = 'Conda yaml manifest'
36
34
documentation_url = 'https://docs.conda.io/'
37
35
36
+ @classmethod
37
+ def parse (cls , location , package_only = False ):
38
+ with open (location ) as fi :
39
+ conda_data = saneyaml .load (fi .read ())
40
+ dependencies = get_conda_yaml_dependencies (conda_data = conda_data )
41
+ name = conda_data .get ('name' )
42
+ extra_data = {}
43
+ channels = conda_data .get ('channels' )
44
+ if channels :
45
+ extra_data ['channels' ] = channels
46
+ if name or dependencies :
47
+ package_data = dict (
48
+ datasource_id = cls .datasource_id ,
49
+ type = cls .default_package_type ,
50
+ name = name ,
51
+ primary_language = cls .default_primary_language ,
52
+ dependencies = dependencies ,
53
+ extra_data = extra_data ,
54
+ )
55
+ yield models .PackageData .from_data (package_data , package_only )
56
+
38
57
39
58
class CondaMetaYamlHandler (models .DatafileHandler ):
40
59
datasource_id = 'conda_meta_yaml'
@@ -83,9 +102,7 @@ def parse(cls, location, package_only=False):
83
102
metayaml = get_meta_yaml_data (location )
84
103
package_element = metayaml .get ('package' ) or {}
85
104
package_name = package_element .get ('name' )
86
- if not package_name :
87
- return
88
- version = package_element .get ('version' )
105
+ package_version = package_element .get ('version' )
89
106
90
107
# FIXME: source is source, not download
91
108
source = metayaml .get ('source' ) or {}
@@ -99,6 +116,7 @@ def parse(cls, location, package_only=False):
99
116
vcs_url = about .get ('dev_url' )
100
117
101
118
dependencies = []
119
+ extra_data = {}
102
120
requirements = metayaml .get ('requirements' ) or {}
103
121
for scope , reqs in requirements .items ():
104
122
# requirements format is like:
@@ -107,33 +125,152 @@ def parse(cls, location, package_only=False):
107
125
# u'progressbar2', u'python >=3.6'])])
108
126
for req in reqs :
109
127
name , _ , requirement = req .partition (" " )
110
- purl = PackageURL (type = cls .default_package_type , name = name )
128
+ version = None
129
+ if requirement .startswith ("==" ):
130
+ _ , version = requirement .split ("==" )
131
+
132
+ # requirements may have namespace, version too
133
+ # - conda-forge::numpy=1.15.4
134
+ namespace = None
135
+ if "::" in name :
136
+ namespace , name = name .split ("::" )
137
+
138
+ is_pinned = False
139
+ if "=" in name :
140
+ name , version = name .split ("=" )
141
+ is_pinned = True
142
+ requirement = f"={ version } "
143
+
144
+ if name in ('pip' , 'python' ):
145
+ if not scope in extra_data :
146
+ extra_data [scope ] = [req ]
147
+ else :
148
+ extra_data [scope ].append (req )
149
+ continue
150
+
151
+ purl = PackageURL (
152
+ type = cls .default_package_type ,
153
+ name = name ,
154
+ namespace = namespace ,
155
+ version = version ,
156
+ )
157
+ if "run" in scope :
158
+ is_runtime = True
159
+ is_optional = False
160
+ else :
161
+ is_runtime = False
162
+ is_optional = True
163
+
111
164
dependencies .append (
112
165
models .DependentPackage (
113
166
purl = purl .to_string (),
114
167
extracted_requirement = requirement ,
115
168
scope = scope ,
116
- is_runtime = True ,
117
- is_optional = False ,
169
+ is_runtime = is_runtime ,
170
+ is_optional = is_optional ,
171
+ is_pinned = is_pinned ,
172
+ is_direct = True ,
118
173
)
119
174
)
120
175
121
176
package_data = dict (
122
177
datasource_id = cls .datasource_id ,
123
178
type = cls .default_package_type ,
124
179
name = package_name ,
125
- version = version ,
180
+ version = package_version ,
126
181
download_url = download_url ,
127
182
homepage_url = homepage_url ,
128
183
vcs_url = vcs_url ,
129
184
description = description ,
130
185
sha256 = sha256 ,
131
186
extracted_license_statement = extracted_license_statement ,
132
187
dependencies = dependencies ,
188
+ extra_data = extra_data ,
133
189
)
134
190
yield models .PackageData .from_data (package_data , package_only )
135
191
136
192
193
+ def get_conda_yaml_dependencies (conda_data ):
194
+ """
195
+ Return a list of DependentPackage mappins from conda and pypi
196
+ dependencies present in a `conda_data` mapping.
197
+ """
198
+ dependencies = conda_data .get ('dependencies' ) or []
199
+ deps = []
200
+ for dep in dependencies :
201
+ if isinstance (dep , str ):
202
+ namespace = None
203
+ specs = None
204
+ is_pinned = False
205
+
206
+ if "::" in dep :
207
+ namespace , dep = dep .split ("::" )
208
+
209
+ req = parse_requirement_line (dep )
210
+ if req :
211
+ name = req .name
212
+ version = None
213
+
214
+ specs = str (req .specs )
215
+ if '==' in specs :
216
+ version = specs .replace ('==' ,'' )
217
+ is_pinned = True
218
+ purl = PackageURL (type = 'pypi' , name = name , version = version )
219
+ else :
220
+ if "=" in dep :
221
+ dep , version = dep .split ("=" )
222
+ is_pinned = True
223
+ specs = f"={ version } "
224
+
225
+ purl = PackageURL (
226
+ type = 'conda' ,
227
+ namespace = namespace ,
228
+ name = dep ,
229
+ version = version ,
230
+ )
231
+
232
+ if purl .name in ('pip' , 'python' ):
233
+ continue
234
+
235
+ deps .append (
236
+ models .DependentPackage (
237
+ purl = purl .to_string (),
238
+ extracted_requirement = specs ,
239
+ scope = 'dependencies' ,
240
+ is_runtime = True ,
241
+ is_optional = False ,
242
+ is_pinned = is_pinned ,
243
+ is_direct = True ,
244
+ ).to_dict ()
245
+ )
246
+
247
+ elif isinstance (dep , dict ):
248
+ for line in dep .get ('pip' , []):
249
+ req = parse_requirement_line (line )
250
+ if req :
251
+ name = req .name
252
+ version = None
253
+ is_pinned = False
254
+ specs = str (req .specs )
255
+ if '==' in specs :
256
+ version = specs .replace ('==' ,'' )
257
+ is_pinned = True
258
+ purl = PackageURL (type = 'pypi' , name = name , version = version )
259
+ deps .append (
260
+ models .DependentPackage (
261
+ purl = purl .to_string (),
262
+ extracted_requirement = specs ,
263
+ scope = 'dependencies' ,
264
+ is_runtime = True ,
265
+ is_optional = False ,
266
+ is_pinned = is_pinned ,
267
+ is_direct = True ,
268
+ ).to_dict ()
269
+ )
270
+
271
+ return deps
272
+
273
+
137
274
def get_meta_yaml_data (location ):
138
275
"""
139
276
Return a mapping of conda metadata loaded from a meta.yaml files. The format
@@ -158,10 +295,21 @@ def get_meta_yaml_data(location):
158
295
# Replace the variable with the value
159
296
if '{{' in line and '}}' in line :
160
297
for variable , value in variables .items ():
161
- line = line .replace ('{{ ' + variable + ' }}' , value )
298
+ if "|lower" in line :
299
+ line = line .replace ('{{ ' + variable + '|lower' + ' }}' , value .lower ())
300
+ else :
301
+ line = line .replace ('{{ ' + variable + ' }}' , value )
162
302
yaml_lines .append (line )
163
303
164
- return saneyaml .load ('\n ' .join (yaml_lines ))
304
+ # Cleanup any remaining complex jinja template lines
305
+ # as the yaml load fails otherwise for unresolved jinja
306
+ cleaned_yaml_lines = [
307
+ line
308
+ for line in yaml_lines
309
+ if not "{{" in line
310
+ ]
311
+
312
+ return saneyaml .load ('' .join (cleaned_yaml_lines ))
165
313
166
314
167
315
def get_variables (location ):
0 commit comments