Skip to content

Commit

Permalink
Merge branch 'main' into add-regex-support
Browse files Browse the repository at this point in the history
  • Loading branch information
emdoyle authored Feb 23, 2024
2 parents d738075 + f1d2e2c commit 6f3c5a5
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 32 deletions.
43 changes: 26 additions & 17 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,44 +70,53 @@ modguard.public(x, allowlist=["project.core.domain", r"core\.project\.*"])
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
import modguard

@modguard.public(allowlist=["project.core.domain", r"core\.project\.*"])
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`).
### Module-level
When `public` is used without a `path` argument (the most common case), it signifies that the containing module and its descendants are public. This means that any descendant of the module or the module itself can be imported externally (subject to `allowlist`). Note that adding a `Boundary` in a descendant module will prevent that module from being treated as public by default.
```python
# In project/core/logic.py
import modguard

modguard.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
import modguard

modguard.Boundary()
modguard.public()
...
# In project/core/utils/__init__.py
import modguard

modguard.Boundary()
...

# In project/cli.py

# This import is allowed because "project.core" is public
from project import core
from project.core import logic
# This import is NOT allowed because "project.core.utils" has
# a Boundary and is not marked public
from project.core import utils
```

### 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
import modguard

@modguard.public(allowlist=["project.core.domain"])
def my_pub_function():
...
```


## `modguard-ignore`
To ignore a particular import which should be allowed unconditionally, use the `modguard-ignore` comment directive.
```python
Expand Down
1 change: 1 addition & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Modguard works by analyzing the abstract syntax tree (AST) of your codebase. The

### 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.
Expand Down
3 changes: 1 addition & 2 deletions modguard/check.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import re
from dataclasses import dataclass
from typing import Optional

Expand Down Expand Up @@ -60,7 +59,7 @@ def check_import(
(
public_member
for public_member_name, public_member in nearest_boundary.public_members.items()
if re.match(rf"^{public_member_name}(\.[\w*]+)?$", import_mod_path)
if import_mod_path.startswith(public_member_name)
),
None,
)
Expand Down
4 changes: 2 additions & 2 deletions tests/example/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .domain_one.interface import domain_one_interface, domain_one_var
from .domain_three.api import PublicForDomainTwo
from .domain_four.subsystem import private_subsystem_call
from .domain_five.inner import private
from .domain_five.inner import private_fn

# OK import
import example.domain_four
Expand All @@ -18,8 +18,8 @@


# Usages
private()
pub_fn()
private_fn()
domain_one_interface()
example_usage = domain_one_var
PublicForDomainTwo()
Expand Down
6 changes: 0 additions & 6 deletions tests/example/domain_five/inner.py

This file was deleted.

13 changes: 13 additions & 0 deletions tests/example/domain_five/inner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import modguard


modguard.Boundary()


@modguard.public
def pub_fn():
...


def private_fn():
...
8 changes: 4 additions & 4 deletions tests/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ def test_check_example_dir_end_to_end():
boundary_path="example.domain_four.subsystem",
),
ErrorInfo(
import_mod_path="example.domain_five.inner.private_fn",
location="example/__init__.py",
import_mod_path="example.domain_one.interface.domain_one_var",
boundary_path="example.domain_one",
boundary_path="example.domain_five.inner",
),
ErrorInfo(
location="example/__init__.py",
import_mod_path="example.domain_five.inner.private",
boundary_path="example.domain_five",
import_mod_path="example.domain_one.interface.domain_one_var",
boundary_path="example.domain_one",
),
ErrorInfo(
location="example/domain_three/__init__.py",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ def test_get_imports():
"example.domain_four",
"example.domain_four.subsystem.private_subsystem_call",
"example.domain_one.interface.domain_one_var",
"example.domain_five.inner.private",
"example.domain_five.inner.private_fn",
"example.domain_five.pub_fn",
}

0 comments on commit 6f3c5a5

Please sign in to comment.