Skip to content

spec_index

Graph of ODD specification elements: elements, classes, macros, datatypes, processing models, and the direct facts about them — memberships and content-model references — indexed for lookup.

opm.spec_index

In-memory graph of TEI ODD specification elements.

This is the documentation counterpart of opm.odd_compiler.parse_odd: that module merges elementSpecs so they can be compiled, while this one indexes elementSpec / classSpec / macroSpec / dataSpec and inverts their class memberships and content-model references, from which opm.odd_expand derives what the TEI Stylesheets show as "Contained by", "May contain", "Members" and "Used by".

The input is a compiled TEI document or ODD (p5subset.xml, a full Guidelines p5.xml, or the output of compile_schema). Nested TEI in desc, gloss, remarks and exemplum is left as lxml so a processing-model transform can render it.

SpecRef dataclass

SpecRef(ident: str, kind: str, module: str | None = None)

A pointer to another spec, used in membership / content lists.

AttDefView dataclass

AttDefView(ident: str, node: _Element)

An attribute a spec declares itself.

SpecIndex

SpecIndex(
    specs: dict[str, Spec],
    *,
    lang: str = "en",
    title: str = "",
)

The specs of a schema and the direct facts about them, indexed.

The primitives: specs by ident, direct class memberships and content-model references, both inverted so that "who is a member of X" and "who refers to X" are lookups rather than scans, and the two graph walks everything else is built from. What those facts mean on a documentation page — contained-by, may-contain, used-by, attribute inheritance — is derived on top, in opm.odd_expand. It is the counterpart of the indexes eXist gave the XQuery version of this code.

Source code in src/opm/spec_index.py
def __init__(
    self,
    specs: dict[str, Spec],
    *,
    lang: str = 'en',
    title: str = '',
):
    self._specs = specs
    self.lang = lang
    self.title = title
    self._build_lookups()

attributes

attributes() -> list[tuple[str, list[Spec]]]

Attribute ident → specs (classes/elements) that define it, A–Z.

Source code in src/opm/spec_index.py
def attributes(self) -> list[tuple[str, list[Spec]]]:
    """Attribute ident → specs (classes/elements) that define it, A–Z."""
    owners: dict[str, list[Spec]] = defaultdict(list)
    for spec in self.all():
        if spec.kind not in {'class', 'element'}:
            continue
        for att in spec.local_atts:
            owners[att.ident].append(spec)
    return sorted(owners.items(), key=lambda kv: kv[0].lower())

members_of

members_of(key: str) -> list[SpecRef]

Specs whose memberOf names key directly, A–Z.

Source code in src/opm/spec_index.py
def members_of(self, key: str) -> list[SpecRef]:
    """Specs whose ``memberOf`` names *key* directly, A–Z."""
    return unique_refs([self.ref(ident) for ident in self._members_of.get(key, ())])

referrers

referrers(kind: str, key: str) -> list[SpecRef]

Specs whose content model refers to key directly.

kind is the reference: element (elementRef), class (classRef), macro (macroRef) or data (dataRef). In document order, duplicates kept, as the references stand.

Source code in src/opm/spec_index.py
def referrers(self, kind: str, key: str) -> list[SpecRef]:
    """Specs whose content model refers to *key* directly.

    *kind* is the reference: ``element`` (``elementRef``), ``class``
    (``classRef``), ``macro`` (``macroRef``) or ``data`` (``dataRef``).
    In document order, duplicates kept, as the references stand.
    """
    return list(self._referrers.get((kind, key), ()))

content_refs

content_refs(ident: str) -> list[tuple[str, str]]

(kind, key) for every reference in ident's content model.

Source code in src/opm/spec_index.py
def content_refs(self, ident: str) -> list[tuple[str, str]]:
    """``(kind, key)`` for every reference in *ident*'s content model."""
    return list(self._content_refs.get(ident, ()))

class_members_transitive

class_members_transitive(ident: str) -> list[SpecRef]

Elements in ident or any subclass, walking memberOf downward.

Source code in src/opm/spec_index.py
def class_members_transitive(self, ident: str) -> list[SpecRef]:
    """Elements in *ident* or any subclass, walking ``memberOf`` downward."""
    out: list[SpecRef] = []
    seen: set[str] = set()
    stack = [ident]
    while stack:
        current = stack.pop()
        if current in seen:
            continue
        seen.add(current)
        for member in self._members_of.get(current, ()):
            spec = self._specs.get(member)
            if spec is None:
                continue
            if spec.kind == 'element':
                out.append(self.ref(spec.ident))
            elif spec.kind == 'class':
                stack.append(spec.ident)
    return unique_refs(out)

expand_model_ancestors

expand_model_ancestors(keys: list[str]) -> list[str]

Walk up model-class membership (p → model.pLike → …).

Source code in src/opm/spec_index.py
def expand_model_ancestors(self, keys: list[str]) -> list[str]:
    """Walk *up* model-class membership (``p`` → ``model.pLike`` → …)."""
    out: list[str] = []
    seen: set[str] = set()
    stack = list(keys)
    while stack:
        key = stack.pop()
        if not key or key in seen:
            continue
        seen.add(key)
        out.append(key)
        spec = self.get(key)
        if spec is None:
            continue
        stack.extend(spec.model_classes)
    return out

strip_comments

strip_comments(root: _Element) -> _Element

Remove comments from root, except those inside egXML examples.

Text after a comment lives in the comment's .tail; that must be merged back before removal, or Guidelines headings like <head><!--4.1-->Historical Considerations</head> lose their title.

Source code in src/opm/spec_index.py
def strip_comments(root: etree._Element) -> etree._Element:
    """Remove comments from *root*, except those inside egXML examples.

    Text after a comment lives in the comment's ``.tail``; that must be merged
    back before removal, or Guidelines headings like
    ``<head><!--4.1-->Historical Considerations</head>`` lose their title.
    """
    for comment in root.xpath('.//comment()[not(ancestor::*[namespace-uri() = $ns])]', ns=EX_NS):
        parent = comment.getparent()
        if parent is None:
            continue
        if comment.tail:
            prev = comment.getprevious()
            if prev is not None:
                prev.tail = (prev.tail or '') + comment.tail
            else:
                parent.text = (parent.text or '') + comment.tail
        parent.remove(comment)
    return root

parse_xml

parse_xml(path: Path | str) -> _Element

Parse path with comments removed outside egXML.

Source code in src/opm/spec_index.py
def parse_xml(path: Path | str) -> etree._Element:
    """Parse *path* with comments removed outside egXML."""
    return strip_comments(etree.parse(str(path), _PARSER).getroot())

unique_refs

unique_refs(refs: list[SpecRef]) -> list[SpecRef]

refs without repeats, character data first, then A–Z.

Source code in src/opm/spec_index.py
def unique_refs(refs: list[SpecRef]) -> list[SpecRef]:
    """*refs* without repeats, character data first, then A–Z."""
    seen: set[tuple[str, str]] = set()
    out: list[SpecRef] = []
    for ref in refs:
        key = (ref.kind, ref.ident)
        if key in seen:
            continue
        seen.add(key)
        out.append(ref)
    out.sort(key=lambda r: (r.kind != 'text', (r.ident or '').lower()))
    return out

iter_canonical_specs

iter_canonical_specs(root: _Element)

Yield schema specs, skipping examples embedded in egXML.

Source code in src/opm/spec_index.py
def iter_canonical_specs(root: etree._Element):
    """Yield schema specs, skipping examples embedded in ``egXML``."""
    for tag, kind in SPEC_TAGS.items():
        for el in root.iter(qn(tag)):
            if _inside_egxml(el):
                continue
            if el.get('mode') == 'delete':
                continue
            yield el, kind

What the index means on a documentation page — contained-by, may-contain, members, used-by, the attribute tree — is derived in opm.odd_expand.