Dependency sources and precedence¶
Note: Background reading -- useful for understanding what shows up in a generated Source SBOM's dependency list and why, not needed to just generate one.
Two kinds of dependency information¶
Every Source SBOM (loom project/loom generate) always includes your
project's declared dependencies -- the version ranges listed in
pyproject.toml's [project.dependencies] (or [tool.poetry.dependencies]
for a Poetry project), e.g. requests>=2.0. These are read directly and
always present, with or without a lock file.
If a lock file is also present next to pyproject.toml, Pitloom
additionally reads its exact resolved versions and adds any dependency
they introduce that your declared list doesn't already name -- your
project's transitive dependencies, pinned exactly (e.g. idna==3.7).
When a direct dependency is declared with a version range and a lock
file is present, Pitloom uses the lock file's exact resolved version
for the emitted SBOM package. For example, if pyproject.toml
declares requests>=2.0 and your lock file resolved it to 2.31.0, the
package's software_packageVersion and PyPI PURL become 2.31.0 (with
enrichment fetching metadata for that exact release), while the
original declared range is preserved in the element's
declared_constraint provenance annotation. Transitive dependencies
revealed only by the lock file (e.g. idna==3.7) are added as new,
additive entries.
Supported lock formats, and what counts as "resolved"¶
| Priority | Format | File | What's included |
|---|---|---|---|
| 1 (highest) | PEP 751 | pylock.toml |
Every resolved package the file records for its declared default-groups (a package needed only for a non-default dependency-group/extra, per its own marker field, is excluded). |
| 2 | uv | uv.lock |
Your project's own main/runtime dependencies, walked transitively (dependencies of dependencies, and so on), including any extra a real dependency requests on another package (e.g. uvicorn[standard] pulls in uvicorn's own standard extra). Your project's own optional-dependencies/dev-dependencies groups (the ones a user would have to opt into, e.g. pip install yourpkg[dev]) are not included. A dependency pinned to more than one version for different Python versions is skipped, not guessed at, and nothing depending only on it is walked into either -- see below. |
| 3 | Poetry | poetry.lock |
Packages in the main dependency group only (not [tool.poetry.group.*] dev/extra groups). |
| 4 | PDM | pdm.lock |
Packages in the default dependency group only. |
| 5 | Pipenv | Pipfile.lock |
Packages in the default section only (not develop). A package whose resolved version isn't a single exact ==/=== pin is skipped, not guessed at. |
| 6 (lowest) | -- | pinned requirements.txt |
Not a real lock file -- only used when every line in the file is already a single exact ==/=== pin. If even one line is unpinned, ranged, a pip option (-e, -r, --hash, ...), a URL-based requirement (even one that looks like it points at a tagged release), or one package name is pinned to two conflicting versions, the whole file is skipped, not just that line -- see below. |
requirements.txt's entry is tagged Method: pinned_requirements in
its provenance annotation (see "How to tell which source was used"
below), not Method: resolved_lockfile like every format above it --
it's the one source here that isn't a real lock file, just a list of
lines that happen to already be fully pinned.
A URL-based requirements.txt line is never treated as a version
pin, even when the URL looks like it points at a tagged release (e.g.
name @ https://github.com/org/repo/archive/refs/tags/v2.31.0.zip). A
git tag or release filename is an arbitrary string with no guaranteed
relationship to the package's real, normalised version -- Pitloom
doesn't fetch the URL to check, so a line like that disqualifies the
whole file the same as an unpinned or ranged one would.
Only the single highest-priority usable lock file present is used. If more than one lock file exists in the same project directory (uncommon, but possible after a build-tool migration), Pitloom picks the highest-ranked one in the table above that it can actually read and parse, and ignores every other one entirely -- it never merges two lock files' resolutions together. "Usable" matters: a higher-priority lock file that's absent, unparseable, or otherwise not a genuine file of its claimed format doesn't win by merely being present -- Pitloom moves on to the next-highest-priority source instead, the same as if that file weren't there at all. This holds even when the winning lock resolves to zero dependencies: a real, successfully-parsed lock file that legitimately has nothing to add is still a definitive answer, and a lower-priority lock present alongside it is still ignored, not used to fill in what looks like a gap.
A lock entry that can't be resolved to one exact version is left out,
not guessed. uv.lock in particular can record the same package
pinned to genuinely different versions for different Python versions in
one file; Pitloom doesn't evaluate environment markers to pick one, so
such a dependency is simply omitted from the additional (transitive)
list rather than added with a possibly-wrong version. Check stderr for a
WARNING: naming the skipped package if a dependency you expected is
missing.
Version comparison: PEP 440, not SemVer¶
Pitloom compares dependency versions using PEP 440 equality,
not SemVer. "Same version" means the two version strings normalise to
the identical release under PEP 440 -- trailing-zero components are
padded and compared, so 1.0, 1.0.0, and 1.0.0.0 are all the same
version. It does not mean "the latest release compatible with 1.0"
or any other range/caret-style resolution: 1.0 and 1.0.1 are
different versions under this comparison, exactly as they'd differ under
strict string equality, even though a SemVer-style ^1.0.0 range would
consider 1.0.1 compatible.
This comparison is what decides whether two version strings for the same package are treated as agreeing or genuinely conflicting. It shows up in two places:
- A lock file's own duplicate entries. If one lock file records the
same package name more than once (e.g. a platform-specific variant),
entries that normalise to the same PEP 440 release are silently
collapsed into one; entries that don't get a
WARNING:naming both versions, and that package is left out of the transitive list entirely rather than guessed at. - A declared range vs. the lock file's resolved version. When a
direct dependency is unpinned or declared as a range, the lock file's
resolved version is used (see above); when it's already pinned
exactly (e.g.
requests==2.31.0) and the lock file separately resolved it to a version that doesn't normalise the same way (e.g.2.31.1), Pitloom logs aWARNING:but keeps the declared pin -- the lock's differing value never silently overrides an exact pin the project itself declared. Either direction, a genuine disagreement also adds aconflictAnnotation (field: "dependency_version") to the generated SBOM, not just a stderrWARNING:-- see Metadata provenance.
Which commands use lock files at all¶
Lock-file resolution only ever applies to a Source SBOM
(loom project, loom generate, and the equivalent
Python API call) -- describing your project as
declared in source, before a build happens.
It's never consulted by:
loom wheel,loom embed-wheel,loom verify-wheel/validate-wheel-- a built wheel's own installed metadata is the ground truth for an Analyzed SBOM; a lock file (which describes what a future build might resolve to) is beside the point once a real wheel exists.loom env-- describing what's actually installed in an environment is more authoritative than a lock file that may be stale relative to it.- The Hatchling build hook -- SBOMs it embeds
during
hatch build/pip install .describe the build artifact itself, the same "real build, not a lock's prediction" reasoning asloom wheelabove.
So it's normal for loom project's SBOM to list more transitive
dependencies than an SBOM embedded by the Hatchling build hook for the
same project -- they're describing different things (a hypothetical
resolution vs. what a real build actually installed), not a bug in
either.
How to tell which source was used¶
Every transitive SBOM package introduced by a lock file carries a
provenance annotation naming the file and method Pitloom used, e.g.
Source: pylock.toml | Method: resolved_lockfile. Direct dependencies
retain their declared source (e.g. Source: pyproject.toml), with
Version resolved: Project lock file noting when a declared range was
resolved to an exact version by the lock file. The cascade stops at
the first usable source it tries, so it doesn't itself check whether a
still-lower-priority lock file is also present on disk -- the one
case it does detect and annotate is poetry.lock, since that one is
resolved earlier, before the cascade runs, and the cascade can see its
already-set result: if a higher-priority format then wins over it, the
annotation adds a note, e.g. Source: pylock.toml | Method:
resolved_lockfile | Note: supersedes poetry.lock. Two lock files that
are both tried by the cascade itself (e.g. pdm.lock and Pipfile.lock
both present) never produce this note -- only the single winning
source's own annotation appears. See Metadata
provenance for how to read these annotations
in the generated SBOM.
Configuration and flags¶
There is currently no setting to change the priority order above or
choose a specific lock file -- which one wins is automatic, based purely
on which lock file (if any) is present next to pyproject.toml.
To turn lock-file reading off entirely -- falling back to direct
dependencies plus environment introspection only -- pass
--no-use-lockfile on loom project, loom generate, or loom enrich
(the last only together with --project-dir -- no project metadata is
read at all on a bare loom enrich <model>, so the flag has no effect
without it), or set [tool.pitloom] use-lockfile = false in
pyproject.toml. On by default (an explicit CLI flag always wins over
the config value); see Configuration.
loom enrich --project-dir DIR computes the fragment's target document
identity from that same setting: pass --use-lockfile/--no-use-lockfile
explicitly to match whatever produced the base SBOM, or omit it to
auto-match DIR's own [tool.pitloom] use-lockfile config. A mismatch here
silently produces a fragment referencing the wrong document identity.
--use-lockfile/--no-use-lockfile has no effect for an sdist archive
target (loom project/loom generate on a .tar.gz/.zip): no lock/pin
cascade support exists for archives yet, so a real lock file in the
archive's own build environment was never read into it in the first
place. An explicit flag passed for an archive target logs a WARNING:
and is otherwise ignored.
--offline (also settable via [tool.pitloom] offline --
see Configuration) is unrelated to lock-file
reading: it only controls whether Pitloom's own PyPI JSON API lookups
(used to fill in a dependency package's supplier/license/copyright/integrity gaps)
are attempted. A lock file is always read from disk regardless of this
setting -- there's no network involved in reading it. When a supported lock
file contains SHA-256 digests, Pitloom uses them for verifiedUsing
checksums across both online and offline builds, taking priority over
PyPI JSON API lookups and eliminating the need for network access.
See also¶
- Command line and Python API for how to run a Source SBOM generation that reads lock files this way.
- Metadata provenance for the general provenance-annotation mechanism this page's "how to tell which source was used" section relies on.
- Configuration for
--offlineand every other[tool.pitloom]setting.