-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into speed-improvements
- Loading branch information
Showing
15 changed files
with
492 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
name: publish_docs | ||
on: | ||
push: | ||
branches: [ "main" ] | ||
permissions: | ||
contents: write | ||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Configure Git Credentials | ||
run: | | ||
git config user.name github-actions[bot] | ||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com | ||
- uses: actions/setup-python@v5 | ||
with: | ||
python-version: 3.x | ||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV | ||
- uses: actions/cache@v4 | ||
with: | ||
key: mkdocs-material-${{ env.cache_id }} | ||
path: .cache | ||
restore-keys: | | ||
mkdocs-material- | ||
- run: pip install mkdocs-material | ||
- run: mkdocs gh-deploy --force |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# API | ||
|
||
## `modguard.Boundary` | ||
A `Boundary` makes all internal members private by default. | ||
|
||
`Boundary` accepts no arguments, and has no runtime behavior. It is detected statically by `modguard`. | ||
```python | ||
# project/core/__init__.py | ||
from modguard import Boundary | ||
|
||
Boundary() | ||
``` | ||
### In `__init__.py` | ||
When a `Boundary` appears in `__init__.py`, this marks the contents of the entire package as private by default. | ||
```python | ||
# project/core/inner.py | ||
# This function will be considered private | ||
# due to the boundary at 'project.core' | ||
def private_function(): | ||
... | ||
``` | ||
|
||
### In Modules | ||
When a `Boundary` appears in a standalone Python file, this marks the contents of the file itself as private by default. | ||
```python | ||
# project/core/other.py | ||
from modguard import Boundary | ||
|
||
Boundary() | ||
|
||
# This function will be considered private | ||
# due to the boundary at 'project.core.other' | ||
def other_private_function(): | ||
... | ||
``` | ||
|
||
## `modguard.public` | ||
Marking a member as `public` allows it to be imported outside its boundary. This should be used to mark the intended public interface of a package or module. | ||
|
||
`public` can be used as either a decorator or a bare function, and has no runtime behavior. It is detected statically by `modguard`. | ||
|
||
### Parameters | ||
`public` accepts one optional positional argument (`path`) and one optional keyword argument (`allowlist`). | ||
|
||
When present, `path` identifies the object being marked as public. | ||
```python | ||
from modguard import public | ||
|
||
x: int = 3 | ||
|
||
public("x") | ||
public(x) | ||
``` | ||
|
||
When present, `allowlist` defines a list of module paths which are allowed to import the object. Modules which are descendants of the modules in the `allowlist` are also allowed. If any other modules import the object, they will be flagged as errors by `modguard`. | ||
```python | ||
# In project/utils.py | ||
from modguard import public | ||
|
||
x: int = 3 | ||
|
||
public(x, allowlist=["project.core.domain"]) | ||
|
||
... | ||
# In project/core/other_domain/logic.py | ||
# This import is not allowed, | ||
# because the module ('project.core.other_domain.logic') | ||
# is not contained by any module in the allowlist | ||
from project.utils import x | ||
``` | ||
|
||
### As a Decorator | ||
`public` can also be used as a decorator to mark functions and classes as public. Its behavior is the same as when used as a function, and it accepts the same keyword arguments (the decorated object is treated as `path`) | ||
|
||
```python | ||
from modguard import public | ||
|
||
@public(allowlist=["project.core.domain"]) | ||
def my_pub_function(): | ||
... | ||
``` | ||
|
||
### Entire Module | ||
When `public` is used without a `path` argument, it signifies that the entire containing module is public. This means that any top-level member of the module or the module itself can be imported externally (subject to `allowlist`). | ||
```python | ||
# In project/core/logic.py | ||
from modguard import public | ||
|
||
public() | ||
... | ||
# In project/cli.py | ||
# This import is allowed because 'project.core.logic' is public | ||
from project.core import logic | ||
``` | ||
|
||
### In `__init__.py` | ||
When `public` is used without a `path` argument in the `__init__.py` of a package, the top-level module of the package is treated as public. | ||
```python | ||
# In project/core/__init__.py | ||
from modguard import Boundary, public | ||
|
||
Boundary() | ||
public() | ||
... | ||
# In project/cli.py | ||
# This import is allowed because 'project.core' is public | ||
from project import core | ||
``` | ||
|
||
## `modguard-ignore` | ||
To ignore a particular import which should be allowed unconditionally, use the `modguard-ignore` comment directive. | ||
```python | ||
# modguard-ignore | ||
from core.main import private_function | ||
``` | ||
The directive can also be specific about the import to ignore, which is particularly useful when importing multiple modules. | ||
```python | ||
# modguard-ignore private_function | ||
from core.main import private_function, public_function | ||
``` | ||
Note: Names given to `modguard-ignore` should match the alias as it is used in the subsequent import line, not the full module path from the project root. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# FAQ | ||
|
||
### How does it work? | ||
Modguard works by analyzing the abstract syntax tree (AST) of your codebase. The `Boundary` class and `@public` decorator have no runtime impact, and are detected by modguard statically. Boundary violations are detected at import time. | ||
### Why does `modguard` live in my application code? | ||
Modguard is written as a Python library for a few main reasons: | ||
|
||
- **Visibility**: When boundary information is co-located with application code, it is visible to a code reviewer or future maintainer. | ||
- **Maintenance**: When packages or public members are moved, renamed, or removed, in-line `modguard` will automatically match the new state (since it will move along with the code, or be removed along with the code). | ||
- **Extensibility**: Having `modguard` in-line will support future dynamic configuration or runtime violation monitoring. | ||
### What is a boundary? | ||
A **boundary** can be thought of as defining a logical module within your project. A project composed of decoupled logical modules with explicit public interfaces is easier to test and maintain. | ||
### Are conditional imports checked? | ||
At the moment, `modguard` will check all imports in your source files, including those which are called conditionally. This is an intentional design decision, but may be made configurable in the future. | ||
|
||
### Can you catch dynamic references? | ||
Since `modguard` uses the AST to find imports and public members, dynamic imports (e.g. using a string path) and dynamic names (e.g. using `setattr`, `locals`, `globals`) are generally not supported. If these usages cause `modguard` to report incorrect errors, the [ignore directive](api.md#modguard-ignore) should be sufficient to reach a passing state. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Getting Started | ||
|
||
## Installation | ||
|
||
[PyPi package](https://pypi.org/project/modguard/) | ||
|
||
|
||
Install modguard into a Python environment with `pip` | ||
|
||
```bash | ||
pip install modguard | ||
``` | ||
|
||
Verify your installation is working correctly | ||
```bash | ||
modguard --help | ||
``` | ||
|
||
## Guarding a Project | ||
|
||
If you are adding `modguard` to an existing project, you have two main options: | ||
|
||
1. Use [`modguard init`](usage.md#modguard-init) to automatically set up package boundaries and identify necessary public members | ||
2. Manually mark boundaries and public members in your code ([See API docs](api.md)) | ||
|
||
## Checking Boundaries | ||
|
||
```bash | ||
# From the root of your Python project | ||
modguard . | ||
``` | ||
|
||
After guarding your project, running `modguard` from the root will check all imports to verify that packages remain correctly decoupled. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Overview | ||
|
||
## What is modguard? | ||
Modguard enables you to explicitly define the public interface for a given module. Marking a package with a `Boundary` makes all of its internals private by default, exposing only the members marked with the `@public` decorator. | ||
|
||
This enforces an architecture of decoupled and well defined modules, and ensures the communication between domains is only done through their expected public interfaces. | ||
|
||
Modguard is incredibly lightweight, and has no impact on the runtime of your code. Instead, its checks are performed through a static analysis CLI tool. | ||
|
||
## Commands | ||
|
||
* [`modguard [dir-name]`](usage.md#modguard) - Check boundaries are respected throughout a directory. | ||
* [`modguard init [dir-name]`](usage.md#modguard-init) - Initialize package boundaries in a directory. | ||
* `modguard show [dir-name]` - Generate a YAML representation of the boundaries in a directory. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Usage | ||
|
||
## modguard | ||
Modguard will flag any unwanted imports between modules. It is recommended to run `modguard` in a similar way as a linter or test runner, e.g. in pre-commit hooks, on-save hooks, and in CI pipelines. | ||
|
||
``` | ||
usage: modguard [-h] [-e file_or_path,...] path | ||
positional arguments: | ||
path The path of the root of your project that contains all defined boundaries. | ||
options: | ||
-h, --help show this help message and exit | ||
-e file_or_path,..., --exclude file_or_path,... | ||
Comma separated path list to exclude. tests/,ci/,etc. | ||
Make sure modguard is run from the root of your repo that a directory is being specified. For example: `modguard .` | ||
``` | ||
|
||
|
||
## modguard init | ||
Modguard comes bundled with a command to set up and define your initial boundaries. | ||
|
||
By running `modguard init` from the root of your python project, modguard will declare boundaries on each python package within your project. Additionally, each member of that package which is imported from outside the boundary will be marked `public`. | ||
|
||
This will automatically lock-in the public interface for each package within your project, and instantly reach a passing state when running `modguard` | ||
``` | ||
usage: modguard init [-h] [-e file_or_path,...] path | ||
Initialize boundaries in a repository with modguard | ||
positional arguments: | ||
path The path of the Python project in which boundaries should be initialized. | ||
options: | ||
-h, --help show this help message and exit | ||
-e file_or_path,..., --exclude file_or_path,... | ||
Comma separated path list to exclude. tests/,ci/,etc. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Why modguard? |
Oops, something went wrong.