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.
Files changed (66) hide show
  1. arborescent-0.2.0/LICENSE +21 -0
  2. arborescent-0.2.0/PKG-INFO +185 -0
  3. arborescent-0.2.0/README.md +134 -0
  4. arborescent-0.2.0/pyproject.toml +161 -0
  5. arborescent-0.2.0/setup.cfg +4 -0
  6. arborescent-0.2.0/src/arborescent/__init__.py +13 -0
  7. arborescent-0.2.0/src/arborescent/__main__.py +3 -0
  8. arborescent-0.2.0/src/arborescent/adapters.py +11 -0
  9. arborescent-0.2.0/src/arborescent/baseline.py +150 -0
  10. arborescent-0.2.0/src/arborescent/bench.py +93 -0
  11. arborescent-0.2.0/src/arborescent/certification.py +251 -0
  12. arborescent-0.2.0/src/arborescent/cli.py +210 -0
  13. arborescent-0.2.0/src/arborescent/cli_install.py +119 -0
  14. arborescent-0.2.0/src/arborescent/cli_report.py +107 -0
  15. arborescent-0.2.0/src/arborescent/clock_adapter.py +12 -0
  16. arborescent-0.2.0/src/arborescent/config.py +217 -0
  17. arborescent-0.2.0/src/arborescent/domain.py +248 -0
  18. arborescent-0.2.0/src/arborescent/engine.py +305 -0
  19. arborescent-0.2.0/src/arborescent/engines.py +132 -0
  20. arborescent-0.2.0/src/arborescent/facts.py +273 -0
  21. arborescent-0.2.0/src/arborescent/filesystem_adapter.py +64 -0
  22. arborescent-0.2.0/src/arborescent/generate.py +197 -0
  23. arborescent-0.2.0/src/arborescent/install_tools.py +41 -0
  24. arborescent-0.2.0/src/arborescent/installer_adapter.py +210 -0
  25. arborescent-0.2.0/src/arborescent/ports.py +136 -0
  26. arborescent-0.2.0/src/arborescent/practices.py +205 -0
  27. arborescent-0.2.0/src/arborescent/presets.py +64 -0
  28. arborescent-0.2.0/src/arborescent/py.typed +0 -0
  29. arborescent-0.2.0/src/arborescent/rules.py +213 -0
  30. arborescent-0.2.0/src/arborescent/scanner.py +252 -0
  31. arborescent-0.2.0/src/arborescent/selftest.py +168 -0
  32. arborescent-0.2.0/src/arborescent/stdlib.py +111 -0
  33. arborescent-0.2.0/src/arborescent/suggestions.py +110 -0
  34. arborescent-0.2.0/src/arborescent/testing.py +183 -0
  35. arborescent-0.2.0/src/arborescent/tool_runner.py +100 -0
  36. arborescent-0.2.0/src/arborescent/typecheckers.py +181 -0
  37. arborescent-0.2.0/src/arborescent.egg-info/PKG-INFO +185 -0
  38. arborescent-0.2.0/src/arborescent.egg-info/SOURCES.txt +64 -0
  39. arborescent-0.2.0/src/arborescent.egg-info/dependency_links.txt +1 -0
  40. arborescent-0.2.0/src/arborescent.egg-info/entry_points.txt +2 -0
  41. arborescent-0.2.0/src/arborescent.egg-info/requires.txt +39 -0
  42. arborescent-0.2.0/src/arborescent.egg-info/top_level.txt +1 -0
  43. arborescent-0.2.0/tests/test_adapter_boundaries.py +13 -0
  44. arborescent-0.2.0/tests/test_baseline_ratchet.py +228 -0
  45. arborescent-0.2.0/tests/test_certification.py +58 -0
  46. arborescent-0.2.0/tests/test_coverage.py +129 -0
  47. arborescent-0.2.0/tests/test_customizations.py +43 -0
  48. arborescent-0.2.0/tests/test_dogfood_and_speed.py +128 -0
  49. arborescent-0.2.0/tests/test_effect_policy.py +113 -0
  50. arborescent-0.2.0/tests/test_engine.py +404 -0
  51. arborescent-0.2.0/tests/test_engine_ports.py +330 -0
  52. arborescent-0.2.0/tests/test_facts.py +138 -0
  53. arborescent-0.2.0/tests/test_filesystem.py +165 -0
  54. arborescent-0.2.0/tests/test_fs_behaviour.py +244 -0
  55. arborescent-0.2.0/tests/test_install_tools.py +471 -0
  56. arborescent-0.2.0/tests/test_policy.py +156 -0
  57. arborescent-0.2.0/tests/test_practices.py +147 -0
  58. arborescent-0.2.0/tests/test_presets.py +138 -0
  59. arborescent-0.2.0/tests/test_ruff_engine.py +129 -0
  60. arborescent-0.2.0/tests/test_rule_ids.py +155 -0
  61. arborescent-0.2.0/tests/test_rules.py +189 -0
  62. arborescent-0.2.0/tests/test_seams.py +112 -0
  63. arborescent-0.2.0/tests/test_stdlib.py +105 -0
  64. arborescent-0.2.0/tests/test_suggestions.py +81 -0
  65. arborescent-0.2.0/tests/test_todo.py +45 -0
  66. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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"]