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.
@@ -1,6 +1,6 @@
1
1
  """ArchUnitPython - Architecture testing library for Python projects."""
2
2
 
3
- __version__ = "1.6.0"
3
+ __version__ = "1.7.0"
4
4
 
5
5
  # Files API
6
6
  # Common
@@ -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
- 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,
194
- file_path,
195
- project_path,
196
- resolution_kind,
197
- )
198
- if fallback and not fallback_is_external:
199
- resolved, is_external = fallback, False
200
- if resolved and resolved != _normalize(file_path):
201
- # Check if the resolved path is in our project
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.6.0
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=CU7R2ZvgfwPhbYLR99lt6Jy6NXLqXpYtf5M91Yo8i3Y,1282
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=SZ3-ZYI1RlIIF3saDoduXXzEHMwmflmhN60oWISeFOM,21608
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.6.0.dist-info/METADATA,sha256=Xi6XRsySw9v7D_T4pyNDQs2qGvZPenNKUMy8nqX4XW4,34913
83
- archunitpython-1.6.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
84
- archunitpython-1.6.0.dist-info/licenses/LICENSE,sha256=kaQWfzfHk45CNIx4sIW7Uf1sNW5rmo6BpZ-R8GruuK0,1102
85
- archunitpython-1.6.0.dist-info/RECORD,,
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,,