archunitpython 1.6.0__py3-none-any.whl → 1.7.0__py3-none-any.whl
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/__init__.py +1 -1
- archunitpython/common/extraction/extract_graph.py +80 -31
- {archunitpython-1.6.0.dist-info → archunitpython-1.7.0.dist-info}/METADATA +7 -1
- {archunitpython-1.6.0.dist-info → archunitpython-1.7.0.dist-info}/RECORD +6 -6
- {archunitpython-1.6.0.dist-info → archunitpython-1.7.0.dist-info}/WHEEL +0 -0
- {archunitpython-1.6.0.dist-info → archunitpython-1.7.0.dist-info}/licenses/LICENSE +0 -0
archunitpython/__init__.py
CHANGED
|
@@ -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,
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
archunitpython/__init__.py,sha256=
|
|
1
|
+
archunitpython/__init__.py,sha256=2GNl1l7I8BSz7KehGw8thDEMYr09vjFDgfcnVXxaK9I,1282
|
|
2
2
|
archunitpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
archunitpython/common/__init__.py,sha256=TKL39Z0kBpWqMH99jU4LsDSUZ5lQ_rcAJfLnmUTDTOI,656
|
|
4
4
|
archunitpython/common/pattern_matching.py,sha256=HMAfo8GsooHvA4d2IxCi3btrYTdwfOQXw4bDNr117P0,2669
|
|
@@ -9,7 +9,7 @@ archunitpython/common/assertion/violation.py,sha256=TnMOykN3kPoGrSf1KcxBXYRfaxfX
|
|
|
9
9
|
archunitpython/common/error/__init__.py,sha256=UWcdIKpGAJvo4WEGOVSaUjQQnKoPi8R496uDGXBWKsE,116
|
|
10
10
|
archunitpython/common/error/errors.py,sha256=y7mcXoZPyK7uD2dMMO1qBt1y7KcIeGlRYiVeI6bjms4,249
|
|
11
11
|
archunitpython/common/extraction/__init__.py,sha256=RkJOcxJoLYQxEagh3uWhjsUCprK3Nr3Bn3o7nmzk4_Q,284
|
|
12
|
-
archunitpython/common/extraction/extract_graph.py,sha256=
|
|
12
|
+
archunitpython/common/extraction/extract_graph.py,sha256=wxLpOtEIOjpXTUU0LXAcwsGiup9sh2W3NgjykuYX-gI,23268
|
|
13
13
|
archunitpython/common/extraction/graph.py,sha256=s4QW0WYfwjqXteBNygaNBdTgSC1XKil8cBFLtxl5rJE,1124
|
|
14
14
|
archunitpython/common/fluentapi/__init__.py,sha256=LeS7qS2p9-FqqBhDL8xRPex__W5Qk0UWfcpl7nVVLfI,178
|
|
15
15
|
archunitpython/common/fluentapi/checkable.py,sha256=BJFYibVhHLMFlnWdISPCIL3DYI1_3JN2-1jEQOaVaGE,1547
|
|
@@ -79,7 +79,7 @@ archunitpython/testing/common/__init__.py,sha256=Wc4bC-N4t6giChKjj6wuTGKkb39thcY
|
|
|
79
79
|
archunitpython/testing/common/color_utils.py,sha256=2I8Z1SZfWhhgudMgmXY6PPydGxGl35cK_To-GXnSyJg,1226
|
|
80
80
|
archunitpython/testing/common/violation_factory.py,sha256=yWlvv2U7u-7KoTMfp1QCeJXxTLpPEagKqDHZmPSyrlk,4677
|
|
81
81
|
archunitpython/testing/pytest_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
|
-
archunitpython-1.
|
|
83
|
-
archunitpython-1.
|
|
84
|
-
archunitpython-1.
|
|
85
|
-
archunitpython-1.
|
|
82
|
+
archunitpython-1.7.0.dist-info/METADATA,sha256=KSwZ_XskzaMaiY0ZK1VxbfSiadBtKyq0u5ycdyZ6PZ4,35168
|
|
83
|
+
archunitpython-1.7.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
84
|
+
archunitpython-1.7.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
|
|
85
|
+
archunitpython-1.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|