Files
Jyotisha/tests/test_runtime_jhora_import_scan.py
T
jesse-uxandGrok (agent loop) c644881a30 feat(report): 完整数据版末尾增加独立年度方法参考
PyJHora 只在子进程里计算请求年及后两年。对话、校正和其余章节保持不导入时的结果。结果指纹不算这一章。

Windows 自测:隐私标记、导入扫描、结果指纹和 S7 退出码 0。阅读页、英文和年运章在此之前已通过。虚构盘 0 中英一起最高 59 秒。

Co-Authored-By: Grok (agent loop)
2026-10-23 20:13:28 +08:00

236 lines
8.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",
"scripts/unified_consultation_orchestrator.py",
"scripts/rectification_three_engine_packet.py",
"scripts/report_render/annual_method_reference.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",
"scripts/unified_consultation_orchestrator.py",
"scripts/rectification_three_engine_packet.py",
"scripts/report_render/annual_method_reference.py",
):
assert required in visited
assert all("pyjhora" not in path and "/research/" not in path for path in visited)
worker = "pyjhora_annual_reference_worker"
for path in visited:
tree = ast.parse((ROOT / path).read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
names = [alias.name for alias in node.names]
elif isinstance(node, ast.ImportFrom):
names = [node.module or ""]
else:
continue
assert all(worker not in name for name in names), path
source = (ROOT / "scripts/report_render/annual_method_reference.py").read_text(encoding="utf-8")
assert f'"-m", {worker.upper()}' not in source
assert '"-m", WORKER_MODULE' in source
assert "pyjhora_annual_reference_worker" in source
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