Skip to content

geomotif.io.spec

A design's recipe rather than its points: the motif and its parameters.

:func:~geomotif.io.points.save_points writes what a design is; this module writes what would produce it. The file is a few hundred bytes instead of a few hundred kilobytes, it survives a change of point count or spacing curve, and it is what the gallery manifest and the CLI's --spec flag are built on.

The on-disk shape is the same nested object at every depth::

{
  "geomotif": "1.1.0",
  "motif": "polygon.star",
  "params": {"points": 7, "step": 3, "center": [0.0, 0.0]}
}

A parameter that is itself a motif -- the composers in :mod:geomotif.compose take one -- is written as that same {"motif": ..., "params": ...} object, so a spec nests without needing a second notation. A parameter that is a value dataclass (:class:~geomotif.Bounds, Ring, IFSMap) becomes a {"$type": ...} object naming its class.

Between them those two cases cover every builtin motif except the two whose parameter is a Python function: a motif defined by code cannot be rebuilt from data, and asking for its spec says so rather than writing a file that will not load.

An optional top-level :data:ANIMATION_KEY ("animation") carries the recipe for a moving picture -- the same JSON the CLI's --animation flag reads and the web explorer encodes into a share URL. A spec without it is a still, and :func:from_spec ignores the key when building the motif, so an old spec keeps loading unchanged.

Functions:

Name Description
to_spec

Return the JSON-ready recipe for a motif, or for the design it built.

from_spec

Rebuild the motif a spec describes.

save_spec

Write a motif's recipe to a JSON file and return the path written.

load_spec

Read a spec file and return the motif it describes.

to_spec

to_spec(source: SupportsBuild | Design, *, animation: Mapping[str, object] | None = None) -> dict[str, object]

Return the JSON-ready recipe for a motif, or for the design it built.

Parameters:

Name Type Description Default
source Motif or Design

A motif, or any design whose :attr:~geomotif.Design.meta records the motif that produced it -- which every builtin motif's does.

required
animation mapping

An animation recipe to carry alongside the still, so a moving picture round-trips through the same file the CLI's --animation flag reads. The value is written verbatim under :data:ANIMATION_KEY; a still spec simply omits the key.

None

Returns:

Type Description
dict

{"geomotif": version, "motif": name, "params": {...}}, holding only JSON types and ready for :func:json.dumps. When animation is given, an "animation" key sits beside them.

Raises:

Type Description
ValueError

If source is a design with no motif recorded in its metadata.

TypeError

If a parameter cannot be written as data -- see the module docstring.

Examples:

>>> from geomotif.motifs import Star
>>> to_spec(Star(points=7))["motif"]
'star'
Source code in src/geomotif/io/spec.py
def to_spec(
    source: SupportsBuild | Design, *, animation: Mapping[str, object] | None = None
) -> dict[str, object]:
    """Return the JSON-ready recipe for a motif, or for the design it built.

    Parameters
    ----------
    source : Motif or Design
        A motif, or any design whose :attr:`~geomotif.Design.meta` records the
        motif that produced it -- which every builtin motif's does.
    animation : mapping, optional
        An animation recipe to carry alongside the still, so a moving picture
        round-trips through the same file the CLI's ``--animation`` flag reads.
        The value is written verbatim under :data:`ANIMATION_KEY`; a still
        spec simply omits the key.

    Returns
    -------
    dict
        ``{"geomotif": version, "motif": name, "params": {...}}``, holding only
        JSON types and ready for :func:`json.dumps`. When ``animation`` is
        given, an ``"animation"`` key sits beside them.

    Raises
    ------
    ValueError
        If ``source`` is a design with no motif recorded in its metadata.
    TypeError
        If a parameter cannot be written as data -- see the module docstring.

    Examples
    --------
    >>> from geomotif.motifs import Star
    >>> to_spec(Star(points=7))["motif"]
    'star'
    """
    live = _live_spec(source)
    name = live[registry.NAME_KEY]
    params = {
        key: value
        for key, value in live.items()
        if key != registry.NAME_KEY and key not in _STYLE_KEYS
    }
    # Imported at call time rather than at module scope: this module is part of
    # the package whose version it reads, so the two would import in a cycle.
    from .. import __version__

    blob: dict[str, object] = {
        VERSION_KEY: __version__,
        registry.NAME_KEY: name,
        PARAMS_KEY: _encode(params, where=str(name)),
    }
    # Styles sit beside the parameters rather than among them: they belong to
    # the design rather than to the motif, and feeding one back to a
    # constructor as a keyword argument would only raise.
    for key in _STYLE_KEYS:
        if key in live:
            blob[key] = _encode(live[key], where=key)
    if animation is not None:
        blob[ANIMATION_KEY] = dict(animation)
    return blob

from_spec

from_spec(data: Mapping[str, object]) -> Motif

Rebuild the motif a spec describes.

The version stamp is not consulted; a spec is data, and data that loaded once should keep loading.

Raises:

Type Description
ValueError

If the mapping names no motif, or names a value type this library will not import.

KeyError

If the motif name is not registered -- including when it belongs to a plugin that is not installed.

Source code in src/geomotif/io/spec.py
def from_spec(data: Mapping[str, object]) -> Motif:
    """Rebuild the motif a spec describes.

    The version stamp is not consulted; a spec is data, and data that loaded
    once should keep loading.

    Raises
    ------
    ValueError
        If the mapping names no motif, or names a value type this library will
        not import.
    KeyError
        If the motif name is not registered -- including when it belongs to a
        plugin that is not installed.
    """
    return _decode_spec(data, _importable_packages(), where="spec")

save_spec

save_spec(source: SupportsBuild | Design, path: str | PathLike[str], *, indent: int | None = 2) -> Path

Write a motif's recipe to a JSON file and return the path written.

Parameters:

Name Type Description Default
source Motif or Design

What to describe; see :func:to_spec.

required
path str or path - like

Destination file.

required
indent int

Passed through to :func:json.dumps. Indented by default: a spec is a few hundred bytes and is meant to be opened and edited by hand.

2

Returns:

Type Description
Path

The file that was written.

Source code in src/geomotif/io/spec.py
def save_spec(
    source: SupportsBuild | Design,
    path: str | PathLike[str],
    *,
    indent: int | None = 2,
) -> pathlib.Path:
    """Write a motif's recipe to a JSON file and return the path written.

    Parameters
    ----------
    source : Motif or Design
        What to describe; see :func:`to_spec`.
    path : str or path-like
        Destination file.
    indent : int, optional
        Passed through to :func:`json.dumps`. Indented by default: a spec is a
        few hundred bytes and is meant to be opened and edited by hand.

    Returns
    -------
    pathlib.Path
        The file that was written.
    """
    target = pathlib.Path(path)
    target.write_text(json.dumps(to_spec(source), indent=indent) + "\n")
    return target

load_spec

load_spec(path: str | PathLike[str]) -> Motif

Read a spec file and return the motif it describes.

Returns:

Type Description
Motif

Ready to :meth:~geomotif.Motif.build or :meth:~geomotif.Motif.generate.

Source code in src/geomotif/io/spec.py
def load_spec(path: str | PathLike[str]) -> Motif:
    """Read a spec file and return the motif it describes.

    Returns
    -------
    Motif
        Ready to :meth:`~geomotif.Motif.build` or
        :meth:`~geomotif.Motif.generate`.
    """
    data = json.loads(pathlib.Path(path).read_text())
    if not isinstance(data, dict):
        raise ValueError(
            f"{path} is not a spec file: expected a JSON object, got {type(data).__name__}"
        )
    return from_spec(data)