A Python tool to maintain clean dependencies across python modules.
Project description
modguard
A Python tool to enforce a modular, decoupled package architecture.
What is modguard?
Modguard allows you to define boundaries and control dependencies between your Python modules. Each module can also define its public interface.
This enforces an architecture of decoupled modules, and avoids modules becoming tightly intertwined.
If a module tries to import from another module that is not listed as a dependency, modguard will throw an exception.
If a module tries to import from another module and does not use its public interface, with strict: true
set modguard will throw an exception.
Modguard is incredibly lightweight, and has no impact on your runtime. Instead, its checks are performed as a lint check through the CLI.
Installation
pip install modguard
Usage
To define a module, add a module.yml
to the corresponding Python package. Add at least one 'tag' to identify the module:
# core/module.yml
tags: ["core"]
# db/module.yml
tags: ["db"]
# utils/module.yml
tags: ["utils"]
Next, specify the constraints for each tag in modguard.yml
in the root of your project:
# [root]/modguard.yml
constraints:
core:
depends_on: ["db", "utils"]
db:
depends_on: ["utils"]
utils:
depends_on: []
With these rules in place, modules with tag core
can import from modules with tag db
or utils
. Modules tagged with db
can only import from utils
, and modules tagged with utils
cannot import from any other modules in the project.
Modguard will now flag any violation of these boundaries.
# From the root of your Python project (in this example, `project/`)
> modguard check
❌ ./utils/helpers.py: Import "core.PublicAPI" is blocked by boundary "core". Tag(s) ["utils"] do not have access to ["core"].
If you want to define a public interface for the module, import and reference each object you want exposed in the module's __init__.py
:
# db/__init__.py
from db.service import PublicAPI
__all__ = ["PublicAPI"]
Turning on strict: true
in the module's module.yml
will then enforce that all imports from this module occur through __init__.py
# db/module.yml
tags: ["db"]
strict: true
# The only valid import from "db"
from db import PublicAPI
Modguard will now flag any import that is not from __init__.py
in the db
module, in addition to enforcing the dependencies defined above.
# From the root of your Python project (in this example, `project/`)
> modguard check
❌ ./core/main.py: Import "db.PrivateAPI" is blocked by boundary "db". "db" does not list "db.PrivateAPI" in its public interface.
You can also view your entire project's set of dependencies and public interfaces. Run modguard show
to generate a URL where you can interact with the dependency graph, as well as view your public interfaces:
> modguard show .
modguard.com/project/id
If you want to utilize this data for other purposes, run modguard show --write .
This will persist the data about your project in a modguard.json
file.
Setup
Modguard also comes bundled with a command to set up and define your initial boundaries.
modguard init .
By running modguard init
from the root of your Python project, modguard will inspect and define each top-level Python package as a module. Each module will receive a module.yml
with a single tag based on the folder name.
The tool will take into consideration the usages between modules, and write a matching set of dependencies to modguard.yml
in the project root.
> modguard check
#TODO show passing state here
Advanced
Modguard supports specific exceptions. You can mark an import with the modguard-ignore
comment:
# modguard-ignore
from db.main import PrivateAPI
This will stop modguard from flagging this import as a boundary violation.
You can also specify multiple tags for a given module:
# utils/module.yml
tags: ["core", "utils"]
This will expand the set of modules that "utils" can access to include all modules that "core" and "utils" depends_on
as defined in modguard.yml
.
modguard.yml
also accepts regex syntax:
depends_on: [".*"] # Allow imports from anywhere
depends_on: ["shared.*"] # Allow imports from any module with a tag starting with "shared"
By default, modguard ignores hidden directories and files (paths starting with .
). To override this behavior, set exclude_hidden_paths
in modguard.yml
exclude_hidden_paths: false
Details
Modguard works by analyzing the abstract syntax tree (AST) of your codebase. It has no runtime impact, and all operations are performed statically.
Boundary violations are detected at the import layer. This means that specific nonstandard custom syntax to access modules/submodules such as getattr or dynamically generated namespaces will not be caught by modguard.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.