archunitpython 1.3.0__tar.gz → 1.5.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.
- {archunitpython-1.3.0 → archunitpython-1.5.0}/BACKLOG.md +2 -2
- {archunitpython-1.3.0 → archunitpython-1.5.0}/CHANGELOG.md +20 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/PKG-INFO +57 -3
- {archunitpython-1.3.0 → archunitpython-1.5.0}/README.md +56 -2
- {archunitpython-1.3.0 → archunitpython-1.5.0}/pyproject.toml +1 -1
- {archunitpython-1.3.0 → archunitpython-1.5.0}/scripts/check_release_metadata.py +1 -1
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/__init__.py +5 -1
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/extract_graph.py +79 -11
- archunitpython-1.5.0/src/archunitpython/config/__init__.py +5 -0
- archunitpython-1.5.0/src/archunitpython/config/loader.py +119 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/extraction/extract_class_info.py +6 -3
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/metrics.py +2 -2
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_declaration_detector.py +2 -2
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_extract_graph.py +68 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_logger.py +0 -4
- archunitpython-1.5.0/tests/config/__init__.py +1 -0
- archunitpython-1.5.0/tests/config/test_config_loader.py +115 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/files/test_file_assertions.py +0 -2
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/helpers.py +5 -1
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/integration/test_e2e.py +5 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/metrics/test_metrics.py +8 -9
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/metrics/test_metrics_fluentapi.py +26 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.editorconfig +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.gitattributes +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/PAGES.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.gitignore +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/.releaserc.json +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/LICENSE +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/files/fluentapi/files.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/layers/fluentapi/layers.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/fluentapi/slices.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/assertion.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.3.0 → archunitpython-1.5.0}/tests/test_setup.py +0 -0
|
@@ -11,9 +11,9 @@ This backlog collects product and maintenance ideas from project research.
|
|
|
11
11
|
|
|
12
12
|
## P1 - Adoption Workflow
|
|
13
13
|
|
|
14
|
-
- Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
|
|
14
|
+
- [x] Add an `.archignore` or similar file, modeled after `.gitignore`, for files that should never be analyzed.
|
|
15
15
|
- [x] Add a `.because(...)` API so rules can carry user-facing rationale into failure messages and generated architecture documentation.
|
|
16
|
-
- Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
|
|
16
|
+
- [x] Add configuration-file support for common rules, while keeping the fluent Python API as the primary interface.
|
|
17
17
|
- Add support for monorepo and multi-package Python projects.
|
|
18
18
|
|
|
19
19
|
## P1 - Python Import Semantics
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [1.5.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.4.0...v1.5.0) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* load common rules from config ([c7c968a](https://github.com/LukasNiessen/ArchUnitPython/commit/c7c968af20e929f7e7a2372275eaa7e26cb38e5f))
|
|
7
|
+
|
|
8
|
+
# [1.4.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.3.0...v1.4.0) (2026-07-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* clarify metadata and exclude messages ([999bb68](https://github.com/LukasNiessen/ArchUnitPython/commit/999bb689594ff613b5f4445191509bcdaa9a514f))
|
|
14
|
+
* harden archignore loading ([ae88c93](https://github.com/LukasNiessen/ArchUnitPython/commit/ae88c932ae2838cff0eabab687ac948f4920c3eb))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* support archignore exclusions ([c51c401](https://github.com/LukasNiessen/ArchUnitPython/commit/c51c40148425b08ce0257d75dd95c996d0505b00))
|
|
20
|
+
|
|
1
21
|
# [1.3.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.2.1...v1.3.0) (2026-07-05)
|
|
2
22
|
|
|
3
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics.
|
|
5
5
|
Project-URL: Homepage, https://github.com/LukasNiessen/ArchUnitPython
|
|
6
6
|
Project-URL: Repository, https://github.com/LukasNiessen/ArchUnitPython.git
|
|
@@ -188,6 +188,60 @@ options = CheckOptions(
|
|
|
188
188
|
violations = rule.check(options)
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
### Excluding Files With `.archignore`
|
|
192
|
+
|
|
193
|
+
Add a `.archignore` file to your project root to permanently exclude generated or
|
|
194
|
+
irrelevant files from architecture checks and file-based metrics:
|
|
195
|
+
|
|
196
|
+
```gitignore
|
|
197
|
+
# Generated code
|
|
198
|
+
generated/
|
|
199
|
+
|
|
200
|
+
# Migration scripts
|
|
201
|
+
migrations/*.py
|
|
202
|
+
|
|
203
|
+
# A single root-level file
|
|
204
|
+
/legacy_adapter.py
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Patterns support comments, blank lines, glob syntax, root-relative paths, path
|
|
208
|
+
patterns, and directory patterns with a trailing `/`.
|
|
209
|
+
|
|
210
|
+
### Loading Common Rules From Config
|
|
211
|
+
|
|
212
|
+
For straightforward shared rules, you can load a JSON config file and still run
|
|
213
|
+
the resulting rules in your normal test suite:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"project_path": "src",
|
|
218
|
+
"rules": [
|
|
219
|
+
{
|
|
220
|
+
"name": "controllers must not use services directly",
|
|
221
|
+
"type": "forbidden_dependency",
|
|
222
|
+
"source": "**/controllers/**",
|
|
223
|
+
"target": "**/services/**"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "source files have no cycles",
|
|
227
|
+
"type": "no_cycles"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from archunitpython import assert_passes, rules_from_config
|
|
235
|
+
|
|
236
|
+
def test_configured_architecture_rules():
|
|
237
|
+
for rule in rules_from_config("archunitpython.json"):
|
|
238
|
+
assert_passes(rule)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Supported rule types are `no_cycles`, `forbidden_dependency`, and
|
|
242
|
+
`forbidden_external_dependency`. The fluent Python API remains the primary and
|
|
243
|
+
most flexible interface.
|
|
244
|
+
|
|
191
245
|
### Explaining Rules With `.because(...)`
|
|
192
246
|
|
|
193
247
|
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
@@ -450,7 +504,7 @@ def test_no_forbidden_dependency():
|
|
|
450
504
|
|
|
451
505
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
452
506
|
|
|
453
|
-
**Using `requests` library repo for example**
|
|
507
|
+
**Using [`requests`](https://github.com/psf/requests) library repo for example**
|
|
454
508
|
|
|
455
509
|
```python
|
|
456
510
|
from archunitpython import project_graph
|
|
@@ -463,7 +517,7 @@ def test_export_dependency_graph_reports():
|
|
|
463
517
|
if __name__ == "__main__":
|
|
464
518
|
test_export_dependency_graph_reports()
|
|
465
519
|
```
|
|
466
|
-
**
|
|
520
|
+
**Exported mermaid diagram**
|
|
467
521
|
``` mermaid
|
|
468
522
|
flowchart LR
|
|
469
523
|
n0["__init__.py"]
|
|
@@ -155,6 +155,60 @@ options = CheckOptions(
|
|
|
155
155
|
violations = rule.check(options)
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
### Excluding Files With `.archignore`
|
|
159
|
+
|
|
160
|
+
Add a `.archignore` file to your project root to permanently exclude generated or
|
|
161
|
+
irrelevant files from architecture checks and file-based metrics:
|
|
162
|
+
|
|
163
|
+
```gitignore
|
|
164
|
+
# Generated code
|
|
165
|
+
generated/
|
|
166
|
+
|
|
167
|
+
# Migration scripts
|
|
168
|
+
migrations/*.py
|
|
169
|
+
|
|
170
|
+
# A single root-level file
|
|
171
|
+
/legacy_adapter.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Patterns support comments, blank lines, glob syntax, root-relative paths, path
|
|
175
|
+
patterns, and directory patterns with a trailing `/`.
|
|
176
|
+
|
|
177
|
+
### Loading Common Rules From Config
|
|
178
|
+
|
|
179
|
+
For straightforward shared rules, you can load a JSON config file and still run
|
|
180
|
+
the resulting rules in your normal test suite:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"project_path": "src",
|
|
185
|
+
"rules": [
|
|
186
|
+
{
|
|
187
|
+
"name": "controllers must not use services directly",
|
|
188
|
+
"type": "forbidden_dependency",
|
|
189
|
+
"source": "**/controllers/**",
|
|
190
|
+
"target": "**/services/**"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"name": "source files have no cycles",
|
|
194
|
+
"type": "no_cycles"
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from archunitpython import assert_passes, rules_from_config
|
|
202
|
+
|
|
203
|
+
def test_configured_architecture_rules():
|
|
204
|
+
for rule in rules_from_config("archunitpython.json"):
|
|
205
|
+
assert_passes(rule)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Supported rule types are `no_cycles`, `forbidden_dependency`, and
|
|
209
|
+
`forbidden_external_dependency`. The fluent Python API remains the primary and
|
|
210
|
+
most flexible interface.
|
|
211
|
+
|
|
158
212
|
### Explaining Rules With `.because(...)`
|
|
159
213
|
|
|
160
214
|
Attach a rationale to a rule so failing assertions explain why the rule exists:
|
|
@@ -417,7 +471,7 @@ def test_no_forbidden_dependency():
|
|
|
417
471
|
|
|
418
472
|
Generate dependency graph reports in multiple formats and narrow them to the part of the codebase you want to inspect.
|
|
419
473
|
|
|
420
|
-
**Using `requests` library repo for example**
|
|
474
|
+
**Using [`requests`](https://github.com/psf/requests) library repo for example**
|
|
421
475
|
|
|
422
476
|
```python
|
|
423
477
|
from archunitpython import project_graph
|
|
@@ -430,7 +484,7 @@ def test_export_dependency_graph_reports():
|
|
|
430
484
|
if __name__ == "__main__":
|
|
431
485
|
test_export_dependency_graph_reports()
|
|
432
486
|
```
|
|
433
|
-
**
|
|
487
|
+
**Exported mermaid diagram**
|
|
434
488
|
``` mermaid
|
|
435
489
|
flowchart LR
|
|
436
490
|
n0["__init__.py"]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "Architecture testing library for Python projects. Enforce dependency rules, detect cycles, validate metrics."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -17,7 +17,7 @@ def read_project_version() -> str:
|
|
|
17
17
|
content = PYPROJECT.read_text(encoding="utf-8")
|
|
18
18
|
match = re.search(r'^version = "([^"]+)"$', content, re.MULTILINE)
|
|
19
19
|
if match is None:
|
|
20
|
-
raise RuntimeError("Could not find project.version in pyproject.toml")
|
|
20
|
+
raise RuntimeError("Could not find [project].version in pyproject.toml")
|
|
21
21
|
return match.group(1)
|
|
22
22
|
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.5.0"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
@@ -12,6 +12,7 @@ from archunitpython.common import (
|
|
|
12
12
|
Violation,
|
|
13
13
|
)
|
|
14
14
|
from archunitpython.common.extraction import clear_graph_cache, extract_graph
|
|
15
|
+
from archunitpython.config import ConfiguredRule, rules_from_config
|
|
15
16
|
from archunitpython.files import files, project_files
|
|
16
17
|
from archunitpython.graph import dependency_graph, project_graph
|
|
17
18
|
from archunitpython.layers import layers, project_layers
|
|
@@ -35,6 +36,9 @@ __all__ = [
|
|
|
35
36
|
# Layers
|
|
36
37
|
"project_layers",
|
|
37
38
|
"layers",
|
|
39
|
+
# Config
|
|
40
|
+
"rules_from_config",
|
|
41
|
+
"ConfiguredRule",
|
|
38
42
|
# Slices
|
|
39
43
|
"project_slices",
|
|
40
44
|
# Metrics
|
{archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
@@ -29,6 +29,8 @@ _DEFAULT_EXCLUDE = [
|
|
|
29
29
|
"*.egg-info",
|
|
30
30
|
]
|
|
31
31
|
|
|
32
|
+
_ARCHIGNORE_FILE = ".archignore"
|
|
33
|
+
|
|
32
34
|
_IGNORE_DIRECTIVE_REGEX = re.compile(
|
|
33
35
|
r"#\s*archunit(?::|-)\s*ignore"
|
|
34
36
|
r"(?:\([^)]*\))?"
|
|
@@ -88,9 +90,7 @@ def extract_graph(
|
|
|
88
90
|
project_path = os.getcwd()
|
|
89
91
|
|
|
90
92
|
project_path = os.path.abspath(project_path)
|
|
91
|
-
excludes = (
|
|
92
|
-
list(set(exclude_patterns)) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
93
|
-
)
|
|
93
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
94
94
|
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
|
|
95
95
|
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
|
|
96
96
|
|
|
@@ -122,6 +122,34 @@ def _build_cache_key(
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _resolve_exclude_patterns(
|
|
126
|
+
project_path: str,
|
|
127
|
+
exclude_patterns: list[str] | None,
|
|
128
|
+
) -> list[str]:
|
|
129
|
+
"""Resolve exclude patterns (explicit or defaults) plus any .archignore patterns."""
|
|
130
|
+
excludes = list(exclude_patterns) if exclude_patterns is not None else list(_DEFAULT_EXCLUDE)
|
|
131
|
+
excludes.extend(_load_archignore_patterns(project_path))
|
|
132
|
+
return excludes
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_archignore_patterns(project_path: str) -> list[str]:
|
|
136
|
+
"""Load .archignore patterns from a project root, if present."""
|
|
137
|
+
archignore_path = os.path.join(project_path, _ARCHIGNORE_FILE)
|
|
138
|
+
try:
|
|
139
|
+
with open(archignore_path, "r", encoding="utf-8", errors="replace") as f:
|
|
140
|
+
lines = f.readlines()
|
|
141
|
+
except OSError:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
patterns: list[str] = []
|
|
145
|
+
for line in lines:
|
|
146
|
+
pattern = line.strip()
|
|
147
|
+
if not pattern or pattern.startswith("#"):
|
|
148
|
+
continue
|
|
149
|
+
patterns.append(pattern)
|
|
150
|
+
return patterns
|
|
151
|
+
|
|
152
|
+
|
|
125
153
|
def _extract_graph_uncached(
|
|
126
154
|
project_path: str,
|
|
127
155
|
exclude_patterns: list[str],
|
|
@@ -160,7 +188,7 @@ def _extract_graph_uncached(
|
|
|
160
188
|
if resolved and resolved != _normalize(file_path):
|
|
161
189
|
# Check if the resolved path is in our project
|
|
162
190
|
if not is_external and resolved not in normalized_py_file_set:
|
|
163
|
-
|
|
191
|
+
continue
|
|
164
192
|
|
|
165
193
|
edges.append(
|
|
166
194
|
Edge(
|
|
@@ -182,25 +210,65 @@ def _normalize(path: str) -> str:
|
|
|
182
210
|
def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
183
211
|
"""Recursively find all .py files, excluding specified patterns."""
|
|
184
212
|
py_files: list[str] = []
|
|
213
|
+
root = os.path.abspath(root)
|
|
185
214
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
186
215
|
# Filter out excluded directories in-place
|
|
187
|
-
dirnames[:] = [
|
|
216
|
+
dirnames[:] = [
|
|
217
|
+
d
|
|
218
|
+
for d in dirnames
|
|
219
|
+
if not _should_exclude_path(os.path.join(dirpath, d), root, exclude, is_dir=True)
|
|
220
|
+
]
|
|
188
221
|
|
|
189
222
|
for filename in filenames:
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
full_path = os.path.join(dirpath, filename)
|
|
224
|
+
if filename.endswith(".py") and not _should_exclude_path(
|
|
225
|
+
full_path, root, exclude, is_dir=False
|
|
226
|
+
):
|
|
192
227
|
py_files.append(os.path.abspath(full_path))
|
|
193
228
|
|
|
194
229
|
return py_files
|
|
195
230
|
|
|
196
231
|
|
|
197
|
-
def
|
|
198
|
-
|
|
232
|
+
def _should_exclude_path(
|
|
233
|
+
path: str,
|
|
234
|
+
root: str,
|
|
235
|
+
patterns: list[str],
|
|
236
|
+
*,
|
|
237
|
+
is_dir: bool,
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""Check if a path matches any exclude pattern."""
|
|
199
240
|
import fnmatch
|
|
200
241
|
|
|
201
|
-
|
|
202
|
-
|
|
242
|
+
rel_path = _normalize(os.path.relpath(path, root))
|
|
243
|
+
name = os.path.basename(path)
|
|
244
|
+
|
|
245
|
+
for raw_pattern in patterns:
|
|
246
|
+
pattern = raw_pattern.strip().replace("\\", "/")
|
|
247
|
+
if not pattern or pattern.startswith("#"):
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
pattern = pattern.removeprefix("./")
|
|
251
|
+
anchored = pattern.startswith("/")
|
|
252
|
+
if anchored:
|
|
253
|
+
pattern = pattern[1:]
|
|
254
|
+
|
|
255
|
+
dir_only = pattern.endswith("/")
|
|
256
|
+
if dir_only:
|
|
257
|
+
pattern = pattern.rstrip("/")
|
|
258
|
+
if not is_dir:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
if not pattern:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if "/" in pattern or anchored:
|
|
265
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
266
|
+
return True
|
|
267
|
+
if is_dir and rel_path == pattern:
|
|
268
|
+
return True
|
|
269
|
+
elif fnmatch.fnmatch(name, pattern):
|
|
203
270
|
return True
|
|
271
|
+
|
|
204
272
|
return False
|
|
205
273
|
|
|
206
274
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Load common architecture rules from a JSON configuration file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from archunitpython.common.assertion.violation import Violation
|
|
12
|
+
from archunitpython.common.error.errors import UserError
|
|
13
|
+
from archunitpython.common.fluentapi.checkable import Checkable, CheckOptions
|
|
14
|
+
from archunitpython.files.fluentapi.files import project_files
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ConfiguredRule:
|
|
19
|
+
"""A named rule loaded from a configuration file."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
rule: Checkable
|
|
23
|
+
|
|
24
|
+
def check(self, options: CheckOptions | None = None) -> list[Violation]:
|
|
25
|
+
"""Run the configured rule."""
|
|
26
|
+
return self.rule.check(options)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def rules_from_config(config_path: str) -> list[ConfiguredRule]:
|
|
30
|
+
"""Load common architecture rules from a JSON config file.
|
|
31
|
+
|
|
32
|
+
The fluent Python API remains the primary interface. Config files provide a
|
|
33
|
+
lightweight way to share straightforward rules across projects or teams.
|
|
34
|
+
"""
|
|
35
|
+
path = Path(config_path)
|
|
36
|
+
try:
|
|
37
|
+
raw_config = json.loads(path.read_text(encoding="utf-8"))
|
|
38
|
+
except OSError as exc:
|
|
39
|
+
raise UserError(f"Could not read config file: {config_path}") from exc
|
|
40
|
+
except json.JSONDecodeError as exc:
|
|
41
|
+
raise UserError(f"Invalid JSON config file: {config_path}") from exc
|
|
42
|
+
|
|
43
|
+
if not isinstance(raw_config, dict):
|
|
44
|
+
raise UserError("Architecture config must be a JSON object.")
|
|
45
|
+
|
|
46
|
+
project_path = _optional_string(raw_config, "project_path") or os.getcwd()
|
|
47
|
+
rules = raw_config.get("rules")
|
|
48
|
+
if not isinstance(rules, list):
|
|
49
|
+
raise UserError("Architecture config must define a 'rules' list.")
|
|
50
|
+
|
|
51
|
+
base_dir = str(path.parent if path.parent != Path("") else Path.cwd())
|
|
52
|
+
resolved_project_path = _resolve_project_path(base_dir, project_path)
|
|
53
|
+
|
|
54
|
+
return [_build_rule(resolved_project_path, item, index) for index, item in enumerate(rules, 1)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_rule(project_path: str, item: Any, index: int) -> ConfiguredRule:
|
|
58
|
+
if not isinstance(item, dict):
|
|
59
|
+
raise UserError(f"Rule #{index} must be a JSON object.")
|
|
60
|
+
|
|
61
|
+
rule_type = _required_string(item, "type", index)
|
|
62
|
+
name = _optional_string(item, "name") or f"{rule_type} rule #{index}"
|
|
63
|
+
rule: Checkable
|
|
64
|
+
if rule_type == "no_cycles":
|
|
65
|
+
subject = _optional_string(item, "subject")
|
|
66
|
+
builder = project_files(project_path)
|
|
67
|
+
if subject is not None:
|
|
68
|
+
rule = builder.in_path(subject).should().have_no_cycles()
|
|
69
|
+
else:
|
|
70
|
+
rule = builder.should().have_no_cycles()
|
|
71
|
+
elif rule_type == "forbidden_dependency":
|
|
72
|
+
source = _required_string(item, "source", index)
|
|
73
|
+
target = _required_string(item, "target", index)
|
|
74
|
+
rule = (
|
|
75
|
+
project_files(project_path)
|
|
76
|
+
.in_path(source)
|
|
77
|
+
.should_not()
|
|
78
|
+
.depend_on_files()
|
|
79
|
+
.in_path(target)
|
|
80
|
+
)
|
|
81
|
+
elif rule_type == "forbidden_external_dependency":
|
|
82
|
+
source = _required_string(item, "source", index)
|
|
83
|
+
module = _required_string(item, "module", index)
|
|
84
|
+
rule = (
|
|
85
|
+
project_files(project_path)
|
|
86
|
+
.in_path(source)
|
|
87
|
+
.should_not()
|
|
88
|
+
.depend_on_external_modules()
|
|
89
|
+
.matching(module)
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
raise UserError(
|
|
93
|
+
f"Unsupported rule type '{rule_type}'. Supported types: "
|
|
94
|
+
"no_cycles, forbidden_dependency, forbidden_external_dependency."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return ConfiguredRule(name=name, rule=rule)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_project_path(base_dir: str, project_path: str) -> str:
|
|
101
|
+
if os.path.isabs(project_path):
|
|
102
|
+
return project_path
|
|
103
|
+
return os.path.abspath(os.path.join(base_dir, project_path))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _required_string(rule: dict[str, Any], key: str, index: int) -> str:
|
|
107
|
+
value = rule.get(key)
|
|
108
|
+
if not isinstance(value, str) or not value.strip():
|
|
109
|
+
raise UserError(f"Rule #{index} must define a non-empty string '{key}'.")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _optional_string(rule: dict[str, Any], key: str) -> str | None:
|
|
114
|
+
value = rule.get(key)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
if not isinstance(value, str) or not value.strip():
|
|
118
|
+
raise UserError(f"Config value '{key}' must be a non-empty string.")
|
|
119
|
+
return value
|
|
@@ -5,7 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import ast
|
|
6
6
|
import os
|
|
7
7
|
|
|
8
|
-
from archunitpython.common.extraction.extract_graph import
|
|
8
|
+
from archunitpython.common.extraction.extract_graph import (
|
|
9
|
+
_find_python_files,
|
|
10
|
+
_resolve_exclude_patterns,
|
|
11
|
+
)
|
|
9
12
|
from archunitpython.metrics.common.types import (
|
|
10
13
|
ClassInfo,
|
|
11
14
|
EnhancedClassInfo,
|
|
@@ -33,7 +36,7 @@ def extract_class_info(
|
|
|
33
36
|
project_path = os.getcwd()
|
|
34
37
|
|
|
35
38
|
project_path = os.path.abspath(project_path)
|
|
36
|
-
excludes =
|
|
39
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
37
40
|
py_files = _find_python_files(project_path, excludes)
|
|
38
41
|
|
|
39
42
|
classes: list[ClassInfo] = []
|
|
@@ -53,7 +56,7 @@ def extract_enhanced_class_info(
|
|
|
53
56
|
project_path = os.getcwd()
|
|
54
57
|
|
|
55
58
|
project_path = os.path.abspath(project_path)
|
|
56
|
-
excludes =
|
|
59
|
+
excludes = _resolve_exclude_patterns(project_path, exclude_patterns)
|
|
57
60
|
py_files = _find_python_files(project_path, excludes)
|
|
58
61
|
|
|
59
62
|
results: list[FileAnalysisResult] = []
|
{archunitpython-1.3.0 → archunitpython-1.5.0}/src/archunitpython/metrics/fluentapi/metrics.py
RENAMED
|
@@ -251,13 +251,13 @@ class FileMetricCondition(RuleRationaleMixin):
|
|
|
251
251
|
import os
|
|
252
252
|
|
|
253
253
|
from archunitpython.common.extraction.extract_graph import (
|
|
254
|
-
_DEFAULT_EXCLUDE,
|
|
255
254
|
_find_python_files,
|
|
255
|
+
_resolve_exclude_patterns,
|
|
256
256
|
)
|
|
257
257
|
|
|
258
258
|
project = self._project_path or os.getcwd()
|
|
259
259
|
project = os.path.abspath(project)
|
|
260
|
-
files = _find_python_files(project,
|
|
260
|
+
files = _find_python_files(project, _resolve_exclude_patterns(project, None))
|
|
261
261
|
violations: list[Violation] = []
|
|
262
262
|
|
|
263
263
|
for file_path in files:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Tests for declaration detection utilities."""
|
|
2
2
|
|
|
3
|
+
import ast
|
|
4
|
+
|
|
3
5
|
from archunitpython.common.util.declaration_detector import (
|
|
4
|
-
DeclarationCounts,
|
|
5
6
|
count_declarations,
|
|
6
7
|
is_abstract_class,
|
|
7
8
|
is_abstract_method,
|
|
8
9
|
is_protocol_class,
|
|
9
10
|
)
|
|
10
|
-
import ast
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class TestIsAbstractClass:
|
|
@@ -11,6 +11,7 @@ from archunitpython.common.extraction.extract_graph import (
|
|
|
11
11
|
_extract_imports,
|
|
12
12
|
_find_python_files,
|
|
13
13
|
_normalize,
|
|
14
|
+
_resolve_exclude_patterns,
|
|
14
15
|
clear_graph_cache,
|
|
15
16
|
extract_graph,
|
|
16
17
|
)
|
|
@@ -164,6 +165,73 @@ class TestExtractGraph:
|
|
|
164
165
|
assert len(edges_with_kinds) > 0
|
|
165
166
|
|
|
166
167
|
|
|
168
|
+
class TestArchignore:
|
|
169
|
+
def setup_method(self):
|
|
170
|
+
clear_graph_cache()
|
|
171
|
+
self._temp_dir = Path(__file__).resolve().parent / ".tmp" / f"project_{uuid4().hex}"
|
|
172
|
+
self._temp_dir.mkdir(parents=True)
|
|
173
|
+
|
|
174
|
+
def teardown_method(self):
|
|
175
|
+
shutil.rmtree(self._temp_dir, ignore_errors=True)
|
|
176
|
+
|
|
177
|
+
def _write(self, relative_path: str, content: str = "") -> None:
|
|
178
|
+
path = self._temp_dir / relative_path
|
|
179
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
path.write_text(content, encoding="utf-8")
|
|
181
|
+
|
|
182
|
+
def test_archignore_excludes_files_and_directories(self):
|
|
183
|
+
self._write(
|
|
184
|
+
".archignore",
|
|
185
|
+
"\n".join(
|
|
186
|
+
[
|
|
187
|
+
"# Ignore generated architecture-test inputs",
|
|
188
|
+
"ignored.py",
|
|
189
|
+
"generated/",
|
|
190
|
+
"nested/*.py",
|
|
191
|
+
"/root_ignored.py",
|
|
192
|
+
]
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
self._write("keep.py")
|
|
196
|
+
self._write("ignored.py")
|
|
197
|
+
self._write("root_ignored.py")
|
|
198
|
+
self._write("generated/generated.py")
|
|
199
|
+
self._write("nested/ignored_nested.py")
|
|
200
|
+
|
|
201
|
+
excludes = _resolve_exclude_patterns(str(self._temp_dir), ["__pycache__"])
|
|
202
|
+
files = _find_python_files(str(self._temp_dir), excludes)
|
|
203
|
+
relative_files = {
|
|
204
|
+
Path(file_path).relative_to(self._temp_dir).as_posix()
|
|
205
|
+
for file_path in files
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
assert relative_files == {"keep.py"}
|
|
209
|
+
|
|
210
|
+
def test_archignore_ignored_files_are_not_dependency_targets(self):
|
|
211
|
+
self._write(".archignore", "ignored.py\n")
|
|
212
|
+
self._write("keep.py", "import ignored\n")
|
|
213
|
+
self._write("ignored.py", "VALUE = 1\n")
|
|
214
|
+
|
|
215
|
+
graph = extract_graph(str(self._temp_dir))
|
|
216
|
+
targets = {edge.target for edge in graph}
|
|
217
|
+
|
|
218
|
+
ignored_path = _normalize(str((self._temp_dir / "ignored.py").resolve()))
|
|
219
|
+
assert ignored_path not in targets
|
|
220
|
+
|
|
221
|
+
def test_archignore_with_invalid_utf8_bytes_does_not_abort_extraction(self):
|
|
222
|
+
(self._temp_dir / ".archignore").write_bytes(b"ignored.py\n\xff\n")
|
|
223
|
+
self._write("keep.py")
|
|
224
|
+
self._write("ignored.py")
|
|
225
|
+
|
|
226
|
+
graph = extract_graph(str(self._temp_dir))
|
|
227
|
+
sources = {edge.source for edge in graph}
|
|
228
|
+
|
|
229
|
+
keep_path = _normalize(str((self._temp_dir / "keep.py").resolve()))
|
|
230
|
+
ignored_path = _normalize(str((self._temp_dir / "ignored.py").resolve()))
|
|
231
|
+
assert keep_path in sources
|
|
232
|
+
assert ignored_path not in sources
|
|
233
|
+
|
|
234
|
+
|
|
167
235
|
class TestTypeCheckingImportHandling:
|
|
168
236
|
def setup_method(self):
|
|
169
237
|
clear_graph_cache()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for configuration-file rule loading."""
|