"""BUG-1273: chart, report, and consult entries must not import PyJHora. The walk starts at cmd_full_reading, the report builder, and the consult-card entries. It follows AST imports and constant import_module strings. Adapter files whose names contain pyjhora, and scripts/research, stay off the walk. A string that merely mentions ``from jhora`` is not an import. """ from __future__ import annotations import ast from pathlib import Path from scripts.run_quality_gate import CORE_PYTEST_TARGETS ROOT = Path(__file__).resolve().parents[1] ENTRIES = ( "scripts/jyotish_engine.py", "scripts/professional_report_reference.py", "scripts/consultation_workflow_service.py", "scripts/consultation_native_layers.py", ) SELF = "tests/test_runtime_jhora_import_scan.py" def _is_jhora_module(module: str) -> bool: return module == "jhora" or module.startswith("jhora.") def _skip_module_name(module: str) -> bool: leaf = module.rsplit(".", 1)[-1] if "pyjhora" in leaf or leaf.startswith("jhora"): return True return module == "research" or module.startswith(("research.", "scripts.research")) def _resolve_script(root: Path, module: str) -> Path | None: parts = module.split(".") if parts[0] == "scripts": parts = parts[1:] if not parts: return None direct = root.joinpath("scripts", *parts) python_file = direct.with_suffix(".py") if python_file.is_file(): return python_file init = direct / "__init__.py" if init.is_file(): return init return None def _import_module_argument(node: ast.Call) -> str | None: func = node.func if isinstance(func, ast.Name): name = func.id elif isinstance(func, ast.Attribute): name = func.attr else: return None if name != "import_module" or not node.args: return None arg = node.args[0] if isinstance(arg, ast.Constant) and isinstance(arg.value, str): return arg.value return None def _enqueue_relative(path: Path, node: ast.ImportFrom, enqueue) -> None: anchor = path.parent for _ in range(node.level - 1): anchor = anchor.parent candidates = [] if node.module: candidates.append(anchor.joinpath(*node.module.split("."))) else: candidates.extend(anchor / alias.name for alias in node.names) for candidate in candidates: python_file = candidate.with_suffix(".py") if python_file.is_file(): enqueue(python_file) elif (candidate / "__init__.py").is_file(): enqueue(candidate / "__init__.py") def product_jhora_import_hits(root: Path, entries: tuple[str, ...]) -> tuple[list[str], list[str]]: """Return ``path:line:module`` hits and the product files the walk opened.""" scripts = (root / "scripts").resolve() root_resolved = root.resolve() hits: list[str] = [] visited: list[str] = [] seen: set[Path] = set() queue: list[Path] = [] def rel(path: Path) -> str: return path.resolve().relative_to(root_resolved).as_posix() def enqueue(path: Path) -> None: resolved = path.resolve() if resolved in seen: return try: script_rel = resolved.relative_to(scripts).as_posix() except ValueError: return if script_rel == "research" or script_rel.startswith("research/"): return stem = resolved.stem if "pyjhora" in stem or stem.startswith("jhora"): return seen.add(resolved) queue.append(resolved) def consider(module: str, lineno: int, source_rel: str) -> None: if _is_jhora_module(module): hits.append(f"{source_rel}:{lineno}:{module}") return if _skip_module_name(module): return found = _resolve_script(root, module) if found is not None: enqueue(found) for entry in entries: enqueue(root / entry) while queue: path = queue.pop() source_rel = rel(path) visited.append(source_rel) try: tree = ast.parse(path.read_text(encoding="utf-8"), filename=source_rel) except (OSError, SyntaxError) as exc: hits.append(f"{source_rel}:0:{exc.__class__.__name__}") continue for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: consider(alias.name, node.lineno, source_rel) elif isinstance(node, ast.ImportFrom): if node.level: _enqueue_relative(path, node, enqueue) continue if node.module: consider(node.module, node.lineno, source_rel) elif isinstance(node, ast.Call): module = _import_module_argument(node) if module: consider(module, node.lineno, source_rel) return hits, visited def test_product_entries_do_not_import_jhora(): hits, visited = product_jhora_import_hits(ROOT, ENTRIES) assert hits == [] for required in ( "scripts/jyotish_engine.py", "scripts/professional_report_reference.py", "scripts/consultation_workflow_service.py", "scripts/consultation_native_layers.py", "scripts/special_lagnas.py", "scripts/native_special_lagnas.py", "scripts/tajika.py", "scripts/pl9_reader_export.py", ): assert required in visited assert all("pyjhora" not in path and "/research/" not in path for path in visited) def test_scanner_is_on_the_quick_gate(): assert SELF in CORE_PYTEST_TARGETS def test_scanner_flags_jhora_and_skips_adapters_research_and_strings(tmp_path): scripts = tmp_path / "scripts" research = scripts / "research" research.mkdir(parents=True) (scripts / "entry.py").write_text( "import helper\n" "import pyjhora_adapter\n" "from jhora.panchanga import drik\n" "import importlib\n" "importlib.import_module('scripts.clean')\n", encoding="utf-8", ) (scripts / "helper.py").write_text( 'code = "from jhora.panchanga import drik"\n', encoding="utf-8", ) (scripts / "pyjhora_adapter.py").write_text("import jhora\n", encoding="utf-8") (scripts / "clean.py").write_text("value = 1\n", encoding="utf-8") (research / "probe.py").write_text("import jhora\n", encoding="utf-8") (scripts / "caller.py").write_text( "import importlib\nimportlib.import_module('jhora.panchanga.drik')\n", encoding="utf-8", ) hits, visited = product_jhora_import_hits(tmp_path, ("scripts/entry.py", "scripts/caller.py")) assert "scripts/entry.py:3:jhora.panchanga" in hits assert "scripts/caller.py:2:jhora.panchanga.drik" in hits assert not any("helper.py" in hit for hit in hits) assert "scripts/helper.py" in visited assert "scripts/clean.py" in visited assert all("pyjhora_adapter" not in path and "research/" not in path for path in visited) def test_relative_import_of_jhora_string_is_not_required(tmp_path): scripts = tmp_path / "scripts" scripts.mkdir() (scripts / "entry.py").write_text("from . import sibling\n", encoding="utf-8") (scripts / "sibling.py").write_text("ready = True\n", encoding="utf-8") hits, visited = product_jhora_import_hits(tmp_path, ("scripts/entry.py",)) assert hits == [] assert "scripts/sibling.py" in visited