2121from itertools import chain
2222from os .path import join
2323from textwrap import dedent
24- from typing import TYPE_CHECKING , Any , Dict , Generator , Iterable , List , Set , Tuple
24+ from typing import TYPE_CHECKING , Any , Dict , FrozenSet , Generator , Iterable , List , Tuple
2525
2626from cyclonedx .exception .model import InvalidUriException , UnknownHashTypeException
2727from cyclonedx .model import ExternalReference , ExternalReferenceType , HashType , Property , XsUri
4444 from cyclonedx .model .bom import Bom
4545 from cyclonedx .model .component import ComponentType
4646
47- NameDict = Dict [str , Any ]
47+ T_NameDict = Dict [str , Any ]
48+ T_LockData = Dict [str , List ['_LockEntry' ]]
4849
4950
5051@dataclass
5152class _LockEntry :
5253 name : str
5354 component : Component
54- dependencies : Dict [str , 'NameDict ' ] # keys MUST go through `normalize_packagename()`
55+ dependencies : Dict [str , 'T_NameDict ' ] # keys MUST go through `normalize_packagename()`
5556 extras : Dict [str , List [str ]] # keys MUST go through `normalize_packagename()`
5657 added2bom : bool
5758
5859
59- _LockData = Dict [str , List [_LockEntry ]]
60-
61-
6260class GroupsNotFoundError (ValueError ):
6361 def __init__ (self , groups : Iterable [str ]) -> None :
6462 self .__groups = frozenset (groups )
@@ -161,15 +159,18 @@ def __call__(self, *, # type:ignore[override]
161159 po_cfg_group = po_cfg .setdefault ('group' , {})
162160 po_cfg_group .setdefault ('main' , {'dependencies' : po_cfg .get ('dependencies' , {})})
163161 po_cfg_group .setdefault ('dev' , {'dependencies' : po_cfg .get ('dev-dependencies' , {})})
164- po_cfg_extras = po_cfg .setdefault ('extras' , {})
162+ po_cfg_extras = po_cfg ['extras' ] = {
163+ normalize_packagename (en ): es
164+ for en , es in po_cfg .get ('extras' , {}).items ()
165+ }
165166
166167 # the group-args shall mimic the ones from poetry, which uses comma-separated lists and multi-use
167168 # values be like: ['foo', 'bar,bazz'] -> ['foo', 'bar', 'bazz']
168- groups_only_s = set (filter (None , ',' .join (groups_only ).split (',' )))
169- groups_with_s = set (filter (None , ',' .join (groups_with ).split (',' )))
170- groups_without_s = set (filter (None , ',' .join (groups_without ).split (',' )))
169+ groups_only_s = frozenset (filter (None , ',' .join (groups_only ).split (',' )))
170+ groups_with_s = frozenset (filter (None , ',' .join (groups_with ).split (',' )))
171+ groups_without_s = frozenset (filter (None , ',' .join (groups_without ).split (',' )))
171172 del groups_only , groups_with , groups_without
172- groups_not_found = set (
173+ groups_not_found = frozenset (
173174 (gn , srcn ) for gns , srcn in [
174175 (groups_only_s , 'only' ),
175176 (groups_with_s , 'with' ),
@@ -182,27 +183,30 @@ def __call__(self, *, # type:ignore[override]
182183 raise ValueError ('some Poetry groups are unknown' ) from groups_error
183184 del groups_not_found
184185
185- # values be like: ['foo', 'bar,bazz'] -> ['foo', 'bar', 'bazz']
186- extras_s = set (filter (None , ',' .join (extras ).split (',' )))
186+ if all_extras :
187+ extras_s = frozenset (po_cfg_extras )
188+ else :
189+ extras_s = frozenset (map (normalize_packagename ,
190+ # values be like: ['foo', 'bar,bazz'] -> ['foo', 'bar', 'bazz']
191+ filter (None , ',' .join (extras ).split (',' ))))
192+ extras_not_found = extras_s - po_cfg_extras .keys ()
193+ if len (extras_not_found ) > 0 :
194+ extras_error = ExtrasNotFoundError (extras_not_found )
195+ self ._logger .error (extras_error )
196+ raise ValueError ('some package extras are unknown' ) from extras_error
197+ del extras_not_found
187198 del extras
188- extras_defined = set (po_cfg_extras )
189- extras_not_found = extras_s - extras_defined
190- if len (extras_not_found ) > 0 :
191- extras_error = ExtrasNotFoundError (extras_not_found )
192- self ._logger .error (extras_error )
193- raise ValueError ('some package extras are unknown' ) from extras_error
194- del extras_not_found
195199
196200 # the group-args shall mimic the ones from Poetry.
197201 # Poetry handles this pseudo-exclusive-group of args programmatically
198202 if no_dev :
199- groups = {'main' , }
203+ groups = frozenset ( {'main' , })
200204 elif len (groups_only_s ) > 0 :
201205 groups = groups_only_s
202206 else :
203207 # When used together, `--without` takes precedence over `--with`.
204208 # see https://python-poetry.org/docs/managing-dependencies/#installing-group-dependencies
205- groups = set (
209+ groups = frozenset (
206210 gn for gn , gc in po_cfg ['group' ].items ()
207211 # all non-optionals and the `with`-whitelisted optionals
208212 if not gc .get ('optional' ) or gn in groups_with_s
@@ -212,12 +216,12 @@ def __call__(self, *, # type:ignore[override]
212216 return self ._make_bom (
213217 project , toml_loads (lock .read ()),
214218 groups ,
215- extras_defined if all_extras else extras_s ,
219+ extras_s ,
216220 mc_type ,
217221 )
218222
219- def _make_bom (self , project : 'NameDict ' , locker : 'NameDict ' ,
220- use_groups : Set [str ], use_extras : Set [str ],
223+ def _make_bom (self , project : 'T_NameDict ' , locker : 'T_NameDict ' ,
224+ use_groups : FrozenSet [str ], use_extras : FrozenSet [str ],
221225 mc_type : 'ComponentType' ) -> 'Bom' :
222226 self ._logger .debug ('use_groups: %r' , use_groups )
223227 self ._logger .debug ('use_extras: %r' , use_extras )
@@ -228,15 +232,17 @@ def _make_bom(self, project: 'NameDict', locker: 'NameDict',
228232
229233 bom .metadata .component = root_c = poetry2component (po_cfg , type = mc_type )
230234 root_c .bom_ref .value = root_c .name
231- root_c .properties .update (Property (
232- name = PropertyName .PackageExtra .value ,
233- value = extra
234- ) for extra in use_extras )
235+ root_c .properties .update (
236+ Property (
237+ name = PropertyName .PackageExtra .value ,
238+ value = extra
239+ ) for extra in use_extras
240+ )
235241 self ._logger .debug ('root-component: %r' , root_c )
236242 root_d = Dependency (root_c .bom_ref )
237243 bom .dependencies .add (root_d )
238244
239- lock_data : '_LockData ' = {}
245+ lock_data : 'T_LockData ' = {}
240246 for lock_entry in self ._parse_lock (locker ):
241247 _ld = lock_data .setdefault (lock_entry .name , [])
242248 _ldl = len (_ld )
@@ -256,8 +262,8 @@ def _make_bom(self, project: 'NameDict', locker: 'NameDict',
256262 )]
257263 del root_c_nname
258264
259- use_extras_dep_names = set (map (normalize_packagename ,
260- chain .from_iterable (po_cfg ['extras' ][e ] for e in use_extras )))
265+ use_extras_dep_names = frozenset (map (normalize_packagename ,
266+ chain .from_iterable (po_cfg ['extras' ][e ] for e in use_extras )))
261267 for group_name in use_groups :
262268 for dep_name , dep_spec in po_cfg ['group' ][group_name ].get ('dependencies' , {}).items ():
263269 dep_name = normalize_packagename (dep_name )
@@ -282,12 +288,7 @@ def _make_bom(self, project: 'NameDict', locker: 'NameDict',
282288
283289 return bom
284290
285- def __add_dep (self , bom : 'Bom' , lock_entry : _LockEntry , use_extras : Iterable [str ], lock_data : '_LockData' ) -> None :
286- use_extras = set (map (normalize_packagename , use_extras ))
287- lock_entry .component .properties .update (Property (
288- name = PropertyName .PackageExtra .value ,
289- value = extra
290- ) for extra in use_extras )
291+ def __add_dep (self , bom : 'Bom' , lock_entry : _LockEntry , use_extras : Iterable [str ], lock_data : 'T_LockData' ) -> None :
291292 if lock_entry .added2bom :
292293 self ._logger .debug ('existing component: %r' , lock_entry .component )
293294 lock_entry_dep = None
@@ -313,6 +314,13 @@ def __add_dep(self, bom: 'Bom', lock_entry: _LockEntry, use_extras: Iterable[str
313314 lock_entry_dep .dependencies .add (Dependency (dep_lock_entry .component .bom_ref ))
314315 self .__add_dep (bom , dep_lock_entry , dep_spec .get ('extras' , ()), lock_data )
315316 if use_extras :
317+ use_extras = frozenset (map (normalize_packagename , use_extras ))
318+ lock_entry .component .properties .update (
319+ Property (
320+ name = PropertyName .PackageExtra .value ,
321+ value = extra
322+ ) for extra in use_extras
323+ )
316324 lock_entry_dep = lock_entry_dep \
317325 or next (filter (lambda d : d .ref is lock_entry .component .bom_ref , bom .dependencies ))
318326 for req in map (
@@ -329,14 +337,14 @@ def __add_dep(self, bom: 'Bom', lock_entry: _LockEntry, use_extras: Iterable[str
329337 self .__add_dep (bom , dep_lock_entry , req .extras , lock_data )
330338
331339 @staticmethod
332- def _get_lockfile_version (locker : 'NameDict ' ) -> Tuple [int , ...]:
340+ def _get_lockfile_version (locker : 'T_NameDict ' ) -> Tuple [int , ...]:
333341 return tuple (map (int , locker ['metadata' ].get ('lock-version' , '1.0' ).split ('.' )))
334342
335- def _parse_lock (self , locker : 'NameDict ' ) -> Generator [_LockEntry , None , None ]:
343+ def _parse_lock (self , locker : 'T_NameDict ' ) -> Generator [_LockEntry , None , None ]:
336344 lock_version = self ._get_lockfile_version (locker )
337345 self ._logger .debug ('lock_version: %r' , lock_version )
338346 metavar_files = locker .get ('metadata' , {}).get ('files' , {}) if lock_version < (2 ,) else {}
339- package : 'NameDict '
347+ package : 'T_NameDict '
340348 for package in locker .get ('package' , []):
341349 package .setdefault ('files' , metavar_files .get (package ['name' ], []))
342350 yield _LockEntry (
@@ -356,7 +364,7 @@ def _parse_lock(self, locker: 'NameDict') -> Generator[_LockEntry, None, None]:
356364 __PACKAGE_SRC_VCS = ['git' ] # not supported yet: hg, svn
357365 __PACKAGE_SRC_LOCAL = ['file' , 'directory' ]
358366
359- def __make_component4lock (self , package : 'NameDict ' ) -> 'Component' :
367+ def __make_component4lock (self , package : 'T_NameDict ' ) -> 'Component' :
360368 source = package .get ('source' , {})
361369 is_vcs = source .get ('type' ) in self .__PACKAGE_SRC_VCS
362370 is_local = source .get ('type' ) in self .__PACKAGE_SRC_LOCAL
@@ -389,7 +397,7 @@ def __make_component4lock(self, package: 'NameDict') -> 'Component':
389397 ) if not is_local else None
390398 )
391399
392- def __purl_qualifiers4lock (self , package : 'NameDict ' ) -> 'NameDict ' :
400+ def __purl_qualifiers4lock (self , package : 'T_NameDict ' ) -> 'T_NameDict ' :
393401 # see https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst
394402 qs = {}
395403
@@ -414,7 +422,7 @@ def __purl_qualifiers4lock(self, package: 'NameDict') -> 'NameDict':
414422
415423 return qs
416424
417- def __extrefs4lock (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
425+ def __extrefs4lock (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
418426 source_type = package .get ('source' , {}).get ('type' , 'legacy' )
419427 if 'legacy' == source_type :
420428 yield from self .__extrefs4lock_legacy (package )
@@ -427,7 +435,7 @@ def __extrefs4lock(self, package: 'NameDict') -> Generator['ExternalReference',
427435 elif source_type in self .__PACKAGE_SRC_VCS :
428436 yield from self .__extrefs4lock_vcs (package )
429437
430- def __extrefs4lock_legacy (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
438+ def __extrefs4lock_legacy (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
431439 source_url = redact_auth_from_url (package .get ('source' , {}).get ('url' , 'https://pypi.org/simple' ))
432440 for file in package ['files' ]:
433441 try :
@@ -441,7 +449,7 @@ def __extrefs4lock_legacy(self, package: 'NameDict') -> Generator['ExternalRefer
441449 self ._logger .debug ('skipped dist-extRef for: %r | %r' , package ['name' ], file , exc_info = error )
442450 del error
443451
444- def __extrefs4lock_url (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
452+ def __extrefs4lock_url (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
445453 try :
446454 yield ExternalReference (
447455 comment = 'from url' ,
@@ -452,7 +460,7 @@ def __extrefs4lock_url(self, package: 'NameDict') -> Generator['ExternalReferenc
452460 except (InvalidUriException , UnknownHashTypeException ) as error : # pragma: nocover
453461 self ._logger .debug ('skipped dist-extRef for: %r' , package ['name' ], exc_info = error )
454462
455- def __extrefs4lock_file (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
463+ def __extrefs4lock_file (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
456464 try :
457465 yield ExternalReference (
458466 comment = 'from file' ,
@@ -463,7 +471,7 @@ def __extrefs4lock_file(self, package: 'NameDict') -> Generator['ExternalReferen
463471 except (InvalidUriException , UnknownHashTypeException ) as error : # pragma: nocover
464472 self ._logger .debug ('skipped dist-extRef for: %r' , package ['name' ], exc_info = error )
465473
466- def __extrefs4lock_directory (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
474+ def __extrefs4lock_directory (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
467475 try :
468476 yield ExternalReference (
469477 comment = 'from directory' ,
@@ -474,7 +482,7 @@ def __extrefs4lock_directory(self, package: 'NameDict') -> Generator['ExternalRe
474482 except InvalidUriException as error : # pragma: nocover
475483 self ._logger .debug ('skipped dist-extRef for: %r' , package ['name' ], exc_info = error )
476484
477- def __extrefs4lock_vcs (self , package : 'NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
485+ def __extrefs4lock_vcs (self , package : 'T_NameDict ' ) -> Generator ['ExternalReference' , None , None ]:
478486 source = package ['source' ]
479487 vcs_ref = source .get ('resolved_reference' , source .get ('reference' , '' ))
480488 try :
0 commit comments