Dependency admission
ImportSpy maps imports to installed distributions before evaluating dependency policy. It reads source paths and packaging metadata without importing the target or its dependencies. No network access is needed.
schema_version: 1
dependencies:
requests:
allowed: true
version: ">=2.32,<3"
declared: true
legacy-package:
allowed: false
import:legacy_module:
allowed: false
dependency_options:
unresolved: deny
undeclared: deny
unlisted: allow
Distribution policy keys use packaging name normalization: My_Package,
my.package, and my-package identify the same distribution. Import names remain
case sensitive. import:<top-level-name> explicitly addresses an import, including
an unresolved import; this avoids guessing a distribution name from an import.
Resolution and inventory
The resolver uses importlib.metadata.packages_distributions() and distribution
metadata. On Python 3.10 it uses the maintained
importlib-metadata backport
so wheels without top_level.txt can be mapped using their RECORD file too.
This handles import/distribution name differences such as yaml and
PyYAML without special cases. One import can map to multiple distributions, and
one distribution can provide several imports. All mapped contributors are retained
and evaluated. The inventory is conservative: importing one child of a namespace
checks every contributor mapped to that namespace's top-level name.
See the Python metadata documentation.
Dependencies are classified as:
| Kind | Meaning |
|---|---|
stdlib |
A built-in or standard-library top-level import in the checking interpreter. |
internal |
A matching source file or package directory under the selected project root, its src directory, or the subject's directory. |
external |
An installed distribution identified by metadata. |
namespace |
Multiple installed distributions, or a local namespace with installed contributors, associated with one import. |
direct |
One distribution with VCS, local-directory, or archive origin metadata. |
unresolved |
No complete supported source or distribution mapping is available. |
Regular project source takes precedence over standard-library names except built-ins. That includes dotted imports whose top-level name is shadowed by a local source module or regular package. Local namespace directories retain installed contributors when metadata identifies them. Relative imports are checked against their source package location. Symlinks escaping the project root are unresolved, rather than classified as project source or silently attributed to an unrelated installed distribution.
Resolution never calls find_spec() on dotted names, which can import a parent
package. Some editable installations omit the metadata needed to map import names;
these remain unresolved. Installed custom metadata finders are trusted host code.
Resolution does not emulate arbitrary import hooks, package __path__ mutations,
zip source trees, or dynamic imports. It does not prove that every submodule or
imported attribute exists. A relative from . import value may remain unresolved
when value is a package attribute rather than a source module.
The inventory includes imports observed in the inspected file, including imports
inside functions and conditional branches. The optional flag records conditional
observations; it does not bypass policy. ImportSpy does not recursively inspect
every imported file or resolve a package manager's transitive dependency graph.
DependencyResolver(project_root) caches the import mapping and distribution
metadata per instance. Reuse an instance for a batch, and create a new one after
changing installed packages or project declarations.
Rules and defaults
| Rule | Semantics |
|---|---|
allowed: false |
Reject any observed matching distribution/import. Defaults to true. |
required: true |
Require a matching dependency in the source inventory, not merely an installed package. |
version: ">=2,<3" |
Apply a PEP 440 specifier to the installed version. Unknown/invalid versions fail. |
declared: true |
Require a matching project declaration; unknown declaration context fails. |
declared: false |
Require known absence from supported project declarations. |
origin |
Match explicitly specified origin fields; unknown facts fail the comparison. |
editable: false |
Require metadata establishing a non-editable installation. Unknown is not false. |
provenance: {required: true} |
Require verified evidence through the admission engine's evidence policy. |
Version constraints use packaging.SpecifierSet, including its normal prerelease
rules. A range such as >=2,<3 does not automatically admit prereleases. Poetry's
caret version syntax is not valid in an admission version rule.
Global dependency_options provide independent constraints:
unresolved: denyis the default. Setallowexplicitly when unresolved imports are acceptable for the use case.undeclared: allowis the default.denyrejects external distributions that are undeclared or whose declaration context is unknown.unlisted: allowis the default.denyrequires an explicit distribution rule for each observed external distribution, including all namespace contributors.
Import rules do not bypass these constraints. For example, an import allow rule
does not establish the identity of an unresolved distribution and does not satisfy
unlisted: deny. Standard-library and internal imports are outside external
distribution allowlists, but may be denied using explicit import rules.
Declared, observed, and installed
Declaration detection reads only pyproject.toml in the selected project root.
It does not infer ownership from an unrelated installed package, execute
setup.py, or search arbitrary parent directories.
Supported declaration contexts are:
- PEP 621
[project].dependenciesand all[project.optional-dependencies]groups. - Legacy Poetry
[tool.poetry.dependencies], including optional dependencies. Development dependency groups are excluded.
A declaration means the distribution is named somewhere in those runtime/optional requirements. Environment markers are parsed as part of PEP 508 requirements but are not evaluated to select an active environment. All optional extras are counted as declarations; their activation is not inferred. This is intentional because static source inspection also observes inactive branches. Declared status does not prove a dependency should be installed for a particular selected extra or platform, and does not enforce the project's dependency version specifiers. Use explicit admission version rules for that purpose.
Missing, malformed, unsupported, or dynamically supplied dependency declarations
produce declared: null. PEP 621 metadata takes precedence over legacy Poetry
metadata when both tables exist. A valid static [project] without dependency
entries means an empty declaration set. Each namespace distribution has its own
declaration state; the import-level state is true only when all contributors are
declared, false when a contributor is undeclared, and unknown when the context is
unknown.
Origin and editable installations
dependencies:
internal-payment-sdk:
origin:
type: vcs
repository: https://github.com/acme/payment-sdk
commit: 0123456789abcdef
editable: false
The resolver reads PEP 610 direct URL metadata:
- VCS origin: repository URL and commit identifier.
- Local directory: file URL and editable flag.
- Archive origin: URL and recorded hashes, including the legacy single hash form.
These are metadata observations, not cryptographic verification of installed
files. A missing direct_url.json produces unknown origin and editable state.
It does not prove installation from PyPI or any particular index. The domain
can represent index evidence, but core resolution does not fabricate it.
Origin URLs remove userinfo, query strings, and fragments before inclusion in
decisions. Scheme and host are normalized; default HTTP(S) ports and trailing
slashes are removed. Repository comparison also removes a terminal .git suffix.
Paths retain case and are not percent-decoded. Arbitrary secrets embedded inside
a path are not detectable; avoid credential-bearing URL paths in package metadata.
Malformed metadata is represented as unknown, never as verified provenance.
Origin matching supports type, url, repository, and commit fields.
Provenance verification belongs to explicitly supplied evidence providers. The
engine requires evidence of kind distribution.provenance, with the canonical
distribution name as subject, status: verified, and a truthy observed value.
provenance: true is shorthand for provenance: {required: true}. Merely
finding a direct URL, a commit, or an archive hash cannot satisfy verified
provenance policy.
Library boundary and violation codes
from pathlib import Path
from importspy.dependencies import (
DependencyOptions, DependencyResolver, DependencyRule,
collect_dependency_evidence, evaluate_dependencies,
)
from importspy.domain import ImportReference
root = Path.cwd()
dependencies = DependencyResolver(root).resolve(
[ImportReference(name="yaml")], root / "plugin.py"
)
evidence = collect_dependency_evidence(dependencies)
violations = evaluate_dependencies(
dependencies, {"pyyaml": DependencyRule(version=">=6")},
DependencyOptions(), str(root / "plugin.py"),
)
evaluate_dependencies is pure and performs no file or network access. It handles
dependency facts; provenance requirements are evaluated against provider evidence
by the admission engine.
| Code | Meaning |
|---|---|
ISPY-D101 |
Dependency denied by a rule. |
ISPY-D102 |
Unresolved import denied. |
ISPY-D103 |
Required dependency absent from the inventory. |
ISPY-D104 |
Version constraint not established. |
ISPY-D105 |
Declaration constraint not established. |
ISPY-D106 |
Origin mismatch or missing origin fact. |
ISPY-D107 |
Editable constraint not established. |
ISPY-D108 |
Unlisted external distribution denied. |