From 1d1f3ea4fe23f501c1a6b974825ea42bbb0e7f8e Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 23 Apr 2025 12:30:01 +0200 Subject: [PATCH 1/4] wip Signed-off-by: Jan Kowalleck --- .github/workflows/python.yml | 5 ++--- docs/upgrading.rst | 2 +- pyproject.toml | 19 +++++++++---------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 631a46f8..7015e228 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -77,7 +77,7 @@ jobs: - python-version: '3.13' # latest os: ubuntu-latest toxenv-factors: '-current' - - python-version: '3.8' # lowest + - python-version: '3.9' # lowest os: ubuntu-latest toxenv-factors: '-lowest' steps: @@ -169,8 +169,7 @@ jobs: - "3.12" - "3.11" - "3.10" - - "3.9" - - "3.8" # lowest supported -- handled in include + - "3.9" # lowest supported -- handled in include steps: - name: Checkout # see https://github.com/actions/checkout diff --git a/docs/upgrading.rst b/docs/upgrading.rst index fced547c..1cc94620 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -10,7 +10,7 @@ This document is not a full :doc:`change log `, but a migration path. Python support -------------- -* This tool requires Python 3.8 or later. +* This tool requires Python 3.9 or later. It is tested with CPython, support for PyPy is best effort. diff --git a/pyproject.toml b/pyproject.toml index 1120095f..165a7fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ classifiers = [ "Topic :: Software Development", "Topic :: System :: Software Distribution", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -78,21 +77,21 @@ chardet = "^5.1" [tool.poetry.group.dev.dependencies] # pin to exact versions, if the tool/lib/plugin is process-relevant -coverage = "7.6.1" +coverage = "7.8.0" ddt = "1.7.2" -flake8 = { version = "7.1.2", python = ">=3.8.1" } -flake8-annotations = { version = "3.1.1", python = ">=3.8.1" } -flake8-bugbear = { version = "24.12.12", python = ">=3.8.1" } +flake8 = "7.2.0" +flake8-annotations = "3.1.1" +flake8-bugbear = "24.12.12" flake8-copyright-validator = "^0.0.1" -flake8-isort = "6.1.1" +flake8-isort = "6.1.2" flake8-quotes = "3.4.0" flake8-use-fstring = "1.4" pep8-naming = "0.14.1" flake8-logging = "1.6.0" -isort = "5.13.2" -autopep8 = "2.3.1" -mypy = "1.14.1" -bandit = "1.7.10" +isort = "6.0.1" +autopep8 = "2.3.2" +mypy = "1.15.0" +bandit = "1.8.3" tomli = { version = "^2.0.1", python = "<3.11" } tox = "4.25.0" From 9fe92e392e9bce9248c7cbd0776209113df61c7c Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 23 Apr 2025 13:01:16 +0200 Subject: [PATCH 2/4] wip Signed-off-by: Jan Kowalleck --- README.md | 2 +- cyclonedx_py/_internal/cli.py | 36 +++++++---------------------------- pyproject.toml | 2 +- tox.ini | 4 ++-- 4 files changed, 11 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 19406553..74d211e1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Read the full [documentation][link_rtfd] for more details. ## Requirements -* Python `>=3.8,<4` +* Python `>=3.9,<4` However, there are older versions of this tool available, which support Python `>=2.7`. diff --git a/cyclonedx_py/_internal/cli.py b/cyclonedx_py/_internal/cli.py index 970e3432..849a0133 100644 --- a/cyclonedx_py/_internal/cli.py +++ b/cyclonedx_py/_internal/cli.py @@ -17,7 +17,7 @@ import logging import sys -from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter +from argparse import ArgumentParser, BooleanOptionalAction, FileType, RawDescriptionHelpFormatter from itertools import chain from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Sequence, TextIO, Type, Union @@ -35,20 +35,11 @@ from .utils.args import argparse_type4enum, choices4enum if TYPE_CHECKING: # pragma: no cover - from argparse import Action - from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from . import BomBuilder - BooleanOptionalAction: Optional[Type[Action]] - -if sys.version_info >= (3, 9): - from argparse import BooleanOptionalAction -else: - BooleanOptionalAction = None - OPTION_OUTPUT_STDOUT = '-' @@ -121,25 +112,12 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar type=FileType('wt', encoding='utf8'), dest='output_file', default=OPTION_OUTPUT_STDOUT) - if BooleanOptionalAction: - op.add_argument('--validate', - help='Whether to validate resulting BOM before outputting.' - ' (default: %(default)s)', - action=BooleanOptionalAction, - dest='should_validate', - default=True) - else: - vg = op.add_mutually_exclusive_group() - vg.add_argument('--validate', - help='Validate resulting BOM before outputting.' - ' (default: %(default)s)', - action='store_true', - dest='should_validate', - default=True) - vg.add_argument('--no-validate', - help='Disable validation of resulting BOM.', - dest='should_validate', - action='store_false') + op.add_argument('--validate', + help='Whether to validate resulting BOM before outputting.' + ' (default: %(default)s)', + action=BooleanOptionalAction, + dest='should_validate', + default=True) scbbc: Type['BomBuilder'] sct: str diff --git a/pyproject.toml b/pyproject.toml index 165a7fc6..027f0855 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ classifiers = [ cyclonedx-py = "cyclonedx_py._internal.cli:run" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" cyclonedx-python-lib = { version = "^8.0 || ^9.0 || ^10", extras = ["validation"] } packageurl-python = ">=0.11, <2" # keep in sync with same dep in `cyclonedx-python-lib` pip-requirements-parser = "^32.0" diff --git a/tox.ini b/tox.ini index 4918a15d..c0ce392b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ minversion = 4.0 envlist = flake8 mypy-{current,lowest} - py{313,312,311,310,39,38} + py{313,312,311,310,39} bandit skip_missing_interpreters = True usedevelop = False @@ -35,7 +35,7 @@ skip_install = True commands = # mypy config is on own file: `.mypy.ini` !lowest: poetry run mypy - lowest: poetry run mypy --python-version=3.8 + lowest: poetry run mypy --python-version=3.9 [testenv:flake8] skip_install = True From 40bec4f9f08b0b7e866a40e9850594146e4c25b9 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 23 Apr 2025 13:34:27 +0200 Subject: [PATCH 3/4] wip Signed-off-by: Jan Kowalleck --- cyclonedx_py/_internal/cli.py | 11 +++++----- cyclonedx_py/_internal/environment.py | 13 ++++++------ cyclonedx_py/_internal/pipenv.py | 21 ++++++++++--------- cyclonedx_py/_internal/poetry.py | 24 +++++++++++----------- cyclonedx_py/_internal/requirements.py | 5 +++-- cyclonedx_py/_internal/utils/args.py | 11 +++++----- cyclonedx_py/_internal/utils/cdx.py | 5 +++-- cyclonedx_py/_internal/utils/packaging.py | 3 ++- cyclonedx_py/_internal/utils/pep610.py | 10 ++++----- cyclonedx_py/_internal/utils/pep621.py | 11 +++++----- cyclonedx_py/_internal/utils/pep639.py | 3 ++- cyclonedx_py/_internal/utils/poetry.py | 11 +++++----- cyclonedx_py/_internal/utils/pyproject.py | 9 ++++---- tests/integration/test_cli_environment.py | 3 ++- tests/integration/test_cli_pipenv.py | 3 ++- tests/integration/test_cli_poetry.py | 3 ++- tests/integration/test_cli_requirements.py | 5 +++-- tests/unit/test_utils_cdx.py | 9 ++++---- 18 files changed, 88 insertions(+), 72 deletions(-) diff --git a/cyclonedx_py/_internal/cli.py b/cyclonedx_py/_internal/cli.py index 849a0133..53d7ea64 100644 --- a/cyclonedx_py/_internal/cli.py +++ b/cyclonedx_py/_internal/cli.py @@ -18,8 +18,9 @@ import logging import sys from argparse import ArgumentParser, BooleanOptionalAction, FileType, RawDescriptionHelpFormatter +from collections.abc import Sequence from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Sequence, TextIO, Type, Union +from typing import TYPE_CHECKING, Any, NoReturn, Optional, TextIO, Union from cyclonedx.model import Property from cyclonedx.output import make_outputter @@ -119,9 +120,9 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar dest='should_validate', default=True) - scbbc: Type['BomBuilder'] + scbbc: type['BomBuilder'] sct: str - scta: List[str] + scta: list[str] for scbbc, sct, *scta in ( (EnvironmentBB, 'environment', 'env', 'venv'), (RequirementsBB, 'requirements'), @@ -149,7 +150,7 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar } @classmethod - def _clean_kwargs(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]: + def _clean_kwargs(cls, kwargs: dict[str, Any]) -> dict[str, Any]: return {k: kwargs[k] for k in kwargs if k not in cls.__OWN_ARGS} def __init__(self, *, @@ -159,7 +160,7 @@ def __init__(self, *, spec_version: SchemaVersion, output_reproducible: bool, should_validate: bool, - _bbc: Type['BomBuilder'], + _bbc: type['BomBuilder'], **kwargs: Any) -> None: self._logger = logger self._short_purls = short_purls diff --git a/cyclonedx_py/_internal/environment.py b/cyclonedx_py/_internal/environment.py index dfb2a288..0a179918 100644 --- a/cyclonedx_py/_internal/environment.py +++ b/cyclonedx_py/_internal/environment.py @@ -17,6 +17,7 @@ from argparse import OPTIONAL, ArgumentParser +from collections.abc import Iterable from importlib.metadata import distributions from json import loads from os import getcwd, name as os_name @@ -24,7 +25,7 @@ from subprocess import run # nosec from sys import path as sys_path from textwrap import dedent -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Optional from cyclonedx.model import Property from cyclonedx.model.component import Component, ComponentEvidence, ComponentType @@ -46,7 +47,7 @@ from .utils.pep610 import PackageSource - T_AllComponents = Dict[str, Tuple['Component', Iterable[Requirement]]] + T_AllComponents = dict[str, tuple['Component', Iterable[Requirement]]] class EnvironmentBB(BomBuilder): @@ -155,7 +156,7 @@ def __call__(self, *, # type:ignore[override] root_d = tuple(pyproject2dependencies(pyproject)) rc = (root_c, root_d) - path: List[str] + path: list[str] if python: path = self.__path4python(python) else: @@ -168,7 +169,7 @@ def __call__(self, *, # type:ignore[override] return bom def __add_components(self, bom: 'Bom', - rc: Optional[Tuple['Component', Iterable['Requirement']]], + rc: Optional[tuple['Component', Iterable['Requirement']]], **kwargs: Any) -> None: all_components: 'T_AllComponents' = {} self._logger.debug('distribution context args: %r', kwargs) @@ -229,7 +230,7 @@ def __add_components(self, bom: 'Bom', def __finalize_dependencies(self, bom: 'Bom', all_components: 'T_AllComponents') -> None: for component, requires in all_components.values(): - component_deps: List[Component] = [] + component_deps: list[Component] = [] for req in requires: req_component: Optional[Component] = all_components.get(normalize_packagename(req.name), (None,))[0] if req_component is None: @@ -297,7 +298,7 @@ def __py_interpreter(value: str) -> str: raise ValueError(f'Failed to find python in directory: {value}') return value - def __path4python(self, python: str) -> List[str]: + def __path4python(self, python: str) -> list[str]: cmd = self.__py_interpreter(python), '-c', 'import json,sys;json.dump(sys.path,sys.stdout)' self._logger.debug('fetch `path` from python interpreter cmd: %r', cmd) res = run(cmd, capture_output=True, encoding='utf8', shell=False) # nosec diff --git a/cyclonedx_py/_internal/pipenv.py b/cyclonedx_py/_internal/pipenv.py index 91cf397f..3401dcf0 100644 --- a/cyclonedx_py/_internal/pipenv.py +++ b/cyclonedx_py/_internal/pipenv.py @@ -17,11 +17,12 @@ from argparse import OPTIONAL, ArgumentParser +from collections.abc import Generator from json import loads as json_loads from os import getenv from os.path import join from textwrap import dedent -from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Optional from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri @@ -41,7 +42,7 @@ from cyclonedx.model.bom import Bom - NameDict = Dict[str, Any] + NameDict = dict[str, Any] class PipenvBB(BomBuilder): @@ -96,7 +97,7 @@ def __init__(self, *, def __call__(self, *, # type:ignore[override] project_directory: str, - categories: List[str], + categories: list[str], dev: bool, pyproject_file: Optional[str], mc_type: 'ComponentType', @@ -104,7 +105,7 @@ def __call__(self, *, # type:ignore[override] # the group-args shall mimic the ones from Pipenv, which uses (comma and/or space)-separated lists # values be like: 'foo bar,bazz' -> ['foo', 'bar', 'bazz'] - lock_groups: Set[str] = set() + lock_groups: set[str] = set() if len(categories) == 0: lock_groups.add('default') if dev: @@ -138,7 +139,7 @@ def __call__(self, *, # type:ignore[override] frozenset(lock_groups)) def _make_bom(self, root_c: Optional['Component'], - locker: 'NameDict', use_groups: FrozenSet[str]) -> 'Bom': + locker: 'NameDict', use_groups: frozenset[str]) -> 'Bom': self._logger.debug('use_groups: %r', use_groups) bom = make_bom() @@ -146,14 +147,14 @@ def _make_bom(self, root_c: Optional['Component'], self._logger.debug('root-component: %r', root_c) meta: NameDict = locker[self.__LOCKFILE_META] - source_urls: Dict[str, str] = { + source_urls: dict[str, str] = { source['name']: redact_auth_from_url(source['url']).rstrip('/') for source in meta.get('sources', ()) } if self._pypi_url is not None: source_urls['pypi'] = redact_auth_from_url(self._pypi_url).rstrip('/') - all_components: Dict[str, Component] = {} + all_components: dict[str, Component] = {} if root_c: # root for possible self-installs all_components[normalize_packagename(root_c.name)] = root_c @@ -218,7 +219,7 @@ def __is_local(self, data: 'NameDict') -> bool: see https://pip.pypa.io/en/latest/topics/vcs-support/#vcs-support """ - def __package_vcs(self, data: 'NameDict') -> Optional[Tuple[str, str]]: + def __package_vcs(self, data: 'NameDict') -> Optional[tuple[str, str]]: for vct in self.__VCS_TYPES: if vct in data: url: str = data[vct] @@ -227,7 +228,7 @@ def __package_vcs(self, data: 'NameDict') -> Optional[Tuple[str, str]]: return vct, url[:hash_pos] if hash_pos >= 0 else url return None - def __make_extrefs(self, name: str, data: 'NameDict', source_urls: Dict[str, str] + def __make_extrefs(self, name: str, data: 'NameDict', source_urls: dict[str, str] ) -> Generator['ExternalReference', None, None]: hashes = (HashType.from_composite_str(package_hash) for package_hash @@ -267,7 +268,7 @@ def __make_extrefs(self, name: str, data: 'NameDict', source_urls: Dict[str, str except (InvalidUriException, UnknownHashTypeException, KeyError) as error: # pragma: nocover self._logger.debug('skipped dist-extRef for: %r', name, exc_info=error) - def __purl_qualifiers4lock(self, data: 'NameDict', sourcees: Dict[str, str]) -> 'NameDict': + def __purl_qualifiers4lock(self, data: 'NameDict', sourcees: dict[str, str]) -> 'NameDict': # see https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst qs = {} vcs_source = self.__package_vcs(data) diff --git a/cyclonedx_py/_internal/poetry.py b/cyclonedx_py/_internal/poetry.py index 1587c0d0..09ba68da 100644 --- a/cyclonedx_py/_internal/poetry.py +++ b/cyclonedx_py/_internal/poetry.py @@ -17,12 +17,13 @@ from argparse import OPTIONAL, ArgumentParser +from collections.abc import Generator, Iterable from dataclasses import dataclass from itertools import chain from os.path import join from re import compile as re_compile from textwrap import dedent -from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, Iterable, List, Set, Tuple +from typing import TYPE_CHECKING, Any from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri @@ -40,21 +41,20 @@ if TYPE_CHECKING: # pragma: no cover from logging import Logger - from typing import Type from cyclonedx.model.bom import Bom from cyclonedx.model.component import ComponentType - T_NameDict = Dict[str, Any] - T_LockData = Dict[str, List['_LockEntry']] + T_NameDict = dict[str, Any] + T_LockData = dict[str, list['_LockEntry']] @dataclass class _LockEntry: name: str component: Component - dependencies: Dict[str, 'T_NameDict'] # keys MUST go through `normalize_packagename()` - extras: Dict[str, List[str]] # keys MUST go through `normalize_packagename()` + dependencies: dict[str, 'T_NameDict'] # keys MUST go through `normalize_packagename()` + extras: dict[str, list[str]] # keys MUST go through `normalize_packagename()` added2bom: bool @@ -77,13 +77,13 @@ def __str__(self) -> str: @dataclass(frozen=True) class _PoetryPackageRequirement: name: str - extras: Set[str] + extras: set[str] # the pattern is good enough for the job __lock_pattern = re_compile(r'^([a-zA-Z0-9._-]+)(?:\[(.+?)\])?') @classmethod - def from_poetry_lock(cls: 'Type[_PoetryPackageRequirement]', r: str) -> '_PoetryPackageRequirement': + def from_poetry_lock(cls: type['_PoetryPackageRequirement'], r: str) -> '_PoetryPackageRequirement': matches = cls.__lock_pattern.match(r) if matches is None: raise ValueError(f'cannot parse: {r}') @@ -163,9 +163,9 @@ def __init__(self, *, def __call__(self, *, # type:ignore[override] project_directory: str, - groups_without: List[str], groups_with: List[str], groups_only: List[str], + groups_without: list[str], groups_with: list[str], groups_only: list[str], no_dev: bool, - extras: List[str], all_extras: bool, + extras: list[str], all_extras: bool, mc_type: 'ComponentType', **__: Any) -> 'Bom': pyproject_file = join(project_directory, 'pyproject.toml') @@ -248,7 +248,7 @@ def __call__(self, *, # type:ignore[override] ) def _make_bom(self, project: 'T_NameDict', locker: 'T_NameDict', - use_groups: FrozenSet[str], use_extras: FrozenSet[str], + use_groups: frozenset[str], use_extras: frozenset[str], mc_type: 'ComponentType') -> 'Bom': self._logger.debug('use_groups: %r', use_groups) self._logger.debug('use_extras: %r', use_extras) @@ -371,7 +371,7 @@ def __add_dep(self, bom: 'Bom', lock_entry: _LockEntry, use_extras: Iterable[str self.__add_dep(bom, dep_lock_entry, req.extras, lock_data) @staticmethod - def _get_lockfile_version(locker: 'T_NameDict') -> Tuple[int, ...]: + def _get_lockfile_version(locker: 'T_NameDict') -> tuple[int, ...]: return tuple(map(int, locker['metadata'].get('lock-version', '1.0').split('.'))) def _parse_lock(self, locker: 'T_NameDict') -> Generator[_LockEntry, None, None]: diff --git a/cyclonedx_py/_internal/requirements.py b/cyclonedx_py/_internal/requirements.py index a9a78f1a..dd2e5aff 100644 --- a/cyclonedx_py/_internal/requirements.py +++ b/cyclonedx_py/_internal/requirements.py @@ -17,11 +17,12 @@ from argparse import OPTIONAL, ArgumentParser +from collections.abc import Generator, Iterable from functools import reduce from itertools import chain from os import unlink from textwrap import dedent -from typing import TYPE_CHECKING, Any, FrozenSet, Generator, Iterable, Optional +from typing import TYPE_CHECKING, Any, Optional from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri @@ -168,7 +169,7 @@ def __hashes4req(self, req: 'InstallRequirement') -> Generator['HashType', None, del error def _make_component(self, req: 'InstallRequirement', - index_url: str, extra_index_urls: FrozenSet[str]) -> 'Component': + index_url: str, extra_index_urls: frozenset[str]) -> 'Component': name = req.name version = req.get_pinned_version or None hashes = list(self.__hashes4req(req)) diff --git a/cyclonedx_py/_internal/utils/args.py b/cyclonedx_py/_internal/utils/args.py index aa1c7398..3c3c472b 100644 --- a/cyclonedx_py/_internal/utils/args.py +++ b/cyclonedx_py/_internal/utils/args.py @@ -17,13 +17,14 @@ from argparse import ArgumentTypeError +from collections.abc import Callable from enum import Enum -from typing import Callable, List, Type, TypeVar +from typing import TypeVar _E = TypeVar('_E', bound=Enum) -def argparse_type4enum(enum: Type[_E]) -> Callable[[str], _E]: +def argparse_type4enum(enum: type[_E]) -> Callable[[str], _E]: def str2case(value: str) -> _E: try: return enum[value.upper()] @@ -33,12 +34,12 @@ def str2case(value: str) -> _E: return str2case -def choices4enum(enum: Type[Enum]) -> str: +def choices4enum(enum: type[Enum]) -> str: return f'{{choices: {", ".join(sorted(c.name for c in enum))}}}' -def arparse_split(*seps: str) -> Callable[[str], List[str]]: - def str_split(value: str) -> List[str]: +def arparse_split(*seps: str) -> Callable[[str], list[str]]: + def str_split(value: str) -> list[str]: sep = seps[0] for s in seps[1:]: value = value.replace(s, sep) diff --git a/cyclonedx_py/_internal/utils/cdx.py b/cyclonedx_py/_internal/utils/cdx.py index 1f024060..b5100c6c 100644 --- a/cyclonedx_py/_internal/utils/cdx.py +++ b/cyclonedx_py/_internal/utils/cdx.py @@ -20,8 +20,9 @@ CycloneDX related helpers and utils. """ +from collections.abc import Iterable from re import compile as re_compile -from typing import Any, Dict, Iterable, Optional +from typing import Any, Optional from cyclonedx.builder.this import this_component as lib_component from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri @@ -101,7 +102,7 @@ def licenses_fixup(licenses: Iterable['License']) -> Iterable['License']: return licenses -_MAP_KNOWN_URL_LABELS: Dict[str, ExternalReferenceType] = { +_MAP_KNOWN_URL_LABELS: dict[str, ExternalReferenceType] = { # see https://peps.python.org/pep-0345/#project-url-multiple-use # see https://github.com/pypi/warehouse/issues/5947#issuecomment-699660629 'bugtracker': ExternalReferenceType.ISSUE_TRACKER, diff --git a/cyclonedx_py/_internal/utils/packaging.py b/cyclonedx_py/_internal/utils/packaging.py index 07226dad..4b5c7909 100644 --- a/cyclonedx_py/_internal/utils/packaging.py +++ b/cyclonedx_py/_internal/utils/packaging.py @@ -15,8 +15,9 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. +from collections.abc import Generator from re import compile as re_compile -from typing import TYPE_CHECKING, Generator, List +from typing import TYPE_CHECKING from cyclonedx.exception.model import InvalidUriException from cyclonedx.factory.license import LicenseFactory diff --git a/cyclonedx_py/_internal/utils/pep610.py b/cyclonedx_py/_internal/utils/pep610.py index 54c55699..950d6fee 100644 --- a/cyclonedx_py/_internal/utils/pep610.py +++ b/cyclonedx_py/_internal/utils/pep610.py @@ -25,7 +25,7 @@ from abc import ABC, abstractmethod from json import JSONDecodeError, loads as json_loads -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Optional from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, XsUri @@ -53,7 +53,7 @@ def __init__(self, url: str, subdirectory: Optional[str], @classmethod def from_data(cls, url: str, subdirectory: Optional[str], - info: Dict[str, Any]) -> 'PackageSourceVcs': + info: dict[str, Any]) -> 'PackageSourceVcs': return cls(url, subdirectory, info['vcs'], info.get('requested_revision'), info['commit_id']) @@ -62,13 +62,13 @@ class PackageSourceArchive(PackageSource): # see https://packaging.python.org/en/latest/specifications/direct-url-data-structure/#archive-urls def __init__(self, url: str, subdirectory: Optional[str], - hashes: Dict[str, str]) -> None: + hashes: dict[str, str]) -> None: super().__init__(url, subdirectory) self.hashes = hashes @classmethod def from_data(cls, url: str, subdirectory: Optional[str], - info: Dict[str, Any]) -> 'PackageSourceArchive': + info: dict[str, Any]) -> 'PackageSourceArchive': hashes = {} if 'hashes' in info: hashes = info['hashes'] @@ -94,7 +94,7 @@ def __init__(self, url: str, subdirectory: Optional[str], @classmethod def from_data(cls, url: str, subdirectory: Optional[str], - info: Dict[str, Any]) -> 'PackageSourceLocal': + info: dict[str, Any]) -> 'PackageSourceLocal': return cls(url, subdirectory, info.get('editable', False)) diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index f8caf6e9..a128dff9 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -23,9 +23,10 @@ """ from base64 import b64encode +from collections.abc import Generator, Iterable, Iterator from itertools import chain from os.path import dirname, join -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Iterator +from typing import TYPE_CHECKING, Any from cyclonedx.exception.model import InvalidUriException from cyclonedx.factory.license import LicenseFactory @@ -51,7 +52,7 @@ def classifiers2licenses(classifiers: Iterable[str], lfac: 'LicenseFactory', license_acknowledgement=lack) -def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory', *, +def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *, fpath: str) -> Generator['License', None, None]: lack = LicenseAcknowledgement.DECLARED if classifiers := project.get('classifiers'): @@ -88,7 +89,7 @@ def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory', *, yield license -def project2extrefs(project: Dict[str, Any]) -> Generator['ExternalReference', None, None]: +def project2extrefs(project: dict[str, Any]) -> Generator['ExternalReference', None, None]: # see https://packaging.python.org/en/latest/specifications/pyproject-toml/#urls for label, url in project.get('urls', {}).items(): try: @@ -100,7 +101,7 @@ def project2extrefs(project: Dict[str, Any]) -> Generator['ExternalReference', N pass -def project2component(project: Dict[str, Any], *, +def project2component(project: dict[str, Any], *, ctype: 'ComponentType', fpath: str) -> 'Component': dynamic = project.get('dynamic', ()) return Component( @@ -114,7 +115,7 @@ def project2component(project: Dict[str, Any], *, ) -def project2dependencies(project: Dict[str, Any]) -> Iterator['Requirement']: +def project2dependencies(project: dict[str, Any]) -> Iterator['Requirement']: return ( Requirement(dep) for dep in chain( diff --git a/cyclonedx_py/_internal/utils/pep639.py b/cyclonedx_py/_internal/utils/pep639.py index c9cb53ca..57b41d4d 100644 --- a/cyclonedx_py/_internal/utils/pep639.py +++ b/cyclonedx_py/_internal/utils/pep639.py @@ -22,8 +22,9 @@ """ from base64 import b64encode +from collections.abc import Generator from os.path import join -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING from cyclonedx.factory.license import LicenseFactory from cyclonedx.model import AttachedText, Encoding diff --git a/cyclonedx_py/_internal/utils/poetry.py b/cyclonedx_py/_internal/utils/poetry.py index b09710b4..19cb8826 100644 --- a/cyclonedx_py/_internal/utils/poetry.py +++ b/cyclonedx_py/_internal/utils/poetry.py @@ -21,8 +21,9 @@ See https://python-poetry.org/docs/pyproject/ """ +from collections.abc import Generator from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, List +from typing import TYPE_CHECKING, Any from cyclonedx.exception.model import InvalidUriException from cyclonedx.factory.license import LicenseFactory @@ -39,7 +40,7 @@ from cyclonedx.model.license import License -def poetry2extrefs(poetry: Dict[str, Any]) -> Generator['ExternalReference', None, None]: +def poetry2extrefs(poetry: dict[str, Any]) -> Generator['ExternalReference', None, None]: for ers, ert in ( ('homepage', ExternalReferenceType.WEBSITE), ('repository', ExternalReferenceType.VCS), @@ -62,8 +63,8 @@ def poetry2extrefs(poetry: Dict[str, Any]) -> Generator['ExternalReference', Non pass -def poetry2component(poetry: Dict[str, Any], *, ctype: 'ComponentType') -> 'Component': - licenses: List['License'] = [] +def poetry2component(poetry: dict[str, Any], *, ctype: 'ComponentType') -> 'Component': + licenses: list['License'] = [] lfac = LicenseFactory() lack = LicenseAcknowledgement.DECLARED if 'classifiers' in poetry: @@ -85,7 +86,7 @@ def poetry2component(poetry: Dict[str, Any], *, ctype: 'ComponentType') -> 'Comp ) -def poetry2dependencies(poetry: Dict[str, Any]) -> Generator['Requirement', None, None]: +def poetry2dependencies(poetry: dict[str, Any]) -> Generator['Requirement', None, None]: for name, spec in chain( poetry.get('dependencies', {}).items(), diff --git a/cyclonedx_py/_internal/utils/pyproject.py b/cyclonedx_py/_internal/utils/pyproject.py index ee04726f..eccb47a2 100644 --- a/cyclonedx_py/_internal/utils/pyproject.py +++ b/cyclonedx_py/_internal/utils/pyproject.py @@ -19,7 +19,8 @@ # use pyproject from pep621 # use pyproject from poetry implementation -from typing import TYPE_CHECKING, Any, Dict, Iterator +from collections.abc import Iterator +from typing import TYPE_CHECKING, Any from .pep621 import project2component, project2dependencies from .poetry import poetry2component, poetry2dependencies @@ -30,7 +31,7 @@ from packaging.requirements import Requirement -def pyproject2component(data: Dict[str, Any], *, +def pyproject2component(data: dict[str, Any], *, ctype: 'ComponentType', fpath: str) -> 'Component': tool = data.get('tool', {}) if poetry := tool.get('poetry'): @@ -40,7 +41,7 @@ def pyproject2component(data: Dict[str, Any], *, raise ValueError('Unable to build component from pyproject') -def pyproject_load(pyproject_file: str) -> Dict[str, Any]: +def pyproject_load(pyproject_file: str) -> dict[str, Any]: try: pyproject_fh = open(pyproject_file, 'rt', encoding='utf8', errors='replace') except OSError as err: @@ -57,7 +58,7 @@ def pyproject_file2component(pyproject_file: str, *, ) -def pyproject2dependencies(data: Dict[str, Any]) -> Iterator['Requirement']: +def pyproject2dependencies(data: dict[str, Any]) -> Iterator['Requirement']: tool = data.get('tool', {}) if 'poetry' in tool: return poetry2dependencies(tool['poetry']) diff --git a/tests/integration/test_cli_environment.py b/tests/integration/test_cli_environment.py index 09e8b13e..31ddff32 100644 --- a/tests/integration/test_cli_environment.py +++ b/tests/integration/test_cli_environment.py @@ -16,12 +16,13 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. import random +from collections.abc import Generator from glob import glob from os import name as os_name from os.path import basename, dirname, join from subprocess import run # nosec:B404 from sys import executable, stderr -from typing import Any, Generator +from typing import Any from unittest import TestCase, skipIf from cyclonedx.schema import OutputFormat, SchemaVersion diff --git a/tests/integration/test_cli_pipenv.py b/tests/integration/test_cli_pipenv.py index 317bff8a..559ee875 100644 --- a/tests/integration/test_cli_pipenv.py +++ b/tests/integration/test_cli_pipenv.py @@ -17,9 +17,10 @@ import random +from collections.abc import Generator from glob import glob from os.path import basename, dirname, join -from typing import Any, Generator +from typing import Any from unittest import TestCase from cyclonedx.schema import OutputFormat, SchemaVersion diff --git a/tests/integration/test_cli_poetry.py b/tests/integration/test_cli_poetry.py index 02f19675..097d4a5b 100644 --- a/tests/integration/test_cli_poetry.py +++ b/tests/integration/test_cli_poetry.py @@ -17,9 +17,10 @@ import random +from collections.abc import Generator from glob import glob from os.path import basename, dirname, join -from typing import Any, Generator +from typing import Any from unittest import TestCase from cyclonedx.schema import OutputFormat, SchemaVersion diff --git a/tests/integration/test_cli_requirements.py b/tests/integration/test_cli_requirements.py index 454483d7..f47b38d7 100644 --- a/tests/integration/test_cli_requirements.py +++ b/tests/integration/test_cli_requirements.py @@ -18,9 +18,10 @@ import os import random +from collections.abc import Generator from glob import glob from os.path import basename, join, splitext -from typing import Any, Generator, Tuple +from typing import Any from unittest import TestCase from cyclonedx.schema import OutputFormat, SchemaVersion @@ -48,7 +49,7 @@ def test_data_file_filter(s: str) -> Generator[Any, None, None]: def test_data_os_filter(data: Any) -> bool: return True else: - def test_data_os_filter(data: Tuple[Any, str, Any, Any]) -> bool: + def test_data_os_filter(data: tuple[Any, str, Any, Any]) -> bool: # skip windows encoded files on non-windows return '.cp125' not in data[1] diff --git a/tests/unit/test_utils_cdx.py b/tests/unit/test_utils_cdx.py index d7464622..8c7471bd 100644 --- a/tests/unit/test_utils_cdx.py +++ b/tests/unit/test_utils_cdx.py @@ -16,7 +16,8 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. -from typing import Any, Dict, Iterable, Tuple, Union +from collections.abc import Iterable +from typing import Any, Union from unittest import TestCase from cyclonedx.model import ExternalReference, ExternalReferenceType @@ -35,7 +36,7 @@ def __first_ers_uri(t: ExternalReferenceType, ers: Iterable[ExternalReference]) def assertExtRefs( # noqa:N802 self: Union[TestCase, 'ExtRefsTestMixin'], - p: Dict[str, Any], ers: Iterable[ExternalReference] + p: dict[str, Any], ers: Iterable[ExternalReference] ) -> None: self.assertEqual(p['tool']['poetry']['homepage'], self.__first_ers_uri( ExternalReferenceType.WEBSITE, ers)) @@ -66,7 +67,7 @@ def test_basics(self) -> None: def test_license(self) -> None: p = load_pyproject() c = self.__get_c_by_name(EXPECTED_TOOL_NAME) - ls: Tuple[License, ...] = tuple(c.licenses) + ls: tuple[License, ...] = tuple(c.licenses) self.assertEqual(1, len(ls)) l = ls[0] # noqa:E741 self.assertIs(LicenseAcknowledgement.DECLARED, l.acknowledgement) @@ -76,5 +77,5 @@ def test_license(self) -> None: def test_extrefs(self) -> None: p = load_pyproject() c = self.__get_c_by_name(EXPECTED_TOOL_NAME) - ers: Tuple[ExternalReference, ...] = tuple(c.external_references) + ers: tuple[ExternalReference, ...] = tuple(c.external_references) self.assertExtRefs(p, ers) From c130558f882bd7baa04a81332fe0cd079b59c9a3 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 23 Apr 2025 13:55:02 +0200 Subject: [PATCH 4/4] wip Signed-off-by: Jan Kowalleck --- cyclonedx_py/_internal/utils/packaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyclonedx_py/_internal/utils/packaging.py b/cyclonedx_py/_internal/utils/packaging.py index 4b5c7909..5b664bb3 100644 --- a/cyclonedx_py/_internal/utils/packaging.py +++ b/cyclonedx_py/_internal/utils/packaging.py @@ -43,7 +43,7 @@ def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None, lack = LicenseAcknowledgement.DECLARED if 'Classifier' in metadata: # see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use - classifiers: List[str] = metadata.get_all('Classifier') # type:ignore[assignment] + classifiers: list[str] = metadata.get_all('Classifier') # type:ignore[assignment] yield from classifiers2licenses(classifiers, lfac, lack) for mlicense in set(metadata.get_all('License', ())): # see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#license