Skip to content

Commit a68ae24

Browse files
authored
Feat: typing, typehints, & overload (#463)
also: bump `py-serializable@^0.14.0` --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 2240b4d commit a68ae24

File tree

15 files changed

+197
-112
lines changed

15 files changed

+197
-112
lines changed

.mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[mypy]
22

3-
files = cyclonedx/
3+
files = cyclonedx/, examples/
44
mypy_path = $MYPY_CONFIG_FILE_DIR/typings
55

66
show_error_codes = True

cyclonedx/model/bom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from .service import Service
5151
from .vulnerability import Vulnerability
5252

53-
if TYPE_CHECKING:
53+
if TYPE_CHECKING: # pragma: no cover
5454
from packageurl import PackageURL
5555

5656

cyclonedx/output/__init__.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,27 @@
2020
import os
2121
from abc import ABC, abstractmethod
2222
from importlib import import_module
23-
from typing import Any, Iterable, Optional, Type, Union
23+
from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional, Type, Union, overload
2424

25-
from ..model.bom import Bom
26-
from ..model.component import Component
2725
from ..schema import OutputFormat, SchemaVersion
2826

27+
if TYPE_CHECKING: # pragma: no cover
28+
from ..model.bom import Bom
29+
from ..model.component import Component
30+
from .json import Json as JsonOutputter
31+
from .xml import Xml as XmlOutputter
32+
2933
LATEST_SUPPORTED_SCHEMA_VERSION = SchemaVersion.V1_4
3034

3135

3236
class BaseOutput(ABC):
3337

34-
def __init__(self, bom: Bom, **kwargs: int) -> None:
38+
def __init__(self, bom: 'Bom', **kwargs: int) -> None:
3539
super().__init__(**kwargs)
3640
self._bom = bom
3741
self._generated: bool = False
3842

39-
def _chained_components(self, container: Union[Bom, Component]) -> Iterable[Component]:
43+
def _chained_components(self, container: Union['Bom', 'Component']) -> Iterable['Component']:
4044
for component in container.components:
4145
yield component
4246
yield from self._chained_components(component)
@@ -59,10 +63,10 @@ def generated(self) -> bool:
5963
def generated(self, generated: bool) -> None:
6064
self._generated = generated
6165

62-
def get_bom(self) -> Bom:
66+
def get_bom(self) -> 'Bom':
6367
return self._bom
6468

65-
def set_bom(self, bom: Bom) -> None:
69+
def set_bom(self, bom: 'Bom') -> None:
6670
self._bom = bom
6771

6872
@abstractmethod
@@ -89,17 +93,39 @@ def output_to_file(self, filename: str, allow_overwrite: bool = False, *,
8993
f_out.write(self.output_as_string(indent=indent).encode('utf-8'))
9094

9195

92-
def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML,
96+
@overload
97+
def get_instance(bom: 'Bom', output_format: Literal[OutputFormat.JSON],
98+
schema_version: SchemaVersion = ...) -> 'JsonOutputter':
99+
...
100+
101+
102+
@overload
103+
def get_instance(bom: 'Bom', output_format: Literal[OutputFormat.XML] = ...,
104+
schema_version: SchemaVersion = ...) -> 'XmlOutputter':
105+
...
106+
107+
108+
@overload
109+
def get_instance(bom: 'Bom', output_format: OutputFormat = ...,
110+
schema_version: SchemaVersion = ...
111+
) -> Union['XmlOutputter', 'JsonOutputter']:
112+
...
113+
114+
115+
def get_instance(bom: 'Bom', output_format: OutputFormat = OutputFormat.XML,
93116
schema_version: SchemaVersion = LATEST_SUPPORTED_SCHEMA_VERSION) -> BaseOutput:
94117
"""
95118
Helper method to quickly get the correct output class/formatter.
96119
97120
Pass in your BOM and optionally an output format and schema version (defaults to XML and latest schema version).
98121
122+
123+
Raises error when no instance could be built.
124+
99125
:param bom: Bom
100126
:param output_format: OutputFormat
101127
:param schema_version: SchemaVersion
102-
:return:
128+
:return: BaseOutput
103129
"""
104130
# all exceptions are undocumented, as they are pure functional, and should be prevented by correct typing...
105131
if not isinstance(output_format, OutputFormat):
@@ -108,9 +134,9 @@ def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML,
108134
raise TypeError(f"unexpected schema_version: {schema_version!r}")
109135
try:
110136
module = import_module(f'.{output_format.name.lower()}', __package__)
111-
except ImportError as error: # pragma: no cover
137+
except ImportError as error:
112138
raise ValueError(f'Unknown output_format: {output_format.name}') from error
113139
klass: Optional[Type[BaseOutput]] = module.BY_SCHEMA_VERSION.get(schema_version, None)
114-
if klass is None: # pragma: no cover
140+
if klass is None:
115141
raise ValueError(f'Unknown {output_format.name}/schema_version: {schema_version.name}')
116142
return klass(bom=bom)

cyclonedx/output/json.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717

1818
from abc import abstractmethod
1919
from json import dumps as json_dumps, loads as json_loads
20-
from typing import Any, Dict, Optional, Type, Union
20+
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, Union
2121

2222
from ..exception.output import FormatNotSupportedException
23-
from ..model.bom import Bom
2423
from ..schema import OutputFormat, SchemaVersion
2524
from ..schema.schema import (
2625
SCHEMA_VERSIONS,
@@ -33,10 +32,13 @@
3332
)
3433
from . import BaseOutput
3534

35+
if TYPE_CHECKING: # pragma: no cover
36+
from ..model.bom import Bom
37+
3638

3739
class Json(BaseOutput, BaseSchemaVersion):
3840

39-
def __init__(self, bom: Bom) -> None:
41+
def __init__(self, bom: 'Bom') -> None:
4042
super().__init__(bom=bom)
4143
self._bom_json: Dict[str, Any] = dict()
4244

@@ -45,7 +47,7 @@ def schema_version(self) -> SchemaVersion:
4547
return self.schema_version_enum
4648

4749
@property
48-
def output_format(self) -> OutputFormat:
50+
def output_format(self) -> Literal[OutputFormat.JSON]:
4951
return OutputFormat.JSON
5052

5153
def generate(self, force_regeneration: bool = False) -> None:
@@ -63,9 +65,10 @@ def generate(self, force_regeneration: bool = False) -> None:
6365
'specVersion': self.schema_version.to_version()
6466
}
6567
_view = SCHEMA_VERSIONS.get(self.schema_version_enum)
66-
self.get_bom().validate()
68+
bom = self.get_bom()
69+
bom.validate()
6770
bom_json: Dict[str, Any] = json_loads(
68-
self.get_bom().as_json( # type:ignore[attr-defined]
71+
bom.as_json( # type:ignore[attr-defined]
6972
view_=_view))
7073
bom_json.update(_json_core)
7174
self._bom_json = bom_json
@@ -85,38 +88,38 @@ def _get_schema_uri(self) -> Optional[str]:
8588

8689
class JsonV1Dot0(Json, SchemaVersion1Dot0):
8790

88-
def _get_schema_uri(self) -> Optional[str]:
91+
def _get_schema_uri(self) -> None:
8992
return None
9093

9194

9295
class JsonV1Dot1(Json, SchemaVersion1Dot1):
9396

94-
def _get_schema_uri(self) -> Optional[str]:
97+
def _get_schema_uri(self) -> None:
9598
return None
9699

97100

98101
class JsonV1Dot2(Json, SchemaVersion1Dot2):
99102

100-
def _get_schema_uri(self) -> Optional[str]:
103+
def _get_schema_uri(self) -> str:
101104
return 'http://cyclonedx.org/schema/bom-1.2b.schema.json'
102105

103106

104107
class JsonV1Dot3(Json, SchemaVersion1Dot3):
105108

106-
def _get_schema_uri(self) -> Optional[str]:
109+
def _get_schema_uri(self) -> str:
107110
return 'http://cyclonedx.org/schema/bom-1.3a.schema.json'
108111

109112

110113
class JsonV1Dot4(Json, SchemaVersion1Dot4):
111114

112-
def _get_schema_uri(self) -> Optional[str]:
115+
def _get_schema_uri(self) -> str:
113116
return 'http://cyclonedx.org/schema/bom-1.4.schema.json'
114117

115118

116119
BY_SCHEMA_VERSION: Dict[SchemaVersion, Type[Json]] = {
117-
SchemaVersion.V1_4: JsonV1Dot4, # type:ignore[type-abstract]
118-
SchemaVersion.V1_3: JsonV1Dot3, # type:ignore[type-abstract]
119-
SchemaVersion.V1_2: JsonV1Dot2, # type:ignore[type-abstract]
120-
SchemaVersion.V1_1: JsonV1Dot1, # type:ignore[type-abstract]
121-
SchemaVersion.V1_0: JsonV1Dot0, # type:ignore[type-abstract]
120+
SchemaVersion.V1_4: JsonV1Dot4,
121+
SchemaVersion.V1_3: JsonV1Dot3,
122+
SchemaVersion.V1_2: JsonV1Dot2,
123+
SchemaVersion.V1_1: JsonV1Dot1,
124+
SchemaVersion.V1_0: JsonV1Dot0,
122125
}

cyclonedx/output/xml.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818

19-
from typing import Any, Dict, Optional, Type, Union
19+
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, Union
2020
from xml.dom.minidom import parseString as dom_parseString
2121
from xml.etree.ElementTree import Element as XmlElement, tostring as xml_dumps
2222

23-
from ..model.bom import Bom
2423
from ..schema import OutputFormat, SchemaVersion
2524
from ..schema.schema import (
2625
SCHEMA_VERSIONS,
@@ -33,9 +32,12 @@
3332
)
3433
from . import BaseOutput
3534

35+
if TYPE_CHECKING: # pragma: no cover
36+
from ..model.bom import Bom
37+
3638

3739
class Xml(BaseSchemaVersion, BaseOutput):
38-
def __init__(self, bom: Bom) -> None:
40+
def __init__(self, bom: 'Bom') -> None:
3941
super().__init__(bom=bom)
4042
self._bom_xml: str = ''
4143

@@ -44,18 +46,19 @@ def schema_version(self) -> SchemaVersion:
4446
return self.schema_version_enum
4547

4648
@property
47-
def output_format(self) -> OutputFormat:
49+
def output_format(self) -> Literal[OutputFormat.XML]:
4850
return OutputFormat.XML
4951

5052
def generate(self, force_regeneration: bool = False) -> None:
5153
if self.generated and not force_regeneration:
5254
return
5355

5456
_view = SCHEMA_VERSIONS[self.schema_version_enum]
55-
self.get_bom().validate()
57+
bom = self.get_bom()
58+
bom.validate()
5659
xmlns = self.get_target_namespace()
5760
self._bom_xml = '<?xml version="1.0" ?>\n' + xml_dumps(
58-
self.get_bom().as_xml( # type:ignore[attr-defined]
61+
bom.as_xml( # type:ignore[attr-defined]
5962
_view, as_string=False, xmlns=xmlns),
6063
method='xml', default_namespace=xmlns, encoding='unicode',
6164
# `xml-declaration` is inconsistent/bugged in py38, especially on Windows it will print a non-UTF8 codepage.
@@ -109,9 +112,9 @@ class XmlV1Dot4(Xml, SchemaVersion1Dot4):
109112

110113

111114
BY_SCHEMA_VERSION: Dict[SchemaVersion, Type[Xml]] = {
112-
SchemaVersion.V1_4: XmlV1Dot4, # type:ignore[type-abstract]
113-
SchemaVersion.V1_3: XmlV1Dot3, # type:ignore[type-abstract]
114-
SchemaVersion.V1_2: XmlV1Dot2, # type:ignore[type-abstract]
115-
SchemaVersion.V1_1: XmlV1Dot1, # type:ignore[type-abstract]
116-
SchemaVersion.V1_0: XmlV1Dot0, # type:ignore[type-abstract]
115+
SchemaVersion.V1_4: XmlV1Dot4,
116+
SchemaVersion.V1_3: XmlV1Dot3,
117+
SchemaVersion.V1_2: XmlV1Dot2,
118+
SchemaVersion.V1_1: XmlV1Dot1,
119+
SchemaVersion.V1_0: XmlV1Dot0,
117120
}

cyclonedx/schema/__init__.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,28 @@
1313
# SPDX-License-Identifier: Apache-2.0
1414

1515
from enum import Enum, auto, unique
16+
from typing import Any, Type, TypeVar
1617

1718

1819
@unique
1920
class OutputFormat(Enum):
2021
"""Output formats.
2122
22-
Do not rely on the actual/literal values, just use enum cases.
23+
Cases are hashable.
24+
25+
Do not rely on the actual/literal values, just use enum cases, like so:
26+
my_of = OutputFormat.XML
2327
"""
28+
2429
JSON = auto()
2530
XML = auto()
2631

32+
def __hash__(self) -> int:
33+
return hash(self.name)
34+
35+
36+
_SV = TypeVar('_SV', bound='SchemaVersion')
37+
2738

2839
@unique
2940
class SchemaVersion(Enum):
@@ -33,52 +44,54 @@ class SchemaVersion(Enum):
3344
Cases are hashable.
3445
Cases are comparable(!=,>=,>,==,<,<=)
3546
36-
Do not rely on the actual/literal values, just use enum cases.
47+
Do not rely on the actual/literal values, just use enum cases, like so:
48+
my_sv = SchemaVersion.V1_3
3749
"""
50+
3851
V1_4 = (1, 4)
3952
V1_3 = (1, 3)
4053
V1_2 = (1, 2)
4154
V1_1 = (1, 1)
4255
V1_0 = (1, 0)
4356

4457
@classmethod
45-
def from_version(cls, version: str) -> 'SchemaVersion':
46-
"""Return instance from a version string - e.g. `1.4`"""
58+
def from_version(cls: Type[_SV], version: str) -> _SV:
59+
"""Return instance based of a version string - e.g. `1.4`"""
4760
return cls(tuple(map(int, version.split('.')))[:2])
4861

4962
def to_version(self) -> str:
5063
"""Return as a version string - e.g. `1.4`"""
5164
return '.'.join(map(str, self.value))
5265

53-
def __ne__(self, other: object) -> bool:
54-
return self.value != other.value \
55-
if isinstance(other, self.__class__) \
56-
else NotImplemented # type:ignore[return-value]
57-
58-
def __lt__(self, other: object) -> bool:
59-
return self.value < other.value \
60-
if isinstance(other, self.__class__) \
61-
else NotImplemented # type:ignore[return-value]
62-
63-
def __le__(self, other: object) -> bool:
64-
return self.value <= other.value \
65-
if isinstance(other, self.__class__) \
66-
else NotImplemented # type:ignore[return-value]
67-
68-
def __eq__(self, other: object) -> bool:
69-
return self.value == other.value \
70-
if isinstance(other, self.__class__) \
71-
else NotImplemented # type:ignore[return-value]
72-
73-
def __ge__(self, other: object) -> bool:
74-
return self.value >= other.value \
75-
if isinstance(other, self.__class__) \
76-
else NotImplemented # type:ignore[return-value]
77-
78-
def __gt__(self, other: object) -> bool:
79-
return self.value > other.value \
80-
if isinstance(other, self.__class__) \
81-
else NotImplemented # type:ignore[return-value]
66+
def __ne__(self, other: Any) -> bool:
67+
if isinstance(other, self.__class__):
68+
return self.value != other.value
69+
return NotImplemented # pragma: no cover
70+
71+
def __lt__(self, other: Any) -> bool:
72+
if isinstance(other, self.__class__):
73+
return self.value < other.value
74+
return NotImplemented # pragma: no cover
75+
76+
def __le__(self, other: Any) -> bool:
77+
if isinstance(other, self.__class__):
78+
return self.value <= other.value
79+
return NotImplemented # pragma: no cover
80+
81+
def __eq__(self, other: Any) -> bool:
82+
if isinstance(other, self.__class__):
83+
return self.value == other.value
84+
return NotImplemented # pragma: no cover
85+
86+
def __ge__(self, other: Any) -> bool:
87+
if isinstance(other, self.__class__):
88+
return self.value >= other.value
89+
return NotImplemented # pragma: no cover
90+
91+
def __gt__(self, other: Any) -> bool:
92+
if isinstance(other, self.__class__):
93+
return self.value > other.value
94+
return NotImplemented # pragma: no cover
8295

8396
def __hash__(self) -> int:
8497
return hash(self.name)

0 commit comments

Comments
 (0)