"""Lean-build safety: core must not hard-import a plugin (ADR-013 Phase 5). For a per-site build that omits a plugin, any core `from plugins. import` outside a try/except would crash the whole app when that plugin is absent. This statically asserts every such import in shopdb/core and shopdb/cli is guarded. """ import ast import os CORE_ROOTS = ['shopdb/core', 'shopdb/cli'] def _imports_plugin(node): if isinstance(node, ast.ImportFrom): return bool(node.module and node.module.startswith('plugins.')) if isinstance(node, ast.Import): return any(alias.name.startswith('plugins.') for alias in node.names) return False def _unguarded_in(path): found = [] tree = ast.parse(open(path).read()) class Visitor(ast.NodeVisitor): def __init__(self): self.try_depth = 0 def visit_Try(self, node): self.try_depth += 1 for child in node.body: self.visit(child) self.try_depth -= 1 for handler in node.handlers: for child in handler.body: self.visit(child) for child in node.orelse + node.finalbody: self.visit(child) def generic_visit(self, node): if _imports_plugin(node) and self.try_depth == 0: found.append(f"{path}:{node.lineno}") super().generic_visit(node) Visitor().visit(tree) return found def test_no_unguarded_plugin_imports_in_core(): unguarded = [] for root in CORE_ROOTS: for dirpath, _, filenames in os.walk(root): for name in filenames: if name.endswith('.py'): unguarded += _unguarded_in(os.path.join(dirpath, name)) assert unguarded == [], ( "Core imports a plugin without a try/except ImportError guard; a lean " "build omitting that plugin would crash. Wrap in try/except:\n " + "\n ".join(unguarded))