Files
Jyotisha/tests/test_runtime_jhora_import_scan.py
T
jesse-ux 795b8845c7
Independent Staging Quality Gate / validate (push) Successful in 19m4s
Independent Staging Quality Gate / publish (push) Successful in 3m51s
fix(special-lagnas): compute eight birth points without PyJHora
Production images do not install PyJHora, so those eight points were blocked.
Swiss Ephemeris now computes them in-process. Rectification keeps the old
Hora and Ghati formulas.

BUG-1273.
2026-10-08 15:01:44 +08:00

215 lines
7.4 KiB
Python

"""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