diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 356877d..012a475 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: priority: 3 args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/gaborbernat/simplint-pre-commit - rev: "c8719dca99c13381570bc69f8955c9448fc271b7" # frozen: v0.7.1 + rev: "5f12b15e1199c42bdeb3e13ea1c8b45952fbd802" # frozen: v0.11.0 hooks: - id: simplint priority: 4 diff --git a/bench/__main__.py b/bench/__main__.py index f22ed1b..e0fd8fc 100644 --- a/bench/__main__.py +++ b/bench/__main__.py @@ -209,7 +209,7 @@ def _profile(work: Path, out: Path, names: list[str], *, py_spy: bool) -> None: # a helper of main's because, inlined into main, it pushes main over ruff's C901 complexity limit -def _once(name: str, work: Path, stats_file: Path | None) -> None: # simplint: ignore[SMP505] # see above +def _once(name: str, work: Path, stats_file: Path | None) -> None: # simplint: ignore[single-caller-helper] # see above # without a stats file for an outside profiler such as py-spy with tempfile.TemporaryDirectory(dir=work, prefix="scratch-") as scratch: step = make_step(name, load(work), Path(scratch)) diff --git a/docs/examples/auditwheel/src/greet/cli.py b/docs/examples/auditwheel/src/greet/cli.py index 3ff0417..4508247 100644 --- a/docs/examples/auditwheel/src/greet/cli.py +++ b/docs/examples/auditwheel/src/greet/cli.py @@ -5,5 +5,5 @@ from ._greet import hello -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print(hello()) diff --git a/docs/examples/flit-core/src/greet/cli.py b/docs/examples/flit-core/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/flit-core/src/greet/cli.py +++ b/docs/examples/flit-core/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/hatchling/src/greet/cli.py b/docs/examples/hatchling/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/hatchling/src/greet/cli.py +++ b/docs/examples/hatchling/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/meson-python/src/greet/cli.py b/docs/examples/meson-python/src/greet/cli.py index 3ff0417..4508247 100644 --- a/docs/examples/meson-python/src/greet/cli.py +++ b/docs/examples/meson-python/src/greet/cli.py @@ -5,5 +5,5 @@ from ._greet import hello -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print(hello()) diff --git a/docs/examples/pdm-backend/src/greet/cli.py b/docs/examples/pdm-backend/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/pdm-backend/src/greet/cli.py +++ b/docs/examples/pdm-backend/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/poetry-core/src/greet/cli.py b/docs/examples/poetry-core/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/poetry-core/src/greet/cli.py +++ b/docs/examples/poetry-core/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/scikit-build-core/src/greet/cli.py b/docs/examples/scikit-build-core/src/greet/cli.py index 3ff0417..4508247 100644 --- a/docs/examples/scikit-build-core/src/greet/cli.py +++ b/docs/examples/scikit-build-core/src/greet/cli.py @@ -5,5 +5,5 @@ from ._greet import hello -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print(hello()) diff --git a/docs/examples/setuptools/src/greet/cli.py b/docs/examples/setuptools/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/setuptools/src/greet/cli.py +++ b/docs/examples/setuptools/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/uv-build/src/greet/cli.py b/docs/examples/uv-build/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/uv-build/src/greet/cli.py +++ b/docs/examples/uv-build/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/docs/examples/zipapp/src/greet/cli.py b/docs/examples/zipapp/src/greet/cli.py index 3b351ab..cd60924 100644 --- a/docs/examples/zipapp/src/greet/cli.py +++ b/docs/examples/zipapp/src/greet/cli.py @@ -3,5 +3,5 @@ from __future__ import annotations -def main() -> None: +def main() -> None: # simplint: ignore[undocumented-public-api] # CLI entry point print("hello") diff --git a/hatch_build.py b/hatch_build.py index 1c36882..edd2939 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -13,11 +13,11 @@ from python_sbom.hatch import SbomBuildHook -class SelfSbomHook(BuildHookInterface): # simplint: ignore[SMP409] # hatchling loads it from this script +class SelfSbomHook(BuildHookInterface): # simplint: ignore[unused-definition] # hatchling loads it from this script def initialize( self, version: str, - build_data: dict[str, Any], # simplint: ignore[SMP602] # hatchling's signature + build_data: dict[str, Any], # simplint: ignore[imprecise-annotation] # hatchling's signature ) -> None: self._hook.initialize(version, build_data) @@ -39,7 +39,7 @@ def _hook(self) -> SbomBuildHook: def finalize( self, version: str, - build_data: dict[str, Any], # simplint: ignore[SMP602] # hatchling's signature + build_data: dict[str, Any], # simplint: ignore[imprecise-annotation] # hatchling's signature artifact_path: str, ) -> None: self._hook.finalize(version, build_data, artifact_path) diff --git a/pyproject.toml b/pyproject.toml index 3c7337f..a1fb7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,6 +217,7 @@ lint.ignore = [ "undocumented-public-init", # undocumented-public-api (simplint SMP903) covers the public API instead "undocumented-public-method", # undocumented-public-api (simplint SMP903) covers the public API instead "undocumented-public-nested-class", # undocumented-public-api (simplint SMP903) covers the public API instead + "unnecessary-regular-expression-compile", # conflicts with simplint inline-regex ] lint.per-file-ignores."bench/**/*.py" = [ "import-private-name", # Benchmarks time the private entry points the public API wraps @@ -327,7 +328,7 @@ matrix.platforms = [ "linux_x86_64" ] # release-creator 4.4.0 needs a seven-day cutoff exception. # expires 2026-11-17 packages.release-creator.uploaded-prior-to = 2026-11-02T21:45:00Z -# simplint 0.7.1, the pre-commit hook, needs a seven-day cutoff exception. +# simplint 0.11.0 needs a seven-day cutoff exception. # expires 2026-11-13 packages.simplint.uploaded-prior-to = 2026-11-06T21:30:00Z # ty 0.0.100 renamed unresolved-attribute to missing-attribute, needs a seven-day cutoff exception. diff --git a/src/python_sbom/__init__.py b/src/python_sbom/__init__.py index 6345a1f..b7c8343 100644 --- a/src/python_sbom/__init__.py +++ b/src/python_sbom/__init__.py @@ -3,12 +3,12 @@ from __future__ import annotations from ._errors import MissingExtraError, SbomError -from ._model._content import BundledWheelInfo, ContentProvider, Contributions, ProviderContext -from ._model._identity import ProjectIdentity -from ._model._inputs import Settings -from ._model._types import ( +from ._model import ( + BundledWheelInfo, Component, Contact, + ContentProvider, + Contributions, Dependency, Evidence, ExternalReference, @@ -18,7 +18,10 @@ License, LicenseInfo, OrganizationalEntity, + ProjectIdentity, Property, + ProviderContext, + Settings, ) from .version import __version__ diff --git a/src/python_sbom/__main__.py b/src/python_sbom/__main__.py index a528915..fd5e81a 100644 --- a/src/python_sbom/__main__.py +++ b/src/python_sbom/__main__.py @@ -4,7 +4,7 @@ import sys -from ._cli._main import main +from ._cli import main if __name__ == "__main__": main(sys.argv[1:]) diff --git a/src/python_sbom/_cli/__init__.py b/src/python_sbom/_cli/__init__.py index f60ec27..53c9d74 100644 --- a/src/python_sbom/_cli/__init__.py +++ b/src/python_sbom/_cli/__init__.py @@ -1 +1,9 @@ """The command line: parsing options and running one command per subcommand.""" + +from __future__ import annotations + +from ._main import main + +__all__ = [ + "main", +] diff --git a/src/python_sbom/_cli/_archive.py b/src/python_sbom/_cli/_archive.py index c46a3a9..61259ea 100644 --- a/src/python_sbom/_cli/_archive.py +++ b/src/python_sbom/_cli/_archive.py @@ -6,7 +6,7 @@ from pathlib import Path, PurePosixPath from typing import Protocol -from python_sbom._generate._archive import archive_sbom +from python_sbom._generate._archive import archive_sbom # simplint: ignore[private-import-path] # avoid root export from ._parser import BuildOptions @@ -36,5 +36,5 @@ def run(options: _Options) -> bool: __all__ = [ - "run", # simplint: ignore[SMP402] # the parser names it for pkgutil.resolve_name + "run", # simplint: ignore[unused-export] # the parser names it for pkgutil.resolve_name ] diff --git a/src/python_sbom/_cli/_embed.py b/src/python_sbom/_cli/_embed.py index 7887dbc..b00479f 100644 --- a/src/python_sbom/_cli/_embed.py +++ b/src/python_sbom/_cli/_embed.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Protocol -from python_sbom._generate._embed import embed_sbom +from python_sbom._generate._embed import embed_sbom # simplint: ignore[private-import-path] # avoid root export from ._parser import BuildOptions @@ -26,5 +26,5 @@ def run(options: _Options) -> bool: __all__ = [ - "run", # simplint: ignore[SMP402] # the parser names it for pkgutil.resolve_name + "run", # simplint: ignore[unused-export] # the parser names it for pkgutil.resolve_name ] diff --git a/src/python_sbom/_cli/_main.py b/src/python_sbom/_cli/_main.py index 13edf9c..d3e3eb2 100644 --- a/src/python_sbom/_cli/_main.py +++ b/src/python_sbom/_cli/_main.py @@ -1,4 +1,4 @@ -"""The command line's entry point: parse the arguments and run the chosen subcommand.""" +"""Run the command line.""" # simplint: ignore[entrypoint-module-name] # console target from __future__ import annotations diff --git a/src/python_sbom/_cli/_parser.py b/src/python_sbom/_cli/_parser.py index a8f7ab8..4c6d9a3 100644 --- a/src/python_sbom/_cli/_parser.py +++ b/src/python_sbom/_cli/_parser.py @@ -2,12 +2,16 @@ from __future__ import annotations -from argparse import ArgumentParser, ArgumentTypeError, _SubParsersAction +from argparse import ( + ArgumentParser, + ArgumentTypeError, + _SubParsersAction, # simplint: ignore[private-import-path] # argparse has no public type +) from pathlib import Path from typing import Protocol # the limits module imports nothing, so --help and --version load none of validation -from python_sbom._validate._limits import MAX_COMPONENTS +from python_sbom._validate._limits import MAX_COMPONENTS # simplint: ignore[private-import-path] # avoid root export from python_sbom.version import __version__ diff --git a/src/python_sbom/_cli/_spdx.py b/src/python_sbom/_cli/_spdx.py index 3d7342e..f63a6d7 100644 --- a/src/python_sbom/_cli/_spdx.py +++ b/src/python_sbom/_cli/_spdx.py @@ -5,14 +5,21 @@ from typing import TYPE_CHECKING, Protocol from python_sbom import SbomError -from python_sbom._formats._json import dumps, is_document, load_json -from python_sbom._formats._spdx import render_spdx -from python_sbom._validate._limits import MAX_COMPONENTS, byte_limit +from python_sbom._formats._json import ( # simplint: ignore[private-import-path] # avoid root export + dumps, + is_document, + load_json, +) +from python_sbom._formats._spdx import render_spdx # simplint: ignore[private-import-path] # avoid root export +from python_sbom._validate._limits import ( # simplint: ignore[private-import-path] # avoid root export + MAX_COMPONENTS, + byte_limit, +) if TYPE_CHECKING: from pathlib import Path - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # avoid root export class _Options(Protocol): @@ -58,5 +65,5 @@ def _read(source: Path) -> Document: __all__ = [ - "run", # simplint: ignore[SMP402] # the parser names it for pkgutil.resolve_name + "run", # simplint: ignore[unused-export] # the parser names it for pkgutil.resolve_name ] diff --git a/src/python_sbom/_cli/_validate.py b/src/python_sbom/_cli/_validate.py index 3db5ac2..5267d46 100644 --- a/src/python_sbom/_cli/_validate.py +++ b/src/python_sbom/_cli/_validate.py @@ -5,7 +5,7 @@ import sys from typing import TYPE_CHECKING, Protocol -from python_sbom._validate._artifact import validate +from python_sbom._validate._artifact import validate # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from pathlib import Path @@ -38,5 +38,5 @@ def run(options: _Options) -> bool: __all__ = [ - "run", # simplint: ignore[SMP402] # the parser names it for pkgutil.resolve_name + "run", # simplint: ignore[unused-export] # the parser names it for pkgutil.resolve_name ] diff --git a/src/python_sbom/_collect/__init__.py b/src/python_sbom/_collect/__init__.py index 3ee5431..ec5d7a1 100644 --- a/src/python_sbom/_collect/__init__.py +++ b/src/python_sbom/_collect/__init__.py @@ -1 +1,13 @@ """What the edge reads: settings, build facts, the source revision, the project tree, the artifact and the provider.""" + +from __future__ import annotations + +from ._revision import REVISION_FILE, default_revision, revision_record +from ._settings import settings_from_pyproject + +__all__ = [ + "REVISION_FILE", + "default_revision", + "revision_record", + "settings_from_pyproject", +] diff --git a/src/python_sbom/_collect/_archive.py b/src/python_sbom/_collect/_archive.py index df77b14..363c42d 100644 --- a/src/python_sbom/_collect/_archive.py +++ b/src/python_sbom/_collect/_archive.py @@ -5,16 +5,21 @@ from typing import TYPE_CHECKING, Final from python_sbom import SbomError -from python_sbom._model._archive import bundled_members -from python_sbom._model._inputs import ArchiveTree, RecordRow -from python_sbom._model._metadata import parse_metadata -from python_sbom._model._names import archive_sbom_file_name +from python_sbom._model._archive import bundled_members # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # avoid root export + ArchiveTree, + RecordRow, +) +from python_sbom._model._metadata import parse_metadata # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._names import ( # simplint: ignore[private-import-path] # avoid root export + archive_sbom_file_name, +) if TYPE_CHECKING: from collections.abc import Container - from python_sbom._model._metadata import CoreMetadata - from python_sbom._wheel._reader import ArtifactReader + from python_sbom._model._metadata import CoreMetadata # simplint: ignore[private-import-path] # avoid root export + from python_sbom._wheel._reader import ArtifactReader # simplint: ignore[private-import-path] # avoid root export def read_archive(artifact: ArtifactReader) -> ArchiveTree: diff --git a/src/python_sbom/_collect/_content.py b/src/python_sbom/_collect/_content.py index 9cafcf9..501fb16 100644 --- a/src/python_sbom/_collect/_content.py +++ b/src/python_sbom/_collect/_content.py @@ -10,10 +10,10 @@ from typing import TYPE_CHECKING, Final from python_sbom import SbomError -from python_sbom._model._content import ContentProvider +from python_sbom._model._content import ContentProvider # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # avoid root export def resolve_provider( diff --git a/src/python_sbom/_collect/_facts.py b/src/python_sbom/_collect/_facts.py index 9c40ec2..793803c 100644 --- a/src/python_sbom/_collect/_facts.py +++ b/src/python_sbom/_collect/_facts.py @@ -19,16 +19,21 @@ from packaging.utils import canonicalize_name from python_sbom import SbomError -from python_sbom._model._inputs import BuildFacts, InstalledDistribution, Interpreter, RecordRow -from python_sbom._model._metadata import parse_metadata -from python_sbom._model._record import record_rows +from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # avoid root export + BuildFacts, + InstalledDistribution, + Interpreter, + RecordRow, +) +from python_sbom._model._metadata import parse_metadata # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._record import record_rows # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from collections.abc import Container, Sequence from importlib.metadata import Distribution from pathlib import Path - from python_sbom._model._metadata import CoreMetadata + from python_sbom._model._metadata import CoreMetadata # simplint: ignore[private-import-path] # avoid root export from ._revision import RevisionReader diff --git a/src/python_sbom/_collect/_revision.py b/src/python_sbom/_collect/_revision.py index 52d0938..f196f09 100644 --- a/src/python_sbom/_collect/_revision.py +++ b/src/python_sbom/_collect/_revision.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Final, Protocol from python_sbom import SbomError -from python_sbom._model._inputs import Revision +from python_sbom._model._inputs import Revision # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from pathlib import Path diff --git a/src/python_sbom/_collect/_settings.py b/src/python_sbom/_collect/_settings.py index 2e6cb98..ca22b00 100644 --- a/src/python_sbom/_collect/_settings.py +++ b/src/python_sbom/_collect/_settings.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, TypeAlias from python_sbom import SbomError -from python_sbom._model._inputs import Settings +from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from collections.abc import Mapping @@ -14,7 +14,9 @@ from pathlib import Path from typing import TypeIs # new in 3.13; annotations here are never evaluated - from python_sbom._model._types import OrganizationalEntity + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # avoid root export + OrganizationalEntity, + ) #: a value of a TOML table, as :mod:`tomllib` or hatchling hand it over Toml: TypeAlias = "str | int | float | bool | datetime | date | time | list[Toml] | dict[str, Toml]" diff --git a/src/python_sbom/_collect/_source.py b/src/python_sbom/_collect/_source.py index 6864e90..b584d57 100644 --- a/src/python_sbom/_collect/_source.py +++ b/src/python_sbom/_collect/_source.py @@ -6,14 +6,14 @@ from typing import TYPE_CHECKING, Final from python_sbom import SbomError -from python_sbom._model._inputs import BuildScript -from python_sbom._wheel._zip import sha256_hex +from python_sbom._model._inputs import BuildScript # simplint: ignore[private-import-path] # avoid root export +from python_sbom._wheel._zip import sha256_hex # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from collections.abc import Iterable from pathlib import Path, PurePosixPath - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # avoid root export def copyright_notice(root: Path, license_files: list[str]) -> str | None: diff --git a/src/python_sbom/_collect/_wheel.py b/src/python_sbom/_collect/_wheel.py index 217711c..d46787a 100644 --- a/src/python_sbom/_collect/_wheel.py +++ b/src/python_sbom/_collect/_wheel.py @@ -5,9 +5,9 @@ from typing import TYPE_CHECKING, Final from python_sbom import SbomError -from python_sbom._model._inputs import WheelTree -from python_sbom._model._names import sbom_file_name -from python_sbom._wheel._zip import read_wheel_metadata +from python_sbom._model._inputs import WheelTree # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._names import sbom_file_name # simplint: ignore[private-import-path] # avoid root export +from python_sbom._wheel._zip import read_wheel_metadata # simplint: ignore[private-import-path] # avoid root export from ._source import copyright_notice, file_sha256, hash_build_scripts, selected_wheels @@ -15,8 +15,8 @@ from collections.abc import Iterable, Iterator from pathlib import Path, PurePosixPath - from python_sbom._model._inputs import Settings - from python_sbom._wheel._reader import ArtifactReader + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # avoid root export + from python_sbom._wheel._reader import ArtifactReader # simplint: ignore[private-import-path] # avoid root export def read_wheel( diff --git a/src/python_sbom/_formats/_json.py b/src/python_sbom/_formats/_json.py index 31a290b..96fa8ab 100644 --- a/src/python_sbom/_formats/_json.py +++ b/src/python_sbom/_formats/_json.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from typing import TypeIs # new in 3.13; annotations here are never evaluated - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # avoid root export from ._spdx import Spdx diff --git a/src/python_sbom/_formats/_spdx.py b/src/python_sbom/_formats/_spdx.py index b2ad4a9..4e75952 100644 --- a/src/python_sbom/_formats/_spdx.py +++ b/src/python_sbom/_formats/_spdx.py @@ -8,12 +8,17 @@ from datetime import UTC, datetime from typing import TYPE_CHECKING, Final, NotRequired, TypedDict -from python_sbom._model._graph import walk_components -from python_sbom._model._names import SBOM_FILE, SBOM_REPOSITORY, property_prefix, sbom_property +from python_sbom._model._graph import walk_components # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._names import ( # simplint: ignore[private-import-path] # avoid root export + SBOM_FILE, + SBOM_REPOSITORY, + property_prefix, + sbom_property, +) from python_sbom.version import __version__ if TYPE_CHECKING: - from python_sbom._model._types import ( + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # avoid root export Component, Document, ExternalReference, @@ -39,6 +44,7 @@ "BLAKE3": "BLAKE3", } _LICENSE_REF: Final[re.Pattern[str]] = re.compile(r"LicenseRef-[A-Za-z0-9.-]+") +_SPDX_ID_PATTERN: Final[re.Pattern[str]] = re.compile(r"[^A-Za-z0-9.]+") class _LegacyTool(TypedDict): @@ -223,7 +229,7 @@ def _package(component: Component, project: str) -> _Package: def _spdx_id(bom_ref: str) -> str: # the readable part drops every other character, so refs that differ only in those share it; only the full digest, # not a prefix of it, keeps their ids apart - readable: Final[str] = re.sub(r"[^A-Za-z0-9.]+", "-", bom_ref).strip("-") + readable: Final[str] = _SPDX_ID_PATTERN.sub("-", bom_ref).strip("-") return f"SPDXRef-{readable}-{hashlib.sha256(bom_ref.encode()).hexdigest()}" diff --git a/src/python_sbom/_generate/__init__.py b/src/python_sbom/_generate/__init__.py index 49187f2..ef7d476 100644 --- a/src/python_sbom/_generate/__init__.py +++ b/src/python_sbom/_generate/__init__.py @@ -1 +1,9 @@ """The pipelines that build a wheel's and an archive's SBOM from what _collect gathers, and write it.""" + +from __future__ import annotations + +from ._embed import embed_built + +__all__ = [ + "embed_built", +] diff --git a/src/python_sbom/_generate/_archive.py b/src/python_sbom/_generate/_archive.py index aed94ab..dd2be2b 100644 --- a/src/python_sbom/_generate/_archive.py +++ b/src/python_sbom/_generate/_archive.py @@ -5,28 +5,43 @@ from typing import TYPE_CHECKING from python_sbom import SbomError -from python_sbom._collect._archive import read_archive -from python_sbom._collect._content import resolve_provider -from python_sbom._collect._facts import collect_facts -from python_sbom._collect._revision import revision_reader -from python_sbom._collect._settings import settings_from_pyproject -from python_sbom._collect._source import copyright_notice, hash_build_scripts -from python_sbom._formats._json import dumps -from python_sbom._model._archive import archive_artifact, bundled_paths -from python_sbom._model._bom import assemble, checked_contributions -from python_sbom._model._content import ProviderContext -from python_sbom._model._identity import project_identity -from python_sbom._model._names import archive_sbom_file_name -from python_sbom._wheel._reader import open_artifact -from python_sbom._wheel._write import append_member +from python_sbom._collect._archive import read_archive # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._content import resolve_provider # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._facts import collect_facts # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._revision import revision_reader # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._settings import ( # simplint: ignore[private-import-path] # avoid root export + settings_from_pyproject, +) +from python_sbom._collect._source import ( # simplint: ignore[private-import-path] # avoid root export + copyright_notice, + hash_build_scripts, +) +from python_sbom._formats._json import dumps # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._archive import ( # simplint: ignore[private-import-path] # avoid root export + archive_artifact, + bundled_paths, +) +from python_sbom._model._bom import ( # simplint: ignore[private-import-path] # avoid root export + assemble, + checked_contributions, +) +from python_sbom._model._content import ProviderContext # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._identity import project_identity # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._names import ( # simplint: ignore[private-import-path] # avoid root export + archive_sbom_file_name, +) +from python_sbom._wheel._reader import open_artifact # simplint: ignore[private-import-path] # avoid root export +from python_sbom._wheel._write import append_member # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from collections.abc import Sequence from pathlib import Path, PurePosixPath - from python_sbom._model._content import ContentProvider - from python_sbom._model._inputs import BuildFacts - from python_sbom._model._types import Document + from python_sbom._model._content import ( # simplint: ignore[private-import-path] # avoid root export + ContentProvider, + ) + from python_sbom._model._inputs import BuildFacts # simplint: ignore[private-import-path] # avoid root export + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # avoid root export def archive_sbom( # ruff: ignore[too-many-arguments] # every command-line option reaches the build diff --git a/src/python_sbom/_generate/_embed.py b/src/python_sbom/_generate/_embed.py index e6224ea..d984665 100644 --- a/src/python_sbom/_generate/_embed.py +++ b/src/python_sbom/_generate/_embed.py @@ -5,25 +5,36 @@ from pathlib import PurePosixPath from typing import TYPE_CHECKING -from python_sbom._collect._content import resolve_provider -from python_sbom._collect._facts import collect_facts -from python_sbom._collect._revision import revision_reader -from python_sbom._collect._settings import settings_from_pyproject -from python_sbom._collect._wheel import read_wheel -from python_sbom._formats._json import dumps -from python_sbom._model._bom import assemble, checked_contributions -from python_sbom._model._content import ProviderContext -from python_sbom._model._identity import project_identity -from python_sbom._model._wheel import wheel_artifact -from python_sbom._wheel._reader import open_artifact -from python_sbom._wheel._write import write_sbom +from python_sbom._collect._content import resolve_provider # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._facts import collect_facts # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._revision import revision_reader # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._settings import ( # simplint: ignore[private-import-path] # avoid root export + settings_from_pyproject, +) +from python_sbom._collect._wheel import read_wheel # simplint: ignore[private-import-path] # avoid root export +from python_sbom._formats._json import dumps # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._bom import ( # simplint: ignore[private-import-path] # avoid root export + assemble, + checked_contributions, +) +from python_sbom._model._content import ProviderContext # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._identity import project_identity # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._wheel import wheel_artifact # simplint: ignore[private-import-path] # avoid root export +from python_sbom._wheel._reader import open_artifact # simplint: ignore[private-import-path] # avoid root export +from python_sbom._wheel._write import write_sbom # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from collections.abc import Sequence from pathlib import Path - from python_sbom._model._content import ContentProvider - from python_sbom._model._inputs import BuildFacts, Revision, Settings + from python_sbom._model._content import ( # simplint: ignore[private-import-path] # avoid root export + ContentProvider, + ) + from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # avoid root export + BuildFacts, + Revision, + Settings, + ) def embed_sbom( diff --git a/src/python_sbom/_model/__init__.py b/src/python_sbom/_model/__init__.py index 40aeade..ec5f8e7 100644 --- a/src/python_sbom/_model/__init__.py +++ b/src/python_sbom/_model/__init__.py @@ -1 +1,43 @@ -"""The CycloneDX document model: its types, its inputs and the pure functions that build it.""" +"""The document types exported by python-sbom.""" + +from __future__ import annotations + +from ._content import BundledWheelInfo, ContentProvider, Contributions, ProviderContext +from ._identity import ProjectIdentity +from ._inputs import Revision, Settings +from ._types import ( + Component, + Contact, + Dependency, + Evidence, + ExternalReference, + Hash, + Identity, + IdentityMethod, + License, + LicenseInfo, + OrganizationalEntity, + Property, +) + +__all__ = [ + "BundledWheelInfo", + "Component", + "Contact", + "ContentProvider", + "Contributions", + "Dependency", + "Evidence", + "ExternalReference", + "Hash", + "Identity", + "IdentityMethod", + "License", + "LicenseInfo", + "OrganizationalEntity", + "ProjectIdentity", + "Property", + "ProviderContext", + "Revision", + "Settings", +] diff --git a/src/python_sbom/_model/_components.py b/src/python_sbom/_model/_components.py index 4a7d584..c7f2926 100644 --- a/src/python_sbom/_model/_components.py +++ b/src/python_sbom/_model/_components.py @@ -21,6 +21,9 @@ from ._metadata import CoreMetadata from ._types import Component, Contact, ExternalReference, Identity, Property +_CONTACT_PATTERN: Final[re.Pattern[str]] = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+") +_PROJECT_REFERENCES_PATTERN: Final[re.Pattern[str]] = re.compile(r"https?:", re.IGNORECASE) + def bundled_wheel_component(wheel: BundledWheel, property_prefix: str, info: BundledWheelInfo) -> Component: component = component_from_metadata(wheel.metadata) @@ -89,7 +92,7 @@ def component_from_metadata(metadata: CoreMetadata) -> Component: def _contact(name: str, email: str) -> Contact: - if email and not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email): + if email and not _CONTACT_PATTERN.fullmatch(email): # an address the schema rejects as an email, such as "jane at example.com", is kept as the name return {"name": " ".join(part for part in (name, email) if part)} contact: Final[Contact] = {} @@ -106,7 +109,7 @@ def _project_references(metadata: CoreMetadata) -> tuple[list[ExternalReference] references: Final[list[ExternalReference]] = [] unlinked: Final[list[Property]] = [] # a home page that is not an http(s) URL, such as UNKNOWN, is left out - if (home_page := metadata.first("home_page")) and re.match(r"https?:", home_page, re.IGNORECASE): + if (home_page := metadata.first("home_page")) and _PROJECT_REFERENCES_PATTERN.match(home_page): if (url := iri_reference(home_page)) is not None: references.append({"type": "website", "url": url}) if url != home_page: diff --git a/src/python_sbom/_model/_iri.py b/src/python_sbom/_model/_iri.py index ec27f8a..ca5dfda 100644 --- a/src/python_sbom/_model/_iri.py +++ b/src/python_sbom/_model/_iri.py @@ -10,7 +10,14 @@ # RFC 3986 appendix B: splits any text into scheme, authority, path, query and fragment _PARTS: Final[re.Pattern[str]] = re.compile( - r"(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?", re.DOTALL + r""" + (?:(?P[^:/?#]+):)? # scheme and colon + (?://(?P[^/?#]*))? # authority after two slashes + (?P[^?#]*) # path + (?:\?(?P[^#]*))? # query after a question mark + (?:\#(?P.*))? # fragment after a hash + """, + re.DOTALL | re.VERBOSE, ) _SCHEME: Final[re.Pattern[str]] = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*") # what may follow the host: nothing, or a colon and the port's digits @@ -38,7 +45,7 @@ def iri_reference(text: str) -> str | None: # keeps what the text points at; a scheme, IP literal or port no encoding mends, as in the scp-style # git@github.com:org/repo.git, leaves None; the pattern matches any text, every part of it optional match: Final = cast("re.Match[str]", _PARTS.fullmatch(text)) - scheme, authority, path, query, fragment = match.groups() + scheme, authority, path, query, fragment = match.group("scheme", "authority", "path", "query", "fragment") if scheme is not None and not _SCHEME.fullmatch(scheme): return None parts: Final[list[str]] = [] if scheme is None else [f"{scheme}:"] diff --git a/src/python_sbom/_model/_licenses.py b/src/python_sbom/_model/_licenses.py index 15480a6..7cf6053 100644 --- a/src/python_sbom/_model/_licenses.py +++ b/src/python_sbom/_model/_licenses.py @@ -11,6 +11,8 @@ from ._metadata import CoreMetadata from ._types import License +_SPDX_LICENSE_PATTERN: Final[re.Pattern[str]] = re.compile(r"[A-Za-z0-9.-]+") + def licenses(metadata: CoreMetadata) -> list[License]: # a License-Expression is taken as declared; otherwise each License field and License classifier name is declared, @@ -52,7 +54,7 @@ def _spdx_license(name: str) -> str | None: except InvalidLicenseExpression: return None # a CycloneDX license id holds one entry of the SPDX license list, never an expression or a LicenseRef - return canonical if re.fullmatch(r"[A-Za-z0-9.-]+", canonical) and not canonical.startswith("LicenseRef-") else None + return canonical if _SPDX_LICENSE_PATTERN.fullmatch(canonical) and not canonical.startswith("LicenseRef-") else None __all__ = [ diff --git a/src/python_sbom/_model/_markers.py b/src/python_sbom/_model/_markers.py index 57c5bf8..a835895 100644 --- a/src/python_sbom/_model/_markers.py +++ b/src/python_sbom/_model/_markers.py @@ -7,7 +7,10 @@ from packaging.markers import Marker, UndefinedComparison if TYPE_CHECKING: - from packaging._parser import MarkerAtom, MarkerList # see _extra_verdict + from packaging._parser import ( # simplint: ignore[private-import-path] # no public marker AST + MarkerAtom, + MarkerList, + ) from packaging.requirements import Requirement diff --git a/src/python_sbom/_model/_names.py b/src/python_sbom/_model/_names.py index 6838b7f..080fc51 100644 --- a/src/python_sbom/_model/_names.py +++ b/src/python_sbom/_model/_names.py @@ -74,8 +74,11 @@ def tool_ref(ref: str) -> str: return f"tool:{ref}" +_REFERENCE_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"[^a-z]") + + def reference_type(label: str) -> str: - return _URL_LABEL_TO_REFERENCE_TYPE.get(re.sub(r"[^a-z]", "", label.lower()), "other") + return _URL_LABEL_TO_REFERENCE_TYPE.get(_REFERENCE_TYPE_PATTERN.sub("", label.lower()), "other") __all__ = [ diff --git a/src/python_sbom/_validate/_artifact.py b/src/python_sbom/_validate/_artifact.py index 481c747..dbaeb74 100644 --- a/src/python_sbom/_validate/_artifact.py +++ b/src/python_sbom/_validate/_artifact.py @@ -5,7 +5,7 @@ import zipfile from typing import IO, TYPE_CHECKING, Final -from python_sbom._wheel._zip import CHUNK, open_archive +from python_sbom._wheel._zip import CHUNK, open_archive # simplint: ignore[private-import-path] # avoid root export from ._document import validate_document from ._limits import MAX_COMPONENTS, byte_limit diff --git a/src/python_sbom/_validate/_document.py b/src/python_sbom/_validate/_document.py index 358af8d..aa97697 100644 --- a/src/python_sbom/_validate/_document.py +++ b/src/python_sbom/_validate/_document.py @@ -8,17 +8,30 @@ from typing import TYPE_CHECKING, Final from python_sbom import MissingExtraError, SbomError -from python_sbom._formats._json import Json, has_path, is_document, load_json -from python_sbom._formats._spdx import render_spdx -from python_sbom._model._graph import artifact_contents, reference_problems, repeated, walk_components -from python_sbom._model._names import SBOM_FILE +from python_sbom._formats._json import ( # simplint: ignore[private-import-path] # avoid root export + Json, + has_path, + is_document, + load_json, +) +from python_sbom._formats._spdx import render_spdx # simplint: ignore[private-import-path] # avoid root export +from python_sbom._model._graph import ( # simplint: ignore[private-import-path] # avoid root export + artifact_contents, + reference_problems, + repeated, + walk_components, +) +from python_sbom._model._names import SBOM_FILE # simplint: ignore[private-import-path] # avoid root export from ._limits import MAX_COMPONENTS, MAX_TEXT, byte_limit if TYPE_CHECKING: from collections.abc import Iterable - from python_sbom._model._types import Component, Document + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # avoid root export + Component, + Document, + ) # the schema leaves these optional, yet every document python-sbom writes carries them and the checks below read them _REQUIRED: Final[tuple[tuple[str, ...], ...]] = ( @@ -29,7 +42,17 @@ ("components",), ("dependencies",), ) -_SERIAL: Final[re.Pattern[str]] = re.compile(r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") +_SERIAL: Final[re.Pattern[str]] = re.compile( + r""" + urn:uuid: # UUID namespace + [0-9a-f]{8}- # time low + [0-9a-f]{4}- # time middle + [0-9a-f]{4}- # time high and version + [0-9a-f]{4}- # variant and clock sequence + [0-9a-f]{12} # node + """, + re.VERBOSE, +) # the modules of the validate extra _EXTRA: Final[tuple[str, ...]] = ("jsonschema_rs", "license_expression", "spdx_tools") # the schema validator recurses once per level and crashes the process past a few thousand levels on a small thread diff --git a/src/python_sbom/_validate/_licenses.py b/src/python_sbom/_validate/_licenses.py index dbe85b6..e9a4969 100644 --- a/src/python_sbom/_validate/_licenses.py +++ b/src/python_sbom/_validate/_licenses.py @@ -13,12 +13,15 @@ get_license_index, ) -from python_sbom._model._graph import shipped_libraries +from python_sbom._model._graph import shipped_libraries # simplint: ignore[private-import-path] # avoid root export if TYPE_CHECKING: from license_expression import LicenseExpression - from python_sbom._model._types import Component, License + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # avoid root export + Component, + License, + ) # the SPDX ids alone, without the aliases get_spdx_licensing() adds: those rename ids (LicenseRef-Apache becomes # Apache-2.0, Net-SNMP becomes LicenseRef-scancode-net-snmp), so a policy would judge an id other than the one declared; diff --git a/src/python_sbom/_validate/_schemas.py b/src/python_sbom/_validate/_schemas.py index d9e39e1..ababc7e 100644 --- a/src/python_sbom/_validate/_schemas.py +++ b/src/python_sbom/_validate/_schemas.py @@ -25,8 +25,8 @@ from jsonschema_rs import ValidationError from spdx_tools.spdx.model import Document, SpdxNoAssertion, SpdxNone - from python_sbom._formats._json import Json - from python_sbom._formats._spdx import Spdx + from python_sbom._formats._json import Json # simplint: ignore[private-import-path] # avoid root export + from python_sbom._formats._spdx import Spdx # simplint: ignore[private-import-path] # avoid root export def schema_error(document: Json) -> str | None: diff --git a/src/python_sbom/_wheel/__init__.py b/src/python_sbom/_wheel/__init__.py index 24df4da..5024da2 100644 --- a/src/python_sbom/_wheel/__init__.py +++ b/src/python_sbom/_wheel/__init__.py @@ -1 +1,9 @@ """Zip artifacts: one checked handle per build, verifying the wheels they bundle, and writing an SBOM in.""" + +from __future__ import annotations + +from ._sdist import add_to_sdist + +__all__ = [ + "add_to_sdist", +] diff --git a/src/python_sbom/_wheel/_bundled.py b/src/python_sbom/_wheel/_bundled.py index 0084635..d31fcd1 100644 --- a/src/python_sbom/_wheel/_bundled.py +++ b/src/python_sbom/_wheel/_bundled.py @@ -11,9 +11,15 @@ from packaging.requirements import InvalidRequirement, Requirement from python_sbom import SbomError -from python_sbom._model._inputs import BundledWheel, RecordRow -from python_sbom._model._metadata import CoreMetadata, parse_metadata -from python_sbom._model._record import record_rows +from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # avoid root export + BundledWheel, + RecordRow, +) +from python_sbom._model._metadata import ( # simplint: ignore[private-import-path] # avoid root export + CoreMetadata, + parse_metadata, +) +from python_sbom._model._record import record_rows # simplint: ignore[private-import-path] # avoid root export from ._zip import open_archive, read_member, read_wheel_metadata, sha256_hex diff --git a/src/python_sbom/_wheel/_reader.py b/src/python_sbom/_wheel/_reader.py index 7f5918b..cc47a42 100644 --- a/src/python_sbom/_wheel/_reader.py +++ b/src/python_sbom/_wheel/_reader.py @@ -15,7 +15,7 @@ from collections.abc import Generator from pathlib import Path, PurePosixPath - from python_sbom._model._inputs import BundledWheel + from python_sbom._model._inputs import BundledWheel # simplint: ignore[private-import-path] # avoid root export # a bundled wheel is read whole, to hash it and open it as a zip, so its size is capped as metadata's is _WHEEL_LIMIT: Final[int] = 512 * 1024 * 1024 diff --git a/src/python_sbom/_wheel/_zip.py b/src/python_sbom/_wheel/_zip.py index 914bed3..0130ed4 100644 --- a/src/python_sbom/_wheel/_zip.py +++ b/src/python_sbom/_wheel/_zip.py @@ -12,7 +12,10 @@ from installer.sources import WheelFile from python_sbom import SbomError -from python_sbom._model._metadata import CoreMetadata, parse_metadata +from python_sbom._model._metadata import ( # simplint: ignore[private-import-path] # avoid root export + CoreMetadata, + parse_metadata, +) if TYPE_CHECKING: from collections.abc import Generator diff --git a/src/python_sbom/backend.py b/src/python_sbom/backend.py index d78a5bd..6f2c020 100644 --- a/src/python_sbom/backend.py +++ b/src/python_sbom/backend.py @@ -7,8 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Final, Protocol, cast -from ._collect._revision import REVISION_FILE, default_revision, revision_record -from ._collect._settings import settings_from_pyproject +from ._collect import REVISION_FILE, default_revision, revision_record, settings_from_pyproject from ._errors import SbomError if TYPE_CHECKING: @@ -53,7 +52,7 @@ def build_wheel( :returns: the wheel's file name """ - from ._generate._embed import embed_built # ruff: ignore[import-outside-top-level] # sdist builds skip it + from ._generate import embed_built # ruff: ignore[import-outside-top-level] # sdist builds skip it root = Path.cwd() # read before the backend writes into the checkout, which may not ignore its build directory @@ -76,7 +75,7 @@ def build_sdist(sdist_directory: str, config_settings: ConfigSettings = None) -> :returns: the sdist's file name """ - from ._wheel._sdist import add_to_sdist # ruff: ignore[import-outside-top-level] # wheel builds skip it + from ._wheel import add_to_sdist # ruff: ignore[import-outside-top-level] # wheel builds skip it revision = default_revision(Path.cwd()) name = _wrapped().build_sdist(sdist_directory, config_settings) diff --git a/src/python_sbom/hatch/_hook.py b/src/python_sbom/hatch/_hook.py index 0297db3..5d56140 100644 --- a/src/python_sbom/hatch/_hook.py +++ b/src/python_sbom/hatch/_hook.py @@ -14,15 +14,23 @@ from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT from python_sbom import SbomError -from python_sbom._collect._content import resolve_provider -from python_sbom._collect._revision import REVISION_FILE, default_revision, revision_record -from python_sbom._collect._settings import settings_from_table +from python_sbom._collect._content import resolve_provider # simplint: ignore[private-import-path] # avoid root export +from python_sbom._collect._revision import ( # simplint: ignore[private-import-path] # avoid root export + REVISION_FILE, + default_revision, + revision_record, +) +from python_sbom._collect._settings import ( # simplint: ignore[private-import-path] # avoid root export + settings_from_table, +) if TYPE_CHECKING: from collections.abc import Callable - from python_sbom._model._content import ContentProvider - from python_sbom._model._inputs import Settings + from python_sbom._model._content import ( # simplint: ignore[private-import-path] # avoid root export + ContentProvider, + ) + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # avoid root export # options of every hatchling hook table, which hatchling itself reads _HATCHLING_OPTIONS: Final[frozenset[str]] = frozenset({ @@ -53,7 +61,7 @@ class SbomBuildHook(BuildHookInterface): def initialize( self, version: str, - build_data: dict[str, Any], # simplint: ignore[SMP602] # hatchling's signature + build_data: dict[str, Any], # simplint: ignore[imprecise-annotation] # hatchling's signature ) -> None: """ Read the source revision, and record it in an sdist. @@ -81,7 +89,7 @@ def finalize( self, version: str, build_data: ( # ruff: ignore[unused-method-argument] # hatchling's signature - dict[str, Any] # simplint: ignore[SMP602] # see above + dict[str, Any] # simplint: ignore[imprecise-annotation] # see above ), artifact_path: str, ) -> None: @@ -98,8 +106,12 @@ def finalize( if self.target_name != "wheel" or version == "editable": return # what a wheel needs loads only when one is built, so an sdist build stays light - from python_sbom._collect._facts import collect_facts # ruff: ignore[import-outside-top-level] # see above - from python_sbom._generate._embed import embed_into # ruff: ignore[import-outside-top-level] # see above + from python_sbom._collect._facts import ( # ruff: ignore[import-outside-top-level] # simplint: ignore[private-import-path] # lazy + collect_facts, + ) + from python_sbom._generate._embed import ( # ruff: ignore[import-outside-top-level] # simplint: ignore[private-import-path] # lazy + embed_into, + ) root = Path(self.root) # content_provider() may be a subclass's override, so its result goes through resolve_provider again, which diff --git a/src/python_sbom/pdm.py b/src/python_sbom/pdm.py index 8908983..bc48d18 100644 --- a/src/python_sbom/pdm.py +++ b/src/python_sbom/pdm.py @@ -7,12 +7,12 @@ from pathlib import Path from typing import TYPE_CHECKING -from ._collect._revision import REVISION_FILE, default_revision, revision_record +from ._collect import REVISION_FILE, default_revision, revision_record if TYPE_CHECKING: from pdm.backend.hooks import Context - from ._model._inputs import Revision + from ._model import Revision class SbomBuildHook: @@ -70,7 +70,7 @@ def pdm_build_finalize(self, context: Context, artifact: Path) -> None: # what a wheel needs loads only when one is built, so an sdist build stays light from pdm.backend.wheel import ZIPINFO_DATE_TIME # ruff: ignore[import-outside-top-level] # see above - from ._generate._embed import embed_built # ruff: ignore[import-outside-top-level] # see above + from ._generate import embed_built # ruff: ignore[import-outside-top-level] # see above # pdm-backend dates every zip entry at a fixed time when SOURCE_DATE_EPOCH is unset, so the SBOM agrees embed_built(Path(artifact), context.root, self._revision, default_epoch=calendar.timegm(ZIPINFO_DATE_TIME)) diff --git a/src/python_sbom/setuptools.py b/src/python_sbom/setuptools.py index 3b2c3d2..9a0b3bf 100644 --- a/src/python_sbom/setuptools.py +++ b/src/python_sbom/setuptools.py @@ -9,13 +9,13 @@ from setuptools.command.bdist_wheel import bdist_wheel from setuptools.command.sdist import sdist -from ._collect._revision import REVISION_FILE, default_revision, revision_record +from ._collect import REVISION_FILE, default_revision, revision_record from ._errors import SbomError if TYPE_CHECKING: from setuptools.dist import Distribution - from ._model._inputs import Revision + from ._model import Revision def finalize_distribution_options(distribution: Distribution) -> None: @@ -50,7 +50,7 @@ def finalize_distribution_options(distribution: Distribution) -> None: class SbomWheel(wheel_base): def run(self) -> None: - from ._generate._embed import embed_built # ruff: ignore[import-outside-top-level] # wheels only + from ._generate import embed_built # ruff: ignore[import-outside-top-level] # wheels only # read before this command writes into build/; a checkout that does not ignore build/ or the egg-info # directory an earlier step wrote reads as dirty diff --git a/tests/_cli/test_embed.py b/tests/_cli/test_embed.py index f713682..6e6f1bd 100644 --- a/tests/_cli/test_embed.py +++ b/tests/_cli/test_embed.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from pathlib import Path - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model from tests.conftest import Install _SBOM: Final[str] = "native-1.0.dist-info/sboms/native.cdx.json" diff --git a/tests/_collect/test_facts.py b/tests/_collect/test_facts.py index 48c7f0c..d5abc04 100644 --- a/tests/_collect/test_facts.py +++ b/tests/_collect/test_facts.py @@ -18,7 +18,9 @@ from conftest import Install - from python_sbom._model._inputs import BuildFacts + from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # test uses internal model + BuildFacts, + ) def test_epoch_from_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/_collect/test_source.py b/tests/_collect/test_source.py index e0f4dc0..419a9dd 100644 --- a/tests/_collect/test_source.py +++ b/tests/_collect/test_source.py @@ -16,7 +16,7 @@ from collections.abc import Mapping from pathlib import Path - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # test uses internal model @pytest.mark.parametrize( diff --git a/tests/_collect/test_wheel.py b/tests/_collect/test_wheel.py index 3525854..bd1c860 100644 --- a/tests/_collect/test_wheel.py +++ b/tests/_collect/test_wheel.py @@ -17,7 +17,7 @@ from conftest import WriteWheel - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # test uses internal model _PIP: Final[dict[str, bytes]] = {"pip/__init__.py": b"", "pip-1.0.dist-info/METADATA": b"Name: pip\nVersion: 1.0\n"} _NATIVE: Final[dict[str, bytes]] = { diff --git a/tests/_formats/test_spdx.py b/tests/_formats/test_spdx.py index 31bd2e7..7310615 100644 --- a/tests/_formats/test_spdx.py +++ b/tests/_formats/test_spdx.py @@ -9,7 +9,7 @@ import pytest from python_sbom._formats._json import dumps -from python_sbom._formats._spdx import render_spdx +from python_sbom._formats._spdx import Spdx, render_spdx from python_sbom._validate._schemas import spdx_problems from python_sbom.version import __version__ @@ -17,11 +17,11 @@ from conftest import Describe from python_sbom import Component - from python_sbom._formats._spdx import _Package - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # test uses internal model # a JSON value, as json.loads hands it over _Json: TypeAlias = "str | int | float | bool | list[_Json] | dict[str, _Json] | None" +_SPDX_ID_PATTERN: Final[re.Pattern[str]] = re.compile(r"SPDXRef-[A-Za-z0-9.-]+") # what auditwheel writes: CycloneDX 1.4 conventions, a legacy tools array, no timestamp and no serial number _FOREIGN: Final[str] = json.dumps({ @@ -105,7 +105,7 @@ def test_minimal(describe: Describe) -> None: def test_spdx_ids_are_valid_and_tell_similar_refs_apart(describe: Describe) -> None: document = describe("Requires-Dist: click; os_name == 'nt'\nRequires-Dist: click; os_name != 'nt'\n") ids = [package["SPDXID"] for package in render_spdx(document)["packages"]] - assert all(re.fullmatch(r"SPDXRef-[A-Za-z0-9.-]+", spdx_id) for spdx_id in ids) + assert all(_SPDX_ID_PATTERN.fullmatch(spdx_id) for spdx_id in ids) assert len(set(ids)) == len(ids) @@ -280,26 +280,28 @@ def test_supplier_without_name_is_no_creator(describe: Describe) -> None: ], ) def test_hash_algorithms_take_spdx_names(algorithm: str, expected: str) -> None: - (package,) = packages_of({"hashes": [{"alg": algorithm, "content": "ab" * 32}]}) + (package,) = spdx_with({"hashes": [{"alg": algorithm, "content": "ab" * 32}]})["packages"] assert package.get("checksums") == [{"algorithm": expected, "checksumValue": "ab" * 32}] -def packages_of(fields: dict[str, _Json]) -> list[_Package]: +def spdx_with(fields: dict[str, _Json]) -> Spdx: document = json.loads(_FOREIGN) document["components"] = [{"type": "library", "bom-ref": "lib", "name": "lib", **fields}] - return [package for package in render_spdx(document)["packages"] if package["name"] == "lib"] + spdx = render_spdx(document) + spdx["packages"] = [package for package in spdx["packages"] if package["name"] == "lib"] + return spdx def test_hash_algorithm_spdx_lacks_is_dropped() -> None: - (package,) = packages_of({"hashes": [{"alg": "SHA-224", "content": "ab" * 28}]}) + (package,) = spdx_with({"hashes": [{"alg": "SHA-224", "content": "ab" * 28}]})["packages"] assert "checksums" not in package def test_component_supplier_without_name_is_dropped() -> None: - (package,) = packages_of({"supplier": {"url": ["https://example.org"]}}) + (package,) = spdx_with({"supplier": {"url": ["https://example.org"]}})["packages"] assert "supplier" not in package def test_property_without_value() -> None: - (package,) = packages_of({"properties": [{"name": "plain:extra"}]}) + (package,) = spdx_with({"properties": [{"name": "plain:extra"}]})["packages"] assert package["comment"] == "plain:extra: " diff --git a/tests/_generate/test_archive.py b/tests/_generate/test_archive.py index 2145d2f..d358984 100644 --- a/tests/_generate/test_archive.py +++ b/tests/_generate/test_archive.py @@ -20,7 +20,7 @@ from conftest import WriteWheel from python_sbom import Component - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model _COMMIT: Final[str] = "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff" _LIBRARY: Final[str] = "__bundled__/distlib-0.4-py3-none-any" diff --git a/tests/_model/test_bom.py b/tests/_model/test_bom.py index ad17d84..4344daa 100644 --- a/tests/_model/test_bom.py +++ b/tests/_model/test_bom.py @@ -23,8 +23,15 @@ from conftest import Describe from python_sbom import Component, Dependency, Property - from python_sbom._model._inputs import BuildFacts, Settings - from python_sbom._model._types import Body, Formula, OrganizationalEntity + from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # test uses internal model + BuildFacts, + Settings, + ) + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # test uses internal model + Body, + Formula, + OrganizationalEntity, + ) _INTERPRETER: Final[str] = "tool:pkg:generic/cpython@3.15.0" diff --git a/tests/_model/test_components.py b/tests/_model/test_components.py index df37e9f..e146e0d 100644 --- a/tests/_model/test_components.py +++ b/tests/_model/test_components.py @@ -16,7 +16,10 @@ if TYPE_CHECKING: from python_sbom import Component, Property - from python_sbom._model._types import License, OrganizationalEntity + from python_sbom._model._types import ( # simplint: ignore[private-import-path] # test uses internal model + License, + OrganizationalEntity, + ) _WHEEL: Final[str] = "src/example/wheels/pip-1.0-py3-none-any.whl" diff --git a/tests/_model/test_identity.py b/tests/_model/test_identity.py index 3abc30d..40cfad4 100644 --- a/tests/_model/test_identity.py +++ b/tests/_model/test_identity.py @@ -10,7 +10,7 @@ from python_sbom._model._metadata import parse_metadata if TYPE_CHECKING: - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # test uses internal model @pytest.mark.parametrize( diff --git a/tests/_model/test_tools.py b/tests/_model/test_tools.py index 704704b..1842a4f 100644 --- a/tests/_model/test_tools.py +++ b/tests/_model/test_tools.py @@ -9,7 +9,9 @@ from python_sbom._model._tools import script_tool, tool_components if TYPE_CHECKING: - from python_sbom._model._inputs import BuildFacts + from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # test uses internal model + BuildFacts, + ) def test_tools_list_interpreter_then_distributions_by_name(facts: BuildFacts) -> None: diff --git a/tests/_model/test_wheel.py b/tests/_model/test_wheel.py index d8d999c..9bf6e9f 100644 --- a/tests/_model/test_wheel.py +++ b/tests/_model/test_wheel.py @@ -12,7 +12,7 @@ from conftest import Describe - from python_sbom._model._inputs import Settings + from python_sbom._model._inputs import Settings # simplint: ignore[private-import-path] # test uses internal model def test_declared_dependency(describe: Describe) -> None: diff --git a/tests/_seeder/src/seeder/embed/__init__.py b/tests/_seeder/src/seeder/embed/__init__.py index 1f40729..aad2835 100644 --- a/tests/_seeder/src/seeder/embed/__init__.py +++ b/tests/_seeder/src/seeder/embed/__init__.py @@ -3,8 +3,8 @@ from pathlib import Path # the layout of virtualenv's seed/wheels/embed/__init__.py: another assignment comes before the table -BUNDLE_FOLDER = Path(__file__).absolute().parent -BUNDLE_SUPPORT = { +BUNDLE_FOLDER = Path(__file__).absolute().parent # simplint: ignore[missing-final] # virtualenv fixture +BUNDLE_SUPPORT = { # simplint: ignore[missing-final] # virtualenv fixture "3.14": {"pip": "pip-1.0-py3-none-any.whl"}, "3.15": {"pip": "pip-1.0-py3-none-any.whl"}, } diff --git a/tests/_validate/test_document.py b/tests/_validate/test_document.py index 6e58f99..9c63826 100644 --- a/tests/_validate/test_document.py +++ b/tests/_validate/test_document.py @@ -18,9 +18,11 @@ from pytest_mock import MockerFixture from python_sbom import Component, License - from python_sbom._formats._json import Json - from python_sbom._model._inputs import BuildFacts - from python_sbom._model._types import Document + from python_sbom._formats._json import Json # simplint: ignore[private-import-path] # test uses internal model + from python_sbom._model._inputs import ( # simplint: ignore[private-import-path] # test uses internal model + BuildFacts, + ) + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model # what auditwheel writes: CycloneDX 1.4 conventions, a legacy tools array, no timestamp _FOREIGN: Final[bytes] = json.dumps({ diff --git a/tests/_validate/test_schemas.py b/tests/_validate/test_schemas.py index 6d69f34..a3ef1f3 100644 --- a/tests/_validate/test_schemas.py +++ b/tests/_validate/test_schemas.py @@ -21,8 +21,8 @@ from conftest import Describe from python_sbom import Component - from python_sbom._formats._json import Json - from python_sbom._formats._spdx import Spdx + from python_sbom._formats._json import Json # simplint: ignore[private-import-path] # test uses internal model + from python_sbom._formats._spdx import Spdx # simplint: ignore[private-import-path] # test uses internal model @pytest.mark.parametrize( diff --git a/tests/conftest.py b/tests/conftest.py index 618e42c..355deac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Mapping - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model class WriteWheel(Protocol): diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 1a5e102..5dd3735 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -15,11 +15,12 @@ import pytest if TYPE_CHECKING: - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model _EXAMPLES: Final[Path] = Path(__file__).parents[2] / "docs" / "examples" # a wheel's interpreter, ABI and platform tags depend on where the example runs, while the docs show one platform's _TAGS: Final[re.Pattern[str]] = re.compile(r"-[^-\s/]+-[^-\s/]+-[^-\s/]+\.whl\b") +_PROMPT: Final[re.Pattern[str]] = re.compile(r"^\$ ", re.MULTILINE) # a frontend's isolated environment would install python-sbom from PyPI; the environment the test runs in holds the # checkout's _ADDED: Final[dict[tuple[str, ...], list[str]]] = { @@ -47,7 +48,7 @@ def played(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFact shutil.copytree(_EXAMPLES / example, directory) expected: Final[list[tuple[str, list[str]]]] = [] output: Final[list[tuple[str, list[str]]]] = [] - for block in re.split(r"^\$ ", (_EXAMPLES / example / session).read_text(encoding="utf-8"), flags=re.MULTILINE)[1:]: + for block in _PROMPT.split((_EXAMPLES / example / session).read_text(encoding="utf-8"))[1:]: command, *lines = block.splitlines() expected.append((command, [_TAGS.sub("-TAGS.whl", line) for line in lines])) output.append((command, [_TAGS.sub("-TAGS.whl", line) for line in _run(command, directory)])) diff --git a/tests/hatch/test_hook.py b/tests/hatch/test_hook.py index 5ee5173..0aa983f 100644 --- a/tests/hatch/test_hook.py +++ b/tests/hatch/test_hook.py @@ -19,7 +19,7 @@ from conftest import BuildWheel - from python_sbom._model._types import Document + from python_sbom._model._types import Document # simplint: ignore[private-import-path] # test uses internal model from tests.conftest import Git _SBOM: Final[str] = "example-1.0.dist-info/sboms/example.cdx.json" diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 45efe2f..d6fab3d 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator -_Hint = object # simplint: ignore[SMP602] # hints are arbitrary typing objects +_Hint = object # simplint: ignore[imprecise-annotation] # hints are arbitrary typing objects _PUBLIC: Final[set[str]] = { "BundledWheelInfo",