Skip to content

Commit

Permalink
Merge pull request #12 from Never-Over/update-docs-show
Browse files Browse the repository at this point in the history
cli/doc improvements
  • Loading branch information
caelean authored Feb 13, 2024
2 parents 24cd43f + 3576659 commit 064cac8
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 122 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ jobs:
- name: Check modguard
run: |
pip install .
modguard --exclude tests .
modguard check --exclude tests .
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Modguard is incredibly lightweight, and has no impact on the runtime of your cod
```bash
pip install modguard
```

### Usage
Add a `Boundary` to the `__init__.py` of the module you're creating an interface for.
```python
Expand All @@ -29,7 +28,6 @@ import modguard

modguard.Boundary()
```

Add the `public` decorator to any callable in the module that should be exported. You can also export individual members by passing them to `public` as function call arguments.
```python
# project/core/main.py
Expand All @@ -44,17 +42,28 @@ def public_function(user_id: int) -> str:
def private_function():
...

PUBLIC_CONSTANT = "Hello, world"
# This exports PUBLIC_CONSTANT from this module
PUBLIC_CONSTANT = "Hello world"
# Allow export of PUBLIC_CONSTANT from this module
public(PUBLIC_CONSTANT)
```
Modguard will now flag any incorrect dependencies between modules.
```bash
# From the root of your python project (in this example, `project/`)
> modguard .
> modguard check .
❌ ./utils/helpers.py: Import 'core.main.private_function' in ./utils/helpers.py is blocked by boundary 'core.main'
```

You can also view your entire project's set of dependencies and public interfaces. Boundaries will be marked with a `[B]`, and public members will be marked with a `[P]`. Note that a module can be both public and a boundary.
```bash
> modguard show .
example
[B]core
main
[P]public_function
[P]PUBLIC_CONSTANT
[P][B]utils
helpers
```
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.yaml` file.
### Setup
Modguard also comes bundled with a command to set up and define your initial boundaries.
```bash
Expand All @@ -66,13 +75,17 @@ This will automatically create boundaries and define your public interface for e


### Advanced Usage
Modguard also supports specific allow lists within the `public()` decorator.
Modguard also supports specific allow lists within `public`.
```python
@modguard.public(allowlist=['utils.helpers'])
def public_function(user_id: int) -> str:
...

PUBLIC_CONSTANT = "Hello world"
public(PUBLIC_CONSTANT, allowlist=['utils.helpers'])

```
This will allow for `public_function` to be imported and used in `utils.helpers`, but restrict its usage elsewhere.
This will allow for `public_function` and `PUBLIC_CONSTANT` to be imported and used in `utils.helpers`, but restrict its usage elsewhere.

Alternatively, you can mark an import with the `modguard-ignore` comment:
```python
Expand Down Expand Up @@ -102,9 +115,10 @@ modguard.public()
```
This syntax also supports the `allowlist` parameter.


### Details
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 the import layer; specific nonstandard custom syntax to access modules/submodules such as getattr or dynamically generated namespaces may not be caught by modguard.
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 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](https://pypi.org/project/modguard/)

Expand Down
4 changes: 3 additions & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

### 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.

Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ Modguard is incredibly lightweight, and has no impact on the runtime of your cod

## Commands

* [`modguard [dir-name]`](usage.md#modguard) - Check boundaries are respected throughout a directory.
* [`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 show [dir-name]` - Generate a YAML representation of the boundaries in a directory.
* `modguard show [dir-name]` - View and optionally generate a YAML representation of the boundaries in a directory.
46 changes: 33 additions & 13 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,59 @@
# 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.
## modguard check
Modguard will flag any unwanted imports between modules. It is recommended to run `modguard check` 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
```bash
usage: modguard check [-h] [-e file_or_path,...] path

Check boundaries with modguard

positional arguments:
path The path of the root of your project that contains all defined boundaries.
path The path of the root of your Python project.

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 .`
Comma separated path list to exclude. tests/, ci/, etc.
```


## 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`.
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`
```
```bash
usage: modguard init [-h] [-e file_or_path,...] path

Initialize boundaries in a repository with modguard
Initialize boundaries with modguard

positional arguments:
path The path of the Python project in which boundaries should be initialized.
path The path of the root of your Python project.

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.
Comma separated path list to exclude. tests/, ci/, etc.
```


## modguard show
Modguard can display your current set of boundaries and public interfaces.

By running `modguard show .` from the root of your python project, you can view your full project's file tree annotated with both boundaries (`[B]`) and members that have been defined as public (`[P]`). Optionally, specifying `-w/--write` will write the output to a `modguard.yaml` file, which can then be consumed for external usecases.
```bash
usage: modguard show [-h] [-e file_or_path,...] [-w] path

Show your existing boundaries in modguard

positional arguments:
path The path of the root of your Python project.

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.
-w, --write Write the output to a `modguard.yaml` file
```
4 changes: 3 additions & 1 deletion modguard/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ def check_import(

def check(root: str, exclude_paths: Optional[list[str]] = None) -> list[ErrorInfo]:
if not os.path.isdir(root):
return [ErrorInfo(exception_message=f"The path {root} is not a directory.")]
return [
ErrorInfo(exception_message=f"The path {root} is not a valid directory.")
]

# This 'canonicalizes' the path arguments, resolving directory traversal
root = fs.canonical(root)
Expand Down
Loading

0 comments on commit 064cac8

Please sign in to comment.