Python module graph — enforce direct architecture boundaries#
Use python-module-graph when a Python rule is about which repository modules may import which
other modules. Every scanned .py file becomes a pythonModule component. A pinned structured
parser records statically declared imports as policy-independent imports edges before evaluation.
This profile is available in the v0.3.1 registry release. It is additive: the released
python-import-surface profile remains available with its existing behavior. Install the exact
version and run the structured proof:
npm view bce-engine@0.3.1 version dist.integrity
npm install --save-dev --save-exact bce-engine@0.3.1
npx --no-install bce demo --recipe python-module-layering
The conforming tree has the API adapter depend inward on domain code and scores 100. The drift tree adds one reverse domain-to-API import; C3 names the importer, resolved target, and source line.
Directional layering#
This is the core fragment from the complete executable fixture:
{
"minEngineVersion": "0.3.0",
"constraints": [
{
"id": "domain-cannot-import-api",
"type": "forbiddenDependency",
"severity": "critical",
"from": "*",
"to": "module:src/service/api.py",
"scopePaths": ["src/service/domain/**/*.py"]
}
],
"extraction": {
"profile": "python-module-graph",
"paths": ["src/**/*.py"],
"pythonRoots": ["src"],
"minFiles": 4
}
}
extraction.paths determines which files exist in the observed graph. scopePaths selects the
importers governed by C2 or C3. to selects the dependency target. Keep the scan broad enough to
include repository targets while keeping each constraint's importer scope as narrow as its intent.
Run the measured author → RED → fix → GREEN Python walkthrough to create a boundary against a four-file service package.
Python roots are explicit#
pythonRoots are repository-relative import roots such as src, lib, or .. They define how a
file path becomes an import name: under root src, src/service/api.py is service.api, and
src/service/__init__.py is service. BCE does not infer roots from the shell, an active virtual
environment, or sys.path.
Every scanned Python file must belong to exactly one configured root. BCE refuses missing, duplicate, overlapping, globbed, escaping, or symlink-escaping roots; duplicate import names and case-fold collisions also refuse. These checks keep the graph stable across operating systems and working directories.
Target selectors#
| Selector | Matches | Example |
|---|---|---|
module:<repo-path-or-glob> | a resolved .py file inside the repository | module:src/service/api.py |
package:<import-root> | one external Python import namespace | package:requests |
Python package selectors name import namespaces, not PyPI distribution names. For example, a
distribution may install a differently named namespace; BCE does not consult installed package
metadata. This profile does not support Node-style builtin: targets.
What is observed#
The parser records import x, dotted and aliased imports, from x import y, relative imports,
parenthesized imports, imports after semicolons, and imports inside suites or nested blocks. When an
imported member is itself a scanned module, BCE records that module edge. Otherwise an absolute
canonical name becomes package:<top-level-namespace>.
C1 can require at least one pythonModule. C2 and C3 grade direct imports edges and require both
scopePaths and to. C2 means every in-scope importer must carry the selected direct edge. C3
rejects every matching direct edge. C4, C5, and C6 continue to work over component paths, scanned
files, and line content respectively. Python call and egress semantics are not claimed.
Uncertainty blocks relevant boundaries#
BCE locates dynamic __import__, importlib.import_module, exec, eval, and mutations of
sys.path, sys.meta_path, or sys.path_hooks as unresolved import facts. An unresolved fact in a
C2 or C3 importer scope produces a blocking fail-closed violation instead of evidence for GREEN.
The extractor also refuses invalid Python syntax, non-UTF-8 source or coding cookies, symlinked
source files, relative imports that escape a root, ambiguous namespace imports, non-.py files,
and the line-scan extractor kind. Source is decoded as UTF-8; a UTF-8 BOM is accepted and removed.
Honest limits#
This is a direct static-import graph. It does not claim transitive reachability, cycle detection,
runtime-only loading, installed-distribution resolution, custom import hooks, .pyi stub semantics,
decorator or call graphs, egress observation, or automatic import-root inference. Declare the files
and roots you intend to govern, set a real minFiles floor, and treat every refusal as work to make
the boundary observable.
Next: choose another executable boundary, read C1–C4, or install the gate in advisory mode.