86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""
|
|
Morphology registry — reconstructive root and inflection structure.
|
|
|
|
The registry is deliberately pure schema + stdlib. It does not compile
|
|
versors, import algebra, or participate in propagation. Its job is to make
|
|
LexicalEntry.morphology_id resolve to ordered morphological structure.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from language_packs.schema import MorphologyEntry
|
|
|
|
_DATA_DIR = Path(__file__).parent.parent / "language_packs" / "data"
|
|
|
|
|
|
class MorphologyRegistry:
|
|
"""Immutable morphology lookup table for one language pack."""
|
|
|
|
def __init__(self, entries: list[MorphologyEntry]) -> None:
|
|
self._entries: tuple[MorphologyEntry, ...] = tuple(entries)
|
|
self._by_id: dict[str, MorphologyEntry] = {}
|
|
self._by_surface: dict[str, MorphologyEntry] = {}
|
|
for entry in self._entries:
|
|
if entry.morphology_id in self._by_id:
|
|
raise ValueError(f"Duplicate morphology_id: {entry.morphology_id}")
|
|
self._by_id[entry.morphology_id] = entry
|
|
self._by_surface[entry.surface] = entry
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._entries)
|
|
|
|
@property
|
|
def entries(self) -> tuple[MorphologyEntry, ...]:
|
|
return self._entries
|
|
|
|
def get(self, morphology_id: str) -> MorphologyEntry | None:
|
|
"""Return an entry by morphology_id, or None if absent."""
|
|
return self._by_id.get(morphology_id)
|
|
|
|
def for_surface(self, surface: str) -> MorphologyEntry | None:
|
|
"""Return an entry by surface form, or None if absent."""
|
|
return self._by_surface.get(surface)
|
|
|
|
def require(self, morphology_id: str) -> MorphologyEntry:
|
|
"""Return an entry by morphology_id, raising if the link is broken."""
|
|
entry = self.get(morphology_id)
|
|
if entry is None:
|
|
raise KeyError(f"Morphology id '{morphology_id}' not in registry.")
|
|
return entry
|
|
|
|
|
|
def _parse_entry(payload: dict) -> MorphologyEntry:
|
|
return MorphologyEntry(
|
|
morphology_id=payload["morphology_id"],
|
|
surface=payload["surface"],
|
|
lemma=payload["lemma"],
|
|
language=payload["language"],
|
|
root=payload.get("root"),
|
|
prefix_chain=tuple(payload.get("prefix_chain", [])),
|
|
stem=payload.get("stem"),
|
|
inflection=dict(payload.get("inflection", {})),
|
|
suffix_chain=tuple(payload.get("suffix_chain", [])),
|
|
)
|
|
|
|
|
|
def load_morphology(pack_id: str) -> MorphologyRegistry:
|
|
"""
|
|
Load MorphologyEntry records from language_packs/data/<pack_id>/morphology.jsonl.
|
|
|
|
Packs without morphology data return an empty registry; packs that set
|
|
LexicalEntry.morphology_id are validated by the compiler against this
|
|
registry.
|
|
"""
|
|
morphology_path = _DATA_DIR / pack_id / "morphology.jsonl"
|
|
if not morphology_path.exists():
|
|
return MorphologyRegistry([])
|
|
|
|
entries: list[MorphologyEntry] = []
|
|
for line in morphology_path.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if line:
|
|
entries.append(_parse_entry(json.loads(line)))
|
|
return MorphologyRegistry(entries)
|