Files
typikon/tests/python/test_typikon.py
T
funkpower 5d7e02cffb answer from Python too
The engine is the same; only the front door is new. pyo3 behind an optional
feature, maturin for the wheel, and the bindings mirror DayJson rather than Day
because DayJson is already the shape front ends read and the one that has to stay
still. to_dict() therefore gives exactly what --json gives, and a test holds the
two to that.

That test earned its place immediately. The bindings were calling
jurisdictions::resolve(None), which answers with a built-in default that knows
nothing about the machine, so Python was reckoning by the New Calendar while the
command line on the same box reckoned by ROCOR — every field downstream of the
calendar differed, including the fast. They now take the same two steps main.rs
takes, and agree field for field on Pascha, the Nativity and Theophany.

The wheel is abi3, so one build serves 3.9 upward rather than one per
interpreter. That costs the datetime C API, which the limited API does not
expose, so a date is read through its attributes instead — which incidentally
makes datetime.datetime work as well as datetime.date. Constructing dates is
unaffected, pyo3 calls Python's own constructor for that.

panic = "abort" comes out of the release profile. An extension module has to be
able to unwind a panic into a Python exception; aborting would take the
interpreter down with it. The CLI keeps the same behaviour it always had, a
panic there just unwinds now.

Mixed layout so the wheel can carry py.typed and a stub file, which a bare
extension module has nowhere to put. mypy --strict is clean against the stubs
and catches a deliberate error, so the Typing :: Typed classifier is not a claim
about nothing.
2026-09-18 11:28:08 -04:00

115 lines
4.0 KiB
Python

"""Tests for the Python bindings.
The one that matters is `test_matches_cli`: the bindings and the binary have to
answer identically for the same date, because they are the same engine and any
divergence means the Python layer has quietly grown its own opinion. It caught a
real one — the bindings were resolving a built-in default jurisdiction while the
CLI read the configured one, so every field downstream of that differed.
python -m pytest tests/python -q
Needs the wheel installed (`maturin develop` or `pip install target/wheels/*.whl`)
and, for the parity test, `cargo build --release`.
"""
import datetime
import json
import re
import subprocess
from pathlib import Path
import pytest
typikon = pytest.importorskip("typikon")
BINARY = Path(__file__).resolve().parents[2] / "target" / "release" / "typikon"
# Pascha, Nativity, Theophany and an ordinary weekday: the movable cycle, the
# fixed calendar, the julian offset and the plain case.
DATES = [
datetime.date(2026, 4, 12),
datetime.date(2026, 12, 25),
datetime.date(2027, 1, 7),
datetime.date(2026, 3, 18),
]
def _snake(s: str) -> str:
return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
@pytest.mark.parametrize("date", DATES, ids=str)
def test_matches_cli(date: datetime.date) -> None:
if not BINARY.exists():
pytest.skip(f"{BINARY} not built")
out = subprocess.run(
[str(BINARY), "--json", "--date", date.isoformat()],
capture_output=True, text=True, check=True,
).stdout
cli = {_snake(k): v for k, v in json.loads(out).items()}
assert typikon.describe(date).to_dict() == cli
def test_pascha_is_its_own_zero() -> None:
for year in (2024, 2025, 2026, 2030):
p = typikon.pascha(year)
assert typikon.describe(p).days_to_pascha == 0
assert p.year == year
def test_week_length_and_order() -> None:
start = datetime.date(2026, 4, 10)
days = typikon.week(start, 5)
assert len(days) == 5
assert [d.date for d in days] == [
start + datetime.timedelta(days=i) for i in range(5)
]
def test_week_rejects_nonsense() -> None:
with pytest.raises(ValueError):
typikon.week(datetime.date(2026, 4, 10), 0)
def test_unknown_jurisdiction_raises() -> None:
with pytest.raises(ValueError, match="unknown jurisdiction"):
typikon.describe(datetime.date(2026, 4, 12), jurisdiction="narnia")
def test_jurisdiction_actually_changes_the_answer() -> None:
# Old and new calendar disagree about which church date a civil date is, so
# on 25 December the new calendar keeps the Nativity and the old calendar is
# still thirteen days short of it. julian_offset is not the witness to reach
# for: it is the gap between the calendars, so it reads 13 either way.
old = typikon.describe(datetime.date(2026, 12, 25), jurisdiction="old")
new = typikon.describe(datetime.date(2026, 12, 25), jurisdiction="new")
assert old.church_date != new.church_date
assert old.title != new.title
assert "Nativity" in (new.feast or "")
def test_every_jurisdiction_resolves() -> None:
keys = [j["key"] for j in typikon.jurisdictions()]
assert keys, "no jurisdictions listed"
for key in keys:
assert typikon.describe(datetime.date(2026, 4, 12), jurisdiction=key)
def test_day_is_immutable() -> None:
day = typikon.describe(datetime.date(2026, 4, 12))
with pytest.raises(AttributeError):
day.feast = "something else" # type: ignore[misc]
def test_accepts_datetime_as_well_as_date() -> None:
# to_date reads attributes rather than the datetime C API, so anything with
# year/month/day works. Worth pinning: it is a consequence of the abi3 build,
# not a decision, and it would be easy to lose by accident.
d = datetime.date(2026, 4, 12)
assert typikon.describe(datetime.datetime(2026, 4, 12, 13, 30)).to_dict() == \
typikon.describe(d).to_dict()
def test_to_dict_is_json_serialisable() -> None:
assert json.dumps(typikon.describe(datetime.date(2026, 4, 12)).to_dict())