archunitpython 1.6.1__tar.gz → 1.7.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.7.0}/BACKLOG.md +1 -1
- {archunitpython-1.6.1 → archunitpython-1.7.0}/CHANGELOG.md +12 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/PKG-INFO +7 -1
- {archunitpython-1.6.1 → archunitpython-1.7.0}/README.md +6 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/pyproject.toml +1 -1
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/__init__.py +1 -1
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/extraction/extract_graph.py +80 -31
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_extract_graph.py +156 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.editorconfig +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.gitattributes +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/CODEOWNERS +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/FUNDING.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/PAGES.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/dependabot.yml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/pull_request_template.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/workflows/docs.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/workflows/integrate.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.github/workflows/stale.yaml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.gitignore +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/.releaserc.json +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/CONTRIBUTING.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/LICENSE +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/assets/logo-rounded.png +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/research/product-direction/architecture-testing-landscape.md +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/scripts/bump_release_version.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/scripts/check_release_metadata.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/assertion/violation.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/error/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/error/errors.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/extraction/graph.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/fluentapi/checkable.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/logging/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/logging/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/pattern_matching.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/cycle_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/johnsons_apsp.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/tarjan_scc.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/edge_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_edges.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_nodes.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/regex_factory.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/util/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/util/declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/util/logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/config/loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/custom_file_logic.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/cycle_free.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/depend_on_external_modules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/depend_on_files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/matching_files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/fluentapi/files.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/graph/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/graph/graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/assertion/layer_dependencies.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/fluentapi/layers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/assertion/metric_thresholds.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/count.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/distance.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/lcom.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/common/types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/extraction/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/extraction/extract_class_info.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/export_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/metrics.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/py.typed +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/assertion/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/assertion/admissible_edges.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/fluentapi/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/fluentapi/slices.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/projection/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/projection/slicing_projections.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/uml/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/uml/export_diagram.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/uml/generate_rules.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/assertion.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/common/color_utils.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/common/violation_factory.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/pytest_plugin/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_core_types.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_cycles.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_declaration_detector.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_logger.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_pattern_matching.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/common/test_projection.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/config/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/config/test_config_loader.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/files/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/files/test_file_assertions.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/files/test_files_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/metrics_project/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/architecture.puml +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/controllers/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/controllers/controller.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/models/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/models/model.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service_a.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service_b.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/utils/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/utils/helpers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/graph/test_graph_reporter.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/integration/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/integration/test_e2e.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/layers/test_layers.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/metrics/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/metrics/test_export.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/metrics/test_metrics.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/metrics/test_metrics_fluentapi.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/slices/__init__.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.0}/tests/slices/test_slices.py +0 -0
- {archunitpython-1.6.1 → archunitpython-1.7.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,15 @@
|
|
|
1
|
+
# [1.7.0](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.1...v1.7.0) (2026-09-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* preserve mixed namespace import targets ([489624b](https://github.com/LukasNiessen/ArchUnitPython/commit/489624b4de5465847f6e83106cbe0a398a7c1e8e))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* resolve namespace package submodule imports ([86c53e6](https://github.com/LukasNiessen/ArchUnitPython/commit/86c53e619ad25dfcdfb109de4593967d3d5603a6))
|
|
12
|
+
|
|
1
13
|
## [1.6.1](https://github.com/LukasNiessen/ArchUnitPython/compare/v1.6.0...v1.6.1) (2026-09-13)
|
|
2
14
|
|
|
3
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archunitpython
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.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
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "archunitpython"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.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"
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/extraction/extract_graph.py
RENAMED
|
@@ -45,6 +45,7 @@ class _LocatedImport:
|
|
|
45
45
|
line_number: int
|
|
46
46
|
resolution_kind: ImportKind | None = None
|
|
47
47
|
fallback_module_name: str | None = None
|
|
48
|
+
aliases: tuple[str, ...] = ()
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
@dataclass(frozen=True)
|
|
@@ -177,39 +178,28 @@ def _extract_graph_uncached(
|
|
|
177
178
|
|
|
178
179
|
imports = _extract_located_imports(file_path)
|
|
179
180
|
for located_import in imports:
|
|
180
|
-
module_name = located_import.module_name
|
|
181
181
|
import_kind = located_import.import_kind
|
|
182
182
|
if (
|
|
183
183
|
ignore_type_checking_imports
|
|
184
184
|
and import_kind == ImportKind.TYPE_IMPORT
|
|
185
185
|
):
|
|
186
186
|
continue
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if not is_external and resolved not in normalized_py_file_set:
|
|
203
|
-
continue
|
|
204
|
-
|
|
205
|
-
edges.append(
|
|
206
|
-
Edge(
|
|
207
|
-
source=_normalize(file_path),
|
|
208
|
-
target=resolved,
|
|
209
|
-
external=is_external,
|
|
210
|
-
import_kinds=_edge_import_kinds(located_import),
|
|
187
|
+
for resolved, is_external in _resolve_import_targets(
|
|
188
|
+
located_import, file_path, project_path
|
|
189
|
+
):
|
|
190
|
+
if resolved and resolved != _normalize(file_path):
|
|
191
|
+
# Check if the resolved path is in our project
|
|
192
|
+
if not is_external and resolved not in normalized_py_file_set:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
edges.append(
|
|
196
|
+
Edge(
|
|
197
|
+
source=_normalize(file_path),
|
|
198
|
+
target=resolved,
|
|
199
|
+
external=is_external,
|
|
200
|
+
import_kinds=_edge_import_kinds(located_import),
|
|
201
|
+
)
|
|
211
202
|
)
|
|
212
|
-
)
|
|
213
203
|
|
|
214
204
|
return _merge_edges(edges)
|
|
215
205
|
|
|
@@ -342,14 +332,16 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
|
|
|
342
332
|
fallback_module_name = (
|
|
343
333
|
"." * node.level if node.level and node.module is None else None
|
|
344
334
|
)
|
|
335
|
+
aliases = _module_aliases(node) if node.module else ()
|
|
345
336
|
for module_name in _import_from_module_names(node):
|
|
346
337
|
imports.append(
|
|
347
338
|
_LocatedImport(
|
|
348
|
-
module_name,
|
|
349
|
-
kind,
|
|
350
|
-
node.lineno,
|
|
351
|
-
syntax_kind,
|
|
352
|
-
fallback_module_name,
|
|
339
|
+
module_name=module_name,
|
|
340
|
+
import_kind=kind,
|
|
341
|
+
line_number=node.lineno,
|
|
342
|
+
resolution_kind=syntax_kind,
|
|
343
|
+
fallback_module_name=fallback_module_name,
|
|
344
|
+
aliases=aliases,
|
|
353
345
|
)
|
|
354
346
|
)
|
|
355
347
|
|
|
@@ -429,6 +421,11 @@ def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
|
|
|
429
421
|
return []
|
|
430
422
|
|
|
431
423
|
|
|
424
|
+
def _module_aliases(node: ast.ImportFrom) -> tuple[str, ...]:
|
|
425
|
+
"""Return aliases that may refer to imported submodules."""
|
|
426
|
+
return tuple(alias.name for alias in node.names if alias.name != "*")
|
|
427
|
+
|
|
428
|
+
|
|
432
429
|
def _import_from_module_names(node: ast.ImportFrom) -> tuple[str, ...]:
|
|
433
430
|
"""Return resolvable module names for a from-import statement."""
|
|
434
431
|
dots = "." * (node.level or 0)
|
|
@@ -573,6 +570,58 @@ def _resolve_import(
|
|
|
573
570
|
return _resolve_absolute_import(import_name, project_root)
|
|
574
571
|
|
|
575
572
|
|
|
573
|
+
def _resolve_import_targets(
|
|
574
|
+
import_: _LocatedImport,
|
|
575
|
+
source_file: str,
|
|
576
|
+
project_root: str,
|
|
577
|
+
) -> list[tuple[str, bool]]:
|
|
578
|
+
"""Resolve an import, including namespace-package submodule aliases."""
|
|
579
|
+
resolution_kind = import_.resolution_kind or import_.import_kind
|
|
580
|
+
resolved, is_external = _resolve_import(
|
|
581
|
+
import_.module_name,
|
|
582
|
+
source_file,
|
|
583
|
+
project_root,
|
|
584
|
+
resolution_kind,
|
|
585
|
+
)
|
|
586
|
+
if is_external and import_.fallback_module_name is not None:
|
|
587
|
+
fallback, fallback_is_external = _resolve_import(
|
|
588
|
+
import_.fallback_module_name,
|
|
589
|
+
source_file,
|
|
590
|
+
project_root,
|
|
591
|
+
resolution_kind,
|
|
592
|
+
)
|
|
593
|
+
if fallback and not fallback_is_external:
|
|
594
|
+
resolved, is_external = fallback, False
|
|
595
|
+
if not is_external or not import_.aliases:
|
|
596
|
+
return [(resolved, is_external)]
|
|
597
|
+
|
|
598
|
+
alias_targets: list[tuple[str, bool]] = []
|
|
599
|
+
found_internal_alias = False
|
|
600
|
+
for alias in import_.aliases:
|
|
601
|
+
alias_module = _join_import_alias(import_.module_name, alias)
|
|
602
|
+
alias_resolved, alias_is_external = _resolve_import(
|
|
603
|
+
alias_module,
|
|
604
|
+
source_file,
|
|
605
|
+
project_root,
|
|
606
|
+
resolution_kind,
|
|
607
|
+
)
|
|
608
|
+
alias_targets.append((alias_resolved, alias_is_external))
|
|
609
|
+
found_internal_alias = found_internal_alias or not alias_is_external
|
|
610
|
+
|
|
611
|
+
if found_internal_alias:
|
|
612
|
+
return alias_targets
|
|
613
|
+
return [(resolved, is_external)]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _join_import_alias(module_name: str, alias: str) -> str:
|
|
617
|
+
"""Join a from-import module name with a candidate submodule alias."""
|
|
618
|
+
if not module_name:
|
|
619
|
+
return alias
|
|
620
|
+
if set(module_name) == {"."}:
|
|
621
|
+
return f"{module_name}{alias}"
|
|
622
|
+
return f"{module_name}.{alias}"
|
|
623
|
+
|
|
624
|
+
|
|
576
625
|
def _resolve_relative_import(
|
|
577
626
|
import_name: str,
|
|
578
627
|
source_file: str,
|
|
@@ -404,6 +404,162 @@ class TestDynamicImportGraphHandling:
|
|
|
404
404
|
assert ImportKind.DYNAMIC_IMPORT in edges[0].import_kinds
|
|
405
405
|
|
|
406
406
|
|
|
407
|
+
class TestNamespacePackageGraphHandling:
|
|
408
|
+
def setup_method(self):
|
|
409
|
+
clear_graph_cache()
|
|
410
|
+
|
|
411
|
+
def _build_namespace_project(
|
|
412
|
+
self,
|
|
413
|
+
service_source: str,
|
|
414
|
+
*,
|
|
415
|
+
domain_modules: tuple[str, ...] = ("model",),
|
|
416
|
+
) -> str:
|
|
417
|
+
temp_root = Path(__file__).resolve().parent / ".tmp"
|
|
418
|
+
temp_root.mkdir(exist_ok=True)
|
|
419
|
+
project_root = temp_root / f"project_{uuid4().hex}"
|
|
420
|
+
|
|
421
|
+
domain_dir = project_root / "namespace_pkg" / "domain"
|
|
422
|
+
services_dir = project_root / "namespace_pkg" / "services"
|
|
423
|
+
domain_dir.mkdir(parents=True)
|
|
424
|
+
services_dir.mkdir(parents=True)
|
|
425
|
+
|
|
426
|
+
for module in domain_modules:
|
|
427
|
+
(domain_dir / f"{module}.py").write_text(
|
|
428
|
+
"class User:\n pass\n",
|
|
429
|
+
encoding="utf-8",
|
|
430
|
+
)
|
|
431
|
+
(services_dir / "service.py").write_text(service_source, encoding="utf-8")
|
|
432
|
+
|
|
433
|
+
self._temp_dir = project_root
|
|
434
|
+
return str(project_root)
|
|
435
|
+
|
|
436
|
+
def teardown_method(self):
|
|
437
|
+
temp_dir = getattr(self, "_temp_dir", None)
|
|
438
|
+
if temp_dir is not None:
|
|
439
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
440
|
+
|
|
441
|
+
def _service_to_model_edges(self, project_root: str) -> list[Edge]:
|
|
442
|
+
graph = extract_graph(project_root)
|
|
443
|
+
model_path = os.path.abspath(
|
|
444
|
+
os.path.join(project_root, "namespace_pkg", "domain", "model.py")
|
|
445
|
+
).replace("\\", "/")
|
|
446
|
+
service_path = os.path.abspath(
|
|
447
|
+
os.path.join(project_root, "namespace_pkg", "services", "service.py")
|
|
448
|
+
).replace("\\", "/")
|
|
449
|
+
return [
|
|
450
|
+
edge for edge in graph if edge.source == service_path and edge.target == model_path
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
def _service_edges(self, project_root: str) -> list[Edge]:
|
|
454
|
+
graph = extract_graph(project_root)
|
|
455
|
+
service_path = os.path.abspath(
|
|
456
|
+
os.path.join(project_root, "namespace_pkg", "services", "service.py")
|
|
457
|
+
).replace("\\", "/")
|
|
458
|
+
return [
|
|
459
|
+
edge
|
|
460
|
+
for edge in graph
|
|
461
|
+
if edge.source == service_path and edge.target != service_path
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
def test_absolute_from_import_resolves_namespace_package_submodule(self):
|
|
465
|
+
project_root = self._build_namespace_project(
|
|
466
|
+
"from namespace_pkg.domain import model\n"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
edges = self._service_to_model_edges(project_root)
|
|
470
|
+
|
|
471
|
+
assert len(edges) == 1
|
|
472
|
+
assert edges[0].external is False
|
|
473
|
+
assert ImportKind.FROM_IMPORT in edges[0].import_kinds
|
|
474
|
+
|
|
475
|
+
def test_relative_from_import_resolves_namespace_package_submodule(self):
|
|
476
|
+
project_root = self._build_namespace_project("from ..domain import model\n")
|
|
477
|
+
|
|
478
|
+
edges = self._service_to_model_edges(project_root)
|
|
479
|
+
|
|
480
|
+
assert len(edges) == 1
|
|
481
|
+
assert edges[0].external is False
|
|
482
|
+
assert ImportKind.RELATIVE_IMPORT in edges[0].import_kinds
|
|
483
|
+
|
|
484
|
+
def test_mixed_aliases_preserve_internal_and_external_edges(self):
|
|
485
|
+
project_root = self._build_namespace_project(
|
|
486
|
+
"from namespace_pkg.domain import model, remote_model\n"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
edges = self._service_edges(project_root)
|
|
490
|
+
model_path = os.path.abspath(
|
|
491
|
+
os.path.join(project_root, "namespace_pkg", "domain", "model.py")
|
|
492
|
+
).replace("\\", "/")
|
|
493
|
+
|
|
494
|
+
assert any(edge.target == model_path and not edge.external for edge in edges)
|
|
495
|
+
assert any(
|
|
496
|
+
edge.target == "namespace_pkg.domain.remote_model" and edge.external
|
|
497
|
+
for edge in edges
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def test_multiple_internal_aliases_resolve_to_each_submodule(self):
|
|
501
|
+
project_root = self._build_namespace_project(
|
|
502
|
+
"from namespace_pkg.domain import model, audit_model\n",
|
|
503
|
+
domain_modules=("model", "audit_model"),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
internal_targets = {
|
|
507
|
+
edge.target for edge in self._service_edges(project_root) if not edge.external
|
|
508
|
+
}
|
|
509
|
+
expected_targets = {
|
|
510
|
+
os.path.abspath(
|
|
511
|
+
os.path.join(project_root, "namespace_pkg", "domain", f"{module}.py")
|
|
512
|
+
).replace("\\", "/")
|
|
513
|
+
for module in ("model", "audit_model")
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
assert internal_targets == expected_targets
|
|
517
|
+
|
|
518
|
+
def test_all_external_aliases_keep_original_base_edge(self):
|
|
519
|
+
project_root = self._build_namespace_project(
|
|
520
|
+
"from vendor_sdk import Client, Config\n"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
external_targets = {
|
|
524
|
+
edge.target for edge in self._service_edges(project_root) if edge.external
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
assert external_targets == {"vendor_sdk"}
|
|
528
|
+
|
|
529
|
+
def test_as_alias_uses_original_submodule_name(self):
|
|
530
|
+
project_root = self._build_namespace_project(
|
|
531
|
+
"from namespace_pkg.domain import model as domain_model\n"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
assert len(self._service_to_model_edges(project_root)) == 1
|
|
535
|
+
|
|
536
|
+
def test_relative_mixed_aliases_preserve_unresolved_edge(self):
|
|
537
|
+
project_root = self._build_namespace_project(
|
|
538
|
+
"from ..domain import model, remote_model\n"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
edges = self._service_edges(project_root)
|
|
542
|
+
unresolved_path = os.path.abspath(
|
|
543
|
+
os.path.join(project_root, "namespace_pkg", "domain", "remote_model.py")
|
|
544
|
+
).replace("\\", "/")
|
|
545
|
+
|
|
546
|
+
assert len(self._service_to_model_edges(project_root)) == 1
|
|
547
|
+
assert any(
|
|
548
|
+
edge.target == unresolved_path and edge.external for edge in edges
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def test_archignore_suppresses_resolved_namespace_target(self):
|
|
552
|
+
project_root = self._build_namespace_project(
|
|
553
|
+
"from namespace_pkg.domain import model\n"
|
|
554
|
+
)
|
|
555
|
+
Path(project_root, ".archignore").write_text(
|
|
556
|
+
"namespace_pkg/domain/model.py\n",
|
|
557
|
+
encoding="utf-8",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
assert self._service_to_model_edges(project_root) == []
|
|
561
|
+
|
|
562
|
+
|
|
407
563
|
class TestConditionalImportGraphHandling:
|
|
408
564
|
def setup_method(self):
|
|
409
565
|
clear_graph_cache()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/assertion/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/assertion/violation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/extraction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/fluentapi/checkable.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/cycles.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/cycles/model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_cycles.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_edges.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/projection/project_nodes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/common/util/declaration_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/cycle_free.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/depend_on_files.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/assertion/matching_files.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/files/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/layers/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/count.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/distance.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/calculation/lcom.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/extraction/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/export_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/fluentapi/metrics.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/metrics/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/assertion/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/fluentapi/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/projection/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/uml/export_diagram.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/slices/uml/generate_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/common/color_utils.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/common/violation_factory.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/src/archunitpython/testing/pytest_plugin/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/architecture.puml
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/controllers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service_a.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/services/service_b.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/utils/__init__.py
RENAMED
|
File without changes
|
{archunitpython-1.6.1 → archunitpython-1.7.0}/tests/fixtures/sample_project/utils/helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|