Skip to content

geomotif.core.registry

Motif registration, lookup and introspection.

Registering a motif is what makes it discoverable by name -- to the CLI, to the gallery builder, to the conformance test suite, and to anyone who wants to round-trip a design through a JSON spec.

Third-party packages ship their own motifs by declaring an entry point::

[project.entry-points."geomotif.motifs"]
my_motifs = "my_package.motifs:register_all"

The builtin catalog and any such plugins are loaded lazily, on the first registry access rather than at import, so a motif family nobody touches costs nothing to have installed.

Classes:

Name Description
ParamInfo

One constructor parameter of a motif, as the CLI and docs see it.

MotifInfo

Everything known about a registered motif without instantiating it.

Functions:

Name Description
register

Register a motif class under name, returning it unchanged.

names

Return every registered motif name, sorted; optionally one family only.

families

Return every family name that has at least one motif, sorted.

get

Return the motif class registered under name.

create

Instantiate the motif registered under name with params.

describe

Return a motif's documentation and parameter list without building it.

name_for

Return the name cls is registered under, or None if it is not.

spec

Return a reproducible description of motif, for :attr:Design.meta.

ParamInfo dataclass

ParamInfo(name: str, annotation: str, default: object, required: bool, description: str | None = None, min: float | None = None, max: float | None = None, step: float | None = None)

One constructor parameter of a motif, as the CLI and docs see it.

The numeric range fields (min, max, step) come from a :class:~geomotif.Range on the field's metadata; they are None when a motif has not declared a bound, in which case a consumer falls back to its own heuristic rather than guessing zero.

MotifInfo dataclass

MotifInfo(name: str, cls: type[Motif], family: str | None, requires: str | None, summary: str, doc: str, params: tuple[ParamInfo, ...], example: Mapping[str, object])

Everything known about a registered motif without instantiating it.

Attributes:

Name Type Description
available bool

Whether the optional dependency this motif needs is installed.

available property

available: bool

Whether the optional dependency this motif needs is installed.

A motif registered with requires= can be listed, described and documented on a machine that lacks its dependency -- only building one raises. This is what lets a listing say so instead of failing to import, and what the conformance suite checks before it tries.

register

register(name: str | None = None, *, family: str | None = None, requires: str | None = None, example: Mapping[str, object] | None = None) -> Callable[[type[MotifT]], type[MotifT]]

Register a motif class under name, returning it unchanged.

Parameters:

Name Type Description Default
name str

Registry key. Derived from the class name in kebab-case when omitted, so GoldenSpiral becomes golden-spiral.

None
family str

Grouping for geomotif list and the gallery, e.g. "spiral".

None
requires str

Name of an optional dependency the motif needs, e.g. "scipy". Listings report such motifs as unavailable rather than failing to import when the extra is missing.

None
example mapping

Constructor arguments producing a representative instance -- what the gallery renders and what the conformance suite exercises. Required for motifs with parameters that have no default, since those cannot be instantiated any other way.

None

Examples:

::

@register("rose", family="polar", example={"k": 5})
@dataclass(frozen=True, slots=True)
class Rose(PolarMotif): ...
Source code in src/geomotif/core/registry.py
def register(
    name: str | None = None,
    *,
    family: str | None = None,
    requires: str | None = None,
    example: Mapping[str, object] | None = None,
) -> Callable[[type[MotifT]], type[MotifT]]:
    """Register a motif class under ``name``, returning it unchanged.

    Parameters
    ----------
    name : str, optional
        Registry key. Derived from the class name in kebab-case when
        omitted, so ``GoldenSpiral`` becomes ``golden-spiral``.
    family : str, optional
        Grouping for ``geomotif list`` and the gallery, e.g. ``"spiral"``.
    requires : str, optional
        Name of an optional dependency the motif needs, e.g. ``"scipy"``.
        Listings report such motifs as unavailable rather than failing to
        import when the extra is missing.
    example : mapping, optional
        Constructor arguments producing a representative instance -- what
        the gallery renders and what the conformance suite exercises.
        Required for motifs with parameters that have no default, since
        those cannot be instantiated any other way.

    Examples
    --------
    ::

        @register("rose", family="polar", example={"k": 5})
        @dataclass(frozen=True, slots=True)
        class Rose(PolarMotif): ...
    """

    def decorate(cls: type[MotifT]) -> type[MotifT]:
        key = name if name is not None else _derive_name(cls)
        existing = _REGISTRY.get(key)
        if existing is not None and existing.cls is not cls:
            raise ValueError(
                f"motif name {key!r} is already registered to "
                f"{existing.cls.__module__}.{existing.cls.__qualname__}; "
                f"pass a different name to @register"
            )
        if _reserves_the_name_key(cls):
            raise ValueError(
                f"{cls.__qualname__} has a parameter called {NAME_KEY!r}, which is "
                f"the key spec() reserves for a design's own registered name. The "
                f"two would collide in Design.meta and the design could not be "
                f"rebuilt from it -- rename the parameter; the composers in "
                f"geomotif.compose call theirs 'unit'"
            )
        _REGISTRY[key] = _Entry(cls, family, requires, MappingProxyType(dict(example or {})))
        return cls

    return decorate

names

names(*, family: str | None = None) -> tuple[str, ...]

Return every registered motif name, sorted; optionally one family only.

Source code in src/geomotif/core/registry.py
def names(*, family: str | None = None) -> tuple[str, ...]:
    """Return every registered motif name, sorted; optionally one family only."""
    _load_motifs()
    if family is None:
        return tuple(sorted(_REGISTRY))
    return tuple(sorted(k for k, v in _REGISTRY.items() if v.family == family))

families

families() -> tuple[str, ...]

Return every family name that has at least one motif, sorted.

Source code in src/geomotif/core/registry.py
def families() -> tuple[str, ...]:
    """Return every family name that has at least one motif, sorted."""
    _load_motifs()
    return tuple(sorted({v.family for v in _REGISTRY.values() if v.family is not None}))

get

get(name: str) -> type[Motif]

Return the motif class registered under name.

Raises:

Type Description
KeyError

If no such motif is registered. The message lists near misses, since a typo is far more likely than a genuinely missing motif.

Source code in src/geomotif/core/registry.py
def get(name: str) -> type[Motif]:
    """Return the motif class registered under ``name``.

    Raises
    ------
    KeyError
        If no such motif is registered. The message lists near misses, since
        a typo is far more likely than a genuinely missing motif.
    """
    _load_motifs()
    entry = _REGISTRY.get(name)
    if entry is None:
        close = [k for k in sorted(_REGISTRY) if name in k or k in name]
        hint = f"; did you mean {close[0]!r}?" if close else ""
        raise KeyError(f"no motif registered as {name!r}{hint}")
    return entry.cls

create

create(name: str, /, **params: object) -> Motif

Instantiate the motif registered under name with params.

Source code in src/geomotif/core/registry.py
def create(name: str, /, **params: object) -> Motif:
    """Instantiate the motif registered under ``name`` with ``params``."""
    # Typed as a plain factory: the registry stores concrete motif classes
    # whose signatures differ from one another, so the call cannot be checked
    # statically here -- the dataclass constructor validates at runtime.
    factory: Callable[..., Motif] = get(name)
    return factory(**params)

describe

describe(name: str) -> MotifInfo

Return a motif's documentation and parameter list without building it.

Parameters come from :func:dataclasses.fields, which is why every builtin motif is a dataclass: one declaration drives the CLI flags, the docs, the gallery and spec round-tripping. They are listed motif-first -- a motif's own parameters, then the ones it inherits -- which is the opposite of the order __init__ needs and the right one to read.

Source code in src/geomotif/core/registry.py
def describe(name: str) -> MotifInfo:
    """Return a motif's documentation and parameter list without building it.

    Parameters come from :func:`dataclasses.fields`, which is why every
    builtin motif is a dataclass: one declaration drives the CLI flags, the
    docs, the gallery and spec round-tripping. They are listed motif-first --
    a motif's own parameters, then the ones it inherits -- which is the
    opposite of the order ``__init__`` needs and the right one to read.
    """
    _load_motifs()
    cls = get(name)
    entry = _REGISTRY[name]
    # 3.13+ dedents docstrings at compile time; 3.12 keeps the raw indentation,
    # so normalize explicitly to keep the committed artifacts byte-identical.
    doc = inspect.cleandoc(cls.__doc__ or "").strip()
    summary = doc.split("\n\n", 1)[0].replace("\n", " ").strip()

    return MotifInfo(
        name=name,
        cls=cls,
        family=entry.family,
        requires=entry.requires,
        summary=summary,
        doc=doc,
        params=_params_for(cls),
        example=entry.example,
    )

name_for

name_for(cls: type) -> str | None

Return the name cls is registered under, or None if it is not.

Unlike the other lookups this one does not trigger a load: if an instance of the class exists, its module has already been imported, so anything the load would find is either present already or irrelevant.

Source code in src/geomotif/core/registry.py
def name_for(cls: type) -> str | None:
    """Return the name ``cls`` is registered under, or ``None`` if it is not.

    Unlike the other lookups this one does not trigger a load: if an instance
    of the class exists, its module has already been imported, so anything
    the load would find is either present already or irrelevant.
    """
    for key, entry in _REGISTRY.items():
        if entry.cls is cls:
            return key
    return None

spec

spec(motif: SupportsBuild) -> Mapping[str, object]

Return a reproducible description of motif, for :attr:Design.meta.

The result is the motif's registered name under "motif" plus every constructor parameter and its resolved value -- including any resolved random seed -- which is exactly what is needed to rebuild the design later via :func:create.

Parameters:

Name Type Description Default
motif Motif or any object with ``build()``

The motif to describe. Usually a dataclass; anything else reports just its name, since there is nothing to introspect.

required

Returns:

Type Description
Mapping[str, object]

Read-only, so it is safe to hand straight to :class:Design. An unregistered motif reports its class name, which is honest but not reconstructible -- register it to get a round-trippable spec.

Source code in src/geomotif/core/registry.py
def spec(motif: SupportsBuild) -> Mapping[str, object]:
    """Return a reproducible description of ``motif``, for :attr:`Design.meta`.

    The result is the motif's registered name under ``"motif"`` plus every
    constructor parameter and its resolved value -- including any resolved
    random seed -- which is exactly what is needed to rebuild the design
    later via :func:`create`.

    Parameters
    ----------
    motif : Motif or any object with ``build()``
        The motif to describe. Usually a dataclass; anything else reports
        just its name, since there is nothing to introspect.

    Returns
    -------
    Mapping[str, object]
        Read-only, so it is safe to hand straight to :class:`Design`. An
        unregistered motif reports its class name, which is honest but not
        reconstructible -- register it to get a round-trippable spec.
    """
    name = name_for(type(motif)) or type(motif).__qualname__
    return MappingProxyType({NAME_KEY: name, **_field_values(motif)})