Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert single-level restriction on module-level public() call #25

Merged
merged 3 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,44 +70,51 @@ modguard.public(x, allowlist=["project.core.domain"])
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"])
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 would prevent that module from being treated as public by default.
emdoyle marked this conversation as resolved.
Show resolved Hide resolved
```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 @@ -48,7 +47,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",
}