archetype-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- archetype_py-0.1.0/.github/workflows/archetype.yml +36 -0
- archetype_py-0.1.0/.github/workflows/ci.yml +35 -0
- archetype_py-0.1.0/.github/workflows/publish.yml +62 -0
- archetype_py-0.1.0/.gitignore +38 -0
- archetype_py-0.1.0/CHANGELOG.md +15 -0
- archetype_py-0.1.0/LICENSE +21 -0
- archetype_py-0.1.0/PKG-INFO +195 -0
- archetype_py-0.1.0/README.md +172 -0
- archetype_py-0.1.0/archetype/__init__.py +3 -0
- archetype_py-0.1.0/archetype/analysis/__init__.py +3 -0
- archetype_py-0.1.0/archetype/analysis/ast_utils.py +19 -0
- archetype_py-0.1.0/archetype/analysis/imports.py +97 -0
- archetype_py-0.1.0/archetype/analysis/init.py +1 -0
- archetype_py-0.1.0/archetype/analysis/models.py +26 -0
- archetype_py-0.1.0/archetype/check.py +70 -0
- archetype_py-0.1.0/archetype/dsl/__init__.py +3 -0
- archetype_py-0.1.0/archetype/dsl/init.py +1 -0
- archetype_py-0.1.0/archetype/dsl/query.py +114 -0
- archetype_py-0.1.0/archetype/init.py +8 -0
- archetype_py-0.1.0/archetype/plugin/__init__.py +3 -0
- archetype_py-0.1.0/archetype/plugin/init.py +1 -0
- archetype_py-0.1.0/archetype/plugin/pytest_plugin.py +75 -0
- archetype_py-0.1.0/archetype/reporter.py +65 -0
- archetype_py-0.1.0/archetype/rule.py +61 -0
- archetype_py-0.1.0/archetype/rules/__init__.py +8 -0
- archetype_py-0.1.0/archetype/rules/boundaries.py +57 -0
- archetype_py-0.1.0/archetype/rules/cycles.py +69 -0
- archetype_py-0.1.0/archetype/rules/layers.py +69 -0
- archetype_py-0.1.0/archetype/rules/naming.py +116 -0
- archetype_py-0.1.0/architecture.py +18 -0
- archetype_py-0.1.0/pyproject.toml +49 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/api.py +13 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/db.py +1 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/internal/init.py +1 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/internal/tokens.py +1 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/main.py +3 -0
- archetype_py-0.1.0/tests/fixtures/simple_project/simple_project/services.py +12 -0
- archetype_py-0.1.0/tests/test_cli.py +132 -0
- archetype_py-0.1.0/tests/test_dsl.py +60 -0
- archetype_py-0.1.0/tests/test_graph.py +35 -0
- archetype_py-0.1.0/tests/test_plugin.py +100 -0
- archetype_py-0.1.0/tests/test_rule.py +89 -0
- archetype_py-0.1.0/tests/test_rules_boundaries.py +58 -0
- archetype_py-0.1.0/tests/test_rules_cycles.py +76 -0
- archetype_py-0.1.0/tests/test_rules_layers.py +69 -0
- archetype_py-0.1.0/tests/test_rules_naming.py +68 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Archetype Check Template
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
archetype:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
# Pull down your repository code so CI can inspect it.
|
|
15
|
+
- name: Checkout repository
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
# Install Python 3.11 so the Archetype CLI can run.
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.11"
|
|
23
|
+
|
|
24
|
+
# Install Archetype from PyPI into the CI environment.
|
|
25
|
+
- name: Install Archetype
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install archetype-py
|
|
29
|
+
|
|
30
|
+
# Run architecture rules defined in architecture.py.
|
|
31
|
+
# This step exits non-zero on violations, which fails the workflow.
|
|
32
|
+
- name: Run architecture checks
|
|
33
|
+
run: archetype check .
|
|
34
|
+
|
|
35
|
+
# If your team already runs pytest in CI, you can skip this workflow file.
|
|
36
|
+
# Archetype's pytest plugin runs architecture.py rules automatically during pytest.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Archetype Library CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.11", "3.12"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install package and development dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Run full test suite
|
|
32
|
+
run: pytest
|
|
33
|
+
|
|
34
|
+
- name: Run Archetype against this codebase
|
|
35
|
+
run: archetype check .
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
# Checkout repository contents for test execution.
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
# Install Python runtime used for tests and packaging checks.
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
|
|
23
|
+
# Install the project in editable mode with dev dependencies.
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
# Run the full test suite; publication is blocked on any failure.
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: pytest
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs: test
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
contents: read
|
|
39
|
+
|
|
40
|
+
steps:
|
|
41
|
+
# Checkout repository contents so the package can be built.
|
|
42
|
+
- name: Checkout repository
|
|
43
|
+
uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
# Set up Python for building distribution artifacts.
|
|
46
|
+
- name: Set up Python
|
|
47
|
+
uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.11"
|
|
50
|
+
|
|
51
|
+
# Install Hatch and build source/wheel distributions.
|
|
52
|
+
- name: Build package
|
|
53
|
+
run: |
|
|
54
|
+
python -m pip install --upgrade pip
|
|
55
|
+
pip install hatch
|
|
56
|
+
hatch build
|
|
57
|
+
|
|
58
|
+
# Publish using PyPI Trusted Publishing (no API token secret required).
|
|
59
|
+
# Before this works, configure Trusted Publishing in your PyPI project:
|
|
60
|
+
# https://pypi.org/manage/account/publishing
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
ENV/
|
|
17
|
+
|
|
18
|
+
# Test / coverage
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# Type checkers / linters
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
.ruff_cache/
|
|
27
|
+
.pyre/
|
|
28
|
+
|
|
29
|
+
# IDE / editor
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
|
|
33
|
+
# OS files
|
|
34
|
+
.DS_Store
|
|
35
|
+
|
|
36
|
+
# Hatch
|
|
37
|
+
.hatch/
|
|
38
|
+
LAUNCH.md
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-05-09
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Introduced static import graph analysis that maps module dependencies without executing application code.
|
|
7
|
+
- Added a rule authoring model using `@rule` decorators and a central registry so architectural checks are defined as plain Python.
|
|
8
|
+
- Shipped a readable query DSL with project loading, import constraints, and cycle checks for writing architecture policies.
|
|
9
|
+
- Added a CLI command (`archetype check`) that discovers `architecture.py`, executes rules, and returns CI-friendly exit codes.
|
|
10
|
+
- Added a pytest plugin that auto-collects `architecture.py` rules as native pytest test items with readable failure output.
|
|
11
|
+
- Added a shared reporting layer for consistent violation formatting across CLI and pytest execution paths.
|
|
12
|
+
- Added built-in rule packs for layering constraints, module boundaries, naming conventions, and circular import detection.
|
|
13
|
+
- Added test fixtures and comprehensive pytest coverage for graph construction, DSL behavior, CLI behavior, plugin collection, and built-in rules.
|
|
14
|
+
- Added GitHub Actions workflows for reusable architecture checks in downstream projects and matrix CI for Archetype development.
|
|
15
|
+
- Added packaging and release automation for PyPI publication using GitHub Actions Trusted Publishing.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [YEAR] [AUTHOR NAME]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: archetype-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Archetype statically analyzes Python projects to enforce architectural rules as code.
|
|
5
|
+
Project-URL: Homepage, https://github.com/your-org/your-repo
|
|
6
|
+
Project-URL: Documentation, https://github.com/your-org/your-repo
|
|
7
|
+
Author-email: Mossab Arektout <mossabarektout2000@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: click
|
|
17
|
+
Requires-Dist: networkx
|
|
18
|
+
Requires-Dist: rich
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
   
|
|
25
|
+
|
|
26
|
+
## Architectural rules should not live in people’s heads
|
|
27
|
+
Architectural rules usually exist in engineers’ heads but nowhere in the codebase.
|
|
28
|
+
Archetype turns those rules into executable Python checks that run in `archetype check` and `pytest`.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# architecture.py
|
|
32
|
+
from archetype import imports, rule
|
|
33
|
+
|
|
34
|
+
@rule("api does not import db")
|
|
35
|
+
def api_not_db() -> None:
|
|
36
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
37
|
+
|
|
38
|
+
@rule("services only import db")
|
|
39
|
+
def services_only_db() -> None:
|
|
40
|
+
imports("myapp.services").must_only_import_from("myapp.db")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
$ archetype check .
|
|
45
|
+
✓ api does not import db
|
|
46
|
+
✗ services only import db
|
|
47
|
+
- myapp.services.user -> myapp.cache: Module 'myapp.services.user' imports 'myapp.cache', which is outside the allowed set: ('myapp.db',).
|
|
48
|
+
Summary: 1 passed, 1 failed, 2 total rules.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
```bash
|
|
53
|
+
pip install archetype-py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
1. Install Archetype.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install archetype-py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Create `architecture.py` in your project root.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
touch architecture.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. Add your first rule with the imports DSL.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# architecture.py
|
|
73
|
+
from archetype import imports, rule
|
|
74
|
+
|
|
75
|
+
@rule("api does not import db")
|
|
76
|
+
def api_not_db() -> None:
|
|
77
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
4. Run the checker.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
archetype check .
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
5. Read the output and fix violations.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
✓ api does not import db
|
|
90
|
+
Summary: 1 passed, 0 failed, 1 total rules.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If your project already runs `pytest`, running `pytest` is sufficient because Archetype rules are collected and executed by the pytest plugin.
|
|
94
|
+
|
|
95
|
+
## Why Archetype exists
|
|
96
|
+
Style tools enforce how code looks. Type tools enforce what values can flow through code. Architectural tools enforce which parts of the system are allowed to depend on which other parts.
|
|
97
|
+
|
|
98
|
+
Pylint and similar linters are strong at local code quality checks, and Mypy is strong at static type correctness. Neither is designed to express team-level dependency contracts like “API cannot import DB” or “internal modules are private outside their package boundary.”
|
|
99
|
+
|
|
100
|
+
Archetype keeps rules in `architecture.py` as normal Python functions, not static YAML declarations. That makes rules executable, reviewable, testable, and easy to evolve with the codebase using the same language and tooling your team already uses.
|
|
101
|
+
|
|
102
|
+
## Built-in rules reference
|
|
103
|
+
### `layers`
|
|
104
|
+
Enforces that lower layers do not import upper layers.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from archetype import rule
|
|
108
|
+
from archetype.rules import layers
|
|
109
|
+
|
|
110
|
+
@rule("layers are ordered")
|
|
111
|
+
def layer_order() -> None:
|
|
112
|
+
layers(["myapp.api", "myapp.services", "myapp.db"]).are_ordered()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `module` (module boundaries)
|
|
116
|
+
Enforces that a protected internal module is only imported from an allowed parent scope.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from archetype import rule
|
|
120
|
+
from archetype.rules import module
|
|
121
|
+
|
|
122
|
+
@rule("internal auth is private")
|
|
123
|
+
def auth_boundary() -> None:
|
|
124
|
+
module("myapp.auth.internal").only_imported_within("myapp.auth")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `classes_in` and `functions_in` (naming conventions)
|
|
128
|
+
Enforces class naming patterns and required top-level functions in matched modules.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from archetype import rule
|
|
132
|
+
from archetype.rules import classes_in, functions_in
|
|
133
|
+
|
|
134
|
+
@rule("service classes end with Service")
|
|
135
|
+
def class_names() -> None:
|
|
136
|
+
classes_in("myapp.services").all_match(r".*Service$")
|
|
137
|
+
|
|
138
|
+
@rule("api modules expose handle")
|
|
139
|
+
def api_handle_exists() -> None:
|
|
140
|
+
functions_in("myapp.api").must_include("handle")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `no_cycles`
|
|
144
|
+
Enforces that there are no import cycles in the whole project or in a selected module scope.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from archetype import rule
|
|
148
|
+
from archetype.rules import no_cycles
|
|
149
|
+
|
|
150
|
+
@rule("no cycles in services")
|
|
151
|
+
def services_no_cycles() -> None:
|
|
152
|
+
no_cycles("myapp.services")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Writing custom rules
|
|
156
|
+
```python
|
|
157
|
+
from archetype import imports, rule
|
|
158
|
+
|
|
159
|
+
@rule("custom architecture policy")
|
|
160
|
+
def custom_policy() -> None:
|
|
161
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
162
|
+
imports("myapp.services").has_no_cycles()
|
|
163
|
+
imports("myapp.services").must_only_import_from("myapp.db", "myapp.shared")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Any Python function decorated with `@rule` that returns without raising is a passing rule. Rules can use the full Python language, so you can encode architecture constraints that do not fit generic linters or static config.
|
|
167
|
+
|
|
168
|
+
## CI integration
|
|
169
|
+
```yaml
|
|
170
|
+
name: Archetype Check
|
|
171
|
+
|
|
172
|
+
on:
|
|
173
|
+
push:
|
|
174
|
+
branches: [main]
|
|
175
|
+
pull_request:
|
|
176
|
+
|
|
177
|
+
jobs:
|
|
178
|
+
archetype:
|
|
179
|
+
runs-on: ubuntu-latest
|
|
180
|
+
steps:
|
|
181
|
+
- uses: actions/checkout@v4
|
|
182
|
+
- uses: actions/setup-python@v5
|
|
183
|
+
with:
|
|
184
|
+
python-version: "3.11"
|
|
185
|
+
- run: |
|
|
186
|
+
python -m pip install --upgrade pip
|
|
187
|
+
pip install archetype-py
|
|
188
|
+
- run: archetype check .
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If your CI already runs `pytest`, no additional CI configuration is required.
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
Source code and issue tracking are in the GitHub repository: `https://github.com/your-org/your-repo`.
|
|
195
|
+
Contributions are welcome; open an issue first to discuss scope before submitting a pull request.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
   
|
|
2
|
+
|
|
3
|
+
## Architectural rules should not live in people’s heads
|
|
4
|
+
Architectural rules usually exist in engineers’ heads but nowhere in the codebase.
|
|
5
|
+
Archetype turns those rules into executable Python checks that run in `archetype check` and `pytest`.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# architecture.py
|
|
9
|
+
from archetype import imports, rule
|
|
10
|
+
|
|
11
|
+
@rule("api does not import db")
|
|
12
|
+
def api_not_db() -> None:
|
|
13
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
14
|
+
|
|
15
|
+
@rule("services only import db")
|
|
16
|
+
def services_only_db() -> None:
|
|
17
|
+
imports("myapp.services").must_only_import_from("myapp.db")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
$ archetype check .
|
|
22
|
+
✓ api does not import db
|
|
23
|
+
✗ services only import db
|
|
24
|
+
- myapp.services.user -> myapp.cache: Module 'myapp.services.user' imports 'myapp.cache', which is outside the allowed set: ('myapp.db',).
|
|
25
|
+
Summary: 1 passed, 1 failed, 2 total rules.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
```bash
|
|
30
|
+
pip install archetype-py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
1. Install Archetype.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install archetype-py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Create `architecture.py` in your project root.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
touch architecture.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
3. Add your first rule with the imports DSL.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# architecture.py
|
|
50
|
+
from archetype import imports, rule
|
|
51
|
+
|
|
52
|
+
@rule("api does not import db")
|
|
53
|
+
def api_not_db() -> None:
|
|
54
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
4. Run the checker.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
archetype check .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
5. Read the output and fix violations.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
✓ api does not import db
|
|
67
|
+
Summary: 1 passed, 0 failed, 1 total rules.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If your project already runs `pytest`, running `pytest` is sufficient because Archetype rules are collected and executed by the pytest plugin.
|
|
71
|
+
|
|
72
|
+
## Why Archetype exists
|
|
73
|
+
Style tools enforce how code looks. Type tools enforce what values can flow through code. Architectural tools enforce which parts of the system are allowed to depend on which other parts.
|
|
74
|
+
|
|
75
|
+
Pylint and similar linters are strong at local code quality checks, and Mypy is strong at static type correctness. Neither is designed to express team-level dependency contracts like “API cannot import DB” or “internal modules are private outside their package boundary.”
|
|
76
|
+
|
|
77
|
+
Archetype keeps rules in `architecture.py` as normal Python functions, not static YAML declarations. That makes rules executable, reviewable, testable, and easy to evolve with the codebase using the same language and tooling your team already uses.
|
|
78
|
+
|
|
79
|
+
## Built-in rules reference
|
|
80
|
+
### `layers`
|
|
81
|
+
Enforces that lower layers do not import upper layers.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from archetype import rule
|
|
85
|
+
from archetype.rules import layers
|
|
86
|
+
|
|
87
|
+
@rule("layers are ordered")
|
|
88
|
+
def layer_order() -> None:
|
|
89
|
+
layers(["myapp.api", "myapp.services", "myapp.db"]).are_ordered()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `module` (module boundaries)
|
|
93
|
+
Enforces that a protected internal module is only imported from an allowed parent scope.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from archetype import rule
|
|
97
|
+
from archetype.rules import module
|
|
98
|
+
|
|
99
|
+
@rule("internal auth is private")
|
|
100
|
+
def auth_boundary() -> None:
|
|
101
|
+
module("myapp.auth.internal").only_imported_within("myapp.auth")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `classes_in` and `functions_in` (naming conventions)
|
|
105
|
+
Enforces class naming patterns and required top-level functions in matched modules.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from archetype import rule
|
|
109
|
+
from archetype.rules import classes_in, functions_in
|
|
110
|
+
|
|
111
|
+
@rule("service classes end with Service")
|
|
112
|
+
def class_names() -> None:
|
|
113
|
+
classes_in("myapp.services").all_match(r".*Service$")
|
|
114
|
+
|
|
115
|
+
@rule("api modules expose handle")
|
|
116
|
+
def api_handle_exists() -> None:
|
|
117
|
+
functions_in("myapp.api").must_include("handle")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `no_cycles`
|
|
121
|
+
Enforces that there are no import cycles in the whole project or in a selected module scope.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from archetype import rule
|
|
125
|
+
from archetype.rules import no_cycles
|
|
126
|
+
|
|
127
|
+
@rule("no cycles in services")
|
|
128
|
+
def services_no_cycles() -> None:
|
|
129
|
+
no_cycles("myapp.services")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Writing custom rules
|
|
133
|
+
```python
|
|
134
|
+
from archetype import imports, rule
|
|
135
|
+
|
|
136
|
+
@rule("custom architecture policy")
|
|
137
|
+
def custom_policy() -> None:
|
|
138
|
+
imports("myapp.api").must_not_import("myapp.db")
|
|
139
|
+
imports("myapp.services").has_no_cycles()
|
|
140
|
+
imports("myapp.services").must_only_import_from("myapp.db", "myapp.shared")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Any Python function decorated with `@rule` that returns without raising is a passing rule. Rules can use the full Python language, so you can encode architecture constraints that do not fit generic linters or static config.
|
|
144
|
+
|
|
145
|
+
## CI integration
|
|
146
|
+
```yaml
|
|
147
|
+
name: Archetype Check
|
|
148
|
+
|
|
149
|
+
on:
|
|
150
|
+
push:
|
|
151
|
+
branches: [main]
|
|
152
|
+
pull_request:
|
|
153
|
+
|
|
154
|
+
jobs:
|
|
155
|
+
archetype:
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
steps:
|
|
158
|
+
- uses: actions/checkout@v4
|
|
159
|
+
- uses: actions/setup-python@v5
|
|
160
|
+
with:
|
|
161
|
+
python-version: "3.11"
|
|
162
|
+
- run: |
|
|
163
|
+
python -m pip install --upgrade pip
|
|
164
|
+
pip install archetype-py
|
|
165
|
+
- run: archetype check .
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If your CI already runs `pytest`, no additional CI configuration is required.
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
Source code and issue tracking are in the GitHub repository: `https://github.com/your-org/your-repo`.
|
|
172
|
+
Contributions are welcome; open an issue first to discuss scope before submitting a pull request.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""AST traversal helpers used by Archetype static analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_class_names(tree: ast.AST) -> list[str]:
|
|
9
|
+
"""Return all class names defined anywhere in the AST."""
|
|
10
|
+
return [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_top_level_function_names(tree: ast.AST) -> list[str]:
|
|
14
|
+
"""Return names of module-level functions only (excluding class methods)."""
|
|
15
|
+
return [
|
|
16
|
+
node.name
|
|
17
|
+
for node in getattr(tree, "body", [])
|
|
18
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
19
|
+
]
|