Skip to content

niess.dispatch

The registry mechanism every conversion target shares: three-tier resolution keyed on niess provenance or the McStas component type, for a live niess object or for an assembled instance.

niess.provenance is the record it resolves against — written onto every emitted McStas component, and read back by whatever has only the emitted file.

dispatch

Target-neutral registry dispatch over the assembled Instance tree.

Conversion targets (STEP/CAD geometry in :mod:niess.brep, NeXus Structure JSON in :mod:niess.nexus) resolve a builder for each mccode_antlr Instance using three tiers, most specific first:

  1. the niess source_type recorded in the instance's provenance metadata,
  2. the niess role recorded there,
  3. the raw McCode component-type name, which every Instance carries whether or not niess produced it.

The registry only resolves builders; each target invokes them with whatever signature it needs. Keeping resolution and invocation separate lets a caller distinguish "no builder registered" from "a builder ran and declined to emit anything" -- a distinction :mod:niess.nexus depends on for group suppression.

Classes:

  • NiessRegistry –

    Three-tier builder lookup keyed on provenance metadata or component type.

  • ClassHooks –

    Lets a class describe its own conversion, next to the fields it converts.

Functions:

  • expr_float –

    A plain float out of a McCode Expr, or ValueError if it is not one.

  • merged_params –

    Instance parameters evaluated to floats, layered over params.

NiessRegistry

NiessRegistry(parent: 'NiessRegistry[B] | None' = None, hooks: str | None = None)

Bases: Generic[B]

Three-tier builder lookup keyed on provenance metadata or component type.

A registry may extend another by naming it as parent: lookups that find nothing locally fall through to the parent's own three tiers. That is how an instrument-specific registry adds its translators without touching the shared default one, so which translators apply is a property of a single conversion rather than of whatever the process happened to import.

Methods:

Source code in src/niess/dispatch.py
def __init__(self, parent: 'NiessRegistry[B] | None' = None,
             hooks: str | None = None) -> None:
    self.parent = parent
    #: Name of the per-class hook family this target reads, if it has one.
    #: ``hooks='mccode'`` makes ``__mccode_enter__``/``__mccode_leaf__``/
    #: ``__mccode_exit__`` on a class serve as its translator, so the ordinary case
    #: is to write the method and have it found. Registering still wins, which is
    #: what a class you do not own needs, and what scoping a conversion needs so
    #: that importing a module cannot change another instrument's output.
    self.hooks = hooks
    self._source_type_builders: dict[str, B] = {}
    self._role_builders: dict[str, B] = {}
    self._component_type_builders: dict[str, B] = {}

register

register(source: type | str)

Register against a niess class (or its dotted source_type name).

Source code in src/niess/dispatch.py
def register(self, source: type | str):
    """Register against a niess class (or its dotted ``source_type`` name)."""
    source_type = source if isinstance(source, str) else niess_source_type(source)

    def decorator(func: B) -> B:
        self._source_type_builders[source_type] = func
        return func

    return decorator

register_role

register_role(role: str)

Register against a provenance role.

Source code in src/niess/dispatch.py
def register_role(self, role: str):
    """Register against a provenance ``role``."""
    def decorator(func: B) -> B:
        self._role_builders[role] = func
        return func

    return decorator

register_component_type

register_component_type(*comp_type_names: str)

Register against one or more raw McCode component-type names.

Source code in src/niess/dispatch.py
def register_component_type(self, *comp_type_names: str):
    """Register against one or more raw McCode component-type names."""
    def decorator(func: B) -> B:
        for name in comp_type_names:
            self._component_type_builders[name] = func
        return func

    return decorator

resolve_builder

resolve_builder(instance) -> B | None

Return the builder for instance, or None if none is registered.

This registry's own three tiers are tried first, in full, before any parent is consulted: the more specific registry wins outright.

None means unhandled -- it never means "handled, emit nothing". Callers that need that distinction must invoke the returned builder themselves and interpret its return value.

Source code in src/niess/dispatch.py
def resolve_builder(self, instance) -> B | None:
    """Return the builder for ``instance``, or ``None`` if none is registered.

    This registry's own three tiers are tried first, in full, before any parent
    is consulted: the more specific registry wins outright.

    ``None`` means *unhandled* -- it never means "handled, emit nothing".
    Callers that need that distinction must invoke the returned builder
    themselves and interpret its return value.
    """
    provenance = NiessProvenance.from_instance(instance)
    builder = self._resolve_local(instance, provenance)
    if builder is not None:
        return builder
    return None if self.parent is None else self.parent.resolve_builder(instance)

resolve_for_object

resolve_for_object(obj) -> B | None

Return the builder for a niess object, or None if none is registered.

The companion to :meth:resolve_builder, which resolves against an emitted McStas instance. Same tiers, read off the object rather than off a metadata blob it left behind, most specific first:

  1. a translator registered for the object's exact class,
  2. the object's own hooks for this target, if it has any (see :class:ClassHooks) -- as specific as (1), because a class describing its own conversion is describing its conversion,
  3. a translator registered for an ancestor class, nearest first,
  4. a translator registered for the object's role.

A registration for a base class does not beat a hook on the class itself: registering something for Component says what to do with components in general, and a component that says what to do with itself is more specific than that.

There is no McCode-component-type tier: a type name is something an object acquires by being emitted, and nothing here has been.

As everywhere else, None means unhandled; it never means "handled, emit nothing".

Source code in src/niess/dispatch.py
def resolve_for_object(self, obj) -> B | None:
    """Return the builder for a niess *object*, or ``None`` if none is registered.

    The companion to :meth:`resolve_builder`, which resolves against an emitted
    McStas instance. Same tiers, read off the object rather than off a metadata blob
    it left behind, most specific first:

    1. a translator registered for the object's exact class,
    2. the object's own hooks for this target, if it has any (see
       :class:`ClassHooks`) -- as specific as (1), because a class describing its own
       conversion is describing *its* conversion,
    3. a translator registered for an ancestor class, nearest first,
    4. a translator registered for the object's role.

    A registration for a base class does not beat a hook on the class itself:
    registering something for ``Component`` says what to do with components in
    general, and a component that says what to do with itself is more specific than
    that.

    There is no McCode-component-type tier: a type name is something an object
    acquires by being emitted, and nothing here has been.

    As everywhere else, ``None`` means *unhandled*; it never means "handled, emit
    nothing".
    """
    exact = self._exact(obj)
    if exact is not None:
        return exact
    if self.hooks is not None and self._defines_hooks(type(obj)):
        return ClassHooks(obj, self.hooks)
    return self._inherited(obj)

registered_component_types

registered_component_types() -> frozenset[str]

Component-type names this registry handles, inherited ones included.

Source code in src/niess/dispatch.py
def registered_component_types(self) -> frozenset[str]:
    """Component-type names this registry handles, inherited ones included."""
    inherited = frozenset() if self.parent is None else self.parent.registered_component_types()
    return frozenset(self._component_type_builders) | inherited

ClassHooks

ClassHooks(obj, prefix: str)

Lets a class describe its own conversion, next to the fields it converts.

A registry created with hooks='mccode' finds __mccode_enter__, __mccode_leaf__ and __mccode_exit__ on a class and uses them as its translator, so nothing has to be registered for the ordinary case. One with hooks='nexus' does the same for __nexus_*__.

Both idioms are available for every target. Which suits depends on the target: a format with imperative scaffolding to place -- McStas, with its coordinate frames and per-particle state -- reads well as a method on the composite that needs it, while one that is mostly a table of per-type mappings reads better registered.

Source code in src/niess/dispatch.py
def __init__(self, obj, prefix: str):
    self.obj = obj
    self.prefix = prefix

expr_float

expr_float(value)

A plain float out of a McCode Expr, or ValueError if it is not one.

hasattr(expr, 'value') is not the test it looks like: Expr.value is a property that raises for anything not constant, and hasattr only swallows AttributeError, so asking whether an expression has a value used to raise NotImplementedError straight through every caller. Every caller catches TypeError/ValueError -- that is what "this names a run-time parameter, skip it" is spelled as throughout niess -- so an expression that does not reduce says so that way.

Source code in src/niess/dispatch.py
def expr_float(value):
    """A plain float out of a McCode ``Expr``, or ``ValueError`` if it is not one.

    ``hasattr(expr, 'value')`` is not the test it looks like: ``Expr.value`` is a property
    that *raises* for anything not constant, and ``hasattr`` only swallows
    ``AttributeError``, so asking whether an expression has a value used to raise
    ``NotImplementedError`` straight through every caller. Every caller catches
    ``TypeError``/``ValueError`` -- that is what "this names a run-time parameter, skip it"
    is spelled as throughout niess -- so an expression that does not reduce says so that
    way.
    """
    if isinstance(value, (int, float)):
        return float(value)
    simplified = value.simplify() if hasattr(value, 'simplify') else value
    try:
        held = simplified.value
    except (AttributeError, NotImplementedError):
        held = None
    if isinstance(held, (int, float)):
        return float(held)
    try:
        return float(simplified)
    except (AttributeError, NotImplementedError, TypeError) as error:
        raise ValueError(
            f'{simplified!r} does not reduce to a number; it depends on something only '
            f'known at run time'
        ) from error

merged_params

merged_params(instance, params: dict[str, float] | None = None) -> dict[str, float]

Instance parameters evaluated to floats, layered over params.

Parameters that cannot be reduced to a float (they depend on a runtime instrument parameter, say) are skipped rather than raising.

Source code in src/niess/dispatch.py
def merged_params(instance, params: dict[str, float] | None = None) -> dict[str, float]:
    """Instance parameters evaluated to floats, layered over ``params``.

    Parameters that cannot be reduced to a float (they depend on a runtime
    instrument parameter, say) are skipped rather than raising.
    """
    merged = dict(params or {})
    for component_parameter in instance.parameters:
        try:
            evaluated = component_parameter.value.evaluate(merged)
            merged[component_parameter.name] = expr_float(evaluated)
        except Exception:
            continue
    return merged

provenance

What niess wrote a component from, recorded on the component it emitted.

Every McStas instance niess emits carries a niess_provenance METADATA block naming the niess class and instance behind it. This module is both halves of that: writing it during emission, and reading it back when something that has only the emitted instrument needs to know what it came from.

The namespace string in that block is literally this module's own name, and niess_source_type is what puts a class's __module__ into the emitted file -- so the frozen .instr goldens in tests/data/baseline pin the import path of every component class. Moving niess/components/guide.py changes emitted text; moving this module changes the namespace every previously-written file is recognised by. Neither is a rename to make casually.

Below niess.dispatch, which resolves a translator from what it reads here, and therefore below every target. Writing happens through add_niess_metadata, called from the components themselves.

Functions:

  • add_visit_metadata –

    Write provenance for one emitted instance, unless this emission opted out.

add_visit_metadata

add_visit_metadata(visit, instance, source=None, **kwargs)

Write provenance for one emitted instance, unless this emission opted out.

Every McStas emitter on the walk goes through here rather than calling add_niess_metadata directly, so that "emit without the niess METADATA blocks" is one decision made once at to_mccode rather than a flag each of seven call sites has to remember to honour -- and so that a translator added later cannot forget it.

Source code in src/niess/provenance.py
def add_visit_metadata(visit, instance, source=None, **kwargs):
    """Write provenance for one emitted instance, unless this emission opted out.

    Every McStas emitter on the walk goes through here rather than calling
    `add_niess_metadata` directly, so that "emit without the niess METADATA blocks" is
    one decision made once at `to_mccode` rather than a flag each of seven call sites
    has to remember to honour -- and so that a translator added later cannot forget it.
    """
    if not getattr(visit.context, 'provenance', True):
        return instance
    return add_niess_metadata(instance, source, **kwargs)