Skip to content

Commit 9a5e6d8

Browse files
authored
feat!: drop support for python <3.9 (#883)
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 1293b7e commit 9a5e6d8

23 files changed

+113
-121
lines changed

.github/workflows/python.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
- python-version: '3.13' # latest
7878
os: ubuntu-latest
7979
toxenv-factors: '-current'
80-
- python-version: '3.8' # lowest
80+
- python-version: '3.9' # lowest
8181
os: ubuntu-latest
8282
toxenv-factors: '-lowest'
8383
steps:
@@ -169,8 +169,7 @@ jobs:
169169
- "3.12"
170170
- "3.11"
171171
- "3.10"
172-
- "3.9"
173-
- "3.8" # lowest supported -- handled in include
172+
- "3.9" # lowest supported -- handled in include
174173
steps:
175174
- name: Checkout
176175
# see https://github.com/actions/checkout

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Read the full [documentation][link_rtfd] for more details.
4343

4444
## Requirements
4545

46-
* Python `>=3.8,<4`
46+
* Python `>=3.9,<4`
4747

4848
However, there are older versions of this tool available, which
4949
support Python `>=2.7`.

cyclonedx_py/_internal/cli.py

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717

1818
import logging
1919
import sys
20-
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
20+
from argparse import ArgumentParser, BooleanOptionalAction, FileType, RawDescriptionHelpFormatter
21+
from collections.abc import Sequence
2122
from itertools import chain
22-
from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Sequence, TextIO, Type, Union
23+
from typing import TYPE_CHECKING, Any, NoReturn, Optional, TextIO, Union
2324

2425
from cyclonedx.model import Property
2526
from cyclonedx.output import make_outputter
@@ -35,20 +36,11 @@
3536
from .utils.args import argparse_type4enum, choices4enum
3637

3738
if TYPE_CHECKING: # pragma: no cover
38-
from argparse import Action
39-
4039
from cyclonedx.model.bom import Bom
4140
from cyclonedx.model.component import Component
4241

4342
from . import BomBuilder
4443

45-
BooleanOptionalAction: Optional[Type[Action]]
46-
47-
if sys.version_info >= (3, 9):
48-
from argparse import BooleanOptionalAction
49-
else:
50-
BooleanOptionalAction = None
51-
5244
OPTION_OUTPUT_STDOUT = '-'
5345

5446

@@ -121,29 +113,16 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar
121113
type=FileType('wt', encoding='utf8'),
122114
dest='output_file',
123115
default=OPTION_OUTPUT_STDOUT)
124-
if BooleanOptionalAction:
125-
op.add_argument('--validate',
126-
help='Whether to validate resulting BOM before outputting.'
127-
' (default: %(default)s)',
128-
action=BooleanOptionalAction,
129-
dest='should_validate',
130-
default=True)
131-
else:
132-
vg = op.add_mutually_exclusive_group()
133-
vg.add_argument('--validate',
134-
help='Validate resulting BOM before outputting.'
135-
' (default: %(default)s)',
136-
action='store_true',
137-
dest='should_validate',
138-
default=True)
139-
vg.add_argument('--no-validate',
140-
help='Disable validation of resulting BOM.',
141-
dest='should_validate',
142-
action='store_false')
143-
144-
scbbc: Type['BomBuilder']
116+
op.add_argument('--validate',
117+
help='Whether to validate resulting BOM before outputting.'
118+
' (default: %(default)s)',
119+
action=BooleanOptionalAction,
120+
dest='should_validate',
121+
default=True)
122+
123+
scbbc: type['BomBuilder']
145124
sct: str
146-
scta: List[str]
125+
scta: list[str]
147126
for scbbc, sct, *scta in (
148127
(EnvironmentBB, 'environment', 'env', 'venv'),
149128
(RequirementsBB, 'requirements'),
@@ -171,7 +150,7 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar
171150
}
172151

173152
@classmethod
174-
def _clean_kwargs(cls, kwargs: Dict[str, Any]) -> Dict[str, Any]:
153+
def _clean_kwargs(cls, kwargs: dict[str, Any]) -> dict[str, Any]:
175154
return {k: kwargs[k] for k in kwargs if k not in cls.__OWN_ARGS}
176155

177156
def __init__(self, *,
@@ -181,7 +160,7 @@ def __init__(self, *,
181160
spec_version: SchemaVersion,
182161
output_reproducible: bool,
183162
should_validate: bool,
184-
_bbc: Type['BomBuilder'],
163+
_bbc: type['BomBuilder'],
185164
**kwargs: Any) -> None:
186165
self._logger = logger
187166
self._short_purls = short_purls

cyclonedx_py/_internal/environment.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717

1818

1919
from argparse import OPTIONAL, ArgumentParser
20+
from collections.abc import Iterable
2021
from importlib.metadata import distributions
2122
from json import loads
2223
from os import getcwd, name as os_name
2324
from os.path import exists, isdir, join
2425
from subprocess import run # nosec
2526
from sys import path as sys_path
2627
from textwrap import dedent
27-
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
28+
from typing import TYPE_CHECKING, Any, Optional
2829

2930
from cyclonedx.model import Property
3031
from cyclonedx.model.component import Component, ComponentEvidence, ComponentType
@@ -46,7 +47,7 @@
4647

4748
from .utils.pep610 import PackageSource
4849

49-
T_AllComponents = Dict[str, Tuple['Component', Iterable[Requirement]]]
50+
T_AllComponents = dict[str, tuple['Component', Iterable[Requirement]]]
5051

5152

5253
class EnvironmentBB(BomBuilder):
@@ -155,7 +156,7 @@ def __call__(self, *, # type:ignore[override]
155156
root_d = tuple(pyproject2dependencies(pyproject))
156157
rc = (root_c, root_d)
157158

158-
path: List[str]
159+
path: list[str]
159160
if python:
160161
path = self.__path4python(python)
161162
else:
@@ -168,7 +169,7 @@ def __call__(self, *, # type:ignore[override]
168169
return bom
169170

170171
def __add_components(self, bom: 'Bom',
171-
rc: Optional[Tuple['Component', Iterable['Requirement']]],
172+
rc: Optional[tuple['Component', Iterable['Requirement']]],
172173
**kwargs: Any) -> None:
173174
all_components: 'T_AllComponents' = {}
174175
self._logger.debug('distribution context args: %r', kwargs)
@@ -229,7 +230,7 @@ def __add_components(self, bom: 'Bom',
229230

230231
def __finalize_dependencies(self, bom: 'Bom', all_components: 'T_AllComponents') -> None:
231232
for component, requires in all_components.values():
232-
component_deps: List[Component] = []
233+
component_deps: list[Component] = []
233234
for req in requires:
234235
req_component: Optional[Component] = all_components.get(normalize_packagename(req.name), (None,))[0]
235236
if req_component is None:
@@ -297,7 +298,7 @@ def __py_interpreter(value: str) -> str:
297298
raise ValueError(f'Failed to find python in directory: {value}')
298299
return value
299300

300-
def __path4python(self, python: str) -> List[str]:
301+
def __path4python(self, python: str) -> list[str]:
301302
cmd = self.__py_interpreter(python), '-c', 'import json,sys;json.dump(sys.path,sys.stdout)'
302303
self._logger.debug('fetch `path` from python interpreter cmd: %r', cmd)
303304
res = run(cmd, capture_output=True, encoding='utf8', shell=False) # nosec

cyclonedx_py/_internal/pipenv.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717

1818

1919
from argparse import OPTIONAL, ArgumentParser
20+
from collections.abc import Generator
2021
from json import loads as json_loads
2122
from os import getenv
2223
from os.path import join
2324
from textwrap import dedent
24-
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, Optional, Set, Tuple
25+
from typing import TYPE_CHECKING, Any, Optional
2526

2627
from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException
2728
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri
@@ -41,7 +42,7 @@
4142

4243
from cyclonedx.model.bom import Bom
4344

44-
NameDict = Dict[str, Any]
45+
NameDict = dict[str, Any]
4546

4647

4748
class PipenvBB(BomBuilder):
@@ -96,15 +97,15 @@ def __init__(self, *,
9697

9798
def __call__(self, *, # type:ignore[override]
9899
project_directory: str,
99-
categories: List[str],
100+
categories: list[str],
100101
dev: bool,
101102
pyproject_file: Optional[str],
102103
mc_type: 'ComponentType',
103104
**__: Any) -> 'Bom':
104105

105106
# the group-args shall mimic the ones from Pipenv, which uses (comma and/or space)-separated lists
106107
# values be like: 'foo bar,bazz' -> ['foo', 'bar', 'bazz']
107-
lock_groups: Set[str] = set()
108+
lock_groups: set[str] = set()
108109
if len(categories) == 0:
109110
lock_groups.add('default')
110111
if dev:
@@ -138,22 +139,22 @@ def __call__(self, *, # type:ignore[override]
138139
frozenset(lock_groups))
139140

140141
def _make_bom(self, root_c: Optional['Component'],
141-
locker: 'NameDict', use_groups: FrozenSet[str]) -> 'Bom':
142+
locker: 'NameDict', use_groups: frozenset[str]) -> 'Bom':
142143
self._logger.debug('use_groups: %r', use_groups)
143144

144145
bom = make_bom()
145146
bom.metadata.component = root_c
146147
self._logger.debug('root-component: %r', root_c)
147148

148149
meta: NameDict = locker[self.__LOCKFILE_META]
149-
source_urls: Dict[str, str] = {
150+
source_urls: dict[str, str] = {
150151
source['name']: redact_auth_from_url(source['url']).rstrip('/')
151152
for source in meta.get('sources', ())
152153
}
153154
if self._pypi_url is not None:
154155
source_urls['pypi'] = redact_auth_from_url(self._pypi_url).rstrip('/')
155156

156-
all_components: Dict[str, Component] = {}
157+
all_components: dict[str, Component] = {}
157158
if root_c:
158159
# root for possible self-installs
159160
all_components[normalize_packagename(root_c.name)] = root_c
@@ -218,7 +219,7 @@ def __is_local(self, data: 'NameDict') -> bool:
218219
see https://pip.pypa.io/en/latest/topics/vcs-support/#vcs-support
219220
"""
220221

221-
def __package_vcs(self, data: 'NameDict') -> Optional[Tuple[str, str]]:
222+
def __package_vcs(self, data: 'NameDict') -> Optional[tuple[str, str]]:
222223
for vct in self.__VCS_TYPES:
223224
if vct in data:
224225
url: str = data[vct]
@@ -227,7 +228,7 @@ def __package_vcs(self, data: 'NameDict') -> Optional[Tuple[str, str]]:
227228
return vct, url[:hash_pos] if hash_pos >= 0 else url
228229
return None
229230

230-
def __make_extrefs(self, name: str, data: 'NameDict', source_urls: Dict[str, str]
231+
def __make_extrefs(self, name: str, data: 'NameDict', source_urls: dict[str, str]
231232
) -> Generator['ExternalReference', None, None]:
232233
hashes = (HashType.from_composite_str(package_hash)
233234
for package_hash
@@ -267,7 +268,7 @@ def __make_extrefs(self, name: str, data: 'NameDict', source_urls: Dict[str, str
267268
except (InvalidUriException, UnknownHashTypeException, KeyError) as error: # pragma: nocover
268269
self._logger.debug('skipped dist-extRef for: %r', name, exc_info=error)
269270

270-
def __purl_qualifiers4lock(self, data: 'NameDict', sourcees: Dict[str, str]) -> 'NameDict':
271+
def __purl_qualifiers4lock(self, data: 'NameDict', sourcees: dict[str, str]) -> 'NameDict':
271272
# see https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst
272273
qs = {}
273274
vcs_source = self.__package_vcs(data)

cyclonedx_py/_internal/poetry.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818

1919
from argparse import OPTIONAL, ArgumentParser
20+
from collections.abc import Generator, Iterable
2021
from dataclasses import dataclass
2122
from itertools import chain
2223
from os.path import join
2324
from re import compile as re_compile
2425
from textwrap import dedent
25-
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, Iterable, List, Set, Tuple
26+
from typing import TYPE_CHECKING, Any
2627

2728
from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException
2829
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri
@@ -40,21 +41,20 @@
4041

4142
if TYPE_CHECKING: # pragma: no cover
4243
from logging import Logger
43-
from typing import Type
4444

4545
from cyclonedx.model.bom import Bom
4646
from cyclonedx.model.component import ComponentType
4747

48-
T_NameDict = Dict[str, Any]
49-
T_LockData = Dict[str, List['_LockEntry']]
48+
T_NameDict = dict[str, Any]
49+
T_LockData = dict[str, list['_LockEntry']]
5050

5151

5252
@dataclass
5353
class _LockEntry:
5454
name: str
5555
component: Component
56-
dependencies: Dict[str, 'T_NameDict'] # keys MUST go through `normalize_packagename()`
57-
extras: Dict[str, List[str]] # keys MUST go through `normalize_packagename()`
56+
dependencies: dict[str, 'T_NameDict'] # keys MUST go through `normalize_packagename()`
57+
extras: dict[str, list[str]] # keys MUST go through `normalize_packagename()`
5858
added2bom: bool
5959

6060

@@ -77,13 +77,13 @@ def __str__(self) -> str:
7777
@dataclass(frozen=True)
7878
class _PoetryPackageRequirement:
7979
name: str
80-
extras: Set[str]
80+
extras: set[str]
8181

8282
# the pattern is good enough for the job
8383
__lock_pattern = re_compile(r'^([a-zA-Z0-9._-]+)(?:\[(.+?)\])?')
8484

8585
@classmethod
86-
def from_poetry_lock(cls: 'Type[_PoetryPackageRequirement]', r: str) -> '_PoetryPackageRequirement':
86+
def from_poetry_lock(cls: type['_PoetryPackageRequirement'], r: str) -> '_PoetryPackageRequirement':
8787
matches = cls.__lock_pattern.match(r)
8888
if matches is None:
8989
raise ValueError(f'cannot parse: {r}')
@@ -163,9 +163,9 @@ def __init__(self, *,
163163

164164
def __call__(self, *, # type:ignore[override]
165165
project_directory: str,
166-
groups_without: List[str], groups_with: List[str], groups_only: List[str],
166+
groups_without: list[str], groups_with: list[str], groups_only: list[str],
167167
no_dev: bool,
168-
extras: List[str], all_extras: bool,
168+
extras: list[str], all_extras: bool,
169169
mc_type: 'ComponentType',
170170
**__: Any) -> 'Bom':
171171
pyproject_file = join(project_directory, 'pyproject.toml')
@@ -248,7 +248,7 @@ def __call__(self, *, # type:ignore[override]
248248
)
249249

250250
def _make_bom(self, project: 'T_NameDict', locker: 'T_NameDict',
251-
use_groups: FrozenSet[str], use_extras: FrozenSet[str],
251+
use_groups: frozenset[str], use_extras: frozenset[str],
252252
mc_type: 'ComponentType') -> 'Bom':
253253
self._logger.debug('use_groups: %r', use_groups)
254254
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
371371
self.__add_dep(bom, dep_lock_entry, req.extras, lock_data)
372372

373373
@staticmethod
374-
def _get_lockfile_version(locker: 'T_NameDict') -> Tuple[int, ...]:
374+
def _get_lockfile_version(locker: 'T_NameDict') -> tuple[int, ...]:
375375
return tuple(map(int, locker['metadata'].get('lock-version', '1.0').split('.')))
376376

377377
def _parse_lock(self, locker: 'T_NameDict') -> Generator[_LockEntry, None, None]:

cyclonedx_py/_internal/requirements.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717

1818

1919
from argparse import OPTIONAL, ArgumentParser
20+
from collections.abc import Generator, Iterable
2021
from functools import reduce
2122
from itertools import chain
2223
from os import unlink
2324
from textwrap import dedent
24-
from typing import TYPE_CHECKING, Any, FrozenSet, Generator, Iterable, Optional
25+
from typing import TYPE_CHECKING, Any, Optional
2526

2627
from cyclonedx.exception.model import InvalidUriException, UnknownHashTypeException
2728
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType, Property, XsUri
@@ -168,7 +169,7 @@ def __hashes4req(self, req: 'InstallRequirement') -> Generator['HashType', None,
168169
del error
169170

170171
def _make_component(self, req: 'InstallRequirement',
171-
index_url: str, extra_index_urls: FrozenSet[str]) -> 'Component':
172+
index_url: str, extra_index_urls: frozenset[str]) -> 'Component':
172173
name = req.name
173174
version = req.get_pinned_version or None
174175
hashes = list(self.__hashes4req(req))

0 commit comments

Comments
 (0)