Skip to content

Commit 142b8bf

Browse files
authored
BREAKING CHANGE: update models to use Set rather than List (#160)
* BREAKING CHANGE: update models to use `Set` and `Iterable` rather than `List[..]` BREAKING CHANGE: update final models to use `@property` wip Signed-off-by: Paul Horton <[email protected]>
1 parent 0f1fd6d commit 142b8bf

24 files changed

+814
-1258
lines changed

cyclonedx/model/__init__.py

Lines changed: 199 additions & 137 deletions
Large diffs are not rendered by default.

cyclonedx/model/bom.py

Lines changed: 32 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
#
1717
# SPDX-License-Identifier: Apache-2.0
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
19-
2019
from datetime import datetime, timezone
21-
from typing import cast, List, Optional
20+
from typing import Iterable, Optional, Set
2221
from uuid import uuid4, UUID
2322

2423
from . import ExternalReference, ThisTool, Tool
@@ -35,39 +34,28 @@ class BomMetaData:
3534
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.3/#type_metadata
3635
"""
3736

38-
def __init__(self, *, tools: Optional[List[Tool]] = None) -> None:
37+
def __init__(self, *, tools: Optional[Iterable[Tool]] = None) -> None:
3938
self.timestamp = datetime.now(tz=timezone.utc)
40-
self.tools = tools if tools else []
39+
self.tools = set(tools or [])
4140

4241
if not self.tools:
43-
self.add_tool(ThisTool)
42+
self.tools.add(ThisTool)
4443

4544
self.component: Optional[Component] = None
4645

4746
@property
48-
def tools(self) -> List[Tool]:
47+
def tools(self) -> Set[Tool]:
4948
"""
5049
Tools used to create this BOM.
5150
5251
Returns:
53-
`List` of `Tool` objects where there are any, else an empty `List`.
52+
`Set` of `Tool` objects.
5453
"""
5554
return self._tools
5655

5756
@tools.setter
58-
def tools(self, tools: List[Tool]) -> None:
59-
self._tools = tools
60-
61-
def add_tool(self, tool: Tool) -> None:
62-
"""
63-
Add a Tool definition to this Bom Metadata. The `cyclonedx-python-lib` is automatically added - you do not need
64-
to add this yourself.
65-
66-
Args:
67-
tool:
68-
Instance of `Tool` that represents the tool you are using.
69-
"""
70-
self._tools.append(tool)
57+
def tools(self, tools: Iterable[Tool]) -> None:
58+
self._tools = set(tools)
7159

7260
@property
7361
def timestamp(self) -> datetime:
@@ -114,9 +102,7 @@ def __eq__(self, other: object) -> bool:
114102

115103
def __hash__(self) -> int:
116104
return hash((
117-
self.timestamp,
118-
tuple([hash(tool) for tool in set(sorted(self.tools, key=hash))]) if self.tools else None,
119-
self.component
105+
self.timestamp, self.tools, self.component
120106
))
121107

122108
def __repr__(self) -> str:
@@ -146,11 +132,12 @@ def from_parser(parser: BaseParser) -> 'Bom':
146132
`cyclonedx.model.bom.Bom`: A Bom instance that represents the valid data held in the supplied parser.
147133
"""
148134
bom = Bom()
149-
bom.add_components(parser.get_components())
135+
bom.components.update(parser.get_components())
150136
return bom
151137

152-
def __init__(self, *, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None,
153-
external_references: Optional[List[ExternalReference]] = None) -> None:
138+
def __init__(self, *, components: Optional[Iterable[Component]] = None,
139+
services: Optional[Iterable[Service]] = None,
140+
external_references: Optional[Iterable[ExternalReference]] = None) -> None:
154141
"""
155142
Create a new Bom that you can manually/programmatically add data to later.
156143
@@ -159,9 +146,9 @@ def __init__(self, *, components: Optional[List[Component]] = None, services: Op
159146
"""
160147
self.uuid = uuid4()
161148
self.metadata = BomMetaData()
162-
self.components = components
163-
self.services = services
164-
self.external_references = external_references
149+
self.components = set(components or [])
150+
self.services = set(services or [])
151+
self.external_references = set(external_references or [])
165152

166153
@property
167154
def uuid(self) -> UUID:
@@ -195,60 +182,22 @@ def metadata(self, metadata: BomMetaData) -> None:
195182
self._metadata = metadata
196183

197184
@property
198-
def components(self) -> Optional[List[Component]]:
185+
def components(self) -> Set[Component]:
199186
"""
200187
Get all the Components currently in this Bom.
201188
202189
Returns:
203-
List of all Components in this Bom or `None`
190+
Set of `Component` in this Bom
204191
"""
205192
return self._components
206193

207194
@components.setter
208-
def components(self, components: Optional[List[Component]]) -> None:
209-
self._components = components
210-
211-
def add_component(self, component: Component) -> None:
212-
"""
213-
Add a Component to this Bom instance.
214-
215-
Args:
216-
component:
217-
`cyclonedx.model.component.Component` instance to add to this Bom.
218-
219-
Returns:
220-
None
221-
"""
222-
if not self.components:
223-
self.components = [component]
224-
elif not self.has_component(component=component):
225-
self.components.append(component)
226-
227-
def add_components(self, components: List[Component]) -> None:
228-
"""
229-
Add multiple Components at once to this Bom instance.
230-
231-
Args:
232-
components:
233-
List of `cyclonedx.model.component.Component` instances to add to this Bom.
234-
235-
Returns:
236-
None
237-
"""
238-
self.components = (self._components or []) + components
239-
240-
def component_count(self) -> int:
241-
"""
242-
Returns the current count of Components within this Bom.
243-
244-
Returns:
245-
The number of Components in this Bom as `int`.
246-
"""
247-
return len(self._components) if self._components else 0
195+
def components(self, components: Iterable[Component]) -> None:
196+
self._components = set(components)
248197

249198
def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]:
250199
"""
251-
Get a Component already in the Bom by it's PURL
200+
Get a Component already in the Bom by its PURL
252201
253202
Args:
254203
purl:
@@ -257,11 +206,8 @@ def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]:
257206
Returns:
258207
`Component` or `None`
259208
"""
260-
if not self._components:
261-
return None
262-
263209
if purl:
264-
found = list(filter(lambda x: x.purl == purl, cast(List[Component], self.components)))
210+
found = list(filter(lambda x: x.purl == purl, self.components))
265211
if len(found) == 1:
266212
return found[0]
267213

@@ -287,107 +233,35 @@ def has_component(self, component: Component) -> bool:
287233
Returns:
288234
`bool` - `True` if the supplied Component is part of this Bom, `False` otherwise.
289235
"""
290-
if not self.components:
291-
return False
292236
return component in self.components
293237

294238
@property
295-
def services(self) -> Optional[List[Service]]:
239+
def services(self) -> Set[Service]:
296240
"""
297241
Get all the Services currently in this Bom.
298242
299243
Returns:
300-
List of `Service` in this Bom or `None`
244+
Set of `Service` in this BOM
301245
"""
302246
return self._services
303247

304248
@services.setter
305-
def services(self, services: Optional[List[Service]]) -> None:
306-
self._services = services
307-
308-
def add_service(self, service: Service) -> None:
309-
"""
310-
Add a Service to this Bom instance.
311-
312-
Args:
313-
service:
314-
`cyclonedx.model.service.Service` instance to add to this Bom.
315-
316-
Returns:
317-
None
318-
"""
319-
if not self.services:
320-
self.services = [service]
321-
elif not self.has_service(service=service):
322-
self.services.append(service)
323-
324-
def add_services(self, services: List[Service]) -> None:
325-
"""
326-
Add multiple Services at once to this Bom instance.
327-
328-
Args:
329-
services:
330-
List of `cyclonedx.model.service.Service` instances to add to this Bom.
331-
332-
Returns:
333-
None
334-
"""
335-
self.services = (self.services or []) + services
336-
337-
def has_service(self, service: Service) -> bool:
338-
"""
339-
Check whether this Bom contains the provided Service.
340-
341-
Args:
342-
service:
343-
The instance of `cyclonedx.model.service.Service` to check if this Bom contains.
344-
345-
Returns:
346-
`bool` - `True` if the supplied Service is part of this Bom, `False` otherwise.
347-
"""
348-
if not self.services:
349-
return False
350-
351-
return service in self.services
352-
353-
def service_count(self) -> int:
354-
"""
355-
Returns the current count of Services within this Bom.
356-
357-
Returns:
358-
The number of Services in this Bom as `int`.
359-
"""
360-
if not self.services:
361-
return 0
362-
363-
return len(self.services)
249+
def services(self, services: Iterable[Service]) -> None:
250+
self._services = set(services)
364251

365252
@property
366-
def external_references(self) -> Optional[List[ExternalReference]]:
253+
def external_references(self) -> Set[ExternalReference]:
367254
"""
368255
Provides the ability to document external references related to the BOM or to the project the BOM describes.
369256
370257
Returns:
371-
List of `ExternalReference` else `None`
258+
Set of `ExternalReference`
372259
"""
373260
return self._external_references
374261

375262
@external_references.setter
376-
def external_references(self, external_references: Optional[List[ExternalReference]]) -> None:
377-
self._external_references = external_references
378-
379-
def add_external_reference(self, external_reference: ExternalReference) -> None:
380-
"""
381-
Add an external reference to this Bom.
382-
383-
Args:
384-
external_reference:
385-
`ExternalReference` to add to this Bom.
386-
387-
Returns:
388-
None
389-
"""
390-
self.external_references = (self.external_references or []) + [external_reference]
263+
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
264+
self._external_references = set(external_references)
391265

392266
def has_vulnerabilities(self) -> bool:
393267
"""
@@ -397,12 +271,7 @@ def has_vulnerabilities(self) -> bool:
397271
`bool` - `True` if at least one `cyclonedx.model.component.Component` has at least one Vulnerability,
398272
`False` otherwise.
399273
"""
400-
if self.components:
401-
for c in self.components:
402-
if c.has_vulnerabilities():
403-
return True
404-
405-
return False
274+
return any(c.has_vulnerabilities() for c in self.components)
406275

407276
def __eq__(self, other: object) -> bool:
408277
if isinstance(other, Bom):
@@ -411,9 +280,7 @@ def __eq__(self, other: object) -> bool:
411280

412281
def __hash__(self) -> int:
413282
return hash((
414-
self.uuid, self.metadata,
415-
tuple([hash(c) for c in set(sorted(self.components, key=hash))]) if self.components else None,
416-
tuple([hash(s) for s in set(sorted(self.services, key=hash))]) if self.services else None
283+
self.uuid, self.metadata, tuple(self.components), tuple(self.services), tuple(self.external_references)
417284
))
418285

419286
def __repr__(self) -> str:

0 commit comments

Comments
 (0)