Skip to content

odd_compiler

Compile an ODD file into a transform module. compile_odd() is the low-level entry point used by the on-demand cache; CodeGenerator / PythonGenerator are the code-emission backend and load_odd() parses the ODD XML into a ParsedOdd.

For day-to-day use prefer opm.odd_cache.ensure_compiled_module, which writes into the platform user cache and skips recompilation when inputs are unchanged.

opm.odd_compiler

Compile TEI Publisher ODD processing models to target language transformation modules.

CodeGenerator

Bases: ABC

Abstract base class for ODD-to-target-language code generators.

target_name abstractmethod property

target_name: str

Target language identifier (e.g., 'python', 'rust').

file_extension abstractmethod property

file_extension: str

File extension for generated files (e.g., '.py', '.rs').

generate_module abstractmethod

generate_module(
    parsed: ParsedOdd,
    module_name: str,
    *,
    output_mode: str = "web",
    base_css: str | None = None,
) -> str

Generate target language source code from a parsed ODD.

Parameters:

Name Type Description Default
parsed ParsedOdd

The parsed ODD structure

required
module_name str

Logical name for the generated module

required
output_mode str

Output channel (web, markdown, print, etc.)

'web'

Returns:

Type Description
str

Complete source code as a string

Source code in src/opm/odd_compiler/codegen/__init__.py
@abstractmethod
def generate_module(
    self,
    parsed: ParsedOdd,
    module_name: str,
    *,
    output_mode: str = 'web',
    base_css: str | None = None,
) -> str:
    """Generate target language source code from a parsed ODD.

    Args:
        parsed: The parsed ODD structure
        module_name: Logical name for the generated module
        output_mode: Output channel (web, markdown, print, etc.)

    Returns:
        Complete source code as a string
    """
    ...

PythonGenerator

PythonGenerator()

Bases: CodeGenerator

Generate Python source from a parsed ODD.

Source code in src/opm/odd_compiler/codegen/python_generator.py
def __init__(self) -> None:
    #: Set per `generate_module` call from the ODD root's namespace map.
    self._odd_nsmap: dict[str, str] = {}
    #: The schemaSpec namespace, i.e. the default element namespace at run time.
    self._schema_ns = ''
    #: Expressions compiled out, keyed so each is recorded once.
    self._unsupported: dict[tuple, UnsupportedExpression] = {}
    self._problems: dict[str, str | None] = {}

unsupported property

unsupported: list[UnsupportedExpression]

Expressions the last generate_module call compiled out.

Each is one opm can never evaluate (see expression_check). It was replaced by what a failing evaluation returns, so the output is unchanged; the difference is that it is now known and reported instead of failing on every node.

compile_odd

compile_odd(
    odd_path: str,
    *,
    target: str = "python",
    module_name: str = "generated_odd",
    output_mode: str = "web",
    base_css: str | None = None,
    diagnostics: list | None = None,
) -> str

Compile an ODD file to target language source code.

Parameters:

Name Type Description Default
odd_path str

Path to the ODD file

required
target str

Target language ('python', or future 'rust')

'python'
module_name str

Logical name for the generated module

'generated_odd'
output_mode str

Output channel (web, markdown, print, etc.)

'web'
base_css str | None

Rules prepended to the generated stylesheet, replacing the packaged default. None keeps the packaged default.

None
diagnostics list | None

When given, receives one UnsupportedExpression per expression the generator compiled out because opm can never evaluate it.

None

Returns:

Type Description
str

Generated source code as a string

Raises:

Type Description
ValueError

If target language is not supported

Source code in src/opm/odd_compiler/__init__.py
def compile_odd(
    odd_path: str,
    *,
    target: str = 'python',
    module_name: str = 'generated_odd',
    output_mode: str = 'web',
    base_css: str | None = None,
    diagnostics: list | None = None,
) -> str:
    """Compile an ODD file to target language source code.

    Args:
        odd_path: Path to the ODD file
        target: Target language ('python', or future 'rust')
        module_name: Logical name for the generated module
        output_mode: Output channel (web, markdown, print, etc.)
        base_css: Rules prepended to the generated stylesheet, replacing the
            packaged default. ``None`` keeps the packaged default.
        diagnostics: When given, receives one
            `UnsupportedExpression`
            per expression the generator compiled out because opm can never
            evaluate it.

    Returns:
        Generated source code as a string

    Raises:
        ValueError: If target language is not supported
    """
    if target not in _GENERATORS:
        raise ValueError(f"Unsupported target: {target!r}. "
                         f"Supported: {list(_GENERATORS.keys())}")

    parsed = load_odd(odd_path)
    generator = _GENERATORS[target]()
    source = generator.generate_module(
        parsed, module_name, output_mode=output_mode, base_css=base_css
    )
    if diagnostics is not None:
        diagnostics.extend(getattr(generator, 'unsupported', ()))
    return source

Parsed ODD representation

opm.odd_compiler.parse_odd.ParsedOdd dataclass

ParsedOdd(
    tree: _ElementTree,
    schema_ns: str,
    odd_path: str,
    element_specs: list,
    odd_chain: list[str],
    nsmap: dict[str, str],
    licences: list[OddLicence] = list(),
)

Odd cache

opm.odd_cache

Compile-on-demand cache for ODD → Python transform modules.

ResolvedTransform dataclass

ResolvedTransform(
    module_path: Path,
    source_odd: Path | None = None,
    freshly_compiled: bool = False,
    unsupported: tuple = (),
)

A loadable transform module, optionally produced from an ODD.

modules_cache_dir

modules_cache_dir() -> Path

Return …/opm/modules under the platform user cache directory.

Source code in src/opm/odd_cache.py
def modules_cache_dir() -> Path:
    """Return ``…/opm/modules`` under the platform user cache directory."""
    return user_opm_cache_dir() / 'modules'

cache_key

cache_key(
    odd_path: Path,
    output_mode: str,
    base_css: str | None = None,
) -> str

Return a content hash covering the ODD chain, CSS inputs, mode, and opm version.

Source code in src/opm/odd_cache.py
def cache_key(odd_path: Path, output_mode: str, base_css: str | None = None) -> str:
    """Return a content hash covering the ODD chain, CSS inputs, mode, and opm version."""
    odd_path = odd_path.resolve()
    h = hashlib.sha256()
    h.update(opm_version().encode())
    h.update(b'\0')
    h.update(output_mode.encode())
    h.update(b'\0')

    # The generator decides what the cached module contains, and the mode table
    # which models and output functions it compiles against, so editing either
    # has to invalidate the cache. The version alone does not cover that: it
    # stays put across a working checkout, and a stale module would keep being
    # loaded after a codegen change.
    from opm import output_modes
    from opm.odd_compiler import expression_check
    from opm.odd_compiler.codegen import python_generator

    for generator_module in (python_generator, expression_check, output_modes):
        h.update(Path(generator_module.__file__).read_bytes())
        h.update(b'\0')

    # The base rules are compiled into ODD_GENERATED_CSS, so a project that
    # overrides them via [transform] css needs its own cached module — and
    # editing the packaged default in a checkout has to invalidate too.
    from opm.odd_compiler.css_generator import default_base_css

    effective_base = default_base_css() if base_css is None else base_css
    h.update(effective_base.encode())
    h.update(b'\0')

    for path in _iter_input_files(odd_path, leaf_dir=odd_path.parent, seen=set()):
        h.update(str(path).encode())
        h.update(b'\0')
        h.update(path.read_bytes())
        h.update(b'\0')

    return h.hexdigest()

cached_module_path

cached_module_path(
    odd_path: Path,
    output_mode: str,
    digest: str | None = None,
    base_css: str | None = None,
) -> Path

Return the cache path for odd_path in output_mode (does not compile).

Source code in src/opm/odd_cache.py
def cached_module_path(
    odd_path: Path,
    output_mode: str,
    digest: str | None = None,
    base_css: str | None = None,
) -> Path:
    """Return the cache path for *odd_path* in *output_mode* (does not compile)."""
    odd_path = Path(odd_path)
    digest = digest or cache_key(odd_path, output_mode, base_css)
    return modules_cache_dir() / f'{odd_path.stem}-{output_mode}-{digest[:12]}.py'

ensure_compiled_module

ensure_compiled_module(
    odd_path: Path | str,
    *,
    output_mode: str = "web",
    module_name: str | None = None,
    base_css: str | None = None,
    diagnostics: list | None = None,
) -> tuple[Path, bool]

Return (module_path, freshly_compiled) for odd_path.

On a cache miss, compiles the ODD into the user cache directory and returns the new path. On a hit, returns the existing cached module unchanged.

diagnostics, when given, receives the expressions a fresh compile skipped (see compile_odd); a cache hit leaves it empty.

Source code in src/opm/odd_cache.py
def ensure_compiled_module(
    odd_path: Path | str,
    *,
    output_mode: str = 'web',
    module_name: str | None = None,
    base_css: str | None = None,
    diagnostics: list | None = None,
) -> tuple[Path, bool]:
    """Return ``(module_path, freshly_compiled)`` for *odd_path*.

    On a cache miss, compiles the ODD into the user cache directory and returns
    the new path. On a hit, returns the existing cached module unchanged.

    *diagnostics*, when given, receives the expressions a fresh compile skipped
    (see [`compile_odd`][opm.odd_compiler.compile_odd]); a cache hit leaves it empty.
    """
    odd_path = Path(odd_path).resolve()
    if not odd_path.is_file():
        raise FileNotFoundError(f'ODD not found: {odd_path}')

    mode = mode_named(output_mode).name
    digest = cache_key(odd_path, mode, base_css)
    dest = cached_module_path(odd_path, mode, digest, base_css)
    if dest.is_file():
        return dest, False

    dest.parent.mkdir(parents=True, exist_ok=True)
    name = module_name or odd_path.stem
    src = compile_odd(
        str(odd_path), module_name=name, output_mode=mode, base_css=base_css,
        diagnostics=diagnostics,
    )
    dest.write_text(src, encoding='utf-8')
    return dest, True

resolve_transform_module

resolve_transform_module(
    *,
    module: Path | None = None,
    odd: Path | None = None,
    output_mode: str = "web",
    use_packaged_default: bool = True,
    base_css: str | None = None,
) -> ResolvedTransform

Resolve a loadable .py module from an explicit path, ODD, or packaged default.

Precedence: moduleodd → packaged teipublisher.odd (when use_packaged_default is true).

base_css replaces the packaged rules prepended to the generated stylesheet, and is part of the cache key, so a project overriding them gets its own compiled module.

Source code in src/opm/odd_cache.py
def resolve_transform_module(
    *,
    module: Path | None = None,
    odd: Path | None = None,
    output_mode: str = 'web',
    use_packaged_default: bool = True,
    base_css: str | None = None,
) -> ResolvedTransform:
    """Resolve a loadable ``.py`` module from an explicit path, ODD, or packaged default.

    Precedence: *module* → *odd* → packaged ``teipublisher.odd`` (when
    *use_packaged_default* is true).

    *base_css* replaces the packaged rules prepended to the generated
    stylesheet, and is part of the cache key, so a project overriding them gets
    its own compiled module.
    """
    if module is not None:
        return ResolvedTransform(module_path=Path(module), source_odd=None, freshly_compiled=False)

    odd_path = Path(odd) if odd is not None else None
    if odd_path is None and use_packaged_default:
        from opm.resources import packaged_odd

        odd_path = packaged_odd('teipublisher')

    if odd_path is None:
        raise ValueError(
            'No transform ODD specified. '
            'Pass --odd, set transform.odd (or transform.<type>.odd) in config, '
            'or install the package with stock ODDs.',
        )

    unsupported: list = []
    path, fresh = ensure_compiled_module(
        odd_path, output_mode=output_mode, base_css=base_css, diagnostics=unsupported,
    )
    return ResolvedTransform(
        module_path=path,
        source_odd=odd_path,
        freshly_compiled=fresh,
        unsupported=tuple(unsupported),
    )