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.
215 lines
7.4 KiB
Python
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
|