Skip to content
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
5 changes: 2 additions & 3 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
49 changes: 14 additions & 35 deletions cyclonedx_py/_internal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import logging
import sys
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
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
Expand All @@ -35,20 +36,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 = '-'


Expand Down Expand Up @@ -121,29 +113,16 @@ 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')

scbbc: Type['BomBuilder']
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
scta: List[str]
scta: list[str]
for scbbc, sct, *scta in (
(EnvironmentBB, 'environment', 'env', 'venv'),
(RequirementsBB, 'requirements'),
Expand Down Expand Up @@ -171,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, *,
Expand All @@ -181,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
Expand Down
13 changes: 7 additions & 6 deletions cyclonedx_py/_internal/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@


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
from os.path import exists, isdir, join
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
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
21 changes: 11 additions & 10 deletions cyclonedx_py/_internal/pipenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,7 +42,7 @@

from cyclonedx.model.bom import Bom

NameDict = Dict[str, Any]
NameDict = dict[str, Any]


class PipenvBB(BomBuilder):
Expand Down Expand Up @@ -96,15 +97,15 @@ 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',
**__: Any) -> 'Bom':

# 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:
Expand Down Expand Up @@ -138,22 +139,22 @@ 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()
bom.metadata.component = root_c
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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 12 additions & 12 deletions cyclonedx_py/_internal/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand All @@ -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}')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]:
Expand Down
5 changes: 3 additions & 2 deletions cyclonedx_py/_internal/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
Loading