Skip to content

Commit

Permalink
Merge pull request #14 from Never-Over/update-docs
Browse files Browse the repository at this point in the history
why modguard
  • Loading branch information
caelean authored Feb 14, 2024
2 parents b565ba7 + 4794403 commit e205e7c
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# modguard
A Python tool to support and enforce a modular, decoupled package architecture.

[Full Documentation](https://never-over.github.io/modguard/)
[Docs](https://never-over.github.io/modguard/)

### 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.
Expand Down
33 changes: 18 additions & 15 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ 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
import modguard

Boundary()
modguard.Boundary()
```
### In `__init__.py`
When a `Boundary` appears in `__init__.py`, this marks the contents of the entire package as private by default.
Expand Down Expand Up @@ -44,22 +44,23 @@ Marking a member as `public` allows it to be imported outside its boundary. This

When present, `path` identifies the object being marked as public.
```python
from modguard import public
import modguard

x: int = 3

public("x")
public(x)
# These are functionally the same
modguard.public("x")
modguard.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
import modguard

x: int = 3

public(x, allowlist=["project.core.domain"])
modguard.public(x, allowlist=["project.core.domain"])

...
# In project/core/other_domain/logic.py
Expand All @@ -73,9 +74,9 @@ from project.utils import x
`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
import modguard

@public(allowlist=["project.core.domain"])
@modguard.public(allowlist=["project.core.domain"])
def my_pub_function():
...
```
Expand All @@ -84,9 +85,9 @@ def my_pub_function():
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
import modguard

public()
modguard.public()
...
# In project/cli.py
# This import is allowed because 'project.core.logic' is public
Expand All @@ -97,10 +98,10 @@ from project.core import logic
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
import modguard

Boundary()
public()
modguard.Boundary()
modguard.public()
...
# In project/cli.py
# This import is allowed because 'project.core' is public
Expand All @@ -118,4 +119,6 @@ The directive can also be specific about the import to ignore, which is particul
# 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.
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.

Note: 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.
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pip install modguard

Verify your installation is working correctly
```bash
modguard --help
modguard -h
```

## Guarding a Project
Expand All @@ -27,7 +27,7 @@ If you are adding `modguard` to an existing project, you have two main options:

```bash
# From the root of your Python project
modguard .
modguard check .
```

After guarding your project, running `modguard` from the root will check all imports to verify that packages remain correctly decoupled.
5 changes: 2 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# 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.
Modguard enables you to explicitly define a public interface for your Python modules. Marking a package with a `Boundary` will make all of its internals private by default, exposing only the members marked with `public`.

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 check [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 check [dir-name]`](usage.md#modguard-check) - Check boundaries are respected throughout a directory.
* [`modguard show [dir-name]`](usage.md#modguard-show) - View and optionally generate a YAML representation of the boundaries in a directory.
23 changes: 23 additions & 0 deletions docs/why-modguard.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Why modguard?

We built `modguard` to solve a recurring problem that we've experienced on software teams - code sprawl. Unintended cross-module imports would tightly couple together what used to be independent domains, and eventually create "balls of mud". This made it harder to test, and harder to make changes. Mis-use of modules which were intended to be private would then degrade performance and even cause security incidents.

This would happen for a variety of reasons:
- Junior developers had a limited understanding of the existing architecture and/or frameworks being used
- It's significantly easier to add to an existing service than to create a new one
- Python doesn't stop you from importing any code living anywhere
- When changes are in a 'gray area', social desire to not block others would let changes through code review
- External deadlines and management pressure would result in "doing it properly" getting punted and/or never done

The attempts to fix this problem almost always came up short. Inevitably, standards guides would be written and stricter and stricter attempts would be made to enforce style guides, lead developer education efforts, and restrict code review. However, each of these approaches had their own flaws.

The solution was to create a set of definitions for each module and it's public interface, and enforce those domain boundaries through CI. This meant that no developer could ever violate these boundaries without explicitly changing the definition of either the interface or the boundary, a significantly smaller and well scoped set of changes that could then be maintained and managed by those who understood the correct semantics for the system.

With modguard set up, you can collaborate on your codebase with confidence that developers won't violate the intentional design of your modules.

Modguard is:

- fully open source
- able to be adopted incrementally
- implemented with no runtime footprint
- a standalone library with no external dependencies
- interoperable with your existing system (cli, generated config)

0 comments on commit e205e7c

Please sign in to comment.