Skip to main content

A Python tool to maintain clean dependencies across python modules.

Project description

image image image image Checked with pyright Ruff

modguard

A Python tool to enforce a modular, decoupled package architecture.

Docs

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.

PyPi Package

License

GNU GPLv3

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tach-0.0.0.tar.gz (36.6 kB view hashes)

Uploaded Source

Built Distribution

tach-0.0.0-py3-none-any.whl (35.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page