Skip to content

Commit 4a2ac65

Browse files
committed
refactor: move ToolsRepositoryHelper back to tool
so cyclic imports are prevented Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 427add4 commit 4a2ac65

File tree

4 files changed

+121
-118
lines changed

4 files changed

+121
-118
lines changed

cyclonedx/model/bom.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
SchemaVersion1Dot5,
3737
SchemaVersion1Dot6,
3838
)
39-
from ..serialization import LicenseRepositoryHelper, ToolsRepositoryHelper, UrnUuidHelper
39+
from ..serialization import LicenseRepositoryHelper, UrnUuidHelper
4040
from . import ExternalReference, Property, ThisTool
4141
from .bom_ref import BomRef
4242
from .component import Component
4343
from .contact import OrganizationalContact, OrganizationalEntity
4444
from .dependency import Dependable, Dependency
4545
from .license import License, LicenseExpression, LicenseRepository
4646
from .service import Service
47-
from .tool import Tool, ToolsRepository
47+
from .tool import Tool, ToolsRepository, _ToolsRepositoryHelper
4848
from .vulnerability import Vulnerability
4949

5050
if TYPE_CHECKING: # pragma: no cover
@@ -120,7 +120,7 @@ def timestamp(self, timestamp: datetime) -> None:
120120
# ... # TODO since CDX1.5
121121

122122
@property
123-
@serializable.type_mapping(ToolsRepositoryHelper)
123+
@serializable.type_mapping(_ToolsRepositoryHelper)
124124
@serializable.xml_sequence(3)
125125
def tools(self) -> ToolsRepository:
126126
"""

cyclonedx/model/tool.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,25 @@
1414
# Copyright (c) OWASP Foundation. All Rights Reserved.
1515

1616

17-
from typing import Any, Iterable, Optional, Type
17+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Type, Union
1818
from warnings import warn
19+
from xml.etree.ElementTree import Element # nosec B405
1920

2021
import serializable
22+
from serializable.helpers import BaseHelper
2123
from sortedcontainers import SortedSet
2224

2325
from .._internal.compare import ComparableTuple as _ComparableTuple
26+
from ..exception.serialization import CycloneDxDeserializationException
27+
from ..schema import SchemaVersion
2428
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
2529
from . import ExternalReference, HashType, _HashTypeRepositorySerializationHelper
2630
from .component import Component
2731
from .service import Service
2832

33+
if TYPE_CHECKING: # pragma: no cover
34+
from serializable import ObjectMetadataLibrary, ViewType
35+
2936

3037
@serializable.serializable_class
3138
class Tool:
@@ -250,3 +257,108 @@ def __eq__(self, other: object) -> bool:
250257

251258
def __hash__(self) -> int:
252259
return hash((tuple(self._tools), tuple(self._components), tuple(self._services)))
260+
261+
262+
class _ToolsRepositoryHelper(BaseHelper):
263+
264+
@staticmethod
265+
def __all_as_tools(o: ToolsRepository) -> Tuple[Tool, ...]:
266+
return (
267+
*o.tools,
268+
*map(Tool.from_component, o.components),
269+
*map(Tool.from_service, o.services),
270+
)
271+
272+
@staticmethod
273+
def __supports_components_and_services(view: Any) -> bool:
274+
try:
275+
return view is not None and view().schema_version_enum >= SchemaVersion.V1_5
276+
except Exception:
277+
return False
278+
279+
@classmethod
280+
def json_normalize(cls, o: ToolsRepository, *,
281+
view: Optional[Type['ViewType']],
282+
**__: Any) -> Any:
283+
if not o:
284+
return None
285+
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
286+
return cls.__all_as_tools(o)
287+
return {
288+
'components': tuple(o.components) if len(o.components) > 0 else None,
289+
'services': tuple(o.services) if len(o.services) > 0 else None,
290+
}
291+
292+
@classmethod
293+
def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
294+
**__: Any) -> ToolsRepository:
295+
tools = None
296+
components = None
297+
services = None
298+
if isinstance(o, Dict):
299+
components = map(lambda c: Component.from_json( # type:ignore[attr-defined]
300+
c), o.get('components', ()))
301+
services = map(lambda s: Service.from_json( # type:ignore[attr-defined]
302+
s), o.get('services', ()))
303+
elif isinstance(o, Iterable):
304+
tools = map(lambda t: Tool.from_json(t), o) # type:ignore[attr-defined]
305+
return ToolsRepository(components=components, services=services, tools=tools)
306+
307+
@classmethod
308+
def xml_normalize(cls, o: ToolsRepository, *,
309+
element_name: str,
310+
view: Optional[Type['ViewType']],
311+
xmlns: Optional[str],
312+
**__: Any) -> Optional[Element]:
313+
if not o:
314+
return None
315+
elem = Element(element_name)
316+
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
317+
elem.extend(
318+
ti.as_xml( # type:ignore[attr-defined]
319+
view_=view, as_string=False, element_name='tool', xmlns=xmlns)
320+
for ti in cls.__all_as_tools(o)
321+
)
322+
else:
323+
if o.components:
324+
elem_c = Element(f'{{{xmlns}}}components' if xmlns else 'components')
325+
elem_c.extend(
326+
ci.as_xml( # type:ignore[attr-defined]
327+
view_=view, as_string=False, element_name='component', xmlns=xmlns)
328+
for ci in o.components)
329+
elem.append(elem_c)
330+
if o.services:
331+
elem_s = Element(f'{{{xmlns}}}services' if xmlns else 'services')
332+
elem_s.extend(
333+
si.as_xml( # type:ignore[attr-defined]
334+
view_=view, as_string=False, element_name='service', xmlns=xmlns)
335+
for si in o.services)
336+
elem.append(elem_s)
337+
return elem
338+
339+
@classmethod
340+
def xml_denormalize(cls, o: Element, *,
341+
default_ns: Optional[str],
342+
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
343+
ctx: Type[Any],
344+
**kwargs: Any) -> ToolsRepository:
345+
tools = []
346+
components = None
347+
services = None
348+
for e in o:
349+
tag = e.tag if default_ns is None else e.tag.replace(f'{{{default_ns}}}', '')
350+
if tag == 'tool':
351+
tools.append(Tool.from_xml( # type:ignore[attr-defined]
352+
e, default_ns))
353+
elif tag == 'components':
354+
components = map(lambda s: Component.from_xml( # type:ignore[attr-defined]
355+
s, default_ns), e)
356+
elif tag == 'services':
357+
services = map(lambda s: Service.from_xml( # type:ignore[attr-defined]
358+
s, default_ns), e)
359+
else:
360+
raise CycloneDxDeserializationException(f'unexpected: {e!r}')
361+
return ToolsRepository(
362+
tools=tools,
363+
components=components,
364+
services=services)

cyclonedx/model/vulnerability.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from .._internal.compare import ComparableTuple as _ComparableTuple
4242
from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException
4343
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
44-
from ..serialization import BomRefHelper, ToolsRepositoryHelper
44+
from ..serialization import BomRefHelper
4545
from . import Property, XsUri
4646
from .bom_ref import BomRef
4747
from .contact import OrganizationalContact, OrganizationalEntity
@@ -51,7 +51,7 @@
5151
ImpactAnalysisResponse,
5252
ImpactAnalysisState,
5353
)
54-
from .tool import Tool, ToolsRepository
54+
from .tool import Tool, ToolsRepository, _ToolsRepositoryHelper
5555

5656

5757
@serializable.serializable_class
@@ -1247,7 +1247,7 @@ def credits(self, credits: Optional[VulnerabilityCredits]) -> None:
12471247
self._credits = credits
12481248

12491249
@property
1250-
@serializable.type_mapping(ToolsRepositoryHelper)
1250+
@serializable.type_mapping(_ToolsRepositoryHelper)
12511251
@serializable.xml_sequence(17)
12521252
def tools(self) -> ToolsRepository:
12531253
"""

cyclonedx/serialization/__init__.py

Lines changed: 2 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"""
2020

2121
from json import loads as json_loads
22-
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Type, Union
22+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type
2323
from uuid import UUID
2424
from xml.etree.ElementTree import Element # nosec B405
2525

@@ -29,14 +29,10 @@
2929

3030
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException
3131
from ..model.bom_ref import BomRef
32-
from ..model.component import Component
3332
from ..model.license import DisjunctiveLicense, LicenseExpression, LicenseRepository
34-
from ..model.service import Service
35-
from ..model.tool import Tool, ToolsRepository
36-
from ..schema import SchemaVersion
3733

3834
if TYPE_CHECKING: # pragma: no cover
39-
from serializable import ObjectMetadataLibrary, ViewType
35+
from serializable import ViewType
4036

4137

4238
class BomRefHelper(BaseHelper):
@@ -175,108 +171,3 @@ def xml_denormalize(cls, o: Element,
175171
else:
176172
raise CycloneDxDeserializationException(f'unexpected: {li!r}')
177173
return repo
178-
179-
180-
class ToolsRepositoryHelper(BaseHelper):
181-
182-
@staticmethod
183-
def __all_as_tools(o: ToolsRepository) -> Tuple[Tool, ...]:
184-
return (
185-
*o.tools,
186-
*map(Tool.from_component, o.components),
187-
*map(Tool.from_service, o.services),
188-
)
189-
190-
@staticmethod
191-
def __supports_components_and_services(view: Any) -> bool:
192-
try:
193-
return view is not None and view().schema_version_enum >= SchemaVersion.V1_5
194-
except Exception:
195-
return False
196-
197-
@classmethod
198-
def json_normalize(cls, o: ToolsRepository, *,
199-
view: Optional[Type['ViewType']],
200-
**__: Any) -> Any:
201-
if not o:
202-
return None
203-
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
204-
return cls.__all_as_tools(o)
205-
return {
206-
'components': tuple(o.components) if len(o.components) > 0 else None,
207-
'services': tuple(o.services) if len(o.services) > 0 else None,
208-
}
209-
210-
@classmethod
211-
def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
212-
**__: Any) -> ToolsRepository:
213-
tools = None
214-
components = None
215-
services = None
216-
if isinstance(o, Dict):
217-
components = map(lambda c: Component.from_json( # type:ignore[attr-defined]
218-
c), o.get('components', ()))
219-
services = map(lambda s: Service.from_json( # type:ignore[attr-defined]
220-
s), o.get('services', ()))
221-
elif isinstance(o, Iterable):
222-
tools = map(lambda t: Tool.from_json(t), o) # type:ignore[attr-defined]
223-
return ToolsRepository(components=components, services=services, tools=tools)
224-
225-
@classmethod
226-
def xml_normalize(cls, o: ToolsRepository, *,
227-
element_name: str,
228-
view: Optional[Type['ViewType']],
229-
xmlns: Optional[str],
230-
**__: Any) -> Optional[Element]:
231-
if not o:
232-
return None
233-
elem = Element(element_name)
234-
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
235-
elem.extend(
236-
ti.as_xml( # type:ignore[attr-defined]
237-
view_=view, as_string=False, element_name='tool', xmlns=xmlns)
238-
for ti in cls.__all_as_tools(o)
239-
)
240-
else:
241-
if o.components:
242-
elem_c = Element(f'{{{xmlns}}}components' if xmlns else 'components')
243-
elem_c.extend(
244-
ci.as_xml( # type:ignore[attr-defined]
245-
view_=view, as_string=False, element_name='component', xmlns=xmlns)
246-
for ci in o.components)
247-
elem.append(elem_c)
248-
if o.services:
249-
elem_s = Element(f'{{{xmlns}}}services' if xmlns else 'services')
250-
elem_s.extend(
251-
si.as_xml( # type:ignore[attr-defined]
252-
view_=view, as_string=False, element_name='service', xmlns=xmlns)
253-
for si in o.services)
254-
elem.append(elem_s)
255-
return elem
256-
257-
@classmethod
258-
def xml_denormalize(cls, o: Element, *,
259-
default_ns: Optional[str],
260-
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
261-
ctx: Type[Any],
262-
**kwargs: Any) -> ToolsRepository:
263-
tools = []
264-
components = None
265-
services = None
266-
for e in o:
267-
tag = e.tag if default_ns is None else e.tag.replace(f'{{{default_ns}}}', '')
268-
if tag == 'tool':
269-
tools.append(Tool.from_xml( # type:ignore[attr-defined]
270-
e, default_ns))
271-
elif tag == 'components':
272-
components = map(lambda s: Component.from_xml( # type:ignore[attr-defined]
273-
s, default_ns), e)
274-
elif tag == 'services':
275-
services = map(lambda s: Service.from_xml( # type:ignore[attr-defined]
276-
s, default_ns), e)
277-
else:
278-
raise CycloneDxDeserializationException(f'unexpected: {e!r}')
279-
return ToolsRepository(
280-
tools=tools,
281-
components=components,
282-
services=services)

0 commit comments

Comments
 (0)