Skip to content

Commit 0ae22bb

Browse files
committed
fix: preserve relative conditional import targets
1 parent d3dc126 commit 0ae22bb

4 files changed

Lines changed: 300 additions & 33 deletions

File tree

‎BACKLOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ This backlog collects product and maintenance ideas from project research.
2020

2121
- Add support for namespace packages that do not contain `__init__.py`.
2222
- Detect dynamic imports such as `importlib.import_module()` and `__import__()`.
23-
- Detect conditional imports such as `try/except ImportError`.
23+
- [x] Detect conditional imports such as `try/except ImportError`.
2424
- Add better `TYPE_CHECKING` import handling, including options to ignore, include, or report type-only imports separately.
2525
- Improve external dependency rules so users can express allowed and forbidden third-party packages at module or slice level.
2626
- Consider a public-interface rule inspired by Tach, where modules may only import through declared package APIs.

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ from my_app.adapters.sql import Repository # archunit: ignore
350350
Imports inside `try` blocks that handle `ImportError` or
351351
`ModuleNotFoundError` are marked as conditional dependencies. This helps graph
352352
reports distinguish optional imports and fallback implementations from regular
353-
runtime imports.
353+
runtime imports. Conditional dependencies remain part of architecture checks;
354+
relative and dynamic imports also retain their original import kind in graph
355+
reports.
354356

355357
### Naming Conventions
356358

‎src/archunitpython/common/extraction/extract_graph.py‎

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class _LocatedImport:
4343
module_name: str
4444
import_kind: ImportKind
4545
line_number: int
46+
resolution_kind: ImportKind | None = None
4647

4748

4849
@dataclass(frozen=True)
@@ -182,8 +183,9 @@ def _extract_graph_uncached(
182183
and import_kind == ImportKind.TYPE_IMPORT
183184
):
184185
continue
186+
resolution_kind = located_import.resolution_kind or import_kind
185187
resolved, is_external = _resolve_import(
186-
module_name, file_path, project_path, import_kind
188+
module_name, file_path, project_path, resolution_kind
187189
)
188190
if resolved and resolved != _normalize(file_path):
189191
# Check if the resolved path is in our project
@@ -195,7 +197,7 @@ def _extract_graph_uncached(
195197
source=_normalize(file_path),
196198
target=resolved,
197199
external=is_external,
198-
import_kinds=(import_kind,),
200+
import_kinds=_edge_import_kinds(located_import),
199201
)
200202
)
201203

@@ -303,46 +305,52 @@ def _extract_located_imports(file_path: str) -> list[_LocatedImport]:
303305

304306
for node in ast.walk(tree):
305307
if isinstance(node, ast.Import):
308+
syntax_kind = ImportKind.IMPORT
306309
kind = _classify_import(
307310
node,
308-
ImportKind.IMPORT,
311+
syntax_kind,
309312
type_checking_ranges,
310313
conditional_import_ranges,
311314
)
312315
for alias in node.names:
313-
imports.append(_LocatedImport(alias.name, kind, node.lineno))
316+
imports.append(
317+
_LocatedImport(alias.name, kind, node.lineno, syntax_kind)
318+
)
314319

315320
elif isinstance(node, ast.ImportFrom):
316-
if node.level and node.level > 0:
317-
# Relative import
318-
kind = _classify_import(
319-
node,
320-
ImportKind.RELATIVE_IMPORT,
321-
type_checking_ranges,
322-
conditional_import_ranges,
323-
)
324-
module = node.module or ""
325-
dots = "." * node.level
326-
imports.append(_LocatedImport(f"{dots}{module}", kind, node.lineno))
327-
else:
328-
kind = _classify_import(
329-
node,
330-
ImportKind.FROM_IMPORT,
331-
type_checking_ranges,
332-
conditional_import_ranges,
321+
syntax_kind = (
322+
ImportKind.RELATIVE_IMPORT
323+
if node.level and node.level > 0
324+
else ImportKind.FROM_IMPORT
325+
)
326+
kind = _classify_import(
327+
node,
328+
syntax_kind,
329+
type_checking_ranges,
330+
conditional_import_ranges,
331+
)
332+
for module_name in _import_from_module_names(node):
333+
imports.append(
334+
_LocatedImport(
335+
module_name,
336+
kind,
337+
node.lineno,
338+
syntax_kind,
339+
)
333340
)
334-
if node.module:
335-
imports.append(_LocatedImport(node.module, kind, node.lineno))
336341

337342
elif isinstance(node, ast.Call):
343+
syntax_kind = ImportKind.DYNAMIC_IMPORT
338344
kind = _classify_import(
339345
node,
340-
ImportKind.DYNAMIC_IMPORT,
346+
syntax_kind,
341347
type_checking_ranges,
342348
conditional_import_ranges,
343349
)
344350
for module_name in _extract_dynamic_import_names(node):
345-
imports.append(_LocatedImport(module_name, kind, node.lineno))
351+
imports.append(
352+
_LocatedImport(module_name, kind, node.lineno, syntax_kind)
353+
)
346354

347355
return [
348356
import_
@@ -407,6 +415,29 @@ def _extract_dynamic_import_names(node: ast.Call) -> list[str]:
407415
return []
408416

409417

418+
def _import_from_module_names(node: ast.ImportFrom) -> tuple[str, ...]:
419+
"""Return resolvable module names for a from-import statement."""
420+
dots = "." * (node.level or 0)
421+
if node.module:
422+
return (f"{dots}{node.module}",)
423+
424+
aliases = tuple(alias.name for alias in node.names if alias.name != "*")
425+
if dots and aliases:
426+
return tuple(f"{dots}{alias}" for alias in aliases)
427+
return (dots,) if dots else ()
428+
429+
430+
def _edge_import_kinds(import_: _LocatedImport) -> tuple[ImportKind, ...]:
431+
"""Return graph labels without losing syntax for conditional imports."""
432+
resolution_kind = import_.resolution_kind or import_.import_kind
433+
if (
434+
import_.import_kind == ImportKind.CONDITIONAL_IMPORT
435+
and resolution_kind != import_.import_kind
436+
):
437+
return (resolution_kind, import_.import_kind)
438+
return (import_.import_kind,)
439+
440+
410441
def _classify_import(
411442
node: ast.AST,
412443
default_kind: ImportKind,

0 commit comments

Comments
 (0)