Contributing¶
How the code fits together¶
Each artifact has one pipeline in _generate, and the two read alike. An adapter, the hatchling hook or a command of
the command line, gathers the options, the content provider, the build scripts and the build facts (time, source
revision and toolchain). The pipeline opens the artifact once, reads it into a tree (_collect/_wheel.py or
_collect/_archive.py), maps the tree to an Artifact with a pure function (_model/_wheel.py or
_model/_archive.py), and hands that to _model/_bom.py, which adds what every artifact of the project shares.
How it works draws the packages and their imports.
To record a new field, read it at the edge into _model/_inputs.py and write it in _model; to add a subcommand,
write a run function in a new _cli/_<command>.py and register its arguments and the
"python_sbom._cli._<command>:run" handler in _cli/_parser.py, which loads the module only when the command runs.
Tests mirror the source: tests/<package>/test_<module>.py.
What each layer guarantees¶
A change is easiest to review against the guarantee of the layer it touches, and the test named here is the one that fails when that guarantee breaks. Each test crosses a boundary on purpose: unit tests of one layer did not catch the mismatches between layers these guard against.
_wheelreads an artifact through one open handle per build, checked once for clashing member names, and never trusts a member’s stated size. The bytes it hands on are the bytes the artifact ships.tests/_wheel/test_reader.py, andtest_embed_rewrites_record_by_parsed_pathfor rewriting RECORD._collectturns the project and the artifact into inputs. Source configuration only selects: a bundled wheel is described from the artifact’s copy, matched by digest, and a selected wheel the artifact does not ship is refused. A provider module another project already imported in the same process is refused rather than reused.test_bundled_wheel_not_shipped_is_refused,test_provider_of_another_project_is_not_reused._modelbuilds documents with pure functions._model/_graph.pydefines the sets every layer judges alike:artifact_contentsis what the artifact holds or declares wherever the document nests it,shipped_librariesis what it carries bytes for, andreference_problemsis the one check of repeated and unknown references that generation refuses and validation reports.test_archive_bundled_wheel_judged_wherever_it_sits._validatereads hostile input: bounded size, depth and component count before the schema runs, then references, the SPDX rendering and the license policy overartifact_contents.test_repeated_dependency_entry_cannot_hide_an_invalid_one._formatsrenders without losing identity: every SPDX id is unique because it ends in the full digest of itsbom-ref.test_spdx_ids_of_refs_differing_in_dropped_characters_stay_distinct._generateorders reading, building and writing: an artifact stays open while its document is built, is closed before it is rewritten, and is never overwritten by its own SBOM.test_archive_refuses_output_over_archive, intests/_clisince the command line is where an output path reaches it.
Updating the schemas¶
python-sbom validate checks documents against the official CycloneDX schema files, which ship byte for byte under
src/python_sbom/_validate/schemas. Each comes from a tag of CycloneDX/specification: bom-1.6.schema.json and the jsf-0.82.schema.json it references
from 1.6.2, and spdx.schema.json and the Apache-2.0 LICENSE from 1.7.2, the newest, since the
cyclonedx.org URL every schema version references serves the newest license list. These commands fetch the shipped files
from their raw URLs; change the tag to take a newer release:
$ cd src/python_sbom/_validate/schemas
$ curl -fsSLO https://raw.githubusercontent.com/CycloneDX/specification/1.6.2/schema/bom-1.6.schema.json
$ curl -fsSLO https://raw.githubusercontent.com/CycloneDX/specification/1.6.2/schema/jsf-0.82.schema.json
$ curl -fsSLO https://raw.githubusercontent.com/CycloneDX/specification/1.7.2/schema/spdx.schema.json
$ curl -fsSLO https://raw.githubusercontent.com/CycloneDX/specification/1.7.2/LICENSE
$ sha256sum *
Put each file’s tag and SHA-256 into the cases of test_schema_files_are_the_upstream_release in
tests/_validate/test_schemas.py, which fails when a shipped file is not the release it names. .gitattributes
marks the directory -text and .pre-commit-config.yaml excludes it, so neither git nor the hooks rewrite the
files’ line endings, whitespace or formatting.
Benchmarks¶
bench/ times python-sbom’s hot paths with pyperf; CI does not run it. The
bench tox environment passes its arguments to python -m bench:
$ tox r -e bench -- prepare
$ tox r -e bench -- run
$ tox r -e bench -- compare ../baseline/.tox/bench-results/cpython3.15 .tox/bench-results/cpython3.15
prepare builds the fixtures, the only step that needs the network. run times each case in its own pyperf process
tree and writes one JSON file per case under .tox/bench-results/<python>/; options after a second -- reach
pyperf. profile runs each case once under cProfile, and under py-spy with --py-spy. Both take case names to run
only those, and list prints the names. report renders result files as a Markdown table, and compare puts two
result directories side by side. The timed process imports the python-sbom its environment installed, so to compare two
revisions, run the cases from a checkout of each and compare their result directories.
Changelog fragments¶
Add a changelog fragment under docs/changelog.d when a pull request changes what package users can use or observe.
Name it +short-name.<type>.rst, for example +vendored-licenses.feature.rst. The name describes the change; you
do not need a pull request number before opening the pull request. Write one change per fragment, and use several
fragments when a pull request has several user-visible changes.
Choose the type by its effect on users:
feature(Added): a new capability, including adding or removing support for a Python version.change(Changes): a change in behavior that remains available.bugfix(Fixed): a correction to existing behavior.doc(Documentation): a documentation change users need to know about.breaking(Breaking changes): a change that requires users to change their usage.security(Security): a security fix users need to act on.performance(Performance): a visible speed or resource improvement.deprecation(Deprecated): a supported behavior scheduled for removal.removal(Removed): removal of a user-facing capability.packaging(Packaging): a change to installation or distributions.
Apply the skip news label to a pull request with no user-visible change. You can add the label after opening the
pull request. Supplied fragments are still checked when the label is present. Bot pull requests need no fragment unless
they include one.
The release workflow links each fragment to its merged pull request and credits its human author. If several people
authored a change, put their GitHub handles in a by credit in the fragment; omit bots and
reviewers. The changelog uses linked names, while GitHub Release notes use @handle.
Build the documentation with tox r -e docs; tox r -e docs -- -b linkcheck checks its external links.