archunitpython 1.6.1__tar.gz → 1.8.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.6.1 → archunitpython-1.8.0}/BACKLOG.md +1 -1
- {archunitpython-1.6.1 → archunitpython-1.8.0}/CHANGELOG.md +19 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/PKG-INFO +81 -5
- {archunitpython-1.6.1 → archunitpython-1.8.0}/README.md +80 -4
- {archunitpython-1.6.1 → archunitpython-1.8.0}/pyproject.toml +1 -1
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/__init__.py +3 -1
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/extraction/extract_graph.py +124 -58
- archunitpython-1.8.0/src/archunitpython/common/fluentapi/inspection.py +79 -0
- archunitpython-1.8.0/src/archunitpython/common/logging/inspection.py +99 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/logging/types.py +2 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/pattern_matching.py +7 -2
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_edges.py +12 -1
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/fluentapi/files.py +6 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/layers/fluentapi/layers.py +2 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/extraction/extract_class_info.py +17 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/metrics.py +47 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/fluentapi/slices.py +3 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/assertion.py +16 -6
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/common/color_utils.py +12 -1
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_extract_graph.py +156 -0
- archunitpython-1.8.0/tests/common/test_inspection.py +244 -0
- archunitpython-1.8.0/tests/common/test_pretty_output.py +51 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.editorconfig +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.gitattributes +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/PAGES.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.gitignore +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/.releaserc.json +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/LICENSE +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/config/loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/config/test_config_loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.8.0}/tests/test_setup.py +0 -0
|
@@ -18,7 +18,7 @@ This backlog collects product and maintenance ideas from project research.
|
|
|
18
18
|
|
|
19
19
|
## P1 - Python Import Semantics
|
|
20
20
|
|
|
21
|
-
- Add support for namespace packages that do not contain `__init__.py`.
|
|
21
|
+
- [x] Add support for namespace packages that do not contain `__init__.py`.
|
|
22
22
|
- [x] Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
|
|
23
23
|
- [x] Detect conditional imports such as `try/except ImportError`.
|
|
24
24
|
- Add better `TYPE_CHECKING` import handling, including options to ignore, include, or report type-only imports separately.
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [1.8.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.7.0...v1.8.0) (2026-09-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add detailed per-check inspection and styled reports ([#65](https://github.com/LukasNiessen/ArchUnitPython/issues/65)) ([ab0f9ef](https://github.com/LukasNiessen/ArchUnitPython/commit/ab0f9efb71c90712366490121e82c09b112a5d06))
|
|
7
|
+
|
|
8
|
+
# [1.7.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.1...v1.7.0) (2026-09-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* preserve mixed namespace import targets ([489624b](https://github.com/LukasNiessen/ArchUnitPython/commit/489624b4de5465847f6e83106cbe0a398a7c1e8e))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* resolve namespace package submodule imports ([86c53e6](https://github.com/LukasNiessen/ArchUnitPython/commit/86c53e619ad25dfcdfb109de4593967d3d5603a6))
|
|
19
|
+
|
|
1
20
|
## [1.6.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.0...v1.6.1) (2026-09-13)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -377,6 +377,12 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
|
|
|
377
377
|
from my_app.adapters.sql import Repository # archunit: ignore
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
### Namespace Packages
|
|
381
|
+
|
|
382
|
+
ArchUnitPython resolves imports from namespace packages that do not contain
|
|
383
|
+
`__init__.py` files. For example, `from my_app.domain import model` is resolved
|
|
384
|
+
to `my_app/domain/model.py` when `my_app/domain/` is a namespace package.
|
|
385
|
+
|
|
380
386
|
### Conditional Imports
|
|
381
387
|
|
|
382
388
|
Imports inside `try` blocks that handle `ImportError` or
|
|
@@ -920,6 +926,10 @@ The most important differences:
|
|
|
920
926
|
|
|
921
927
|
When tests fail, you get helpful output with file paths and violation details:
|
|
922
928
|
|
|
929
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
930
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
931
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
932
|
+
|
|
923
933
|
```
|
|
924
934
|
Found 2 architecture violation(s):
|
|
925
935
|
|
|
@@ -932,25 +942,91 @@ Found 2 architecture violation(s):
|
|
|
932
942
|
|
|
933
943
|
## 📝 Debug Logging & Configuration
|
|
934
944
|
|
|
935
|
-
|
|
945
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
946
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
947
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
948
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
949
|
+
|
|
950
|
+
| Level | What you see |
|
|
951
|
+
| --- | --- |
|
|
952
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
953
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
954
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
955
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
936
956
|
|
|
937
957
|
### Enabling Debug Logging
|
|
938
958
|
|
|
939
959
|
```python
|
|
940
|
-
from archunitpython import CheckOptions
|
|
941
|
-
|
|
960
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
961
|
+
|
|
962
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
942
963
|
|
|
943
964
|
options = CheckOptions(
|
|
944
965
|
logging=LoggingOptions(
|
|
945
966
|
enabled=True,
|
|
946
967
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
947
|
-
log_file=True, # Creates logs/
|
|
968
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
948
969
|
),
|
|
949
970
|
)
|
|
950
971
|
|
|
951
972
|
violations = rule.check(options)
|
|
952
973
|
```
|
|
953
974
|
|
|
975
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
976
|
+
on your project):
|
|
977
|
+
|
|
978
|
+
```text
|
|
979
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
980
|
+
[DEBUG] Graph cache miss
|
|
981
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
982
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
983
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
984
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
985
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
989
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
990
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
991
|
+
Python's standard `logging` module in your application.
|
|
992
|
+
|
|
993
|
+
### Save detailed inspection without console noise
|
|
994
|
+
|
|
995
|
+
```python
|
|
996
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
997
|
+
enabled=True,
|
|
998
|
+
level="debug",
|
|
999
|
+
console=False,
|
|
1000
|
+
log_file=True,
|
|
1001
|
+
log_path="logs/architecture.log",
|
|
1002
|
+
append_to_log_file=True,
|
|
1003
|
+
))
|
|
1004
|
+
violations = rule.check(options)
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
1008
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
1009
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
1010
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
1011
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
1012
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
1013
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
1014
|
+
not re-run custom predicates or metric calculations.
|
|
1015
|
+
|
|
1016
|
+
### Format results independently of logging
|
|
1017
|
+
|
|
1018
|
+
```python
|
|
1019
|
+
from archunitpython import assert_passes, format_violations
|
|
1020
|
+
|
|
1021
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
1022
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
1023
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
1027
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
1028
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
1029
|
+
|
|
954
1030
|
### CI Pipeline Integration
|
|
955
1031
|
|
|
956
1032
|
```yaml
|
|
@@ -344,6 +344,12 @@ ArchUnitPython detects string-based dynamic imports such as `importlib.import_mo
|
|
|
344
344
|
from my_app.adapters.sql import Repository # archunit: ignore
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
+
### Namespace Packages
|
|
348
|
+
|
|
349
|
+
ArchUnitPython resolves imports from namespace packages that do not contain
|
|
350
|
+
`__init__.py` files. For example, `from my_app.domain import model` is resolved
|
|
351
|
+
to `my_app/domain/model.py` when `my_app/domain/` is a namespace package.
|
|
352
|
+
|
|
347
353
|
### Conditional Imports
|
|
348
354
|
|
|
349
355
|
Imports inside `try` blocks that handle `ImportError` or
|
|
@@ -887,6 +893,10 @@ The most important differences:
|
|
|
887
893
|
|
|
888
894
|
When tests fail, you get helpful output with file paths and violation details:
|
|
889
895
|
|
|
896
|
+
Interactive terminals also highlight the failure summary, numbered headings,
|
|
897
|
+
paths, metric values, and rule rationale. Output stays plain when redirected,
|
|
898
|
+
when `CI=true`, or when `NO_COLOR` is set (even to an empty value).
|
|
899
|
+
|
|
890
900
|
```
|
|
891
901
|
Found 2 architecture violation(s):
|
|
892
902
|
|
|
@@ -899,25 +909,91 @@ Found 2 architecture violation(s):
|
|
|
899
909
|
|
|
900
910
|
## 📝 Debug Logging & Configuration
|
|
901
911
|
|
|
902
|
-
|
|
912
|
+
Logging is disabled by default. Enable it per check to inspect file discovery,
|
|
913
|
+
graph extraction and cache use, selector decisions, edge projections, class
|
|
914
|
+
analysis, metric values, and the resulting violations. File, layer, slice,
|
|
915
|
+
count/LCOM/distance, and custom metric checks use the same logging options.
|
|
916
|
+
|
|
917
|
+
| Level | What you see |
|
|
918
|
+
| --- | --- |
|
|
919
|
+
| `error` | Analysis exceptions (the original exception is still raised). |
|
|
920
|
+
| `warn` | Failed check summaries and every violation, plus errors. |
|
|
921
|
+
| `info` | Check start, completion, duration, and violations, plus errors. |
|
|
922
|
+
| `debug` | All of the above, plus rule configuration, discovered files, dependencies, cache decisions, selector matches, projected/omitted edges, classes, and measured values. |
|
|
903
923
|
|
|
904
924
|
### Enabling Debug Logging
|
|
905
925
|
|
|
906
926
|
```python
|
|
907
|
-
from archunitpython import CheckOptions
|
|
908
|
-
|
|
927
|
+
from archunitpython import CheckOptions, LoggingOptions, project_files
|
|
928
|
+
|
|
929
|
+
rule = project_files("src/").should().have_no_cycles()
|
|
909
930
|
|
|
910
931
|
options = CheckOptions(
|
|
911
932
|
logging=LoggingOptions(
|
|
912
933
|
enabled=True,
|
|
913
934
|
level="debug", # "error" | "warn" | "info" | "debug"
|
|
914
|
-
log_file=True, # Creates logs/
|
|
935
|
+
log_file=True, # Creates a unique timestamped file in logs/
|
|
915
936
|
),
|
|
916
937
|
)
|
|
917
938
|
|
|
918
939
|
violations = rule.check(options)
|
|
919
940
|
```
|
|
920
941
|
|
|
942
|
+
For example, a debug trace includes entries like these (paths and timings depend
|
|
943
|
+
on your project):
|
|
944
|
+
|
|
945
|
+
```text
|
|
946
|
+
[INFO] Starting check: CycleFreeFileCondition
|
|
947
|
+
[DEBUG] Graph cache miss
|
|
948
|
+
[DEBUG] Discovered file: /project/src/api.py
|
|
949
|
+
[DEBUG] Processing file: /project/src/api.py
|
|
950
|
+
[DEBUG] Dependency: /project/src/api.py -> /project/src/storage.py; external=False; kinds=[]
|
|
951
|
+
[DEBUG] Projection: /project/src/api.py -> /project/src/storage.py becomes /project/src/api.py -> /project/src/storage.py
|
|
952
|
+
[INFO] Finished check: CycleFreeFileCondition - 0 violation(s) (0.002s)
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Console diagnostics use the standard `archunitpython` logger when handlers are
|
|
956
|
+
configured, respecting their levels and formatting; otherwise they go to stderr.
|
|
957
|
+
For pytest live logs, use `pytest --log-cli-level=DEBUG`. You can also configure
|
|
958
|
+
Python's standard `logging` module in your application.
|
|
959
|
+
|
|
960
|
+
### Save detailed inspection without console noise
|
|
961
|
+
|
|
962
|
+
```python
|
|
963
|
+
options = CheckOptions(logging=LoggingOptions(
|
|
964
|
+
enabled=True,
|
|
965
|
+
level="debug",
|
|
966
|
+
console=False,
|
|
967
|
+
log_file=True,
|
|
968
|
+
log_path="logs/architecture.log",
|
|
969
|
+
append_to_log_file=True,
|
|
970
|
+
))
|
|
971
|
+
violations = rule.check(options)
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
File logs are UTF-8, timestamped, and contain no added ANSI colors. Automatic
|
|
975
|
+
paths are unique per check; an explicit `log_path` replaces that path.
|
|
976
|
+
`append_to_log_file=False` overwrites an explicit file at the start of each check.
|
|
977
|
+
Each check closes its own output file and isolates its diagnostics from nested
|
|
978
|
+
and concurrent checks. Use separate paths when concurrent checks write to files.
|
|
979
|
+
Diagnostic output is best effort: an unavailable file or failing logging handler
|
|
980
|
+
does not change rule results or hide analysis exceptions. Debug inspection does
|
|
981
|
+
not re-run custom predicates or metric calculations.
|
|
982
|
+
|
|
983
|
+
### Format results independently of logging
|
|
984
|
+
|
|
985
|
+
```python
|
|
986
|
+
from archunitpython import assert_passes, format_violations
|
|
987
|
+
|
|
988
|
+
print(format_violations(violations)) # Detect terminal colors
|
|
989
|
+
text = format_violations(violations, color=False) # Stable plain text for artifacts
|
|
990
|
+
assert_passes(rule, options, color=False) # Plain assertion failure
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Pass `color=True` to explicitly request ANSI styling; `NO_COLOR` always wins.
|
|
994
|
+
The numbered report, rationale, and details remain available at every log level,
|
|
995
|
+
including when logging is disabled. Existing plain-text report wording is retained.
|
|
996
|
+
|
|
921
997
|
### CI Pipeline Integration
|
|
922
998
|
|
|
923
999
|
```yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.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"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""ArchUnitPython - Architecture testing library for Python projects."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.
|
|
3
|
+
__version__ = "1.8.0"
|
|
4
4
|
|
|
5
5
|
# Files API
|
|
6
6
|
# Common
|
|
7
7
|
from archunitpython.common import (
|
|
8
8
|
CheckOptions,
|
|
9
9
|
EmptyTestViolation,
|
|
10
|
+
LoggingOptions,
|
|
10
11
|
TechnicalError,
|
|
11
12
|
UserError,
|
|
12
13
|
Violation,
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"Violation",
|
|
51
52
|
"EmptyTestViolation",
|
|
52
53
|
"CheckOptions",
|
|
54
|
+
"LoggingOptions",
|
|
53
55
|
"TechnicalError",
|
|
54
56
|
"UserError",
|
|
55
57
|
"extract_graph",
|
{archunitpython-1.6.1 → archunitpython-1.8.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
|
|
10
10
|
from archunitpython.common.extraction.graph import Edge, Graph, ImportKind
|
|
11
11
|
from archunitpython.common.fluentapi.checkable import CheckOptions
|
|
12
|
+
from archunitpython.common.logging.inspection import debug, debug_enabled
|
|
12
13
|
|
|
13
14
|
GraphCacheKey = tuple[str, tuple[str, ...], bool]
|
|
14
15
|
|
|
@@ -45,6 +46,7 @@ class _LocatedImport:
|
|
|
45
46
|
line_number: int
|
|
46
47
|
resolution_kind: ImportKind | None = None
|
|
47
48
|
fallback_module_name: str | None = None
|
|
49
|
+
aliases: tuple[str, ...] = ()
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@dataclass(frozen=True)
|
|
@@ -56,8 +58,7 @@ class _IgnoreDirective:
|
|
|
56
58
|
if not self.modules:
|
|
57
59
|
return True
|
|
58
60
|
return any(
|
|
59
|
-
import_.module_name == module
|
|
60
|
-
or import_.module_name.startswith(f"{module}.")
|
|
61
|
+
import_.module_name == module or import_.module_name.startswith(f"{module}.")
|
|
61
62
|
for module in self.modules
|
|
62
63
|
)
|
|
63
64
|
|
|
@@ -96,21 +97,46 @@ def extract_graph(
|
|
|
96
97
|
ignore_type_checking_imports = bool(options and options.ignore_type_checking_imports)
|
|
97
98
|
cache_key = _build_cache_key(project_path, excludes, ignore_type_checking_imports)
|
|
98
99
|
|
|
100
|
+
debug(
|
|
101
|
+
"Extraction root: %s; excludes: %s; ignore type-only imports: %s",
|
|
102
|
+
project_path,
|
|
103
|
+
excludes,
|
|
104
|
+
ignore_type_checking_imports,
|
|
105
|
+
)
|
|
99
106
|
if options and options.clear_cache:
|
|
107
|
+
debug("Clearing graph cache for %s", project_path)
|
|
100
108
|
_graph_cache.pop(cache_key, None)
|
|
101
109
|
|
|
102
110
|
if cache_key in _graph_cache:
|
|
111
|
+
debug("Graph cache hit: %d edges", len(_graph_cache[cache_key]))
|
|
112
|
+
_inspect_graph(_graph_cache[cache_key])
|
|
103
113
|
return _graph_cache[cache_key]
|
|
114
|
+
debug("Graph cache miss")
|
|
104
115
|
|
|
105
116
|
result = _extract_graph_uncached(
|
|
106
117
|
project_path,
|
|
107
118
|
excludes,
|
|
108
119
|
ignore_type_checking_imports=ignore_type_checking_imports,
|
|
109
120
|
)
|
|
121
|
+
_inspect_graph(result)
|
|
110
122
|
_graph_cache[cache_key] = result
|
|
111
123
|
return result
|
|
112
124
|
|
|
113
125
|
|
|
126
|
+
def _inspect_graph(graph: Graph) -> None:
|
|
127
|
+
if not debug_enabled():
|
|
128
|
+
return
|
|
129
|
+
debug("Extracted graph: %d edges (including self-edges)", len(graph))
|
|
130
|
+
for edge in graph:
|
|
131
|
+
debug(
|
|
132
|
+
"Dependency: %s -> %s; external=%s; kinds=%s",
|
|
133
|
+
edge.source,
|
|
134
|
+
edge.target,
|
|
135
|
+
edge.external,
|
|
136
|
+
edge.import_kinds,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
114
140
|
def _build_cache_key(
|
|
115
141
|
project_path: str,
|
|
116
142
|
exclude_patterns: list[str],
|
|
@@ -166,6 +192,7 @@ def _extract_graph_uncached(
|
|
|
166
192
|
normalized_py_file_set = {_normalize(f) for f in py_files_set}
|
|
167
193
|
|
|
168
194
|
for file_path in py_files:
|
|
195
|
+
debug("Processing file: %s", file_path)
|
|
169
196
|
# Add self-referencing edge (ensures the file appears as a node)
|
|
170
197
|
edges.append(
|
|
171
198
|
Edge(
|
|
@@ -177,39 +204,32 @@ def _extract_graph_uncached(
|
|
|
177
204
|
|
|
178
205
|
imports = _extract_located_imports(file_path)
|
|
179
206
|
for located_import in imports:
|
|
180
|
-
module_name = located_import.module_name
|
|
181
207
|
import_kind = located_import.import_kind
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
):
|
|
186
|
-
continue
|
|
187
|
-
resolution_kind = located_import.resolution_kind or import_kind
|
|
188
|
-
resolved, is_external = _resolve_import(
|
|
189
|
-
module_name, file_path, project_path, resolution_kind
|
|
190
|
-
)
|
|
191
|
-
if is_external and located_import.fallback_module_name is not None:
|
|
192
|
-
fallback, fallback_is_external = _resolve_import(
|
|
193
|
-
located_import.fallback_module_name,
|
|
208
|
+
if ignore_type_checking_imports and import_kind == ImportKind.TYPE_IMPORT:
|
|
209
|
+
debug(
|
|
210
|
+
"Skipped type-only import: %s:%s -> %s",
|
|
194
211
|
file_path,
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
located_import.line_number,
|
|
213
|
+
located_import.module_name,
|
|
197
214
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
continue
|
|
216
|
+
for resolved, is_external in _resolve_import_targets(
|
|
217
|
+
located_import, file_path, project_path
|
|
218
|
+
):
|
|
219
|
+
if resolved and resolved != _normalize(file_path):
|
|
220
|
+
# Check if the resolved path is in our project
|
|
221
|
+
if not is_external and resolved not in normalized_py_file_set:
|
|
222
|
+
debug("Skipped excluded dependency: %s -> %s", file_path, resolved)
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
edges.append(
|
|
226
|
+
Edge(
|
|
227
|
+
source=_normalize(file_path),
|
|
228
|
+
target=resolved,
|
|
229
|
+
external=is_external,
|
|
230
|
+
import_kinds=_edge_import_kinds(located_import),
|
|
231
|
+
)
|
|
211
232
|
)
|
|
212
|
-
)
|
|
213
233
|
|
|
214
234
|
return _merge_edges(edges)
|
|
215
235
|
|
|
@@ -237,6 +257,7 @@ def _find_python_files(root: str, exclude: list[str]) -> list[str]:
|
|
|
237
257
|
full_path, root, exclude, is_dir=False
|
|
238
258
|
):
|
|
239
259
|
py_files.append(os.path.abspath(full_path))
|
|
260
|
+
debug("Discovered file: %s", full_path)
|
|
240
261
|
|
|
241
262
|
return py_files
|
|
242
263
|
|
|
@@ -323,9 +344,7 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
323
344
|
conditional_import_ranges,
|
|
324
345
|
)
|
|
325
346
|
for alias in node.names:
|
|
326
|
-
imports.append(
|
|
327
|
-
_LocatedImport(alias.name, kind, node.lineno, syntax_kind)
|
|
328
|
-
)
|
|
347
|
+
imports.append(_LocatedImport(alias.name, kind, node.lineno, syntax_kind))
|
|
329
348
|
|
|
330
349
|
elif isinstance(node, ast.ImportFrom):
|
|
331
350
|
syntax_kind = (
|
|
@@ -339,17 +358,17 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
339
358
|
type_checking_ranges,
|
|
340
359
|
conditional_import_ranges,
|
|
341
360
|
)
|
|
342
|
-
fallback_module_name =
|
|
343
|
-
|
|
344
|
-
)
|
|
361
|
+
fallback_module_name = "." * node.level if node.level and node.module is None else None
|
|
362
|
+
aliases = _module_aliases(node) if node.module else ()
|
|
345
363
|
for module_name in _import_from_module_names(node):
|
|
346
364
|
imports.append(
|
|
347
365
|
_LocatedImport(
|
|
348
|
-
module_name,
|
|
349
|
-
kind,
|
|
350
|
-
node.lineno,
|
|
351
|
-
syntax_kind,
|
|
352
|
-
fallback_module_name,
|
|
366
|
+
module_name=module_name,
|
|
367
|
+
import_kind=kind,
|
|
368
|
+
line_number=node.lineno,
|
|
369
|
+
resolution_kind=syntax_kind,
|
|
370
|
+
fallback_module_name=fallback_module_name,
|
|
371
|
+
aliases=aliases,
|
|
353
372
|
)
|
|
354
373
|
)
|
|
355
374
|
|
|
@@ -362,15 +381,9 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
362
381
|
conditional_import_ranges,
|
|
363
382
|
)
|
|
364
383
|
for module_name in _extract_dynamic_import_names(node):
|
|
365
|
-
imports.append(
|
|
366
|
-
_LocatedImport(module_name, kind, node.lineno, syntax_kind)
|
|
367
|
-
)
|
|
384
|
+
imports.append(_LocatedImport(module_name, kind, node.lineno, syntax_kind))
|
|
368
385
|
|
|
369
|
-
return [
|
|
370
|
-
import_
|
|
371
|
-
for import_ in imports
|
|
372
|
-
if not _is_ignored_import(import_, ignore_directives)
|
|
373
|
-
]
|
|
386
|
+
return [import_ for import_ in imports if not _is_ignored_import(import_, ignore_directives)]
|
|
374
387
|
|
|
375
388
|
|
|
376
389
|
def _find_ignore_directives(source: str) -> dict[int, _IgnoreDirective]:
|
|
@@ -429,6 +442,11 @@ def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
|
|
|
429
442
|
return []
|
|
430
443
|
|
|
431
444
|
|
|
445
|
+
def _module_aliases(node: ast.ImportFrom) -> tuple[str, ...]:
|
|
446
|
+
"""Return aliases that may refer to imported submodules."""
|
|
447
|
+
return tuple(alias.name for alias in node.names if alias.name != "*")
|
|
448
|
+
|
|
449
|
+
|
|
432
450
|
def _import_from_module_names(node: ast.ImportFrom) -> tuple[str, ...]:
|
|
433
451
|
"""Return resolvable module names for a from-import statement."""
|
|
434
452
|
dots = "." * (node.level or 0)
|
|
@@ -558,14 +576,10 @@ def _resolve_import(
|
|
|
558
576
|
Returns (resolved_path, is_external).
|
|
559
577
|
The path is normalized with forward slashes.
|
|
560
578
|
"""
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
ImportKind.TYPE_IMPORT,
|
|
566
|
-
)
|
|
567
|
-
and import_name.startswith(".")
|
|
568
|
-
):
|
|
579
|
+
if kind in (
|
|
580
|
+
ImportKind.RELATIVE_IMPORT,
|
|
581
|
+
ImportKind.TYPE_IMPORT,
|
|
582
|
+
) and import_name.startswith("."):
|
|
569
583
|
# Relative import
|
|
570
584
|
return _resolve_relative_import(import_name, source_file, project_root)
|
|
571
585
|
|
|
@@ -573,6 +587,58 @@ def _resolve_import(
|
|
|
573
587
|
return _resolve_absolute_import(import_name, project_root)
|
|
574
588
|
|
|
575
589
|
|
|
590
|
+
def _resolve_import_targets(
|
|
591
|
+
import_: _LocatedImport,
|
|
592
|
+
source_file: str,
|
|
593
|
+
project_root: str,
|
|
594
|
+
) -> list[tuple[str, bool]]:
|
|
595
|
+
"""Resolve an import, including namespace-package submodule aliases."""
|
|
596
|
+
resolution_kind = import_.resolution_kind or import_.import_kind
|
|
597
|
+
resolved, is_external = _resolve_import(
|
|
598
|
+
import_.module_name,
|
|
599
|
+
source_file,
|
|
600
|
+
project_root,
|
|
601
|
+
resolution_kind,
|
|
602
|
+
)
|
|
603
|
+
if is_external and import_.fallback_module_name is not None:
|
|
604
|
+
fallback, fallback_is_external = _resolve_import(
|
|
605
|
+
import_.fallback_module_name,
|
|
606
|
+
source_file,
|
|
607
|
+
project_root,
|
|
608
|
+
resolution_kind,
|
|
609
|
+
)
|
|
610
|
+
if fallback and not fallback_is_external:
|
|
611
|
+
resolved, is_external = fallback, False
|
|
612
|
+
if not is_external or not import_.aliases:
|
|
613
|
+
return [(resolved, is_external)]
|
|
614
|
+
|
|
615
|
+
alias_targets: list[tuple[str, bool]] = []
|
|
616
|
+
found_internal_alias = False
|
|
617
|
+
for alias in import_.aliases:
|
|
618
|
+
alias_module = _join_import_alias(import_.module_name, alias)
|
|
619
|
+
alias_resolved, alias_is_external = _resolve_import(
|
|
620
|
+
alias_module,
|
|
621
|
+
source_file,
|
|
622
|
+
project_root,
|
|
623
|
+
resolution_kind,
|
|
624
|
+
)
|
|
625
|
+
alias_targets.append((alias_resolved, alias_is_external))
|
|
626
|
+
found_internal_alias = found_internal_alias or not alias_is_external
|
|
627
|
+
|
|
628
|
+
if found_internal_alias:
|
|
629
|
+
return alias_targets
|
|
630
|
+
return [(resolved, is_external)]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _join_import_alias(module_name: str, alias: str) -> str:
|
|
634
|
+
"""Join a from-import module name with a candidate submodule alias."""
|
|
635
|
+
if not module_name:
|
|
636
|
+
return alias
|
|
637
|
+
if set(module_name) == {"."}:
|
|
638
|
+
return f"{module_name}{alias}"
|
|
639
|
+
return f"{module_name}.{alias}"
|
|
640
|
+
|
|
641
|
+
|
|
576
642
|
def _resolve_relative_import(
|
|
577
643
|
import_name: str,
|
|
578
644
|
source_file: str,
|