arborescent 0.2.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.
- arborescent-0.2.0/LICENSE +21 -0
- arborescent-0.2.0/PKG-INFO +185 -0
- arborescent-0.2.0/README.md +134 -0
- arborescent-0.2.0/pyproject.toml +161 -0
- arborescent-0.2.0/setup.cfg +4 -0
- arborescent-0.2.0/src/arborescent/__init__.py +13 -0
- arborescent-0.2.0/src/arborescent/__main__.py +3 -0
- arborescent-0.2.0/src/arborescent/adapters.py +11 -0
- arborescent-0.2.0/src/arborescent/baseline.py +150 -0
- arborescent-0.2.0/src/arborescent/bench.py +93 -0
- arborescent-0.2.0/src/arborescent/certification.py +251 -0
- arborescent-0.2.0/src/arborescent/cli.py +210 -0
- arborescent-0.2.0/src/arborescent/cli_install.py +119 -0
- arborescent-0.2.0/src/arborescent/cli_report.py +107 -0
- arborescent-0.2.0/src/arborescent/clock_adapter.py +12 -0
- arborescent-0.2.0/src/arborescent/config.py +217 -0
- arborescent-0.2.0/src/arborescent/domain.py +248 -0
- arborescent-0.2.0/src/arborescent/engine.py +305 -0
- arborescent-0.2.0/src/arborescent/engines.py +132 -0
- arborescent-0.2.0/src/arborescent/facts.py +273 -0
- arborescent-0.2.0/src/arborescent/filesystem_adapter.py +64 -0
- arborescent-0.2.0/src/arborescent/generate.py +197 -0
- arborescent-0.2.0/src/arborescent/install_tools.py +41 -0
- arborescent-0.2.0/src/arborescent/installer_adapter.py +210 -0
- arborescent-0.2.0/src/arborescent/ports.py +136 -0
- arborescent-0.2.0/src/arborescent/practices.py +205 -0
- arborescent-0.2.0/src/arborescent/presets.py +64 -0
- arborescent-0.2.0/src/arborescent/py.typed +0 -0
- arborescent-0.2.0/src/arborescent/rules.py +213 -0
- arborescent-0.2.0/src/arborescent/scanner.py +252 -0
- arborescent-0.2.0/src/arborescent/selftest.py +168 -0
- arborescent-0.2.0/src/arborescent/stdlib.py +111 -0
- arborescent-0.2.0/src/arborescent/suggestions.py +110 -0
- arborescent-0.2.0/src/arborescent/testing.py +183 -0
- arborescent-0.2.0/src/arborescent/tool_runner.py +100 -0
- arborescent-0.2.0/src/arborescent/typecheckers.py +181 -0
- arborescent-0.2.0/src/arborescent.egg-info/PKG-INFO +185 -0
- arborescent-0.2.0/src/arborescent.egg-info/SOURCES.txt +64 -0
- arborescent-0.2.0/src/arborescent.egg-info/dependency_links.txt +1 -0
- arborescent-0.2.0/src/arborescent.egg-info/entry_points.txt +2 -0
- arborescent-0.2.0/src/arborescent.egg-info/requires.txt +39 -0
- arborescent-0.2.0/src/arborescent.egg-info/top_level.txt +1 -0
- arborescent-0.2.0/tests/test_adapter_boundaries.py +13 -0
- arborescent-0.2.0/tests/test_baseline_ratchet.py +228 -0
- arborescent-0.2.0/tests/test_certification.py +58 -0
- arborescent-0.2.0/tests/test_coverage.py +129 -0
- arborescent-0.2.0/tests/test_customizations.py +43 -0
- arborescent-0.2.0/tests/test_dogfood_and_speed.py +128 -0
- arborescent-0.2.0/tests/test_effect_policy.py +113 -0
- arborescent-0.2.0/tests/test_engine.py +404 -0
- arborescent-0.2.0/tests/test_engine_ports.py +330 -0
- arborescent-0.2.0/tests/test_facts.py +138 -0
- arborescent-0.2.0/tests/test_filesystem.py +165 -0
- arborescent-0.2.0/tests/test_fs_behaviour.py +244 -0
- arborescent-0.2.0/tests/test_install_tools.py +471 -0
- arborescent-0.2.0/tests/test_policy.py +156 -0
- arborescent-0.2.0/tests/test_practices.py +147 -0
- arborescent-0.2.0/tests/test_presets.py +138 -0
- arborescent-0.2.0/tests/test_ruff_engine.py +129 -0
- arborescent-0.2.0/tests/test_rule_ids.py +155 -0
- arborescent-0.2.0/tests/test_rules.py +189 -0
- arborescent-0.2.0/tests/test_seams.py +112 -0
- arborescent-0.2.0/tests/test_stdlib.py +105 -0
- arborescent-0.2.0/tests/test_suggestions.py +81 -0
- arborescent-0.2.0/tests/test_todo.py +45 -0
- arborescent-0.2.0/tests/test_vendor_folders.py +104 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Powers
|
|
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,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arborescent
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: One architecture policy that generates import-linter, tach and type-checker configs, then enforces more.
|
|
5
|
+
Author: Brian Powers
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/112345brian/arborescent
|
|
8
|
+
Project-URL: Repository, https://github.com/112345brian/arborescent
|
|
9
|
+
Project-URL: Changelog, https://github.com/112345brian/arborescent/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: architecture,import-linter,tach,pyright,clean-architecture,lint
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: importlinter
|
|
21
|
+
Requires-Dist: import-linter>=2.0; extra == "importlinter"
|
|
22
|
+
Provides-Extra: tach
|
|
23
|
+
Requires-Dist: tach>=0.30; extra == "tach"
|
|
24
|
+
Provides-Extra: pyright
|
|
25
|
+
Requires-Dist: pyright>=1.1.400; extra == "pyright"
|
|
26
|
+
Provides-Extra: ruff
|
|
27
|
+
Requires-Dist: ruff>=0.16.3; extra == "ruff"
|
|
28
|
+
Provides-Extra: basedpyright
|
|
29
|
+
Requires-Dist: basedpyright>=1.30; extra == "basedpyright"
|
|
30
|
+
Provides-Extra: ty
|
|
31
|
+
Requires-Dist: ty>=0.0.50; extra == "ty"
|
|
32
|
+
Provides-Extra: pyrefly
|
|
33
|
+
Requires-Dist: pyrefly>=0.40; extra == "pyrefly"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: import-linter>=2.0; extra == "all"
|
|
36
|
+
Requires-Dist: tach>=0.30; extra == "all"
|
|
37
|
+
Requires-Dist: pyright>=1.1.400; extra == "all"
|
|
38
|
+
Requires-Dist: ruff>=0.16.3; extra == "all"
|
|
39
|
+
Requires-Dist: ty>=0.0.50; extra == "all"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
42
|
+
Requires-Dist: import-linter>=2.0; extra == "dev"
|
|
43
|
+
Requires-Dist: tach>=0.30; extra == "dev"
|
|
44
|
+
Requires-Dist: pyright>=1.1.400; extra == "dev"
|
|
45
|
+
Requires-Dist: ruff>=0.16.3; extra == "dev"
|
|
46
|
+
Requires-Dist: basedpyright>=1.30; extra == "dev"
|
|
47
|
+
Requires-Dist: ty>=0.0.50; extra == "dev"
|
|
48
|
+
Requires-Dist: pyrefly>=0.40; extra == "dev"
|
|
49
|
+
Requires-Dist: build; extra == "dev"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# Arborescent
|
|
53
|
+
|
|
54
|
+
One architecture policy. It enforces architecture rules itself and runs [import-linter](https://import-linter.readthedocs.io),
|
|
55
|
+
[tach](https://docs.gauge.sh), [Ruff](https://docs.astral.sh/ruff/) and a type checker ([ty](https://github.com/astral-sh/ty) by default, with pyright and basedpyright
|
|
56
|
+
or [pyrefly](https://pyrefly.org)) using their native project configuration. `arborescent generate` can scaffold optional preset files and never overwrites an existing file.
|
|
57
|
+
Arborescent also enforces: every module must be classified, each ring
|
|
58
|
+
has an allow-list of external packages, OS branching is limited to named rings, lazy imports count, entry scripts must
|
|
59
|
+
declare what the code imports, and a set of repository-hygiene practices that each come from a bug that really happened.
|
|
60
|
+
|
|
61
|
+
Arborescent's core has no runtime dependencies; the engines are optional extras run as subprocesses.
|
|
62
|
+
|
|
63
|
+
Install the checks you use with the package, or install every engine for development:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
python -m pip install '.[all]'
|
|
67
|
+
python -m pip install '.[dev]' # tests and all checker adapters
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Ruff is included in `all` and `dev`, and is also available as the `ruff` extra. Ty is the default type checker; list
|
|
71
|
+
`type_checkers = ["ty", "pyright"]` to keep both configs generated and use `--type-checker pyright` when needed.
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
# pyproject.toml
|
|
77
|
+
[tool.arborescent]
|
|
78
|
+
sources = ["src/app"]
|
|
79
|
+
source_roots = ["src"]
|
|
80
|
+
rings = ["domain", "ports", "adapters", "composition"] # innermost first
|
|
81
|
+
type_checkers = ["ty", "pyright"] # first is the default; --type-checker pyright selects strict Pyright
|
|
82
|
+
|
|
83
|
+
[tool.arborescent.groups]
|
|
84
|
+
pure = ["__future__", "dataclasses", "typing"]
|
|
85
|
+
|
|
86
|
+
[tool.arborescent.ring.domain]
|
|
87
|
+
external = ["@pure"]
|
|
88
|
+
[tool.arborescent.ring.ports]
|
|
89
|
+
may_import = ["domain"]
|
|
90
|
+
external = ["@pure"]
|
|
91
|
+
[tool.arborescent.ring.adapters]
|
|
92
|
+
may_import = ["domain", "ports"]
|
|
93
|
+
external = ["@pure", "os", "psutil"]
|
|
94
|
+
os_branching = true # only these rings may read os.name / sys.platform
|
|
95
|
+
[tool.arborescent.ring.composition]
|
|
96
|
+
may_import = ["*"]
|
|
97
|
+
external = ["@pure", "sys"]
|
|
98
|
+
|
|
99
|
+
[tool.arborescent.classify] # first matching glob wins; every module needs one
|
|
100
|
+
"app" = "domain"
|
|
101
|
+
"app.domain" = "domain"
|
|
102
|
+
"app.ports" = "ports"
|
|
103
|
+
"app.adapters" = "adapters"
|
|
104
|
+
"app.main" = "composition"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
arborescent generate # optionally create missing recommended config files; existing files stay untouched
|
|
109
|
+
arborescent check # engines in parallel, own checks, baseline ratchet; generated-preset drift is advisory
|
|
110
|
+
arborescent check --install-tools # prompt to install missing tools, then run the checks
|
|
111
|
+
arborescent install-tools # prompt to install missing tools without running checks
|
|
112
|
+
arborescent check --install-tools --yes # install without prompting, then check
|
|
113
|
+
arborescent check --require-tools --require-profile # require profile v1 and its verified tool versions/rules
|
|
114
|
+
arborescent check --changed origin/main # only files changed since a git ref
|
|
115
|
+
arborescent discover # list Python files that still need ring classification
|
|
116
|
+
arborescent baseline # record existing violations; future checks fail on new findings
|
|
117
|
+
arborescent selftest # mutation-test native rules and fail if a rule stops firing
|
|
118
|
+
arborescent bench DIR… # cold/warm timings of every engine on prepared codebases
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Documentation: [CLI commands, configuration and rules](docs/configuration.md) and
|
|
122
|
+
[Arborescent's own architecture](docs/architecture.md).
|
|
123
|
+
|
|
124
|
+
`check` never installs packages unless you pass `--install-tools`. By default, missing selected tools produce an exact
|
|
125
|
+
install command and the usual skip/fail behavior. `check --install-tools` prints the command, prompts with No as the
|
|
126
|
+
default, installs on confirmation, then runs checks; `--yes` skips the prompt. `arborescent install-tools` uses the same
|
|
127
|
+
setup flow without running checks. Installation targets the active Python environment (or uv targeting that
|
|
128
|
+
interpreter when pip is unavailable). In Poetry projects it uses `poetry add` to place checkers in an `arborescent` group
|
|
129
|
+
included by `dev`, then verifies the lock. Set `disabled_tools = ["import-linter", "tach", "ruff"]` in
|
|
130
|
+
`[tool.arborescent]` (or the top-level `arborescent.toml`) to deliberately omit those tools from checks and setup hints;
|
|
131
|
+
minimum-profile certification still requires Ruff and Tach.
|
|
132
|
+
|
|
133
|
+
## Presets
|
|
134
|
+
|
|
135
|
+
`preset = "strict"` is the default. A preset only changes how much of the external-import and OS rules applies; ring
|
|
136
|
+
layering, classification (every module needs a ring) and lazy-import detection are the same in all of them.
|
|
137
|
+
|
|
138
|
+
| preset | imports that need a ring `external` entry | OS branching limited to named rings |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `strict` (default) | every external import, stdlib included | yes |
|
|
141
|
+
| `standard` | third-party packages and the I/O stdlib set; other stdlib is free | yes |
|
|
142
|
+
| `relaxed` | third-party packages only | no |
|
|
143
|
+
|
|
144
|
+
The I/O stdlib set (`os`, `subprocess`, `socket`, `sqlite3`, `pathlib`, ...: see `arborescent.presets.IO_MODULES`) also drives
|
|
145
|
+
the generated import-linter bans. Replace it per project with `io_modules = ["os", "subprocess", ...]` (an empty list means
|
|
146
|
+
no stdlib module counts as I/O).
|
|
147
|
+
|
|
148
|
+
## Ports
|
|
149
|
+
|
|
150
|
+
- `ToolRunner` (`available`, `run`) -- how engines and git are executed; `arborescent.testing.FakeToolRunner` fakes it.
|
|
151
|
+
- `TypeChecker` (`render_config`, `command`, `passed`) -- pyright, basedpyright, ty and pyrefly adapters in
|
|
152
|
+
`arborescent.typecheckers`. Every strict-mode setting was verified by running the tool (see the module docstring).
|
|
153
|
+
- `FileSystem` -- every file read, write, listing, copy and temporary folder the use cases need; `LocalFileSystem` is the
|
|
154
|
+
real one and `arborescent.testing.FakeFileSystem` an in-memory twin (both run the same contract tests).
|
|
155
|
+
- `ModuleScanner` -- finds modules and parses each file once; `FsScanner` adds a content-hash cache.
|
|
156
|
+
|
|
157
|
+
## What the policy compiles to
|
|
158
|
+
|
|
159
|
+
| engine | optional generated preset | what it cannot say (Arborescent's own check says it) |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| import-linter | one `forbidden` contract per ring for the rings it may not import, one per ring banning I/O stdlib and declared third-party packages the ring does not list, one global `forbid_everywhere` | an external allow-list (it only has deny-lists); single-file entry scripts (not packages) |
|
|
162
|
+
| tach | `layers` (outermost first), one `[[modules]]` per module; `layers_explicit_depends_on` + per-module `depends_on` when a ring skips an inner ring | tach's circular-dependency check is switched off in that mode; Arborescent independently enforces circular-import checks across all first-party modules |
|
|
163
|
+
| Ruff | recommended `ruff.toml`; violations join the baseline, and suppression comments are ignored | ring-dependent effects (Arborescent's native rules) |
|
|
164
|
+
| pyright / basedpyright / ty / pyrefly | strict config, `# type: ignore` ignored, `match` exhaustiveness, no Unknown | -- |
|
|
165
|
+
|
|
166
|
+
## Practices (`[tool.arborescent.practices]`)
|
|
167
|
+
|
|
168
|
+
`gitignore-shadow` (an unanchored `local_voice/` ignored a real package: empty wheel), `sibling-versions` (a meta-package
|
|
169
|
+
pinned `==0.1.1` while the component was `0.2.0`), `secrets-guard` (tests need an autouse conftest fixture that scrubs
|
|
170
|
+
the environment), `docs-sync` (the architecture doc's `| module | ring |` table must equal the policy), `crlf`
|
|
171
|
+
(scripts with CRLF), `undeclared-imports` (third-party import absent from the manifest). Each has a reproduction test in
|
|
172
|
+
`tests/test_practices.py` that builds the broken repository in a temp directory.
|
|
173
|
+
|
|
174
|
+
## Speed
|
|
175
|
+
|
|
176
|
+
Engines run concurrently; Arborescent parses each file once (AST), caches facts by content hash in `.arborescent/`, and
|
|
177
|
+
`--changed` limits Arborescent, Ruff, and the type checker to changed files. import-linter, `tach check`, and `tach check-external`
|
|
178
|
+
are graph-wide, so Arborescent skips them and reports that reason in the engine results. See the repository CHANGELOG for measured numbers; `arborescent bench` reproduces them.
|
|
179
|
+
|
|
180
|
+
## Limits (honest)
|
|
181
|
+
|
|
182
|
+
Effect checks detect direct imports, aliases and attribute chains. They intentionally do not catch calls routed through
|
|
183
|
+
`getattr`, variables or wrapper functions. `module_forbid_imports` can add finer-grained internal-import restrictions
|
|
184
|
+
for selected modules without changing their ring. CI is configured for Windows, macOS and Linux; the account's
|
|
185
|
+
GitHub billing block has prevented hosted runs so far.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Arborescent
|
|
2
|
+
|
|
3
|
+
One architecture policy. It enforces architecture rules itself and runs [import-linter](https://import-linter.readthedocs.io),
|
|
4
|
+
[tach](https://docs.gauge.sh), [Ruff](https://docs.astral.sh/ruff/) and a type checker ([ty](https://github.com/astral-sh/ty) by default, with pyright and basedpyright
|
|
5
|
+
or [pyrefly](https://pyrefly.org)) using their native project configuration. `arborescent generate` can scaffold optional preset files and never overwrites an existing file.
|
|
6
|
+
Arborescent also enforces: every module must be classified, each ring
|
|
7
|
+
has an allow-list of external packages, OS branching is limited to named rings, lazy imports count, entry scripts must
|
|
8
|
+
declare what the code imports, and a set of repository-hygiene practices that each come from a bug that really happened.
|
|
9
|
+
|
|
10
|
+
Arborescent's core has no runtime dependencies; the engines are optional extras run as subprocesses.
|
|
11
|
+
|
|
12
|
+
Install the checks you use with the package, or install every engine for development:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
python -m pip install '.[all]'
|
|
16
|
+
python -m pip install '.[dev]' # tests and all checker adapters
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Ruff is included in `all` and `dev`, and is also available as the `ruff` extra. Ty is the default type checker; list
|
|
20
|
+
`type_checkers = ["ty", "pyright"]` to keep both configs generated and use `--type-checker pyright` when needed.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```toml
|
|
25
|
+
# pyproject.toml
|
|
26
|
+
[tool.arborescent]
|
|
27
|
+
sources = ["src/app"]
|
|
28
|
+
source_roots = ["src"]
|
|
29
|
+
rings = ["domain", "ports", "adapters", "composition"] # innermost first
|
|
30
|
+
type_checkers = ["ty", "pyright"] # first is the default; --type-checker pyright selects strict Pyright
|
|
31
|
+
|
|
32
|
+
[tool.arborescent.groups]
|
|
33
|
+
pure = ["__future__", "dataclasses", "typing"]
|
|
34
|
+
|
|
35
|
+
[tool.arborescent.ring.domain]
|
|
36
|
+
external = ["@pure"]
|
|
37
|
+
[tool.arborescent.ring.ports]
|
|
38
|
+
may_import = ["domain"]
|
|
39
|
+
external = ["@pure"]
|
|
40
|
+
[tool.arborescent.ring.adapters]
|
|
41
|
+
may_import = ["domain", "ports"]
|
|
42
|
+
external = ["@pure", "os", "psutil"]
|
|
43
|
+
os_branching = true # only these rings may read os.name / sys.platform
|
|
44
|
+
[tool.arborescent.ring.composition]
|
|
45
|
+
may_import = ["*"]
|
|
46
|
+
external = ["@pure", "sys"]
|
|
47
|
+
|
|
48
|
+
[tool.arborescent.classify] # first matching glob wins; every module needs one
|
|
49
|
+
"app" = "domain"
|
|
50
|
+
"app.domain" = "domain"
|
|
51
|
+
"app.ports" = "ports"
|
|
52
|
+
"app.adapters" = "adapters"
|
|
53
|
+
"app.main" = "composition"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
arborescent generate # optionally create missing recommended config files; existing files stay untouched
|
|
58
|
+
arborescent check # engines in parallel, own checks, baseline ratchet; generated-preset drift is advisory
|
|
59
|
+
arborescent check --install-tools # prompt to install missing tools, then run the checks
|
|
60
|
+
arborescent install-tools # prompt to install missing tools without running checks
|
|
61
|
+
arborescent check --install-tools --yes # install without prompting, then check
|
|
62
|
+
arborescent check --require-tools --require-profile # require profile v1 and its verified tool versions/rules
|
|
63
|
+
arborescent check --changed origin/main # only files changed since a git ref
|
|
64
|
+
arborescent discover # list Python files that still need ring classification
|
|
65
|
+
arborescent baseline # record existing violations; future checks fail on new findings
|
|
66
|
+
arborescent selftest # mutation-test native rules and fail if a rule stops firing
|
|
67
|
+
arborescent bench DIR… # cold/warm timings of every engine on prepared codebases
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Documentation: [CLI commands, configuration and rules](docs/configuration.md) and
|
|
71
|
+
[Arborescent's own architecture](docs/architecture.md).
|
|
72
|
+
|
|
73
|
+
`check` never installs packages unless you pass `--install-tools`. By default, missing selected tools produce an exact
|
|
74
|
+
install command and the usual skip/fail behavior. `check --install-tools` prints the command, prompts with No as the
|
|
75
|
+
default, installs on confirmation, then runs checks; `--yes` skips the prompt. `arborescent install-tools` uses the same
|
|
76
|
+
setup flow without running checks. Installation targets the active Python environment (or uv targeting that
|
|
77
|
+
interpreter when pip is unavailable). In Poetry projects it uses `poetry add` to place checkers in an `arborescent` group
|
|
78
|
+
included by `dev`, then verifies the lock. Set `disabled_tools = ["import-linter", "tach", "ruff"]` in
|
|
79
|
+
`[tool.arborescent]` (or the top-level `arborescent.toml`) to deliberately omit those tools from checks and setup hints;
|
|
80
|
+
minimum-profile certification still requires Ruff and Tach.
|
|
81
|
+
|
|
82
|
+
## Presets
|
|
83
|
+
|
|
84
|
+
`preset = "strict"` is the default. A preset only changes how much of the external-import and OS rules applies; ring
|
|
85
|
+
layering, classification (every module needs a ring) and lazy-import detection are the same in all of them.
|
|
86
|
+
|
|
87
|
+
| preset | imports that need a ring `external` entry | OS branching limited to named rings |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `strict` (default) | every external import, stdlib included | yes |
|
|
90
|
+
| `standard` | third-party packages and the I/O stdlib set; other stdlib is free | yes |
|
|
91
|
+
| `relaxed` | third-party packages only | no |
|
|
92
|
+
|
|
93
|
+
The I/O stdlib set (`os`, `subprocess`, `socket`, `sqlite3`, `pathlib`, ...: see `arborescent.presets.IO_MODULES`) also drives
|
|
94
|
+
the generated import-linter bans. Replace it per project with `io_modules = ["os", "subprocess", ...]` (an empty list means
|
|
95
|
+
no stdlib module counts as I/O).
|
|
96
|
+
|
|
97
|
+
## Ports
|
|
98
|
+
|
|
99
|
+
- `ToolRunner` (`available`, `run`) -- how engines and git are executed; `arborescent.testing.FakeToolRunner` fakes it.
|
|
100
|
+
- `TypeChecker` (`render_config`, `command`, `passed`) -- pyright, basedpyright, ty and pyrefly adapters in
|
|
101
|
+
`arborescent.typecheckers`. Every strict-mode setting was verified by running the tool (see the module docstring).
|
|
102
|
+
- `FileSystem` -- every file read, write, listing, copy and temporary folder the use cases need; `LocalFileSystem` is the
|
|
103
|
+
real one and `arborescent.testing.FakeFileSystem` an in-memory twin (both run the same contract tests).
|
|
104
|
+
- `ModuleScanner` -- finds modules and parses each file once; `FsScanner` adds a content-hash cache.
|
|
105
|
+
|
|
106
|
+
## What the policy compiles to
|
|
107
|
+
|
|
108
|
+
| engine | optional generated preset | what it cannot say (Arborescent's own check says it) |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| import-linter | one `forbidden` contract per ring for the rings it may not import, one per ring banning I/O stdlib and declared third-party packages the ring does not list, one global `forbid_everywhere` | an external allow-list (it only has deny-lists); single-file entry scripts (not packages) |
|
|
111
|
+
| tach | `layers` (outermost first), one `[[modules]]` per module; `layers_explicit_depends_on` + per-module `depends_on` when a ring skips an inner ring | tach's circular-dependency check is switched off in that mode; Arborescent independently enforces circular-import checks across all first-party modules |
|
|
112
|
+
| Ruff | recommended `ruff.toml`; violations join the baseline, and suppression comments are ignored | ring-dependent effects (Arborescent's native rules) |
|
|
113
|
+
| pyright / basedpyright / ty / pyrefly | strict config, `# type: ignore` ignored, `match` exhaustiveness, no Unknown | -- |
|
|
114
|
+
|
|
115
|
+
## Practices (`[tool.arborescent.practices]`)
|
|
116
|
+
|
|
117
|
+
`gitignore-shadow` (an unanchored `local_voice/` ignored a real package: empty wheel), `sibling-versions` (a meta-package
|
|
118
|
+
pinned `==0.1.1` while the component was `0.2.0`), `secrets-guard` (tests need an autouse conftest fixture that scrubs
|
|
119
|
+
the environment), `docs-sync` (the architecture doc's `| module | ring |` table must equal the policy), `crlf`
|
|
120
|
+
(scripts with CRLF), `undeclared-imports` (third-party import absent from the manifest). Each has a reproduction test in
|
|
121
|
+
`tests/test_practices.py` that builds the broken repository in a temp directory.
|
|
122
|
+
|
|
123
|
+
## Speed
|
|
124
|
+
|
|
125
|
+
Engines run concurrently; Arborescent parses each file once (AST), caches facts by content hash in `.arborescent/`, and
|
|
126
|
+
`--changed` limits Arborescent, Ruff, and the type checker to changed files. import-linter, `tach check`, and `tach check-external`
|
|
127
|
+
are graph-wide, so Arborescent skips them and reports that reason in the engine results. See the repository CHANGELOG for measured numbers; `arborescent bench` reproduces them.
|
|
128
|
+
|
|
129
|
+
## Limits (honest)
|
|
130
|
+
|
|
131
|
+
Effect checks detect direct imports, aliases and attribute chains. They intentionally do not catch calls routed through
|
|
132
|
+
`getattr`, variables or wrapper functions. `module_forbid_imports` can add finer-grained internal-import restrictions
|
|
133
|
+
for selected modules without changing their ring. CI is configured for Windows, macOS and Linux; the account's
|
|
134
|
+
GitHub billing block has prevented hosted runs so far.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "arborescent"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "One architecture policy that generates import-linter, tach and type-checker configs, then enforces more."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Brian Powers" }]
|
|
14
|
+
keywords = ["architecture", "import-linter", "tach", "pyright", "clean-architecture", "lint"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
# The engines are extras: Arborescent runs them as subprocesses and skips (or, with --require-tools, fails) when absent.
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
importlinter = ["import-linter>=2.0"]
|
|
28
|
+
tach = ["tach>=0.30"]
|
|
29
|
+
pyright = ["pyright>=1.1.400"]
|
|
30
|
+
ruff = ["ruff>=0.16.3"]
|
|
31
|
+
basedpyright = ["basedpyright>=1.30"]
|
|
32
|
+
ty = ["ty>=0.0.50"]
|
|
33
|
+
pyrefly = ["pyrefly>=0.40"]
|
|
34
|
+
all = ["import-linter>=2.0", "tach>=0.30", "pyright>=1.1.400", "ruff>=0.16.3", "ty>=0.0.50"]
|
|
35
|
+
dev = ["pytest>=8", "import-linter>=2.0", "tach>=0.30", "pyright>=1.1.400", "ruff>=0.16.3", "basedpyright>=1.30", "ty>=0.0.50", "pyrefly>=0.40", "build"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/112345brian/arborescent"
|
|
39
|
+
Repository = "https://github.com/112345brian/arborescent"
|
|
40
|
+
Changelog = "https://github.com/112345brian/arborescent/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
arborescent = "arborescent.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
packages = ["arborescent"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-dir]
|
|
49
|
+
"" = "src"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.package-data]
|
|
52
|
+
arborescent = ["py.typed"]
|
|
53
|
+
|
|
54
|
+
# Arborescent's own architecture, checked by Arborescent (`arborescent check` in this folder).
|
|
55
|
+
[tool.arborescent]
|
|
56
|
+
python = "3.11"
|
|
57
|
+
sources = ["src/arborescent"]
|
|
58
|
+
source_roots = ["src"]
|
|
59
|
+
exclude = ["__pycache__", "tests", "tools"]
|
|
60
|
+
forbid_circular_imports = true
|
|
61
|
+
rings = ["domain", "ports", "application", "adapters", "composition"]
|
|
62
|
+
type_checkers = ["ty", "pyright"]
|
|
63
|
+
|
|
64
|
+
[tool.arborescent.groups]
|
|
65
|
+
pure = ["__future__", "collections", "dataclasses", "enum", "fnmatch", "re", "typing", "ast", "sys", "tomllib", "io", "tokenize", "posixpath", "json"]
|
|
66
|
+
|
|
67
|
+
[tool.arborescent.ring.domain]
|
|
68
|
+
external = ["@pure", "arborescent"]
|
|
69
|
+
|
|
70
|
+
[tool.arborescent.ring.ports]
|
|
71
|
+
may_import = ["domain"]
|
|
72
|
+
external = ["@pure", "pathlib", "contextlib", "arborescent"]
|
|
73
|
+
|
|
74
|
+
[tool.arborescent.ring.application]
|
|
75
|
+
may_import = ["domain", "ports"]
|
|
76
|
+
# pathlib: Path values only (join, relative_to); every disk access goes through FileSystem (tests/test_filesystem.py)
|
|
77
|
+
external = ["@pure", "pathlib", "concurrent", "arborescent"]
|
|
78
|
+
# The application coordinates parallel engines; all other effect categories keep the strict default.
|
|
79
|
+
forbid = ["clock", "dynamic-import", "env", "identity", "patch", "process", "randomness", "sleep"]
|
|
80
|
+
|
|
81
|
+
[tool.arborescent.ring.adapters]
|
|
82
|
+
may_import = ["domain", "ports"]
|
|
83
|
+
external = ["@pure", "pathlib", "contextlib", "json", "hashlib", "os", "shutil", "subprocess", "tempfile", "time", "sysconfig", "tomllib", "arborescent"]
|
|
84
|
+
os_branching = true
|
|
85
|
+
forbid = ["clock", "dynamic-import", "env", "identity", "patch", "randomness", "sleep", "threads"]
|
|
86
|
+
|
|
87
|
+
[tool.arborescent.ring.composition]
|
|
88
|
+
may_import = ["*"]
|
|
89
|
+
external = ["@pure", "argparse", "pathlib", "os", "shlex", "arborescent"]
|
|
90
|
+
|
|
91
|
+
[tool.arborescent.classify]
|
|
92
|
+
"arborescent" = "domain"
|
|
93
|
+
"arborescent.domain" = "domain"
|
|
94
|
+
"arborescent.facts" = "domain"
|
|
95
|
+
"arborescent.rules" = "domain"
|
|
96
|
+
"arborescent.presets" = "domain"
|
|
97
|
+
"arborescent.stdlib" = "domain"
|
|
98
|
+
"arborescent.ports" = "ports"
|
|
99
|
+
"arborescent.generate" = "application"
|
|
100
|
+
"arborescent.baseline" = "application"
|
|
101
|
+
"arborescent.config" = "application"
|
|
102
|
+
"arborescent.engine" = "application"
|
|
103
|
+
"arborescent.certification" = "application"
|
|
104
|
+
"arborescent.install_tools" = "application"
|
|
105
|
+
"arborescent.practices" = "application"
|
|
106
|
+
"arborescent.selftest" = "application"
|
|
107
|
+
"arborescent.suggestions" = "application"
|
|
108
|
+
"arborescent.bench" = "application"
|
|
109
|
+
"arborescent.adapters" = "adapters"
|
|
110
|
+
"arborescent.installer_adapter" = "adapters"
|
|
111
|
+
"arborescent.clock_adapter" = "adapters"
|
|
112
|
+
"arborescent.filesystem_adapter" = "adapters"
|
|
113
|
+
"arborescent.scanner" = "adapters"
|
|
114
|
+
"arborescent.tool_runner" = "adapters"
|
|
115
|
+
"arborescent.typecheckers" = "adapters"
|
|
116
|
+
"arborescent.engines" = "adapters"
|
|
117
|
+
"arborescent.testing" = "adapters"
|
|
118
|
+
"arborescent.cli" = "composition"
|
|
119
|
+
"arborescent.cli_install" = "composition"
|
|
120
|
+
"arborescent.cli_report" = "composition"
|
|
121
|
+
"arborescent.__main__" = "composition"
|
|
122
|
+
|
|
123
|
+
[tool.arborescent.practices]
|
|
124
|
+
crlf = ["*.sh", "*.bash"]
|
|
125
|
+
undeclared-imports = true
|
|
126
|
+
secrets-guard = "tests"
|
|
127
|
+
docs-sync = "docs/architecture.md"
|
|
128
|
+
|
|
129
|
+
[tool.arborescent.ruff]
|
|
130
|
+
# The subprocess adapter deliberately owns process execution and passes argument vectors with shell disabled.
|
|
131
|
+
|
|
132
|
+
[tool.arborescent.ruff.per-file-ignores]
|
|
133
|
+
"src/arborescent/tool_runner.py" = ["S404", "S603"]
|
|
134
|
+
"src/arborescent/cli_install.py" = ["T20"]
|
|
135
|
+
"src/arborescent/cli_report.py" = ["T20"]
|
|
136
|
+
|
|
137
|
+
[tool.pytest.ini_options]
|
|
138
|
+
testpaths = ["tests"]
|
|
139
|
+
pythonpath = ["src", "tests"]
|
|
140
|
+
addopts = "-q"
|
|
141
|
+
|
|
142
|
+
# Contract that lived in the launch-runner root pyproject: the core and the ports stay free of process/file/network modules.
|
|
143
|
+
# (`lint-imports` reads this table; the generated arborescent-importlinter.toml is checked by `arborescent check`.)
|
|
144
|
+
[tool.importlinter]
|
|
145
|
+
root_packages = ["arborescent"]
|
|
146
|
+
include_external_packages = true
|
|
147
|
+
|
|
148
|
+
[[tool.importlinter.contracts]]
|
|
149
|
+
name = "arborescent core and ports must not import adapters, the engines or process/file/network modules"
|
|
150
|
+
type = "forbidden"
|
|
151
|
+
source_modules = ["arborescent.domain", "arborescent.facts", "arborescent.presets", "arborescent.stdlib", "arborescent.rules", "arborescent.ports"]
|
|
152
|
+
forbidden_modules = [
|
|
153
|
+
"arborescent.adapters", "arborescent.tool_runner", "arborescent.typecheckers", "arborescent.engines", "arborescent.engine", "arborescent.cli", "subprocess", "shutil", "tempfile",
|
|
154
|
+
"socket", "urllib",
|
|
155
|
+
]
|
|
156
|
+
allow_indirect_imports = true
|
|
157
|
+
|
|
158
|
+
[tool.ruff.lint]
|
|
159
|
+
# PIE807 asks for container factories, but Pyright strict reports the field type as
|
|
160
|
+
# partially unknown; explicit lambdas keep its inferred type fully concrete.
|
|
161
|
+
extend-ignore = ["PIE807"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Arborescent: one architecture policy that compiles to import-linter, tach and a type checker, and enforces more.
|
|
2
|
+
|
|
3
|
+
- ``domain`` / ``facts`` / ``rules`` -- the policy, one-pass AST facts, and the pure checks (no I/O).
|
|
4
|
+
- ``ports`` -- ``ToolRunner``, ``TypeChecker``, ``ModuleScanner``.
|
|
5
|
+
- ``generate`` / ``engine`` / ``baseline`` / ``selftest`` / ``practices`` / ``bench`` -- the use cases.
|
|
6
|
+
- ``adapters`` / ``typecheckers`` -- subprocesses, the file system + content-hash cache, pyright/basedpyright/ty/pyrefly.
|
|
7
|
+
- ``testing`` -- fakes. ``cli`` -- the composition root.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .domain import Policy, PolicyError, Violation
|
|
11
|
+
|
|
12
|
+
__all__ = ["Policy", "PolicyError", "Violation"]
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Compatibility exports for Arborescent's runtime adapters.
|
|
2
|
+
|
|
3
|
+
New code should import adapters from their responsibility-specific modules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .clock_adapter import SystemClock
|
|
7
|
+
from .filesystem_adapter import LocalFileSystem
|
|
8
|
+
from .scanner import FsScanner, ScanData
|
|
9
|
+
from .tool_runner import SubprocessToolRunner, resolve_executable
|
|
10
|
+
|
|
11
|
+
__all__ = ["FsScanner", "LocalFileSystem", "ScanData", "SubprocessToolRunner", "SystemClock", "resolve_executable"]
|