diff --git a/.github/workflows/poetry.yml b/.github/workflows/poetry.yml index 7271d34e..02e644cd 100644 --- a/.github/workflows/poetry.yml +++ b/.github/workflows/poetry.yml @@ -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 }} @@ -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 && diff --git a/.gitignore b/.gitignore index ad58cdad..3fc0f2f5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,14 @@ test-reports # Exclude Python Virtual Environment venv/* +.venv/* # Exlude IDE related files .idea/* .vscode/* # pdoc3 HTML output -html/ \ No newline at end of file +html/ + +# mypy caches +/.mypy_cache diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 00000000..4dfe4960 --- /dev/null +++ b/.mypy.ini @@ -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 diff --git a/cyclonedx/exception/__init__.py b/cyclonedx/exception/__init__.py new file mode 100644 index 00000000..2329d1d1 --- /dev/null +++ b/cyclonedx/exception/__init__.py @@ -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 diff --git a/cyclonedx/exception/parser.py b/cyclonedx/exception/parser.py new file mode 100644 index 00000000..9072ba8e --- /dev/null +++ b/cyclonedx/exception/parser.py @@ -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 diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 8a0c019e..824f921a 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -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. @@ -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. @@ -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 @@ -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'' @@ -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. @@ -206,5 +216,5 @@ def get_url(self) -> str: """ return self._url - def __repr__(self): + def __repr__(self) -> str: return f' {self._hashes}' diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index b4919b87..9a3d5e05 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -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 @@ -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]: """ @@ -79,14 +79,14 @@ def get_version(self) -> str: """ return self._version - def __repr__(self): + def __repr__(self) -> str: return ''.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')) @@ -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. @@ -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. @@ -164,7 +164,7 @@ 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. @@ -172,10 +172,10 @@ def __init__(self): 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. @@ -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. @@ -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 diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 27e9dc28..b7f7376a 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -19,8 +19,10 @@ from enum import Enum from os.path import exists -from packageurl import PackageURL -from typing import List +from typing import List, Optional + +# See https://github.com/package-url/packageurl-python/issues/65 +from packageurl import PackageURL # type: ignore from . import ExternalReference, HashAlgorithm, HashType, sha1sum from .vulnerability import Vulnerability @@ -52,7 +54,7 @@ class Component: """ @staticmethod - def for_file(absolute_file_path: str, path_for_bom: str = None): + def for_file(absolute_file_path: str, path_for_bom: Optional[str]) -> 'Component': """ Helper method to create a Component that represents the provided local file as a Component. @@ -69,7 +71,6 @@ def for_file(absolute_file_path: str, path_for_bom: str = None): raise FileExistsError('Supplied file path \'{}\' does not exist'.format(absolute_file_path)) sha1_hash: str = sha1sum(filename=absolute_file_path) - return Component( name=path_for_bom if path_for_bom else absolute_file_path, version='0.0.0-{}'.format(sha1_hash[0:12]), @@ -80,26 +81,27 @@ def for_file(absolute_file_path: str, path_for_bom: str = None): package_url_type='generic' ) - def __init__(self, name: str, version: str, namespace: str = None, qualifiers: str = None, subpath: str = None, - hashes: List[HashType] = None, author: str = None, description: str = None, license_str: str = None, - component_type: ComponentType = ComponentType.LIBRARY, package_url_type: str = 'pypi'): - self._package_url_type = package_url_type - self._namespace = namespace - self._name = name - self._version = version - self._type = component_type - self._qualifiers = qualifiers - self._subpath = subpath - - self._author: str = author - self._description: str = description - self._license: str = license_str + def __init__(self, name: str, version: str, namespace: Optional[str] = None, qualifiers: Optional[str] = None, + subpath: Optional[str] = None, hashes: Optional[List[HashType]] = None, author: Optional[str] = None, + description: Optional[str] = None, license_str: Optional[str] = None, + component_type: ComponentType = ComponentType.LIBRARY, package_url_type: str = 'pypi') -> None: + self._package_url_type: str = package_url_type + self._namespace: Optional[str] = namespace + self._name: str = name + self._version: str = version + self._type: ComponentType = component_type + self._qualifiers: Optional[str] = qualifiers + self._subpath: Optional[str] = subpath + + self._author: Optional[str] = author + self._description: Optional[str] = description + self._license: Optional[str] = license_str self._hashes: List[HashType] = hashes if hashes else [] self._vulnerabilites: List[Vulnerability] = [] self._external_references: List[ExternalReference] = [] - def add_external_reference(self, reference: ExternalReference): + def add_external_reference(self, reference: ExternalReference) -> None: """ Add an `ExternalReference` to this `Component`. @@ -109,7 +111,7 @@ def add_external_reference(self, reference: ExternalReference): """ self._external_references.append(reference) - def add_hash(self, a_hash: HashType): + def add_hash(self, a_hash: HashType) -> None: """ Adds a hash that pins/identifies this Component. @@ -119,7 +121,7 @@ def add_hash(self, a_hash: HashType): """ self._hashes.append(a_hash) - def add_vulnerability(self, vulnerability: Vulnerability): + def add_vulnerability(self, vulnerability: Vulnerability) -> None: """ Add a Vulnerability to this Component. @@ -132,21 +134,21 @@ def add_vulnerability(self, vulnerability: Vulnerability): """ self._vulnerabilites.append(vulnerability) - def get_author(self) -> str: + def get_author(self) -> Optional[str]: """ Get the author of this Component. Returns: - Declared author of this Component as `str`. + Declared author of this Component as `str` if set, else `None`. """ return self._author - def get_description(self) -> str: + def get_description(self) -> Optional[str]: """ Get the description of this Component. Returns: - Declared description of this Component as `str`. + Declared description of this Component as `str` if set, else `None`. """ return self._description @@ -168,12 +170,12 @@ def get_hashes(self) -> List[HashType]: """ return self._hashes - def get_license(self) -> str: + def get_license(self) -> Optional[str]: """ Get the license of this Component. Returns: - Declared license of this Component as `str`. + Declared license of this Component as `str` if set, else `None`. """ return self._license @@ -186,7 +188,7 @@ def get_name(self) -> str: """ return self._name - def get_namespace(self) -> str: + def get_namespace(self) -> Optional[str]: """ Get the namespace of this Component. @@ -202,12 +204,12 @@ def get_purl(self) -> str: Returns: PackageURL or 'PURL' that reflects this Component as `str`. """ - return self.to_package_url().to_string() + return str(self.to_package_url().to_string()) def get_pypi_url(self) -> str: return f'https://pypi.org/project/{self.get_name()}/{self.get_version()}' - def get_subpath(self) -> str: + def get_subpath(self) -> Optional[str]: """ Get the subpath of this Component. @@ -250,9 +252,9 @@ def has_vulnerabilities(self) -> bool: Returns: `True` if this Component has 1 or more vulnerabilities, `False` otherwise. """ - return len(self._vulnerabilites) != 0 + return bool(self.get_vulnerabilities()) - def set_author(self, author: str): + def set_author(self, author: str) -> None: """ Set the author of this Component. @@ -265,7 +267,7 @@ def set_author(self, author: str): """ self._author = author - def set_description(self, description: str): + def set_description(self, description: str) -> None: """ Set the description of this Component. @@ -278,7 +280,7 @@ def set_description(self, description: str): """ self._description = description - def set_license(self, license_str: str): + def set_license(self, license_str: str) -> None: """ Set the license for this Component. @@ -307,8 +309,11 @@ def to_package_url(self) -> PackageURL: subpath=self._subpath ) - def __eq__(self, other): - return other.get_purl() == self.get_purl() + def __eq__(self, other: object) -> bool: + if isinstance(other, Component): + return other.get_purl() == self.get_purl() + else: + raise NotImplementedError - def __repr__(self): - return ''.format(self._name, self._version) + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index 4a304291..64f5670a 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -19,7 +19,7 @@ import re from enum import Enum -from typing import List, Union +from typing import List, Optional, Tuple, Union from urllib.parse import ParseResult, urlparse """ @@ -45,7 +45,7 @@ class VulnerabilitySourceType(Enum): OTHER = 'Other' @staticmethod - def get_from_vector(vector: str): + def get_from_vector(vector: str) -> 'VulnerabilitySourceType': """ Attempt to derive the correct SourceType from an attack vector. @@ -103,7 +103,7 @@ class VulnerabilitySeverity(Enum): UNKNOWN = 'Unknown' @staticmethod - def get_from_cvss_scores(scores: tuple = None): + def get_from_cvss_scores(scores: Union[Tuple[float], float, None]) -> 'VulnerabilitySeverity': """ Derives the Severity of a Vulnerability from it's declared CVSS scores. @@ -119,7 +119,11 @@ def get_from_cvss_scores(scores: tuple = None): if scores is None: return VulnerabilitySeverity.UNKNOWN - max_cvss_score: float = max(scores) + max_cvss_score: float + if isinstance(scores, tuple): + max_cvss_score = max(scores) + else: + max_cvss_score = float(scores) if max_cvss_score >= 9.0: return VulnerabilitySeverity.CRITICAL @@ -141,20 +145,20 @@ class VulnerabilityRating: See `scoreType` in https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd """ - def __init__(self, score_base: float = None, score_impact: float = None, score_exploitability=None, - severity: VulnerabilitySeverity = None, method: VulnerabilitySourceType = None, - vector: str = None): + def __init__(self, score_base: Optional[float] = None, score_impact: Optional[float] = None, + score_exploitability: Optional[float] = None, severity: Optional[VulnerabilitySeverity] = None, + method: Optional[VulnerabilitySourceType] = None, vector: Optional[str] = None) -> None: self._score_base = score_base self._score_impact = score_impact self._score_exploitability = score_exploitability self._severity = severity self._method = method - if self._method: + if self._method and vector: self._vector = self._method.get_localised_vector(vector=vector) else: - self._vector = vector + self._vector = str(vector) - def get_base_score(self) -> float: + def get_base_score(self) -> Optional[float]: """ Get the base score of this VulnerabilityRating. @@ -163,7 +167,7 @@ def get_base_score(self) -> float: """ return self._score_base - def get_impact_score(self) -> float: + def get_impact_score(self) -> Optional[float]: """ Get the impact score of this VulnerabilityRating. @@ -172,7 +176,7 @@ def get_impact_score(self) -> float: """ return self._score_impact - def get_exploitability_score(self) -> float: + def get_exploitability_score(self) -> Optional[float]: """ Get the exploitability score of this VulnerabilityRating. @@ -181,7 +185,7 @@ def get_exploitability_score(self) -> float: """ return self._score_exploitability - def get_severity(self) -> Union[VulnerabilitySeverity, None]: + def get_severity(self) -> Optional[VulnerabilitySeverity]: """ Get the severity score of this VulnerabilityRating. @@ -190,7 +194,7 @@ def get_severity(self) -> Union[VulnerabilitySeverity, None]: """ return self._severity - def get_method(self) -> Union[VulnerabilitySourceType, None]: + def get_method(self) -> Optional[VulnerabilitySourceType]: """ Get the source method of this VulnerabilitySourceType. @@ -199,7 +203,7 @@ def get_method(self) -> Union[VulnerabilitySourceType, None]: """ return self._method - def get_vector(self) -> Union[str, None]: + def get_vector(self) -> Optional[str]: return self._vector def has_score(self) -> bool: @@ -211,50 +215,58 @@ class Vulnerability: Represents """ - def __init__(self, id: str, source_name: str = None, source_url: str = None, - ratings: List[VulnerabilityRating] = None, cwes: List[int] = None, description: str = None, - recommendations: List[str] = None, advisories: List[str] = None): + def __init__(self, id: str, source_name: Optional[str], source_url: Optional[str], + ratings: Optional[List[VulnerabilityRating]], cwes: Optional[List[int]], description: Optional[str], + recommendations: Optional[List[str]], advisories: Optional[List[str]]) -> None: self._id = id self._source_name = source_name - self._source_url: ParseResult = urlparse(source_url) if source_url else None - self._ratings: List[VulnerabilityRating] = ratings - self._cwes: List[int] = cwes + self._source_url: Optional[ParseResult] = urlparse(source_url) if source_url else None + self._ratings: List[VulnerabilityRating] = ratings if ratings else [] + self._cwes: List[int] = cwes if cwes else [] self._description = description - self._recommendations: List[str] = recommendations - self._advisories: List[str] = advisories + self._recommendations: List[str] = recommendations if recommendations else [] + self._advisories: List[str] = advisories if advisories else [] def get_id(self) -> str: return self._id - def get_source_name(self) -> Union[str, None]: + def get_source_name(self) -> Optional[str]: return self._source_name - def get_source_url(self) -> Union[ParseResult, None]: + def get_source_url(self) -> Optional[ParseResult]: return self._source_url def get_ratings(self) -> List[VulnerabilityRating]: + if not self.has_ratings(): + return list() return self._ratings def get_cwes(self) -> List[int]: + if not self.has_cwes(): + return list() return self._cwes - def get_description(self) -> Union[str, None]: + def get_description(self) -> Optional[str]: return self._description - def get_recommendations(self) -> Union[List[str], None]: + def get_recommendations(self) -> List[str]: + if not self.has_recommendations(): + return list() return self._recommendations - def get_advisories(self) -> Union[List[str], None]: + def get_advisories(self) -> List[str]: + if not self.has_advisories(): + return list() return self._advisories def has_ratings(self) -> bool: - return len(self.get_ratings()) > 0 + return bool(self._ratings) def has_cwes(self) -> bool: - return len(self._cwes) > 0 + return bool(self._cwes) def has_recommendations(self) -> bool: - return len(self._recommendations) > 0 + return bool(self._recommendations) def has_advisories(self) -> bool: - return len(self._advisories) > 0 + return bool(self._advisories) diff --git a/cyclonedx/output/__init__.py b/cyclonedx/output/__init__.py index e369c1b3..b0c08dc6 100644 --- a/cyclonedx/output/__init__.py +++ b/cyclonedx/output/__init__.py @@ -23,6 +23,7 @@ import os from abc import ABC, abstractmethod from enum import Enum +from typing import cast, Optional from ..model.bom import Bom @@ -45,20 +46,20 @@ class SchemaVersion(Enum): class BaseOutput(ABC): _bom: Bom - def __init__(self, bom: Bom = None): + def __init__(self, bom: Bom) -> None: self._bom = bom def get_bom(self) -> Bom: return self._bom - def set_bom(self, bom: Bom): + def set_bom(self, bom: Bom) -> None: self._bom = bom @abstractmethod def output_as_string(self) -> str: pass - def output_to_file(self, filename: str, allow_overwrite: bool = False): + def output_to_file(self, filename: str, allow_overwrite: bool = False) -> None: # Check directory writable output_filename = os.path.realpath(filename) output_directory = os.path.dirname(output_filename) @@ -75,7 +76,7 @@ def output_to_file(self, filename: str, allow_overwrite: bool = False): f_out.close() -def get_instance(bom: Bom = None, output_format: OutputFormat = OutputFormat.XML, +def get_instance(bom: Optional[Bom] = None, output_format: OutputFormat = OutputFormat.XML, schema_version: SchemaVersion = DEFAULT_SCHEMA_VERSION) -> BaseOutput: """ Helper method to quickly get the correct output class/formatter. @@ -93,4 +94,4 @@ def get_instance(bom: Bom = None, output_format: OutputFormat = OutputFormat.XML except (ImportError, AttributeError): raise ValueError(f"Unknown format {output_format.value.lower()!r}") from None - return output_klass(bom=bom) + return cast(BaseOutput, output_klass(bom=bom)) diff --git a/cyclonedx/output/json.py b/cyclonedx/output/json.py index dfb6359d..3fde5b15 100644 --- a/cyclonedx/output/json.py +++ b/cyclonedx/output/json.py @@ -18,6 +18,7 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. import json +from typing import Union from . import BaseOutput from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3 @@ -29,7 +30,7 @@ class Json(BaseOutput, BaseSchemaVersion): def output_as_string(self) -> str: return json.dumps(self._get_json()) - def _get_json(self) -> dict: + def _get_json(self) -> object: components = list(map(self._get_component_as_dict, self.get_bom().get_components())) response = { @@ -45,8 +46,9 @@ def _get_json(self) -> dict: return response - def _get_component_as_dict(self, component: Component) -> dict: - c = { + def _get_component_as_dict(self, component: Component) -> object: + c: dict[str, Union[str, list[dict[str, str]], list[dict[str, dict[str, str]]], list[ + dict[str, Union[str, list[dict[str, str]]]]]]] = { "type": component.get_type().value, "name": component.get_name(), "version": component.get_version(), @@ -54,10 +56,10 @@ def _get_component_as_dict(self, component: Component) -> dict: } if component.get_namespace(): - c['group'] = component.get_namespace() + c['group'] = str(component.get_namespace()) if component.get_hashes(): - hashes = [] + hashes: list[dict[str, str]] = [] for component_hash in component.get_hashes(): hashes.append({ "alg": component_hash.get_algorithm().value, @@ -66,30 +68,31 @@ def _get_component_as_dict(self, component: Component) -> dict: c['hashes'] = hashes if component.get_license(): - c['licenses'] = [ + licenses: list[dict[str, dict[str, str]]] = [ { "license": { - "name": component.get_license() + "name": str(component.get_license()) } } ] + c['licenses'] = licenses if self.component_supports_author() and component.get_author(): - c['author'] = component.get_author() + c['author'] = str(component.get_author()) if self.component_supports_external_references() and component.get_external_references(): - c['externalReferences'] = [] + ext_references: list[dict[str, Union[str, list[dict[str, str]]]]] = [] for ext_ref in component.get_external_references(): - ref = { + ref: dict[str, Union[str, list[dict[str, str]]]] = { "type": ext_ref.get_reference_type().value, "url": ext_ref.get_url() } if ext_ref.get_comment(): - ref['comment'] = ext_ref.get_comment() + ref['comment'] = str(ext_ref.get_comment()) if ext_ref.get_hashes(): - ref_hashes = [] + ref_hashes: list[dict[str, str]] = [] for ref_hash in ext_ref.get_hashes(): ref_hashes.append({ "alg": ref_hash.get_algorithm().value, @@ -97,35 +100,36 @@ def _get_component_as_dict(self, component: Component) -> dict: }) ref['hashes'] = ref_hashes - c['externalReferences'].append(ref) + ext_references.append(ref) + c['externalReferences'] = ext_references return c - def _get_metadata_as_dict(self) -> dict: + def _get_metadata_as_dict(self) -> object: bom_metadata = self.get_bom().get_metadata() - metadata = { + metadata: dict[str, Union[str, list[dict[str, Union[str, list[dict[str, str]]]]]]] = { "timestamp": bom_metadata.get_timestamp().isoformat() } - if self.bom_metadata_supports_tools() and len(bom_metadata.get_tools()) > 0: - metadata['tools'] = [] + if self.bom_metadata_supports_tools(): + tools: list[dict[str, Union[str, list[dict[str, str]]]]] = [] for tool in bom_metadata.get_tools(): - tool_dict = { + tool_dict: dict[str, Union[str, list[dict[str, str]]]] = { "vendor": tool.get_vendor(), "name": tool.get_name(), "version": tool.get_version() } if len(tool.get_hashes()) > 0: - hashes = [] + hashes: list[dict[str, str]] = [] for tool_hash in tool.get_hashes(): hashes.append({ "alg": tool_hash.get_algorithm().value, "content": tool_hash.get_hash_value() }) tool_dict['hashes'] = hashes - - metadata['tools'].append(tool_dict) + tools.append(tool_dict) + metadata['tools'] = tools return metadata diff --git a/cyclonedx/output/xml.py b/cyclonedx/output/xml.py index eb811cb0..1acb0f6f 100644 --- a/cyclonedx/output/xml.py +++ b/cyclonedx/output/xml.py @@ -17,14 +17,15 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -from typing import List +from typing import cast, List +from urllib.parse import ParseResult from xml.etree import ElementTree from . import BaseOutput from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3 from ..model import HashType from ..model.component import Component -from ..model.vulnerability import Vulnerability, VulnerabilityRating +from ..model.vulnerability import Vulnerability, VulnerabilityRating, VulnerabilitySeverity, VulnerabilitySourceType class Xml(BaseOutput, BaseSchemaVersion): @@ -137,10 +138,12 @@ def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability # source if vulnerability.get_source_name(): source_element = ElementTree.SubElement( - vulnerability_element, 'v:source', attrib={'name': vulnerability.get_source_name()} + vulnerability_element, 'v:source', attrib={'name': str(vulnerability.get_source_name())} ) if vulnerability.get_source_url(): - ElementTree.SubElement(source_element, 'v:url').text = vulnerability.get_source_url().geturl() + ElementTree.SubElement(source_element, 'v:url').text = str( + cast(ParseResult, vulnerability.get_source_url()).geturl() + ) # ratings if vulnerability.has_ratings(): @@ -162,11 +165,15 @@ def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability # rating.severity if rating.get_severity(): - ElementTree.SubElement(rating_element, 'v:severity').text = rating.get_severity().value + ElementTree.SubElement(rating_element, 'v:severity').text = cast( + VulnerabilitySeverity, rating.get_severity() + ).value # rating.severity if rating.get_method(): - ElementTree.SubElement(rating_element, 'v:method').text = rating.get_method().value + ElementTree.SubElement(rating_element, 'v:method').text = cast( + VulnerabilitySourceType, rating.get_method() + ).value # rating.vector if rating.get_vector(): @@ -211,14 +218,14 @@ def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element: ElementTree.SubElement(tool_e, 'version').text = tool.get_version() if len(tool.get_hashes()) > 0: hashes_e = ElementTree.SubElement(tool_e, 'hashes') - for hash in tool.get_hashes(): + for hash_ in tool.get_hashes(): ElementTree.SubElement(hashes_e, 'hash', - {'alg': hash.get_algorithm().value}).text = hash.get_hash_value() + {'alg': hash_.get_algorithm().value}).text = hash_.get_hash_value() return bom @staticmethod - def _add_hashes_to_element(hashes: List[HashType], element: ElementTree.Element): + def _add_hashes_to_element(hashes: List[HashType], element: ElementTree.Element) -> None: hashes_e = ElementTree.SubElement(element, 'hashes') for h in hashes: ElementTree.SubElement( diff --git a/cyclonedx/parser/__init__.py b/cyclonedx/parser/__init__.py index d5b5f1ea..5f2237d9 100644 --- a/cyclonedx/parser/__init__.py +++ b/cyclonedx/parser/__init__.py @@ -77,7 +77,7 @@ class ParserWarning: _item: str _warning: str - def __init__(self, item: str, warning: str): + def __init__(self, item: str, warning: str) -> None: self._item = item self._warning = warning @@ -87,7 +87,7 @@ def get_item(self) -> str: def get_warning_message(self) -> str: return self._warning - def __repr__(self): + def __repr__(self) -> str: return ''.format(self._item) @@ -95,7 +95,11 @@ class BaseParser(ABC): _components: List[Component] = [] _warnings: List[ParserWarning] = [] - def __init__(self): + def __init__(self) -> None: + """ + + :rtype: object + """ self._components.clear() self._warnings.clear() diff --git a/cyclonedx/parser/conda.py b/cyclonedx/parser/conda.py index b656fbf4..3b0ec748 100644 --- a/cyclonedx/parser/conda.py +++ b/cyclonedx/parser/conda.py @@ -30,7 +30,7 @@ class _BaseCondaParser(BaseParser, metaclass=ABCMeta): """Internal abstract parser - not for programmatic use. """ - def __init__(self, conda_data: str): + def __init__(self, conda_data: str) -> None: super().__init__() self._conda_packages: List[CondaPackage] = [] self._parse_to_conda_packages(data_str=conda_data) @@ -49,7 +49,7 @@ def _parse_to_conda_packages(self, data_str: str) -> None: """ pass - def _conda_packages_to_components(self): + def _conda_packages_to_components(self) -> None: """ Converts the parsed `CondaPackage` instances into `Component` instances. diff --git a/cyclonedx/parser/environment.py b/cyclonedx/parser/environment.py index b974c040..2e5860fa 100644 --- a/cyclonedx/parser/environment.py +++ b/cyclonedx/parser/environment.py @@ -29,11 +29,14 @@ """ import sys +from pkg_resources import DistInfoDistribution # type: ignore if sys.version_info >= (3, 8, 0): from importlib.metadata import metadata + import email else: - from importlib_metadata import metadata + from importlib_metadata import metadata # type: ignore + import email from . import BaseParser @@ -47,12 +50,12 @@ class EnvironmentParser(BaseParser): Best used when you have virtual Python environments per project. """ - def __init__(self): + def __init__(self) -> None: super().__init__() import pkg_resources - i: pkg_resources.DistInfoDistribution + i: DistInfoDistribution for i in iter(pkg_resources.working_set): c = Component(name=i.project_name, version=i.version) @@ -71,7 +74,7 @@ def __init__(self): self._components.append(c) @staticmethod - def _get_metadata_for_package(package_name: str): + def _get_metadata_for_package(package_name: str) -> email.message.Message: if sys.version_info >= (3, 8, 0): return metadata(package_name) else: diff --git a/cyclonedx/parser/pipenv.py b/cyclonedx/parser/pipenv.py index 1ecb5abc..83130858 100644 --- a/cyclonedx/parser/pipenv.py +++ b/cyclonedx/parser/pipenv.py @@ -26,7 +26,7 @@ class PipEnvParser(BaseParser): - def __init__(self, pipenv_contents: str): + def __init__(self, pipenv_contents: str) -> None: super().__init__() pipfile_lock_contents = json.loads(pipenv_contents) @@ -53,6 +53,6 @@ def __init__(self, pipenv_contents: str): class PipEnvFileParser(PipEnvParser): - def __init__(self, pipenv_lock_filename: str): + def __init__(self, pipenv_lock_filename: str) -> None: with open(pipenv_lock_filename) as r: super(PipEnvFileParser, self).__init__(pipenv_contents=r.read()) diff --git a/cyclonedx/parser/poetry.py b/cyclonedx/parser/poetry.py index 88fd246f..fc211592 100644 --- a/cyclonedx/parser/poetry.py +++ b/cyclonedx/parser/poetry.py @@ -17,18 +17,19 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -import toml +from toml import loads as load_toml from . import BaseParser +from ..exception.parser import UnknownHashTypeException from ..model import ExternalReference, ExternalReferenceType, HashType from ..model.component import Component class PoetryParser(BaseParser): - def __init__(self, poetry_lock_contents: str): + def __init__(self, poetry_lock_contents: str) -> None: super().__init__() - poetry_lock = toml.loads(poetry_lock_contents) + poetry_lock = load_toml(poetry_lock_contents) for package in poetry_lock['package']: component = Component( @@ -36,21 +37,23 @@ def __init__(self, poetry_lock_contents: str): ) for file_metadata in poetry_lock['metadata']['files'][package['name']]: - component.add_external_reference(ExternalReference( - reference_type=ExternalReferenceType.DISTRIBUTION, - url=component.get_pypi_url(), - comment=f'Distribution file: {file_metadata["file"]}', - hashes=[ - HashType.from_composite_str(file_metadata['hash']) - ] - )) + try: + component.add_external_reference(ExternalReference( + reference_type=ExternalReferenceType.DISTRIBUTION, + url=component.get_pypi_url(), + comment=f'Distribution file: {file_metadata["file"]}', + hashes=[HashType.from_composite_str(file_metadata['hash'])] + )) + except UnknownHashTypeException: + # @todo add logging for this type of exception? + pass self._components.append(component) class PoetryFileParser(PoetryParser): - def __init__(self, poetry_lock_filename: str): + def __init__(self, poetry_lock_filename: str) -> None: with open(poetry_lock_filename) as r: super(PoetryFileParser, self).__init__(poetry_lock_contents=r.read()) r.close() diff --git a/cyclonedx/parser/requirements.py b/cyclonedx/parser/requirements.py index ba9b3c5d..0b6d70b4 100644 --- a/cyclonedx/parser/requirements.py +++ b/cyclonedx/parser/requirements.py @@ -17,7 +17,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -import pkg_resources +from pkg_resources import parse_requirements as parse_requirements from . import BaseParser, ParserWarning from ..model.component import Component @@ -25,10 +25,10 @@ class RequirementsParser(BaseParser): - def __init__(self, requirements_content: str): + def __init__(self, requirements_content: str) -> None: super().__init__() - requirements = pkg_resources.parse_requirements(requirements_content) + requirements = parse_requirements(requirements_content) for requirement in requirements: """ @todo @@ -55,7 +55,7 @@ def __init__(self, requirements_content: str): class RequirementsFileParser(RequirementsParser): - def __init__(self, requirements_file: str): + def __init__(self, requirements_file: str) -> None: with open(requirements_file) as r: super(RequirementsFileParser, self).__init__(requirements_content=r.read()) r.close() diff --git a/cyclonedx/py.typed b/cyclonedx/py.typed new file mode 100644 index 00000000..1fd0ed8a --- /dev/null +++ b/cyclonedx/py.typed @@ -0,0 +1,2 @@ +# Marker file for PEP 561. This package uses inline types. +# This file is needed to allow other packages to type-check their code against this package. diff --git a/cyclonedx/utils/conda.py b/cyclonedx/utils/conda.py index b76b9522..51508139 100644 --- a/cyclonedx/utils/conda.py +++ b/cyclonedx/utils/conda.py @@ -19,7 +19,7 @@ import json import sys from json import JSONDecodeError -from typing import Union +from typing import Optional if sys.version_info >= (3, 8, 0): from typing import TypedDict @@ -41,26 +41,23 @@ class CondaPackage(TypedDict): name: str platform: str version: str - md5_hash: str + md5_hash: Optional[str] -def parse_conda_json_to_conda_package(conda_json_str: str) -> Union[CondaPackage, None]: +def parse_conda_json_to_conda_package(conda_json_str: str) -> Optional[CondaPackage]: try: package_data = json.loads(conda_json_str) - except JSONDecodeError: - print(f'Failed to decode JSON: {conda_json_str}') - raise ValueError(f'Invalid JSON supplied - cannot be parsed: {conda_json_str}') + except JSONDecodeError as e: + raise ValueError(f'Invalid JSON supplied - cannot be parsed: {conda_json_str}') from e - if 'md5_hash' not in package_data.keys(): - package_data['md5_hash'] = None - - if isinstance(package_data, dict): - return CondaPackage(**package_data) + if not isinstance(package_data, dict): + return None - return None + package_data.setdefault('md5_hash', None) + return CondaPackage(package_data) # type: ignore # @FIXME write proper type safe dict at this point -def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Union[CondaPackage, None]: +def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Optional[CondaPackage]: """ Helper method for parsing a line of output from `conda list --explicit` into our internal `CondaPackage` object. @@ -79,7 +76,7 @@ def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Union[CondaPac return None # Remove any hash - package_hash: str = None + package_hash = None if '#' in line: hash_parts = line.split('#') if len(hash_parts) > 1: @@ -107,13 +104,13 @@ def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Union[CondaPac else: raise ValueError(f'Unexpected build version string for Conda Package: {conda_list_str}') else: - build_string = None + build_string = '' build_number = int(build_number_with_opt_string) build_version = package_nvbs_parts.pop() package_name = '-'.join(package_nvbs_parts) except IndexError as e: - raise ValueError(f'Error parsing {package_nvbs_parts} from {conda_list_str} IndexError: {str(e)}') + raise ValueError(f'Error parsing {package_nvbs_parts} from {conda_list_str}') from e return CondaPackage( base_url=package_url.geturl(), build_number=build_number, build_string=build_string, diff --git a/poetry.lock b/poetry.lock index d1bd4487..bd792fcf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,17 @@ +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + [[package]] name = "backports.entry-points-selectable" version = "1.1.0" @@ -66,6 +80,33 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "flake8-annotations" +version = "2.0.1" +description = "Flake8 Type Annotation Checks" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +flake8 = ">=3.7.9,<4.0.0" +typed-ast = {version = ">=1.4,<2.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "flake8-bugbear" +version = "21.9.2" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "black", "hypothesis", "hypothesmith"] + [[package]] name = "importlib-metadata" version = "4.8.1" @@ -143,6 +184,32 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packageurl-python" version = "0.9.6" @@ -286,6 +353,30 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-setuptools" +version = "57.4.2" +description = "Typing stubs for setuptools" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "types-toml" +version = "0.10.1" +description = "Typing stubs for toml" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -330,9 +421,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "0ad1ca14a41531adcc5b46734c72bd4036b3c1fb79eeaeee39c9e0fbbe4718f2" +content-hash = "22d9e57043098d8c96d833e529362da09a748f9d0498636d8b24c283785c125f" [metadata.files] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] "backports.entry-points-selectable" = [ {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, @@ -401,6 +496,14 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +flake8-annotations = [ + {file = "flake8-annotations-2.0.1.tar.gz", hash = "sha256:a38b44d01abd480586a92a02a2b0a36231ec42dcc5e114de78fa5db016d8d3f9"}, + {file = "flake8_annotations-2.0.1-py3-none-any.whl", hash = "sha256:d5b0e8704e4e7728b352fa1464e23539ff2341ba11cc153b536fa2cf921ee659"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-21.9.2.tar.gz", hash = "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2"}, + {file = "flake8_bugbear-21.9.2-py36.py37.py38-none-any.whl", hash = "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769"}, +] importlib-metadata = [ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, @@ -492,6 +595,35 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] packageurl-python = [ {file = "packageurl-python-0.9.6.tar.gz", hash = "sha256:c01fbaf62ad2eb791e97158d1f30349e830bee2dd3e9503a87f6c3ffae8d1cf0"}, {file = "packageurl_python-0.9.6-py3-none-any.whl", hash = "sha256:676dcb8278721df952e2444bfcd8d7bf3518894498050f0c6a5faddbe0860cd0"}, @@ -543,6 +675,46 @@ tox = [ {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, ] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +types-setuptools = [ + {file = "types-setuptools-57.4.2.tar.gz", hash = "sha256:5499a0f429281d1a3aa9494c79b6599ab356dfe6d393825426bc749e48ea1bf8"}, + {file = "types_setuptools-57.4.2-py3-none-any.whl", hash = "sha256:9c96aab47fdcf066fef83160b2b9ddbfab3d2c8fdc89053579d0b306837bf22a"}, +] +types-toml = [ + {file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"}, + {file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, diff --git a/pyproject.toml b/pyproject.toml index 837f1ede..e669d880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ include = [ "LICENSE" ] classifiers = [ + # Trove classifiers - https://packaging.python.org/specifications/core-metadata/#metadata-classifier + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', @@ -27,7 +29,8 @@ classifiers = [ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9' + 'Programming Language :: Python :: 3.9', + 'Typing :: Typed' ] keywords = [ "BOM", "SBOM", "SCA", "OWASP" @@ -44,12 +47,17 @@ setuptools = "^50.3.2" importlib-metadata = { version = "^4.8.1", python = "~3.6 | ~3.7" } toml = "^0.10.2" typing-extensions = { version = "^3.10.0", python = "~3.6 | ~3.7" } +types-setuptools = "^57.4.2" +types-toml = "^0.10.1" [tool.poetry.dev-dependencies] tox = "^3.24.3" coverage = "^6.1" flake8 = "^3.9.2" pdoc3 = "^0.10.0" +mypy = "^0.910" +flake8-annotations = "^2.0" +flake8-bugbear = "^21.9.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/.DS_Store b/tests/.DS_Store new file mode 100644 index 00000000..465e5f00 Binary files /dev/null and b/tests/.DS_Store differ diff --git a/tests/base.py b/tests/base.py index 275e7961..3e5b7cc6 100644 --- a/tests/base.py +++ b/tests/base.py @@ -24,11 +24,12 @@ from unittest import TestCase from uuid import uuid4 from xml.dom import minidom +from typing import Any if sys.version_info >= (3, 8, 0): from importlib.metadata import version else: - from importlib_metadata import version + from importlib_metadata import version # type: ignore cyclonedx_lib_name: str = 'cyclonedx-python-lib' cyclonedx_lib_version: str = version(cyclonedx_lib_name) @@ -37,13 +38,13 @@ class BaseJsonTestCase(TestCase): - def assertEqualJson(self, a: str, b: str): + def assertEqualJson(self, a: str, b: str) -> None: self.assertEqual( json.dumps(json.loads(a), sort_keys=True), json.dumps(json.loads(b), sort_keys=True) ) - def assertEqualJsonBom(self, a: str, b: str): + def assertEqualJsonBom(self, a: str, b: str) -> None: """ Remove UUID before comparison as this will be unique to each generation """ @@ -74,12 +75,12 @@ def assertEqualJsonBom(self, a: str, b: str): class BaseXmlTestCase(TestCase): - def assertEqualXml(self, a: str, b: str): + def assertEqualXml(self, a: str, b: str) -> None: da, db = minidom.parseString(a), minidom.parseString(b) self.assertTrue(self._is_equal_xml_element(da.documentElement, db.documentElement), 'XML Documents are not equal: \n{}\n{}'.format(da.toxml(), db.toxml())) - def assertEqualXmlBom(self, a: str, b: str, namespace: str): + def assertEqualXmlBom(self, a: str, b: str, namespace: str) -> None: """ Sanitise some fields such as timestamps which cannot have their values directly compared for equality. """ @@ -112,7 +113,7 @@ def assertEqualXmlBom(self, a: str, b: str, namespace: str): xml.etree.ElementTree.tostring(bb, 'unicode') ) - def _is_equal_xml_element(self, a, b): + def _is_equal_xml_element(self, a: Any, b: Any) -> bool: if a.tagName != b.tagName: return False if sorted(a.attributes.items()) != sorted(b.attributes.items()): diff --git a/tests/test_bom.py b/tests/test_bom.py index 0a750c6f..bfd9b632 100644 --- a/tests/test_bom.py +++ b/tests/test_bom.py @@ -27,7 +27,7 @@ class TestBom(TestCase): - def test_bom_simple(self): + def test_bom_simple(self) -> None: parser = RequirementsFileParser( requirements_file=os.path.join(os.path.dirname(__file__), 'fixtures/requirements-simple.txt') ) @@ -38,12 +38,12 @@ def test_bom_simple(self): Component(name='setuptools', version='50.3.2') )) - def test_bom_metadata_tool_this_tool(self): + def test_bom_metadata_tool_this_tool(self) -> None: self.assertEqual(ThisTool.get_vendor(), 'CycloneDX') self.assertEqual(ThisTool.get_name(), 'cyclonedx-python-lib') self.assertNotEqual(ThisTool.get_version(), 'UNKNOWN') - def test_bom_metadata_tool_multiple_tools(self): + def test_bom_metadata_tool_multiple_tools(self) -> None: bom = Bom() self.assertEqual(len(bom.get_metadata().get_tools()), 1) diff --git a/tests/test_component.py b/tests/test_component.py index 6e4efbf8..e4fd82d0 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -41,7 +41,7 @@ def setUpClass(cls) -> None: namespace='name-space', name='setuptools', version='50.0.1', qualifiers='extension=whl' ) - def test_purl_correct(self): + def test_purl_correct(self) -> None: self.assertEqual( str(PackageURL( type='pypi', name='setuptools', version='50.3.2' @@ -49,7 +49,7 @@ def test_purl_correct(self): TestComponent._component.get_purl() ) - def test_purl_incorrect_version(self): + def test_purl_incorrect_version(self) -> None: purl = PackageURL( type='pypi', name='setuptools', version='50.3.1' ) @@ -61,7 +61,7 @@ def test_purl_incorrect_version(self): self.assertEqual(purl.name, 'setuptools') self.assertEqual(purl.version, '50.3.1') - def test_purl_incorrect_name(self): + def test_purl_incorrect_name(self) -> None: purl = PackageURL( type='pypi', name='setuptoolz', version='50.3.2' ) @@ -73,7 +73,7 @@ def test_purl_incorrect_name(self): self.assertEqual(purl.name, 'setuptoolz') self.assertEqual(purl.version, '50.3.2') - def test_purl_with_qualifiers(self): + def test_purl_with_qualifiers(self) -> None: purl = PackageURL( type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' ) @@ -87,31 +87,31 @@ def test_purl_with_qualifiers(self): ) self.assertEqual(purl.qualifiers, {'extension': 'tar.gz'}) - def test_as_package_url_1(self): + def test_as_package_url_1(self) -> None: purl = PackageURL( type='pypi', name='setuptools', version='50.3.2' ) self.assertEqual(TestComponent._component.to_package_url(), purl) - def test_as_package_url_2(self): + def test_as_package_url_2(self) -> None: purl = PackageURL( type='pypi', name='setuptools', version='50.3.1' ) self.assertNotEqual(TestComponent._component.to_package_url(), purl) - def test_as_package_url_3(self): + def test_as_package_url_3(self) -> None: purl = PackageURL( type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' ) self.assertEqual(TestComponent._component_with_qualifiers.to_package_url(), purl) - def test_custom_package_url_type(self): + def test_custom_package_url_type(self) -> None: purl = PackageURL( type='generic', name='/test.py', version='UNKNOWN' ) self.assertEqual(TestComponent._component_generic_file.to_package_url(), purl) - def test_from_file_with_path_for_bom(self): + def test_from_file_with_path_for_bom(self) -> None: test_file = join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.xml') c = Component.for_file(absolute_file_path=test_file, path_for_bom='fixtures/bom_v1.3_setuptools.xml') self.assertEqual(c.get_name(), 'fixtures/bom_v1.3_setuptools.xml') @@ -122,7 +122,7 @@ def test_from_file_with_path_for_bom(self): self.assertEqual(c.to_package_url(), purl) self.assertEqual(len(c.get_hashes()), 1) - def test_has_component_1(self): + def test_has_component_1(self) -> None: bom = Bom() bom.add_component(component=TestComponent._component) bom.add_component(component=TestComponent._component_2) @@ -130,7 +130,7 @@ def test_has_component_1(self): self.assertTrue(bom.has_component(component=TestComponent._component_2)) self.assertIsNot(TestComponent._component, TestComponent._component_2) - def test_get_component_by_purl_1(self): + def test_get_component_by_purl_1(self) -> None: bom = Bom() bom.add_component(component=TestComponent._component) bom.add_component(component=TestComponent._component_2) @@ -142,7 +142,7 @@ def test_get_component_by_purl_1(self): TestComponent._component_2 ) - def test_full_purl_spec_no_subpath(self): + def test_full_purl_spec_no_subpath(self) -> None: self.assertEqual( TestComponent._component_purl_spec_no_subpath.get_purl(), 'pkg:pypi/name-space/setuptools@50.0.1?extension=whl' diff --git a/tests/test_e2e_environment.py b/tests/test_e2e_environment.py index 75468ee4..35ca04a6 100644 --- a/tests/test_e2e_environment.py +++ b/tests/test_e2e_environment.py @@ -36,7 +36,7 @@ class TestE2EEnvironment(TestCase): - def test_json_defaults(self): + def test_json_defaults(self) -> None: outputter: Json = get_instance(bom=Bom.from_parser(EnvironmentParser()), output_format=OutputFormat.JSON) bom_json = json.loads(outputter.output_as_string()) component_this_library = next( @@ -49,7 +49,7 @@ def test_json_defaults(self): self.assertEqual(component_this_library['name'], OUR_PACKAGE_NAME) self.assertEqual(component_this_library['version'], OUR_PACKAGE_VERSION) - def test_xml_defaults(self): + def test_xml_defaults(self) -> None: outputter: Xml = get_instance(bom=Bom.from_parser(EnvironmentParser())) # Check we have cyclonedx-python-lib with Author, Name and Version diff --git a/tests/test_model.py b/tests/test_model.py index 81e75abd..096a9073 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -5,12 +5,12 @@ class TestModel(TestCase): - def test_hash_type_from_composite_str_1(self): + def test_hash_type_from_composite_str_1(self) -> None: h = HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') self.assertEqual(h.get_algorithm(), HashAlgorithm.SHA_256) self.assertEqual(h.get_hash_value(), '806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - def test_hash_type_from_composite_str_2(self): + def test_hash_type_from_composite_str_2(self) -> None: h = HashType.from_composite_str('md5:dc26cd71b80d6757139f38156a43c545') self.assertEqual(h.get_algorithm(), HashAlgorithm.MD5) self.assertEqual(h.get_hash_value(), 'dc26cd71b80d6757139f38156a43c545') diff --git a/tests/test_model_component.py b/tests/test_model_component.py index c5599a3d..b9921e9b 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -6,7 +6,7 @@ class TestModelComponent(TestCase): - def test_empty_basic_component(self): + def test_empty_basic_component(self) -> None: c = Component( name='test-component', version='1.2.3' ) @@ -17,7 +17,7 @@ def test_empty_basic_component(self): self.assertEqual(len(c.get_hashes()), 0) self.assertEqual(len(c.get_vulnerabilities()), 0) - def test_multiple_basic_components(self): + def test_multiple_basic_components(self) -> None: c1 = Component( name='test-component', version='1.2.3' ) @@ -38,7 +38,7 @@ def test_multiple_basic_components(self): self.assertEqual(len(c2.get_hashes()), 0) self.assertEqual(len(c2.get_vulnerabilities()), 0) - def test_external_references(self): + def test_external_references(self) -> None: c = Component( name='test-component', version='1.2.3' ) diff --git a/tests/test_model_vulnerability.py b/tests/test_model_vulnerability.py index e6fce457..d0ca8fcf 100644 --- a/tests/test_model_vulnerability.py +++ b/tests/test_model_vulnerability.py @@ -5,139 +5,139 @@ class TestModelVulnerability(TestCase): - def test_v_rating_scores_empty(self): + def test_v_rating_scores_empty(self) -> None: vr = VulnerabilityRating() self.assertFalse(vr.has_score()) - def test_v_rating_scores_base_only(self): + def test_v_rating_scores_base_only(self) -> None: vr = VulnerabilityRating(score_base=1.0) self.assertTrue(vr.has_score()) - def test_v_rating_scores_all(self): + def test_v_rating_scores_all(self) -> None: vr = VulnerabilityRating(score_base=1.0, score_impact=3.5, score_exploitability=5.6) self.assertTrue(vr.has_score()) - def test_v_severity_from_cvss_scores_single_critical(self): + def test_v_severity_from_cvss_scores_single_critical(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores(9.1), VulnerabilitySeverity.CRITICAL ) - def test_v_severity_from_cvss_scores_multiple_critical(self): + def test_v_severity_from_cvss_scores_multiple_critical(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores((9.1, 9.5)), VulnerabilitySeverity.CRITICAL ) - def test_v_severity_from_cvss_scores_single_high(self): + def test_v_severity_from_cvss_scores_single_high(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores(8.9), VulnerabilitySeverity.HIGH ) - def test_v_severity_from_cvss_scores_single_medium(self): + def test_v_severity_from_cvss_scores_single_medium(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores(4.2), VulnerabilitySeverity.MEDIUM ) - def test_v_severity_from_cvss_scores_single_low(self): + def test_v_severity_from_cvss_scores_single_low(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores(1.1), VulnerabilitySeverity.LOW ) - def test_v_severity_from_cvss_scores_single_none(self): + def test_v_severity_from_cvss_scores_single_none(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores(0.0), VulnerabilitySeverity.NONE ) - def test_v_severity_from_cvss_scores_multiple_high(self): + def test_v_severity_from_cvss_scores_multiple_high(self) -> None: self.assertEqual( VulnerabilitySeverity.get_from_cvss_scores((1.2, 8.9, 2.2, 5.6)), VulnerabilitySeverity.HIGH ) - def test_v_source_parse_cvss3_1(self): + def test_v_source_parse_cvss3_1(self) -> None: self.assertEqual( VulnerabilitySourceType.get_from_vector('CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), VulnerabilitySourceType.CVSS_V3 ) - def test_v_source_parse_cvss2_1(self): + def test_v_source_parse_cvss2_1(self) -> None: self.assertEqual( VulnerabilitySourceType.get_from_vector('CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:N/A:C'), VulnerabilitySourceType.CVSS_V2 ) - def test_v_source_parse_owasp_1(self): + def test_v_source_parse_owasp_1(self) -> None: self.assertEqual( VulnerabilitySourceType.get_from_vector('OWASP/K9:M1:O0:Z2/D1:X1:W1:L3/C2:I1:A1:T1/F1:R1:S2:P3/50'), VulnerabilitySourceType.OWASP ) - def test_v_source_get_localised_vector_cvss3_1(self): + def test_v_source_get_localised_vector_cvss3_1(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V3.get_localised_vector(vector='CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_cvss3_2(self): + def test_v_source_get_localised_vector_cvss3_2(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V3.get_localised_vector(vector='CVSS:3.0AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_cvss3_3(self): + def test_v_source_get_localised_vector_cvss3_3(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V3.get_localised_vector(vector='AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_cvss2_1(self): + def test_v_source_get_localised_vector_cvss2_1(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V2.get_localised_vector(vector='CVSS:2.0/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_cvss2_2(self): + def test_v_source_get_localised_vector_cvss2_2(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V2.get_localised_vector(vector='CVSS:2.1AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_cvss2_3(self): + def test_v_source_get_localised_vector_cvss2_3(self) -> None: self.assertEqual( VulnerabilitySourceType.CVSS_V2.get_localised_vector(vector='AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_owasp_1(self): + def test_v_source_get_localised_vector_owasp_1(self) -> None: self.assertEqual( VulnerabilitySourceType.OWASP.get_localised_vector(vector='OWASP/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_owasp_2(self): + def test_v_source_get_localised_vector_owasp_2(self) -> None: self.assertEqual( VulnerabilitySourceType.OWASP.get_localised_vector(vector='OWASPAV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_owasp_3(self): + def test_v_source_get_localised_vector_owasp_3(self) -> None: self.assertEqual( VulnerabilitySourceType.OWASP.get_localised_vector(vector='AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N'), 'AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N' ) - def test_v_source_get_localised_vector_other_1(self): + def test_v_source_get_localised_vector_other_1(self) -> None: self.assertEqual( VulnerabilitySourceType.OPEN_FAIR.get_localised_vector(vector='SOMETHING_OR_OTHER'), 'SOMETHING_OR_OTHER' ) - def test_v_source_get_localised_vector_other_2(self): + def test_v_source_get_localised_vector_other_2(self) -> None: self.assertEqual( VulnerabilitySourceType.OTHER.get_localised_vector(vector='SOMETHING_OR_OTHER'), 'SOMETHING_OR_OTHER' diff --git a/tests/test_output_generic.py b/tests/test_output_generic.py index 3b02c875..4b2c5729 100644 --- a/tests/test_output_generic.py +++ b/tests/test_output_generic.py @@ -25,14 +25,14 @@ class TestOutputGeneric(TestCase): - def test_get_instance_default(self): + def test_get_instance_default(self) -> None: i = get_instance() self.assertIsInstance(i, XmlV1Dot3) - def test_get_instance_xml(self): + def test_get_instance_xml(self) -> None: i = get_instance(output_format=OutputFormat.XML) self.assertIsInstance(i, XmlV1Dot3) - def test_get_instance_xml_v1_3(self): + def test_get_instance_xml_v1_3(self) -> None: i = get_instance(output_format=OutputFormat.XML, schema_version=SchemaVersion.V1_3) self.assertIsInstance(i, XmlV1Dot3) diff --git a/tests/test_output_json.py b/tests/test_output_json.py index df4fb4de..4a10a077 100644 --- a/tests/test_output_json.py +++ b/tests/test_output_json.py @@ -29,7 +29,7 @@ class TestOutputJson(BaseJsonTestCase): - def test_simple_bom_v1_3(self): + def test_simple_bom_v1_3(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) outputter = get_instance(bom=bom, output_format=OutputFormat.JSON) @@ -38,7 +38,7 @@ def test_simple_bom_v1_3(self): self.assertEqualJsonBom(outputter.output_as_string(), expected_json.read()) expected_json.close() - def test_simple_bom_v1_2(self): + def test_simple_bom_v1_2(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_2) @@ -47,7 +47,7 @@ def test_simple_bom_v1_2(self): self.assertEqualJsonBom(outputter.output_as_string(), expected_json.read()) expected_json.close() - def test_bom_v1_3_with_component_hashes(self): + def test_bom_v1_3_with_component_hashes(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.add_hash( @@ -60,7 +60,7 @@ def test_bom_v1_3_with_component_hashes(self): self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read()) expected_json.close() - def test_bom_v1_3_with_component_external_references(self): + def test_bom_v1_3_with_component_external_references(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.add_hash( @@ -85,7 +85,7 @@ def test_bom_v1_3_with_component_external_references(self): self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read()) expected_json.close() - def test_bom_v1_3_with_component_license(self): + def test_bom_v1_3_with_component_license(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.set_license('MIT License') diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py index 6328c89a..740ccff2 100644 --- a/tests/test_output_xml.py +++ b/tests/test_output_xml.py @@ -31,7 +31,7 @@ class TestOutputXml(BaseXmlTestCase): - def test_simple_bom_v1_3(self): + def test_simple_bom_v1_3(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) outputter: Xml = get_instance(bom=bom) @@ -41,7 +41,7 @@ def test_simple_bom_v1_3(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_simple_bom_v1_2(self): + def test_simple_bom_v1_2(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_2) @@ -51,7 +51,7 @@ def test_simple_bom_v1_2(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_simple_bom_v1_1(self): + def test_simple_bom_v1_1(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_1) @@ -61,7 +61,7 @@ def test_simple_bom_v1_1(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_simple_bom_v1_0(self): + def test_simple_bom_v1_0(self) -> None: bom = Bom() bom.add_component(Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz')) self.assertEqual(len(bom.get_components()), 1) @@ -72,7 +72,7 @@ def test_simple_bom_v1_0(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_simple_bom_v1_3_with_vulnerabilities(self): + def test_simple_bom_v1_3_with_vulnerabilities(self) -> None: bom = Bom() c = Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz') c.add_vulnerability(Vulnerability( @@ -99,7 +99,7 @@ def test_simple_bom_v1_3_with_vulnerabilities(self): expected_xml.close() - def test_simple_bom_v1_0_with_vulnerabilities(self): + def test_simple_bom_v1_0_with_vulnerabilities(self) -> None: bom = Bom() c = Component(name='setuptools', version='50.3.2', qualifiers='extension=tar.gz') c.add_vulnerability(Vulnerability( @@ -126,7 +126,7 @@ def test_simple_bom_v1_0_with_vulnerabilities(self): expected_xml.close() - def test_bom_v1_3_with_component_hashes(self): + def test_bom_v1_3_with_component_hashes(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.add_hash( @@ -140,7 +140,7 @@ def test_bom_v1_3_with_component_hashes(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_bom_v1_3_with_component_external_references(self): + def test_bom_v1_3_with_component_external_references(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.add_hash( @@ -166,7 +166,7 @@ def test_bom_v1_3_with_component_external_references(self): namespace=outputter.get_target_namespace()) expected_xml.close() - def test_with_component_license(self): + def test_with_component_license(self) -> None: bom = Bom() c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz') c.set_license('MIT License') diff --git a/tests/test_parser_conda.py b/tests/test_parser_conda.py index 77e2f69a..3d819442 100644 --- a/tests/test_parser_conda.py +++ b/tests/test_parser_conda.py @@ -25,7 +25,7 @@ class TestCondaParser(TestCase): - def test_conda_list_json(self): + def test_conda_list_json(self) -> None: conda_list_ouptut_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-output.json') with (open(conda_list_ouptut_file, 'r')) as conda_list_ouptut_fh: @@ -41,7 +41,7 @@ def test_conda_list_json(self): self.assertEqual(1, len(c_noarch.get_external_references())) self.assertEqual(0, len(c_noarch.get_external_references()[0].get_hashes())) - def test_conda_list_explicit_md5(self): + def test_conda_list_explicit_md5(self) -> None: conda_list_ouptut_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-explicit-md5.txt') with (open(conda_list_ouptut_file, 'r')) as conda_list_ouptut_fh: diff --git a/tests/test_parser_environment.py b/tests/test_parser_environment.py index a1a6af95..8f14de5e 100644 --- a/tests/test_parser_environment.py +++ b/tests/test_parser_environment.py @@ -25,7 +25,7 @@ class TestEnvironmentParser(TestCase): - def test_simple(self): + def test_simple(self) -> None: """ @todo This test is a vague as it will detect the unique environment where tests are being executed - so is this valid? diff --git a/tests/test_parser_pipenv.py b/tests/test_parser_pipenv.py index 770f6575..88294dab 100644 --- a/tests/test_parser_pipenv.py +++ b/tests/test_parser_pipenv.py @@ -25,7 +25,7 @@ class TestPipEnvParser(TestCase): - def test_simple(self): + def test_simple(self) -> None: tests_pipfile_lock = os.path.join(os.path.dirname(__file__), 'fixtures/pipfile-lock-simple.txt') parser = PipEnvFileParser(pipenv_lock_filename=tests_pipfile_lock) @@ -37,7 +37,7 @@ def test_simple(self): self.assertEqual(len(components[0].get_external_references()), 2) self.assertEqual(len(components[0].get_external_references()[0].get_hashes()), 1) - def test_with_multiple_and_no_index(self): + def test_with_multiple_and_no_index(self) -> None: tests_pipfile_lock = os.path.join(os.path.dirname(__file__), 'fixtures/pipfile-lock-no-index-example.txt') parser = PipEnvFileParser(pipenv_lock_filename=tests_pipfile_lock) diff --git a/tests/test_parser_poetry.py b/tests/test_parser_poetry.py index 22426c38..c9ec6d95 100644 --- a/tests/test_parser_poetry.py +++ b/tests/test_parser_poetry.py @@ -25,7 +25,7 @@ class TestPoetryParser(TestCase): - def test_simple(self): + def test_simple(self) -> None: tests_poetry_lock_file = os.path.join(os.path.dirname(__file__), 'fixtures/poetry-lock-simple.txt') parser = PoetryFileParser(poetry_lock_filename=tests_poetry_lock_file) diff --git a/tests/test_parser_requirements.py b/tests/test_parser_requirements.py index 19d257ba..bb306e51 100644 --- a/tests/test_parser_requirements.py +++ b/tests/test_parser_requirements.py @@ -26,7 +26,7 @@ class TestRequirementsParser(TestCase): - def test_simple(self): + def test_simple(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-simple.txt')) as r: parser = RequirementsParser( requirements_content=r.read() @@ -35,7 +35,7 @@ def test_simple(self): self.assertTrue(1, parser.component_count()) self.assertFalse(parser.has_warnings()) - def test_example_1(self): + def test_example_1(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-example-1.txt')) as r: parser = RequirementsParser( requirements_content=r.read() @@ -44,7 +44,7 @@ def test_example_1(self): self.assertTrue(3, parser.component_count()) self.assertFalse(parser.has_warnings()) - def test_example_with_comments(self): + def test_example_with_comments(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-with-comments.txt')) as r: parser = RequirementsParser( requirements_content=r.read() @@ -53,7 +53,7 @@ def test_example_with_comments(self): self.assertTrue(5, parser.component_count()) self.assertFalse(parser.has_warnings()) - def test_example_multiline_with_comments(self): + def test_example_multiline_with_comments(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-multilines-with-comments.txt')) as r: parser = RequirementsParser( requirements_content=r.read() @@ -63,7 +63,7 @@ def test_example_multiline_with_comments(self): self.assertFalse(parser.has_warnings()) @unittest.skip('Not yet supported') - def test_example_with_hashes(self): + def test_example_with_hashes(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-with-hashes.txt')) as r: parser = RequirementsParser( requirements_content=r.read() @@ -72,7 +72,7 @@ def test_example_with_hashes(self): self.assertTrue(5, parser.component_count()) self.assertFalse(parser.has_warnings()) - def test_example_without_pinned_versions(self): + def test_example_without_pinned_versions(self) -> None: with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-without-pinned-versions.txt')) as r: parser = RequirementsParser( requirements_content=r.read() diff --git a/tests/test_utils_conda.py b/tests/test_utils_conda.py index 03f65fe4..0a201739 100644 --- a/tests/test_utils_conda.py +++ b/tests/test_utils_conda.py @@ -23,7 +23,7 @@ class TestUtilsConda(TestCase): - def test_parse_conda_json_no_hash(self): + def test_parse_conda_json_no_hash(self) -> None: cp: CondaPackage = parse_conda_json_to_conda_package( conda_json_str='{"base_url": "https://repo.anaconda.com/pkgs/main","build_number": 1003,"build_string": ' '"py39hecd8cb5_1003","channel": "pkgs/main","dist_name": "chardet-4.0.0-py39hecd8cb5_1003",' @@ -41,7 +41,7 @@ def test_parse_conda_json_no_hash(self): self.assertEqual(cp['version'], '4.0.0') self.assertIsNone(cp['md5_hash']) - def test_parse_conda_list_str_no_hash(self): + def test_parse_conda_list_str_no_hash(self) -> None: cp: CondaPackage = parse_conda_list_str_to_conda_package( conda_list_str='https://repo.anaconda.com/pkgs/main/osx-64/chardet-4.0.0-py39hecd8cb5_1003.conda' ) @@ -57,7 +57,7 @@ def test_parse_conda_list_str_no_hash(self): self.assertEqual(cp['version'], '4.0.0') self.assertIsNone(cp['md5_hash']) - def test_parse_conda_list_str_with_hash_1(self): + def test_parse_conda_list_str_with_hash_1(self) -> None: cp: CondaPackage = parse_conda_list_str_to_conda_package( conda_list_str='https://repo.anaconda.com/pkgs/main/noarch/tzdata-2021a-h52ac0ba_0.conda' '#d42e4db918af84a470286e4c300604a3' @@ -74,7 +74,7 @@ def test_parse_conda_list_str_with_hash_1(self): self.assertEqual(cp['version'], '2021a') self.assertEqual(cp['md5_hash'], 'd42e4db918af84a470286e4c300604a3') - def test_parse_conda_list_str_with_hash_2(self): + def test_parse_conda_list_str_with_hash_2(self) -> None: cp: CondaPackage = parse_conda_list_str_to_conda_package( conda_list_str='https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2021.7.5-hecd8cb5_1.conda' '#c2d0ae65c08dacdcf86770b7b5bbb187' @@ -91,7 +91,7 @@ def test_parse_conda_list_str_with_hash_2(self): self.assertEqual(cp['version'], '2021.7.5') self.assertEqual(cp['md5_hash'], 'c2d0ae65c08dacdcf86770b7b5bbb187') - def test_parse_conda_list_str_with_hash_3(self): + def test_parse_conda_list_str_with_hash_3(self) -> None: cp: CondaPackage = parse_conda_list_str_to_conda_package( conda_list_str='https://repo.anaconda.com/pkgs/main/noarch/idna-2.10-pyhd3eb1b0_0.tar.bz2' '#153ff132f593ea80aae2eea61a629c92' diff --git a/tox.ini b/tox.ini index 181512d8..19d8b2a2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,47 @@ +# tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + [tox] -basepython = python3.9 -envlist = flake8,py3.9,py3.8,py3.7,py3.6 +minversion = 3.10 +envlist = + flake8 + mypy + py{39,38,37,36} isolated_build = True +skip_missing_interpreters = True +usedevelop = False +download = False [testenv] +# settings in this category apply to all other testenv, if not overwritten +skip_install = True whitelist_externals = poetry -commands = - pip install poetry +deps = poetry +commands_pre = + {envpython} --version poetry install -v +commands = poetry run coverage run --source=cyclonedx -m unittest discover -s tests +[testenv:mypy] +commands = + poetry run mypy + # mypy config is on own file: `.mypy.ini` + [testenv:flake8] -skip_install = True commands = - pip install poetry - poetry install -v poetry run flake8 cyclonedx/ tests/ [flake8] -ignore = E305 -exclude = .git,__pycache__ -max-line-length = 120 \ No newline at end of file +exclude = + build,dist,__pycache__,.eggs,*_cache + .git,.tox,.venv,venv + _OLD,_TEST, + docs +max-line-length = 120 +ignore = + E305 + # ignore `self`, `cls` markers of flake8-annotations>=2.0 + ANN101,ANN102