Source code for hs_py.ontology.namespace

"""Ontology namespace for resolved defs.

The :class:`Namespace` indexes defs from one or more libs, providing
symbol resolution and taxonomy queries (subtype, supertype, transitive
subtype checking).

See: https://project-haystack.org/doc/docHaystack/Namespaces
"""

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any

from hs_py.encoding.trio import parse_trio
from hs_py.kinds import Symbol, sym_name
from hs_py.ontology.defs import Def, Lib

if TYPE_CHECKING:
    from collections.abc import Iterator

__all__ = [
    "Namespace",
    "load_defs_from_trio",
    "load_lib_from_trio",
]


[docs] class Namespace: """Container for resolved ontology definitions. Indexes defs by both qualified (``ph::site``) and unqualified (``site``) symbol names. Provides taxonomy queries over the ``is`` tag hierarchy. :param libs: List of :class:`Lib` instances to include. """ def __init__(self, libs: list[Lib] | None = None) -> None: """Initialise the namespace, optionally loading initial libs. :param libs: :class:`~hs_py.ontology.defs.Lib` instances to include. """ self._libs: list[Lib] = [] self._by_name: dict[str, Def] = {} self._subtypes: dict[str, list[str]] = {} # Conjunct index: frozenset of parts → list of Defs self._conjunct_index: dict[frozenset[str], list[Def]] = {} # Caches (invalidated on add_lib) self._all_defs_cache: list[Def] | None = None self._supertypes_cache: dict[str, list[Def]] = {} if libs: for lib in libs: self.add_lib(lib)
[docs] def add_lib(self, lib: Lib) -> None: """Add a lib and its defs to the namespace. :param lib: Library to add. """ self._libs.append(lib) for d in lib.defs: # Index by qualified name self._by_name[d.symbol.val] = d # Index by unqualified name (first wins) if d.name not in self._by_name: self._by_name[d.name] = d # Rebuild indexes and invalidate caches self._rebuild_subtypes() self._all_defs_cache = None self._supertypes_cache.clear() self._rebuild_conjunct_index()
def _rebuild_subtypes(self) -> None: self._subtypes.clear() for d in self._by_name.values(): for parent_sym in d.is_list: parent = parent_sym.val subs = self._subtypes.setdefault(parent, []) if d.symbol.val not in subs: subs.append(d.symbol.val) def _rebuild_conjunct_index(self) -> None: """Build an index from frozenset-of-parts → list of conjunct Defs.""" from hs_py.ontology.taxonomy import is_conjunct, resolve_conjunct_parts self._conjunct_index.clear() for d in self._get_all_defs(): if not is_conjunct(d.symbol.val): continue parts = frozenset(resolve_conjunct_parts(d.symbol.val)) self._conjunct_index.setdefault(parts, []).append(d)
[docs] def find_conjuncts(self, marker_names: set[str]) -> list[Def]: """Return conjunct defs whose parts are all present in *marker_names*. Uses a pre-built index for efficient lookup. :param marker_names: Set of marker tag names present on an entity. :returns: List of matching conjunct defs. """ result: list[Def] = [] for parts, defs in self._conjunct_index.items(): if parts <= marker_names: for d in defs: if d.symbol.val not in marker_names: result.append(d) return result
# ---- Lookup -------------------------------------------------------------
[docs] def get(self, symbol: str | Symbol) -> Def | None: """Look up a def by symbol name. Accepts both qualified (``ph::site``) and unqualified (``site``) names. :param symbol: Qualified or unqualified symbol name. :returns: The matching :class:`~hs_py.ontology.defs.Def`, or ``None``. """ return self._by_name.get(sym_name(symbol))
[docs] def has(self, symbol: str | Symbol) -> bool: """Check whether a def with *symbol* exists in the namespace. :param symbol: Qualified or unqualified symbol name. :returns: ``True`` if the def exists. """ return self.get(symbol) is not None
# ---- Iteration ----------------------------------------------------------
[docs] def all_defs(self) -> Iterator[Def]: """Iterate all unique defs (deduplicated by qualified name).""" return iter(self._get_all_defs())
def _get_all_defs(self) -> list[Def]: """Return cached list of unique defs.""" if self._all_defs_cache is None: seen: set[str] = set() result: list[Def] = [] for d in self._by_name.values(): key = d.symbol.val if key not in seen: seen.add(key) result.append(d) self._all_defs_cache = result return self._all_defs_cache
[docs] def all_libs(self) -> Iterator[Lib]: """Iterate all libs.""" return iter(self._libs)
@property def def_count(self) -> int: """Number of unique defs.""" return len(self._get_all_defs()) # ---- Taxonomy -----------------------------------------------------------
[docs] def subtypes(self, symbol: str | Symbol) -> list[Def]: """Return direct subtypes of a def. :param symbol: Def symbol to look up. :returns: List of :class:`~hs_py.ontology.defs.Def` instances. """ name = sym_name(symbol) sub_names = self._subtypes.get(name, []) result: list[Def] = [] for sn in sub_names: d = self._by_name.get(sn) if d is not None: result.append(d) return result
[docs] def supertypes(self, symbol: str | Symbol) -> list[Def]: """Return direct supertypes of a def (from its ``is`` tag). :param symbol: Def symbol to look up. :returns: List of parent :class:`~hs_py.ontology.defs.Def` instances. """ d = self.get(symbol) if d is None: return [] result: list[Def] = [] for parent_sym in d.is_list: parent = self.get(parent_sym.val) if parent is not None: result.append(parent) return result
[docs] def is_subtype(self, sub: str | Symbol, sup: str | Symbol) -> bool: """Check whether *sub* is a transitive subtype of *sup*. Also returns ``True`` if *sub* equals *sup*. :param sub: Candidate subtype symbol. :param sup: Candidate supertype symbol. """ sub_name = sym_name(sub) sup_name = sym_name(sup) if sub_name == sup_name: return True visited: set[str] = set() queue = deque([sub_name]) while queue: current = queue.popleft() if current in visited: continue visited.add(current) d = self._by_name.get(current) if d is None: continue for parent_sym in d.is_list: pname = parent_sym.val if pname == sup_name: return True queue.append(pname) return False
[docs] def all_supertypes(self, symbol: str | Symbol) -> list[Def]: """Return all transitive supertypes of a def (cached). :param symbol: Def symbol to look up. :returns: All ancestor :class:`~hs_py.ontology.defs.Def` instances. """ name = sym_name(symbol) cached = self._supertypes_cache.get(name) if cached is not None: return cached result: list[Def] = [] visited: set[str] = set() queue = deque([name]) while queue: current = queue.popleft() if current in visited: continue visited.add(current) d = self._by_name.get(current) if d is None: continue for parent_sym in d.is_list: pname = parent_sym.val parent = self._by_name.get(pname) if parent is not None and pname not in visited: result.append(parent) queue.append(pname) self._supertypes_cache[name] = result return result
[docs] def load_defs_from_trio(text: str) -> list[Def]: """Parse Trio text and return :class:`~hs_py.ontology.defs.Def` objects for each record with a ``def`` tag. :param text: Trio-formatted text. :returns: List of parsed defs. """ defs: list[Def] = [] for tags in parse_trio(text): if "def" in tags: defs.append(Def.from_tags(tags)) return defs
[docs] def load_lib_from_trio( lib_trio: str, def_trios: list[str] | None = None, ) -> Lib: """Load a Lib from lib.trio metadata and optional def trio strings. :param lib_trio: Trio text for the lib metadata (must have one record with ``def``). :param def_trios: Optional list of Trio text strings for def records. :returns: Fully constructed Lib. """ records = parse_trio(lib_trio) lib_meta: dict[str, Any] = {} defs: list[Def] = [] for rec in records: if "def" in rec: sym = rec["def"] if isinstance(sym, Symbol) and sym.val.startswith("lib:"): lib_meta = rec else: defs.append(Def.from_tags(rec)) if not lib_meta: msg = "No lib record found (expected def tag starting with 'lib:')" raise ValueError(msg) if def_trios: for text in def_trios: defs.extend(load_defs_from_trio(text)) return Lib.from_meta(lib_meta, defs)