Skip to content

Commit

Permalink
feat: Typing & PEP 561
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
jkowalleck and madpah authored Nov 10, 2021
1 parent f34e2c2 commit 9144765
Show file tree
Hide file tree
Showing 40 changed files with 652 additions and 281 deletions.
29 changes: 28 additions & 1 deletion .github/workflows/poetry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@ jobs:
- name: Run tox
run: poetry run tox -e flake8

static-code-analysis:
name: Static Coding Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
# see https://github.com/actions/checkout
uses: actions/checkout@v2
- name: Setup Python Environment
# see https://github.com/actions/setup-python
uses: actions/setup-python@v2
with:
python-version: 3.9
architecture: 'x64'
- name: Install poetry
# see https://github.com/marketplace/actions/setup-poetry
uses: Gr1N/setup-poetry@v7
with:
poetry-version: 1.1.8
- uses: actions/cache@v2
with:
path: ~/.cache/pypoetry/virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}
- name: Install dependencies
run: poetry install
- name: Run tox
run: poetry run tox -e mypy

build-and-test:
name: Build & Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -90,7 +117,7 @@ jobs:
- name: Ensure build successful
run: poetry build
- name: Run tox
run: poetry run tox -e py${{ matrix.python-version }}
run: poetry run tox -e py -s false
- name: Generate coverage reports
run: >
poetry run coverage report &&
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ test-reports

# Exclude Python Virtual Environment
venv/*
.venv/*

# Exlude IDE related files
.idea/*
.vscode/*

# pdoc3 HTML output
html/
html/

# mypy caches
/.mypy_cache
34 changes: 34 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[mypy]

files = cyclonedx/

show_error_codes = True
pretty = True

warn_unreachable = True
allow_redefinition = False

# ignore_missing_imports = False
# follow_imports = normal
# follow_imports_for_stubs = True

### Strict mode ###
warn_unused_configs = True
disallow_subclassing_any = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
no_implicit_reexport = True

[mypy-pytest.*]
ignore_missing_imports = True

[mypy-tests.*]
disallow_untyped_decorators = False
24 changes: 24 additions & 0 deletions cyclonedx/exception/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

"""
Exceptions that are specific to the CycloneDX library implementation.
"""


class CycloneDxException(Exception):
pass
29 changes: 29 additions & 0 deletions cyclonedx/exception/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# encoding: utf-8

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

"""
Exceptions that are specific error scenarios during occuring within Parsers in the CycloneDX library implementation.
"""

from . import CycloneDxException


class UnknownHashTypeException(CycloneDxException):
"""
Exception raised when we are unable to determine the type of hash from a composite hash string.
"""
pass
44 changes: 27 additions & 17 deletions cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from enum import Enum
from typing import List, Union

from ..exception.parser import UnknownHashTypeException

"""
Uniform set of models to represent objects within a CycloneDX software bill-of-materials.
Expand Down Expand Up @@ -69,14 +71,14 @@ class HashAlgorithm(Enum):

class HashType:
"""
This is out internal representation of the hashType complex type within the CycloneDX standard.
This is our internal representation of the hashType complex type within the CycloneDX standard.
.. note::
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.3/#type_hashType
"""

@staticmethod
def from_composite_str(composite_hash: str):
def from_composite_str(composite_hash: str) -> 'HashType':
"""
Attempts to convert a string which includes both the Hash Algorithm and Hash Value and represent using our
internal model classes.
Expand All @@ -86,26 +88,34 @@ def from_composite_str(composite_hash: str):
Composite Hash string of the format `HASH_ALGORITHM`:`HASH_VALUE`.
Example: `sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b`.
Raises:
`UnknownHashTypeException` if the type of hash cannot be determined.
Returns:
An instance of `HashType` when possible, else `None`.
An instance of `HashType`.
"""
algorithm = None
parts = composite_hash.split(':')

algorithm_prefix = parts[0].lower()
if algorithm_prefix == 'md5':
algorithm = HashAlgorithm.MD5
return HashType(
algorithm=HashAlgorithm.MD5,
hash_value=parts[1].lower()
)
elif algorithm_prefix[0:3] == 'sha':
algorithm = getattr(HashAlgorithm, 'SHA_{}'.format(algorithm_prefix[3:]))
return HashType(
algorithm=getattr(HashAlgorithm, 'SHA_{}'.format(algorithm_prefix[3:])),
hash_value=parts[1].lower()
)
elif algorithm_prefix[0:6] == 'blake2':
algorithm = getattr(HashAlgorithm, 'BLAKE2b_{}'.format(algorithm_prefix[6:]))
return HashType(
algorithm=getattr(HashAlgorithm, 'BLAKE2b_{}'.format(algorithm_prefix[6:])),
hash_value=parts[1].lower()
)

return HashType(
algorithm=algorithm,
hash_value=parts[1].lower()
)
raise UnknownHashTypeException(f"Unable to determine hash type from '{composite_hash}'")

def __init__(self, algorithm: HashAlgorithm, hash_value: str):
def __init__(self, algorithm: HashAlgorithm, hash_value: str) -> None:
self._algorithm = algorithm
self._value = hash_value

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

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


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

def __init__(self, reference_type: ExternalReferenceType, url: str, comment: str = None,
hashes: List[HashType] = None):
def __init__(self, reference_type: ExternalReferenceType, url: str, comment: str = '',
hashes: Union[List[HashType], None] = None) -> None:
self._reference_type: ExternalReferenceType = reference_type
self._url = url
self._comment = comment
self._hashes: List[HashType] = hashes if hashes else []

def add_hash(self, our_hash: HashType):
def add_hash(self, our_hash: HashType) -> None:
"""
Adds a hash that pins/identifies this External Reference.
Expand Down Expand Up @@ -206,5 +216,5 @@ def get_url(self) -> str:
"""
return self._url

def __repr__(self):
def __repr__(self) -> str:
return f'<ExternalReference {self._reference_type.name}, {self._url}> {self._hashes}'
32 changes: 16 additions & 16 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import datetime
import sys
from typing import List, Union
from typing import List, Optional
from uuid import uuid4

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

def __init__(self, vendor: str, name: str, version: str, hashes: List[HashType] = []):
def __init__(self, vendor: str, name: str, version: str, hashes: Optional[List[HashType]] = None) -> None:
self._vendor = vendor
self._name = name
self._version = version
self._hashes: List[HashType] = hashes
self._hashes: List[HashType] = hashes or []

def get_hashes(self) -> List[HashType]:
"""
Expand Down Expand Up @@ -79,14 +79,14 @@ def get_version(self) -> str:
"""
return self._version

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


if sys.version_info >= (3, 8, 0):
from importlib.metadata import version as meta_version
else:
from importlib_metadata import version as meta_version
from importlib_metadata import version as meta_version # type: ignore

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

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

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

@staticmethod
def from_parser(parser: BaseParser):
def from_parser(parser: BaseParser) -> 'Bom':
"""
Create a Bom instance from a Parser object.
Expand All @@ -164,18 +164,18 @@ def from_parser(parser: BaseParser):
bom.add_components(parser.get_components())
return bom

def __init__(self):
def __init__(self) -> None:
"""
Create a new Bom that you can manually/programmatically add data to later.
Returns:
New, empty `cyclonedx.model.bom.Bom` instance.
"""
self._uuid = uuid4()
self._metadata: BomMetaData = BomMetaData(tools=[])
self._metadata: BomMetaData = BomMetaData()
self._components: List[Component] = []

def add_component(self, component: Component):
def add_component(self, component: Component) -> None:
"""
Add a Component to this Bom instance.
Expand All @@ -189,7 +189,7 @@ def add_component(self, component: Component):
if not self.has_component(component=component):
self._components.append(component)

def add_components(self, components: List[Component]):
def add_components(self, components: List[Component]) -> None:
"""
Add multiple Components at once to this Bom instance.
Expand All @@ -211,7 +211,7 @@ def component_count(self) -> int:
"""
return len(self._components)

def get_component_by_purl(self, purl: str) -> Union[Component, None]:
def get_component_by_purl(self, purl: str) -> Optional[Component]:
"""
Get a Component already in the Bom by it's PURL
Expand Down
Loading

0 comments on commit 9144765

Please sign in to comment.