Skip to content

Commit fcfb863

Browse files
committed
Additioal cleanup and improving semantics
1 parent 2bbd659 commit fcfb863

File tree

2 files changed

+68
-19
lines changed

2 files changed

+68
-19
lines changed

cyclonedx/model/bom.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
from sortedcontainers import SortedSet
2727

2828
from .._internal.time import get_now_utc as _get_now_utc
29-
from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
29+
from ..exception.model import (
30+
LicenseExpressionAlongWithOthersException,
31+
MutuallyExclusivePropertiesException,
32+
UnknownComponentDependencyException,
33+
)
3034
from ..model.tool import Tool, ToolRepository, ToolRepositoryHelper
3135
from ..schema.schema import (
3236
SchemaVersion1Dot0,
@@ -119,27 +123,35 @@ def timestamp(self, timestamp: datetime) -> None:
119123
@serializable.type_mapping(ToolRepositoryHelper)
120124
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tool')
121125
@serializable.xml_sequence(3)
122-
def tools(self) -> ToolRepository:
126+
def tools(self) -> Union[Iterable[Tool], ToolRepository]:
123127
"""
124128
Tools used to create this BOM.
125129
126130
Returns:
127131
`Set` of `Tool` objects.
128132
"""
129-
return self._tools # type: ignore
133+
if self._tools._tools: # pylint: disable=protected-access
134+
return self._tools._tools # pylint: disable=protected-access
135+
136+
return self._tools
130137

131138
@tools.setter
132-
def tools(self, tools: Union[ToolRepository, Iterable[Tool]]) -> None:
139+
def tools(self, tools: Union[Iterable[Tool], ToolRepository]) -> None:
133140
if isinstance(tools, ToolRepository):
134141
self._tools = tools
135142
else:
136143
# This allows the old behavior of assigning the list of tools directly to bom.metadata.tools
137144
warn(
138145
'`bom.metadata.tools` as a list of Tool objects is deprecated from CycloneDX v1.5 '
139146
'onwards. Please use lists of `Component` and `Service` objects as `tools.components` '
140-
'and `tools.services`, respecitvely'
147+
'and `tools.services`, respectively.'
141148
)
142-
self._tools = ToolRepository(tools=tools) # type:ignore
149+
if self._tools._components or self._tools._services:
150+
raise MutuallyExclusivePropertiesException(
151+
'Cannot serialize both old (CycloneDX <= 1.4) and new '
152+
'(CycloneDX >= 1.5) format for tools.'
153+
)
154+
self._tools = ToolRepository(tools=tools)
143155

144156
@property
145157
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'author')

cyclonedx/model/tool.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Iterable, List, Optional, Type, Union
1+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Type, Union
22
from xml.etree.ElementTree import Element # nosec B405
33

44
import serializable
@@ -7,7 +7,8 @@
77
from sortedcontainers import SortedSet
88

99
from .._internal.compare import ComparableTuple as _ComparableTuple
10-
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
10+
from ..exception.model import MutuallyExclusivePropertiesException
11+
from ..exception.serialization import CycloneDxDeserializationException
1112
from ..model import ExternalReference, HashType, _HashTypeRepositorySerializationHelper
1213
from ..model.component import Component
1314
from ..model.service import Service
@@ -152,15 +153,57 @@ def __init__(self, *, components: Optional[Iterable[Component]] = None,
152153
# Must use components/services or tools. Cannot use both
153154
self._tools = tools or SortedSet()
154155

156+
@property
157+
def components(self) -> Iterable[Component]:
158+
"""
159+
Returns:
160+
A SortedSet of Components
161+
"""
162+
return self._components
163+
164+
@components.setter
165+
def components(self, components: Iterable[Component]) -> None:
166+
if self._tools:
167+
raise MutuallyExclusivePropertiesException(
168+
'Cannot serialize both old (CycloneDX <= 1.4) and new '
169+
'(CycloneDX >= 1.5) format for tools.'
170+
)
171+
172+
self._components = components
173+
174+
@property
175+
def services(self) -> Iterable[Service]:
176+
"""
177+
Returns:
178+
A SortedSet of Services
179+
"""
180+
return self._services
181+
182+
@services.setter
183+
def services(self, services: Iterable[Service]) -> None:
184+
if self._tools:
185+
raise MutuallyExclusivePropertiesException(
186+
'Cannot serialize both old (CycloneDX <= 1.4) and new '
187+
'(CycloneDX >= 1.5) format for tools: {o!r}'
188+
)
189+
self._services = services
190+
155191
def __getattr__(self, name: str) -> Any:
156-
if name == 'components':
157-
return self._components
158-
if name == 'services':
159-
return self._services
192+
"""
193+
Enables us to behave as list of tools to maintain
194+
backward compatibility.
160195
196+
Returns:
197+
An attribute of SortedSet
198+
"""
161199
return getattr(self._tools, name)
162200

163-
def __iter__(self) -> Tool:
201+
def __iter__(self) -> Iterator[Tool]:
202+
"""
203+
Also part of acting as a list of tools
204+
205+
Returns Iterator[Tool]
206+
"""
164207
for t in self._tools:
165208
yield t
166209

@@ -173,12 +216,6 @@ def json_normalize(cls, o: ToolRepository, *,
173216
if not any([o._tools, o._components, o._services]): # pylint: disable=protected-access
174217
return None
175218

176-
if o._tools and any([o._components, o._services]): # pylint: disable=protected-access
177-
raise SerializationOfUnexpectedValueException(
178-
'Cannot serialize both old (CycloneDX <= 1.4) and new '
179-
'(CycloneDX >= 1.5) format for tools: {o!r}'
180-
)
181-
182219
if o._tools: # pylint: disable=protected-access
183220
return [Tool.as_json(t) for t in o._tools] # pylint: disable=protected-access
184221

0 commit comments

Comments
 (0)