Skip to content

Commit 9144765

Browse files
jkowalleckmadpah
andauthored
feat: Typing & PEP 561
* adde file for type checkers according to PEP 561 Signed-off-by: Jan Kowalleck <[email protected]> * added static code analysis as a dev-test Signed-off-by: Jan Kowalleck <[email protected]> * added the "typed" trove Signed-off-by: Jan Kowalleck <[email protected]> * added `flake8-annotations` to the tests Signed-off-by: Jan Kowalleck <[email protected]> * added type hints Signed-off-by: Jan Kowalleck <[email protected]> * further typing updates Signed-off-by: Paul Horton <[email protected]> * further typing additions and test updates Signed-off-by: Paul Horton <[email protected]> * further typing Signed-off-by: Paul Horton <[email protected]> * further typing - added type stubs for toml and setuptools Signed-off-by: Paul Horton <[email protected]> * further typing Signed-off-by: Paul Horton <[email protected]> * typing work Signed-off-by: Paul Horton <[email protected]> * coding standards Signed-off-by: Paul Horton <[email protected]> * fixed tox and mypy running in correct python version Signed-off-by: Jan Kowalleck <[email protected]> * supressed mypy for `cyclonedx.utils.conda.parse_conda_json_to_conda_package` Signed-off-by: Jan Kowalleck <[email protected]> * fixed type hints Signed-off-by: Jan Kowalleck <[email protected]> * fixed some typing related flaws Signed-off-by: Jan Kowalleck <[email protected]> * added flake8-bugbear for code analysis Signed-off-by: Jan Kowalleck <[email protected]> Co-authored-by: Paul Horton <[email protected]>
1 parent f34e2c2 commit 9144765

40 files changed

+652
-281
lines changed

.github/workflows/poetry.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ jobs:
4343
- name: Run tox
4444
run: poetry run tox -e flake8
4545

46+
static-code-analysis:
47+
name: Static Coding Analysis
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Checkout
51+
# see https://github.com/actions/checkout
52+
uses: actions/checkout@v2
53+
- name: Setup Python Environment
54+
# see https://github.com/actions/setup-python
55+
uses: actions/setup-python@v2
56+
with:
57+
python-version: 3.9
58+
architecture: 'x64'
59+
- name: Install poetry
60+
# see https://github.com/marketplace/actions/setup-poetry
61+
uses: Gr1N/setup-poetry@v7
62+
with:
63+
poetry-version: 1.1.8
64+
- uses: actions/cache@v2
65+
with:
66+
path: ~/.cache/pypoetry/virtualenvs
67+
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
68+
- name: Install dependencies
69+
run: poetry install
70+
- name: Run tox
71+
run: poetry run tox -e mypy
72+
4673
build-and-test:
4774
name: Build & Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
4875
runs-on: ${{ matrix.os }}
@@ -90,7 +117,7 @@ jobs:
90117
- name: Ensure build successful
91118
run: poetry build
92119
- name: Run tox
93-
run: poetry run tox -e py${{ matrix.python-version }}
120+
run: poetry run tox -e py -s false
94121
- name: Generate coverage reports
95122
run: >
96123
poetry run coverage report &&

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ test-reports
1515

1616
# Exclude Python Virtual Environment
1717
venv/*
18+
.venv/*
1819

1920
# Exlude IDE related files
2021
.idea/*
2122
.vscode/*
2223

2324
# pdoc3 HTML output
24-
html/
25+
html/
26+
27+
# mypy caches
28+
/.mypy_cache

.mypy.ini

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[mypy]
2+
3+
files = cyclonedx/
4+
5+
show_error_codes = True
6+
pretty = True
7+
8+
warn_unreachable = True
9+
allow_redefinition = False
10+
11+
# ignore_missing_imports = False
12+
# follow_imports = normal
13+
# follow_imports_for_stubs = True
14+
15+
### Strict mode ###
16+
warn_unused_configs = True
17+
disallow_subclassing_any = True
18+
disallow_any_generics = True
19+
disallow_untyped_calls = True
20+
disallow_untyped_defs = True
21+
disallow_incomplete_defs = True
22+
check_untyped_defs = True
23+
disallow_untyped_decorators = True
24+
no_implicit_optional = True
25+
warn_redundant_casts = True
26+
warn_unused_ignores = True
27+
warn_return_any = True
28+
no_implicit_reexport = True
29+
30+
[mypy-pytest.*]
31+
ignore_missing_imports = True
32+
33+
[mypy-tests.*]
34+
disallow_untyped_decorators = False

cyclonedx/exception/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# encoding: utf-8
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
#
17+
18+
"""
19+
Exceptions that are specific to the CycloneDX library implementation.
20+
"""
21+
22+
23+
class CycloneDxException(Exception):
24+
pass

cyclonedx/exception/parser.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# encoding: utf-8
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
#
17+
18+
"""
19+
Exceptions that are specific error scenarios during occuring within Parsers in the CycloneDX library implementation.
20+
"""
21+
22+
from . import CycloneDxException
23+
24+
25+
class UnknownHashTypeException(CycloneDxException):
26+
"""
27+
Exception raised when we are unable to determine the type of hash from a composite hash string.
28+
"""
29+
pass

cyclonedx/model/__init__.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from enum import Enum
2020
from typing import List, Union
2121

22+
from ..exception.parser import UnknownHashTypeException
23+
2224
"""
2325
Uniform set of models to represent objects within a CycloneDX software bill-of-materials.
2426
@@ -69,14 +71,14 @@ class HashAlgorithm(Enum):
6971

7072
class HashType:
7173
"""
72-
This is out internal representation of the hashType complex type within the CycloneDX standard.
74+
This is our internal representation of the hashType complex type within the CycloneDX standard.
7375
7476
.. note::
7577
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.3/#type_hashType
7678
"""
7779

7880
@staticmethod
79-
def from_composite_str(composite_hash: str):
81+
def from_composite_str(composite_hash: str) -> 'HashType':
8082
"""
8183
Attempts to convert a string which includes both the Hash Algorithm and Hash Value and represent using our
8284
internal model classes.
@@ -86,26 +88,34 @@ def from_composite_str(composite_hash: str):
8688
Composite Hash string of the format `HASH_ALGORITHM`:`HASH_VALUE`.
8789
Example: `sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b`.
8890
91+
Raises:
92+
`UnknownHashTypeException` if the type of hash cannot be determined.
93+
8994
Returns:
90-
An instance of `HashType` when possible, else `None`.
95+
An instance of `HashType`.
9196
"""
92-
algorithm = None
9397
parts = composite_hash.split(':')
9498

9599
algorithm_prefix = parts[0].lower()
96100
if algorithm_prefix == 'md5':
97-
algorithm = HashAlgorithm.MD5
101+
return HashType(
102+
algorithm=HashAlgorithm.MD5,
103+
hash_value=parts[1].lower()
104+
)
98105
elif algorithm_prefix[0:3] == 'sha':
99-
algorithm = getattr(HashAlgorithm, 'SHA_{}'.format(algorithm_prefix[3:]))
106+
return HashType(
107+
algorithm=getattr(HashAlgorithm, 'SHA_{}'.format(algorithm_prefix[3:])),
108+
hash_value=parts[1].lower()
109+
)
100110
elif algorithm_prefix[0:6] == 'blake2':
101-
algorithm = getattr(HashAlgorithm, 'BLAKE2b_{}'.format(algorithm_prefix[6:]))
111+
return HashType(
112+
algorithm=getattr(HashAlgorithm, 'BLAKE2b_{}'.format(algorithm_prefix[6:])),
113+
hash_value=parts[1].lower()
114+
)
102115

103-
return HashType(
104-
algorithm=algorithm,
105-
hash_value=parts[1].lower()
106-
)
116+
raise UnknownHashTypeException(f"Unable to determine hash type from '{composite_hash}'")
107117

108-
def __init__(self, algorithm: HashAlgorithm, hash_value: str):
118+
def __init__(self, algorithm: HashAlgorithm, hash_value: str) -> None:
109119
self._algorithm = algorithm
110120
self._value = hash_value
111121

@@ -115,7 +125,7 @@ def get_algorithm(self) -> HashAlgorithm:
115125
def get_hash_value(self) -> str:
116126
return self._value
117127

118-
def __repr__(self):
128+
def __repr__(self) -> str:
119129
return f'<Hash {self._algorithm.value}:{self._value}>'
120130

121131

@@ -153,14 +163,14 @@ class ExternalReference:
153163
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.3/#type_externalReference
154164
"""
155165

156-
def __init__(self, reference_type: ExternalReferenceType, url: str, comment: str = None,
157-
hashes: List[HashType] = None):
166+
def __init__(self, reference_type: ExternalReferenceType, url: str, comment: str = '',
167+
hashes: Union[List[HashType], None] = None) -> None:
158168
self._reference_type: ExternalReferenceType = reference_type
159169
self._url = url
160170
self._comment = comment
161171
self._hashes: List[HashType] = hashes if hashes else []
162172

163-
def add_hash(self, our_hash: HashType):
173+
def add_hash(self, our_hash: HashType) -> None:
164174
"""
165175
Adds a hash that pins/identifies this External Reference.
166176
@@ -206,5 +216,5 @@ def get_url(self) -> str:
206216
"""
207217
return self._url
208218

209-
def __repr__(self):
219+
def __repr__(self) -> str:
210220
return f'<ExternalReference {self._reference_type.name}, {self._url}> {self._hashes}'

cyclonedx/model/bom.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import datetime
2121
import sys
22-
from typing import List, Union
22+
from typing import List, Optional
2323
from uuid import uuid4
2424

2525
from . import HashType
@@ -37,11 +37,11 @@ class Tool:
3737
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType
3838
"""
3939

40-
def __init__(self, vendor: str, name: str, version: str, hashes: List[HashType] = []):
40+
def __init__(self, vendor: str, name: str, version: str, hashes: Optional[List[HashType]] = None) -> None:
4141
self._vendor = vendor
4242
self._name = name
4343
self._version = version
44-
self._hashes: List[HashType] = hashes
44+
self._hashes: List[HashType] = hashes or []
4545

4646
def get_hashes(self) -> List[HashType]:
4747
"""
@@ -79,14 +79,14 @@ def get_version(self) -> str:
7979
"""
8080
return self._version
8181

82-
def __repr__(self):
82+
def __repr__(self) -> str:
8383
return '<Tool {}:{}:{}>'.format(self._vendor, self._name, self._version)
8484

8585

8686
if sys.version_info >= (3, 8, 0):
8787
from importlib.metadata import version as meta_version
8888
else:
89-
from importlib_metadata import version as meta_version
89+
from importlib_metadata import version as meta_version # type: ignore
9090

9191
try:
9292
ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version=meta_version('cyclonedx-python-lib'))
@@ -102,13 +102,13 @@ class BomMetaData:
102102
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.3/#type_metadata
103103
"""
104104

105-
def __init__(self, tools: List[Tool] = []):
105+
def __init__(self, tools: Optional[List[Tool]] = None) -> None:
106106
self._timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
107-
self._tools: List[Tool] = tools
108-
if len(tools) == 0:
109-
tools.append(ThisTool)
107+
self._tools: List[Tool] = tools if tools else []
108+
if len(self._tools) < 1:
109+
self._tools.append(ThisTool)
110110

111-
def add_tool(self, tool: Tool):
111+
def add_tool(self, tool: Tool) -> None:
112112
"""
113113
Add a Tool definition to this Bom Metadata. The `cyclonedx-python-lib` is automatically added - you do not need
114114
to add this yourself.
@@ -150,7 +150,7 @@ class Bom:
150150
"""
151151

152152
@staticmethod
153-
def from_parser(parser: BaseParser):
153+
def from_parser(parser: BaseParser) -> 'Bom':
154154
"""
155155
Create a Bom instance from a Parser object.
156156
@@ -164,18 +164,18 @@ def from_parser(parser: BaseParser):
164164
bom.add_components(parser.get_components())
165165
return bom
166166

167-
def __init__(self):
167+
def __init__(self) -> None:
168168
"""
169169
Create a new Bom that you can manually/programmatically add data to later.
170170
171171
Returns:
172172
New, empty `cyclonedx.model.bom.Bom` instance.
173173
"""
174174
self._uuid = uuid4()
175-
self._metadata: BomMetaData = BomMetaData(tools=[])
175+
self._metadata: BomMetaData = BomMetaData()
176176
self._components: List[Component] = []
177177

178-
def add_component(self, component: Component):
178+
def add_component(self, component: Component) -> None:
179179
"""
180180
Add a Component to this Bom instance.
181181
@@ -189,7 +189,7 @@ def add_component(self, component: Component):
189189
if not self.has_component(component=component):
190190
self._components.append(component)
191191

192-
def add_components(self, components: List[Component]):
192+
def add_components(self, components: List[Component]) -> None:
193193
"""
194194
Add multiple Components at once to this Bom instance.
195195
@@ -211,7 +211,7 @@ def component_count(self) -> int:
211211
"""
212212
return len(self._components)
213213

214-
def get_component_by_purl(self, purl: str) -> Union[Component, None]:
214+
def get_component_by_purl(self, purl: str) -> Optional[Component]:
215215
"""
216216
Get a Component already in the Bom by it's PURL
217217

0 commit comments

Comments
 (0)