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.
180 lines
6.9 KiB
Python
180 lines
6.9 KiB
Python
"""BUG-1273: eight birth special lagnas are native, and rectification keeps the old formulas.
|
||
|
||
The frozen PyJHora 4.8.7 answers live in tests/fixtures/special_lagnas_pyjhora_487.json.
|
||
These tests do not import jhora. The offline generator is
|
||
scripts/research/freeze_special_lagnas_pyjhora_fixture.py.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import builtins
|
||
import importlib
|
||
import json
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
import scripts.ayanamsa_utils as ayanamsa_utils
|
||
from scripts.ayanamsa_utils import apply_ayanamsa
|
||
from scripts.special_lagnas import SpecialLagnasCalculator, bhava_house_rows
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
FIXTURE = ROOT / "tests" / "fixtures" / "special_lagnas_pyjhora_487.json"
|
||
EIGHT = (
|
||
"Bhava_Lagna",
|
||
"Hora_Lagna",
|
||
"Ghati_Lagna",
|
||
"ViGhati_Lagna",
|
||
"Sree_Lagna",
|
||
"Indu_Lagna",
|
||
"Pranapada_Lagna",
|
||
"Varnada_Lagna",
|
||
)
|
||
CASE_IDS = ("steve_jobs_1955_aa", "barack_obama_1961_aa", "fictional_reader_main")
|
||
|
||
|
||
def _fixture():
|
||
payload = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||
assert payload["pyjhora_version"] == "4.8.7"
|
||
return payload
|
||
|
||
|
||
def _block_jhora(monkeypatch):
|
||
"""Fail closed if the runtime path imports PyJHora, including a cached package."""
|
||
real_import = builtins.__import__
|
||
|
||
def guarded(name, globals=None, locals=None, fromlist=(), level=0):
|
||
if str(name).split(".", 1)[0] == "jhora":
|
||
raise ImportError("jhora blocked")
|
||
return real_import(name, globals, locals, fromlist, level)
|
||
|
||
monkeypatch.setattr(builtins, "__import__", guarded)
|
||
real_import_module = importlib.import_module
|
||
|
||
def guarded_module(name, package=None):
|
||
if str(name).split(".", 1)[0] == "jhora":
|
||
raise ImportError("jhora blocked")
|
||
return real_import_module(name, package)
|
||
|
||
monkeypatch.setattr(importlib, "import_module", guarded_module)
|
||
for key in list(sys.modules):
|
||
if key == "jhora" or key.startswith("jhora."):
|
||
monkeypatch.delitem(sys.modules, key, raising=False)
|
||
|
||
|
||
def _rows(birth, ayanamsa):
|
||
moment = datetime(
|
||
birth["year"], birth["month"], birth["day"],
|
||
birth["hour"], birth["minute"], birth["second"],
|
||
)
|
||
return SpecialLagnasCalculator().calculate_all_lagnas(
|
||
0.0, 0.0, 0.0, moment, moment.replace(hour=6),
|
||
lat=birth["lat"], lon=birth["lon"], tz_offset=birth["tz"], ayanamsa=ayanamsa,
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("case_id", CASE_IDS)
|
||
@pytest.mark.parametrize("ayanamsa", ["lahiri", "raman"])
|
||
def test_native_matches_frozen_pyjhora_within_one_hundredth(case_id, ayanamsa, monkeypatch):
|
||
_block_jhora(monkeypatch)
|
||
spec = _fixture()["cases"][case_id]
|
||
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||
rows = _rows(spec["birth"], ayanamsa)
|
||
expected = spec["ayanamsa"][ayanamsa]
|
||
for key in EIGHT:
|
||
row = rows[key]
|
||
assert row["status"] == "computed"
|
||
assert row["source"] == "native_special_lagna"
|
||
assert "pyjhora" not in row["source"]
|
||
assert "pyjhora" not in json.dumps(row).lower()
|
||
assert row["sign"] == expected[key]["sign"]
|
||
assert row["sign_degree"] == pytest.approx(expected[key]["sign_degree"], abs=0.01)
|
||
assert row["degree"] == pytest.approx(expected[key]["longitude"], abs=0.01)
|
||
assert row["ayanamsa"] == ayanamsa
|
||
assert abs(rows["Hora_Lagna"]["degree"] - rows["Ghati_Lagna"]["degree"]) > 0.01
|
||
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||
|
||
|
||
def test_jobs_lahiri_signs_stay_on_the_birth_instant(monkeypatch):
|
||
_block_jhora(monkeypatch)
|
||
apply_ayanamsa("raman")
|
||
before = ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
|
||
rows = _rows(birth, "lahiri")
|
||
expected = {
|
||
"Bhava_Lagna": ("Leo", 17.12),
|
||
"Hora_Lagna": ("Aquarius", 22.58),
|
||
"Ghati_Lagna": ("Virgo", 8.94),
|
||
}
|
||
for key, (sign, degree) in expected.items():
|
||
assert rows[key]["sign"] == sign
|
||
assert rows[key]["sign_degree"] == pytest.approx(degree, abs=0.01)
|
||
assert rows["Hora_Lagna"]["sign"] != rows["Ghati_Lagna"]["sign"]
|
||
assert before == ayanamsa_utils.ACTIVE_AYANAMSA_NAME
|
||
|
||
|
||
def test_rectification_hora_and_ghati_formulas_stay_on_the_old_identity():
|
||
birth = datetime(2000, 1, 1, 19, 15, 0)
|
||
sunrise = birth.replace(hour=6, minute=0, second=0)
|
||
calculator = SpecialLagnasCalculator()
|
||
hora = calculator.calculate_hora_lagna(10.0, 0.0, birth, sunrise)
|
||
ghati = calculator.calculate_ghati_lagna(10.0, birth, sunrise)
|
||
assert hora["formula"] == "Asc + (Hora数 × 15°)"
|
||
assert ghati["formula"] == "Asc + (Ghati数 × 6°)"
|
||
assert hora["degree"] == pytest.approx(208.75, abs=1e-4)
|
||
assert ghati["degree"] == pytest.approx(208.75, abs=1e-4)
|
||
assert not hasattr(calculator, "calculate_pyjhora_special_lagnas")
|
||
|
||
|
||
def test_missing_coordinates_keep_old_three_and_block_the_other_five():
|
||
moment = datetime(2000, 1, 1, 19, 15, 0)
|
||
sunrise = moment.replace(hour=6, minute=0, second=0)
|
||
rows = SpecialLagnasCalculator().calculate_all_lagnas(
|
||
10.0, 20.0, 30.0, moment, sunrise,
|
||
)
|
||
assert rows["Bhava_Lagna"]["formula"] == "Asc + (Sun - Moon)"
|
||
assert rows["Hora_Lagna"]["formula"] == "Asc + (Hora数 × 15°)"
|
||
assert rows["Hora_Lagna"]["degree"] == pytest.approx(208.75, abs=1e-4)
|
||
assert rows["Ghati_Lagna"]["formula"] == "Asc + (Ghati数 × 6°)"
|
||
for key in ("ViGhati_Lagna", "Sree_Lagna", "Indu_Lagna", "Pranapada_Lagna", "Varnada_Lagna"):
|
||
assert rows[key]["status"] == "blocked"
|
||
assert rows[key]["reason"] == "birth_coordinates_or_timezone_missing"
|
||
assert rows[key]["source"] == "native_special_lagna"
|
||
assert "pyjhora" not in rows[key]["source"]
|
||
|
||
|
||
def test_full_reading_eight_points_with_jhora_blocked(monkeypatch):
|
||
from scripts.jyotish_engine import cmd_full_reading
|
||
from scripts.professional_report_reference import _export_args
|
||
|
||
_block_jhora(monkeypatch)
|
||
for key in list(sys.modules):
|
||
if key in {
|
||
"special_lagnas",
|
||
"scripts.special_lagnas",
|
||
"native_special_lagnas",
|
||
"scripts.native_special_lagnas",
|
||
} or key == "jhora" or key.startswith("jhora."):
|
||
monkeypatch.delitem(sys.modules, key, raising=False)
|
||
birth = _fixture()["cases"]["steve_jobs_1955_aa"]["birth"]
|
||
report = cmd_full_reading(_export_args({**birth, "ayanamsa": "lahiri", "node_mode": "mean"}))
|
||
points = report["modules"]["special_lagnas"]
|
||
for key in EIGHT:
|
||
row = points[key]
|
||
assert row["status"] == "computed", key
|
||
assert row["source"] == "native_special_lagna"
|
||
assert "pyjhora" not in json.dumps(row).lower()
|
||
|
||
|
||
def test_bhava_house_rows_drop_summary_rows():
|
||
rows = bhava_house_rows({
|
||
"houses": [
|
||
{"house": 1, "sign": "Aries"},
|
||
{"house": "total", "sign": "summary"},
|
||
{"house": 12, "sign": "Pisces"},
|
||
]
|
||
})
|
||
assert [row["house"] for row in rows] == [1, 12]
|