@@ -22,7 +22,7 @@ class Tool:
2222
2323 Tool(s) are the things used in the creation of the CycloneDX document.
2424
25- Tool might be deprecated since CycloneDX 1.5, but it is not deprecated in this library.
25+ ` Tool` is deprecated since CycloneDX 1.5, but it is not deprecated in this library.
2626 In fact, this library will try to provide a compatibility layer if needed.
2727
2828 .. note::
@@ -142,16 +142,31 @@ def __repr__(self) -> str:
142142
143143class ToolRepository :
144144 """
145- The repo of tool formats
145+ The repository of tool formats
146+
147+ This is done so we can maintain backward-compatibility with CycloneDX <= 1.4
148+ If using a SortedSet of tools, the object will behave like a sorted set of
149+ tools. Otherwise, it will behave like an object with `components`
150+ and `services` attributes (which are SortedSets of their respective types).
146151 """
147152
148153 def __init__ (self , * , components : Optional [Iterable [Component ]] = None ,
149154 services : Optional [Iterable [Service ]] = None ,
150155 tools : Optional [Iterable [Tool ]] = None ) -> None :
151- self ._components = components or SortedSet ()
152- self ._services = services or SortedSet ()
153- # Must use components/services or tools. Cannot use both
154- self ._tools = tools or SortedSet ()
156+
157+ if tools and (components or services ):
158+ # Must use components/services or tools. Cannot use both
159+ raise MutuallyExclusivePropertiesException (
160+ 'Cannot define both old (CycloneDX <= 1.4) and new '
161+ '(CycloneDX >= 1.5) format for tools.'
162+ )
163+
164+ self ._components = SortedSet (components ) or SortedSet ()
165+ self ._services = SortedSet (services ) or SortedSet ()
166+ self ._tools = SortedSet (tools ) or SortedSet ()
167+
168+ def __len__ (self ) -> int :
169+ return len (self ._tools )
155170
156171 @property
157172 def components (self ) -> Iterable [Component ]:
@@ -165,11 +180,11 @@ def components(self) -> Iterable[Component]:
165180 def components (self , components : Iterable [Component ]) -> None :
166181 if self ._tools :
167182 raise MutuallyExclusivePropertiesException (
168- 'Cannot serialize both old (CycloneDX <= 1.4) and new '
183+ 'Cannot define both old (CycloneDX <= 1.4) and new '
169184 '(CycloneDX >= 1.5) format for tools.'
170185 )
171186
172- self ._components = components
187+ self ._components = SortedSet ( components )
173188
174189 @property
175190 def services (self ) -> Iterable [Service ]:
@@ -183,10 +198,10 @@ def services(self) -> Iterable[Service]:
183198 def services (self , services : Iterable [Service ]) -> None :
184199 if self ._tools :
185200 raise MutuallyExclusivePropertiesException (
186- 'Cannot serialize both old (CycloneDX <= 1.4) and new '
187- '(CycloneDX >= 1.5) format for tools: {o!r} '
201+ 'Cannot define both old (CycloneDX <= 1.4) and new '
202+ '(CycloneDX >= 1.5) format for tools. '
188203 )
189- self ._services = services
204+ self ._services = SortedSet ( services )
190205
191206 def __getattr__ (self , name : str ) -> Any :
192207 """
@@ -213,19 +228,19 @@ class ToolRepositoryHelper(BaseHelper):
213228 def json_normalize (cls , o : ToolRepository , * ,
214229 view : Optional [Type [ViewType ]],
215230 ** __ : Any ) -> Any :
216- if not any ([o ._tools , o ._components , o ._services ]): # pylint: disable=protected-access
231+ if not any ([o ._tools , o .components , o .services ]): # pylint: disable=protected-access
217232 return None
218233
219234 if o ._tools : # pylint: disable=protected-access
220- return [Tool .as_json (t ) for t in o . _tools ] # pylint: disable=protected-access
235+ return [Tool .as_json (t ) for t in o ] # type: ignore[attr-defined]
221236
222237 result = {}
223238
224- if o ._components : # pylint: disable=protected-access
225- result ['components' ] = [Component .as_json (c ) for c in o ._components ] # pylint: disable=protected-access
239+ if o .components :
240+ result ['components' ] = [Component .as_json (c ) for c in o .components ] # type: ignore[attr-defined]
226241
227- if o ._services : # pylint: disable=protected-access
228- result ['services' ] = [Service .as_json (s ) for s in o ._services ] # pylint: disable=protected-access
242+ if o .services :
243+ result ['services' ] = [Service .as_json (s ) for s in o .services ] # type: ignore[attr-defined]
229244
230245 return result
231246
@@ -240,15 +255,15 @@ def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
240255 if isinstance (o , Dict ):
241256 if 'components' in o :
242257 for c in o ['components' ]:
243- components .append (Component .from_json (c ))
258+ components .append (Component .from_json (c )) # type: ignore[attr-defined]
244259
245260 if 'services' in o :
246261 for s in o ['services' ]:
247- services .append (Service .from_json (s ))
262+ services .append (Service .from_json (s )) # type: ignore[attr-defined]
248263
249264 elif isinstance (o , Iterable ):
250265 for t in o :
251- tools .append (Tool .from_json (t ))
266+ tools .append (Tool .from_json (t )) # type: ignore[attr-defined]
252267 else :
253268 raise CycloneDxDeserializationException ('unexpected: {o!r}' )
254269
@@ -260,7 +275,37 @@ def xml_normalize(cls, o: ToolRepository, *,
260275 view : Optional [Type [ViewType ]],
261276 xmlns : Optional [str ],
262277 ** __ : Any ) -> Optional [Element ]:
263- pass
278+ if not any ([o ._tools , o .components , o .services ]): # pylint: disable=protected-access
279+ return None
280+
281+ elem = Element (element_name )
282+
283+ if o ._tools : # pylint: disable=protected-access
284+ elem .extend (
285+ t .as_xml ( # type: ignore[attr-defined]
286+ view_ = view , as_string = False , element_name = 'tool' , xmlns = xmlns )
287+ for t in o
288+ )
289+
290+ if o .components :
291+ c_elem = Element ('components' )
292+
293+ c_elem .extend (
294+ c .as_xml ( # type: ignore[attr-defined]
295+ view_ = view , as_string = False , element_name = 'component' , xmlns = xmlns )
296+ for c in o .components
297+ )
298+
299+ if o .services :
300+ s_elem = Element ('services' )
301+
302+ s_elem .extend (
303+ s .as_xml ( # type: ignore[attr-defined]
304+ view_ = view , as_string = False , element_name = 'services' , xmlns = xmlns )
305+ for s in o .services
306+ )
307+
308+ return elem
264309
265310 @classmethod
266311 def xml_denormalize (cls , o : Element ,
0 commit comments