Skip to content

feat!: drop support for Python <3.9 #809

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
toxenv-factors: '-current'
- # test with the lowest dependencies
os: ubuntu-latest
python-version: '3.8'
python-version: '3.9'
toxenv-factors: '-lowest'
steps:
- name: Checkout
Expand Down Expand Up @@ -117,15 +117,14 @@ jobs:
matrix:
os:
- ubuntu-latest
- macos-13 # macos-latest might be incompatible to py38 - see https://github.com/CycloneDX/cyclonedx-python-lib/pull/599#issuecomment-2077462142
- macos-13 # macos-latest might be incompatible to py310 - see https://github.com/CycloneDX/cyclonedx-python-lib/pull/599#issuecomment-2077462142
- windows-latest
python-version:
- "3.13" # highest supported
- "3.12"
- "3.11"
- "3.10"
- "3.9"
- "3.8" # lowest supported
- "3.9" # lowest supported
toxenv-factors:
- '-allExtras'
- '-noExtras'
Expand Down Expand Up @@ -219,7 +218,7 @@ jobs:
# see https://github.com/actions/setup-python
uses: actions/setup-python@v5
with:
python-version: '>=3.8 <=3.13' # supported version range
python-version: '>=3.9 <=3.13' # supported version range
- name: Validate Python Environment
shell: python
run: |
Expand Down
6 changes: 3 additions & 3 deletions cyclonedx/_internal/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
"""

from itertools import zip_longest
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
from typing import TYPE_CHECKING, Any, Optional

if TYPE_CHECKING: # pragma: no cover
from packageurl import PackageURL


class ComparableTuple(Tuple[Optional[Any], ...]):
class ComparableTuple(tuple[Optional[Any], ...]):
"""
Allows comparison of tuples, allowing for None values.
"""
Expand Down Expand Up @@ -63,7 +63,7 @@ class ComparableDict(ComparableTuple):
Allows comparison of dictionaries, allowing for missing/None values.
"""

def __new__(cls, d: Dict[Any, Any]) -> 'ComparableDict':
def __new__(cls, d: dict[Any, Any]) -> 'ComparableDict':
return super(ComparableDict, cls).__new__(cls, sorted(d.items()))


Expand Down
29 changes: 15 additions & 14 deletions cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
"""

import re
from collections.abc import Generator, Iterable
from datetime import datetime
from enum import Enum
from functools import reduce
from json import loads as json_loads
from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type, Union
from typing import Any, Optional, Union
from urllib.parse import quote as url_quote
from uuid import UUID
from warnings import warn
Expand Down Expand Up @@ -280,7 +281,7 @@ class HashAlgorithm(str, Enum):
class _HashTypeRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

__CASES: Dict[Type[serializable.ViewType], FrozenSet[HashAlgorithm]] = dict()
__CASES: dict[type[serializable.ViewType], frozenset[HashAlgorithm]] = dict()
__CASES[SchemaVersion1Dot0] = frozenset({
HashAlgorithm.MD5,
HashAlgorithm.SHA_1,
Expand All @@ -304,7 +305,7 @@ class _HashTypeRepositorySerializationHelper(serializable.helpers.BaseHelper):
__CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5]

@classmethod
def __prep(cls, hts: Iterable['HashType'], view: Type[serializable.ViewType]) -> Generator['HashType', None, None]:
def __prep(cls, hts: Iterable['HashType'], view: type[serializable.ViewType]) -> Generator['HashType', None, None]:
cases = cls.__CASES.get(view, ())
for ht in hts:
if ht.alg in cases:
Expand All @@ -315,8 +316,8 @@ def __prep(cls, hts: Iterable['HashType'], view: Type[serializable.ViewType]) ->

@classmethod
def json_normalize(cls, o: Iterable['HashType'], *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> List[Any]:
view: Optional[type[serializable.ViewType]],
**__: Any) -> list[Any]:
assert view is not None
return [
json_loads(
Expand All @@ -328,7 +329,7 @@ def json_normalize(cls, o: Iterable['HashType'], *,
@classmethod
def xml_normalize(cls, o: Iterable['HashType'], *,
element_name: str,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
xmlns: Optional[str],
**__: Any) -> XmlElement:
assert view is not None
Expand All @@ -342,7 +343,7 @@ def xml_normalize(cls, o: Iterable['HashType'], *,

@classmethod
def json_denormalize(cls, o: Any,
**__: Any) -> List['HashType']:
**__: Any) -> list['HashType']:
return [
HashType.from_json( # type:ignore[attr-defined]
ht) for ht in o
Expand All @@ -351,14 +352,14 @@ def json_denormalize(cls, o: Any,
@classmethod
def xml_denormalize(cls, o: 'XmlElement', *,
default_ns: Optional[str],
**__: Any) -> List['HashType']:
**__: Any) -> list['HashType']:
return [
HashType.from_xml( # type:ignore[attr-defined]
ht, default_ns) for ht in o
]


_MAP_HASHLIB: Dict[str, HashAlgorithm] = {
_MAP_HASHLIB: dict[str, HashAlgorithm] = {
# from hashlib.algorithms_guaranteed
'md5': HashAlgorithm.MD5,
'sha1': HashAlgorithm.SHA_1,
Expand Down Expand Up @@ -593,7 +594,7 @@ class ExternalReferenceType(str, Enum):
class _ExternalReferenceSerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

__CASES: Dict[Type[serializable.ViewType], FrozenSet[ExternalReferenceType]] = dict()
__CASES: dict[type[serializable.ViewType], frozenset[ExternalReferenceType]] = dict()
__CASES[SchemaVersion1Dot1] = frozenset({
ExternalReferenceType.VCS,
ExternalReferenceType.ISSUE_TRACKER,
Expand Down Expand Up @@ -649,7 +650,7 @@ class _ExternalReferenceSerializationHelper(serializable.helpers.BaseHelper):
}

@classmethod
def __normalize(cls, extref: ExternalReferenceType, view: Type[serializable.ViewType]) -> str:
def __normalize(cls, extref: ExternalReferenceType, view: type[serializable.ViewType]) -> str:
return (
extref
if extref in cls.__CASES.get(view, ())
Expand All @@ -658,14 +659,14 @@ def __normalize(cls, extref: ExternalReferenceType, view: Type[serializable.View

@classmethod
def json_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> str:
assert view is not None
return cls.__normalize(o, view)

@classmethod
def xml_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> str:
assert view is not None
return cls.__normalize(o, view)
Expand Down Expand Up @@ -703,7 +704,7 @@ class XsUri(serializable.helpers.BaseHelper):
)

@staticmethod
def __spec_replace(v: str, r: Tuple[str, str]) -> str:
def __spec_replace(v: str, r: tuple[str, str]) -> str:
return v.replace(*r)

@classmethod
Expand Down
3 changes: 2 additions & 1 deletion cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from collections.abc import Generator, Iterable
from datetime import datetime
from itertools import chain
from typing import TYPE_CHECKING, Generator, Iterable, Optional, Union
from typing import TYPE_CHECKING, Optional, Union
from uuid import UUID, uuid4
from warnings import warn

Expand Down
4 changes: 2 additions & 2 deletions cyclonedx/model/bom_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException

if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar
from typing import TypeVar

_T_BR = TypeVar('_T_BR', bound='BomRef')

Expand Down Expand Up @@ -90,7 +90,7 @@ def serialize(cls, o: Any) -> Optional[str]:
f'Attempt to serialize a non-BomRef: {o!r}')

@classmethod
def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR':
def deserialize(cls: 'type[_T_BR]', o: Any) -> '_T_BR':
try:
return cls(value=str(o))
except ValueError as err:
Expand Down
21 changes: 11 additions & 10 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

import re
from collections.abc import Iterable
from enum import Enum
from os.path import exists
from typing import Any, Dict, FrozenSet, Iterable, Optional, Set, Type, Union
from typing import Any, Optional, Union
from warnings import warn

# See https://github.com/package-url/packageurl-python/issues/65
Expand Down Expand Up @@ -309,7 +310,7 @@ class ComponentScope(str, Enum):
class _ComponentScopeSerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

__CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentScope]] = dict()
__CASES: dict[type[serializable.ViewType], frozenset[ComponentScope]] = dict()
__CASES[SchemaVersion1Dot0] = frozenset({
ComponentScope.REQUIRED,
ComponentScope.OPTIONAL,
Expand All @@ -324,21 +325,21 @@ class _ComponentScopeSerializationHelper(serializable.helpers.BaseHelper):
__CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5]

@classmethod
def __normalize(cls, cs: ComponentScope, view: Type[serializable.ViewType]) -> Optional[str]:
def __normalize(cls, cs: ComponentScope, view: type[serializable.ViewType]) -> Optional[str]:
return cs.value \
if cs in cls.__CASES.get(view, ()) \
else None

@classmethod
def json_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)

@classmethod
def xml_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
Expand Down Expand Up @@ -375,7 +376,7 @@ class ComponentType(str, Enum):
class _ComponentTypeSerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

__CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentType]] = dict()
__CASES: dict[type[serializable.ViewType], frozenset[ComponentType]] = dict()
__CASES[SchemaVersion1Dot0] = frozenset({
ComponentType.APPLICATION,
ComponentType.DEVICE,
Expand Down Expand Up @@ -403,21 +404,21 @@ class _ComponentTypeSerializationHelper(serializable.helpers.BaseHelper):
}

@classmethod
def __normalize(cls, ct: ComponentType, view: Type[serializable.ViewType]) -> Optional[str]:
def __normalize(cls, ct: ComponentType, view: type[serializable.ViewType]) -> Optional[str]:
if ct in cls.__CASES.get(view, ()):
return ct.value
raise SerializationOfUnsupportedComponentTypeException(f'unsupported {ct!r} for view {view!r}')

@classmethod
def json_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)

@classmethod
def xml_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
view: Optional[type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
Expand Down Expand Up @@ -1734,7 +1735,7 @@ def tags(self) -> 'SortedSet[str]':
def tags(self, tags: Iterable[str]) -> None:
self._tags = SortedSet(tags)

def get_all_nested_components(self, include_self: bool = False) -> Set['Component']:
def get_all_nested_components(self, include_self: bool = False) -> set['Component']:
components = set()
if include_self:
components.add(self)
Expand Down
3 changes: 2 additions & 1 deletion cyclonedx/model/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from typing import Any, Iterable, Optional, Union
from collections.abc import Iterable
from typing import Any, Optional, Union

import py_serializable as serializable
from sortedcontainers import SortedSet
Expand Down
3 changes: 2 additions & 1 deletion cyclonedx/model/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.6/#type_cryptoPropertiesType
"""

from collections.abc import Iterable
from datetime import datetime
from enum import Enum
from typing import Any, Iterable, Optional
from typing import Any, Optional

import py_serializable as serializable
from sortedcontainers import SortedSet
Expand Down
7 changes: 4 additions & 3 deletions cyclonedx/model/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

import re
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Optional, Union

import py_serializable as serializable
from sortedcontainers import SortedSet
Expand All @@ -29,7 +30,7 @@
from .bom_ref import BomRef

if TYPE_CHECKING: # pragma: no cover
from typing import Type, TypeVar
from typing import TypeVar

_T_CreId = TypeVar('_T_CreId', bound='CreId')

Expand Down Expand Up @@ -65,7 +66,7 @@ def serialize(cls, o: Any) -> str:
f'Attempt to serialize a non-CreId: {o!r}')

@classmethod
def deserialize(cls: 'Type[_T_CreId]', o: Any) -> '_T_CreId':
def deserialize(cls: 'type[_T_CreId]', o: Any) -> '_T_CreId':
return cls(id=str(o))

def __eq__(self, other: Any) -> bool:
Expand Down
9 changes: 5 additions & 4 deletions cyclonedx/model/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@


from abc import ABC, abstractmethod
from typing import Any, Iterable, List, Optional, Set
from collections.abc import Iterable
from typing import Any, Optional

import py_serializable as serializable
from sortedcontainers import SortedSet
Expand All @@ -31,14 +32,14 @@ class _DependencyRepositorySerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """

@classmethod
def serialize(cls, o: Any) -> List[str]:
def serialize(cls, o: Any) -> list[str]:
if isinstance(o, (SortedSet, set)):
return [str(i.ref) for i in o]
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-DependencyRepository: {o!r}')

@classmethod
def deserialize(cls, o: Any) -> Set['Dependency']:
def deserialize(cls, o: Any) -> set['Dependency']:
dependencies = set()
if isinstance(o, list):
for v in o:
Expand Down Expand Up @@ -80,7 +81,7 @@ def dependencies(self) -> 'SortedSet[Dependency]':
def dependencies(self, dependencies: Iterable['Dependency']) -> None:
self._dependencies = SortedSet(dependencies)

def dependencies_as_bom_refs(self) -> Set[BomRef]:
def dependencies_as_bom_refs(self) -> set[BomRef]:
return set(map(lambda d: d.ref, self.dependencies))

def __comparable_tuple(self) -> _ComparableTuple:
Expand Down
3 changes: 2 additions & 1 deletion cyclonedx/model/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from collections.abc import Iterable
from enum import Enum
from typing import Any, Iterable, Optional
from typing import Any, Optional

import py_serializable as serializable
from sortedcontainers import SortedSet
Expand Down
Loading