Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ issue tracker. The initial project development roadmap is documented here:

Note any unreleased items inside the comment here. Not visible until release.

### Added

- `register_geometry_yaml` in-memory registration entry point. (#288)

### Fixed

- Kappa equivalent-Eulerian chi axis now matches fourcv/fourch/psic. (#284)
Expand Down
2 changes: 2 additions & 0 deletions src/ad_hoc_diffractometer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .factories import register_geometry
from .geometry_loader import load_geometry_file
from .geometry_loader import register_geometry_file
from .geometry_loader import register_geometry_yaml
from .lattice import Lattice
from .mode import REQUIRED
from .mode import BisectConstraint
Expand Down Expand Up @@ -67,6 +68,7 @@
"make_geometry",
"register_geometry",
"register_geometry_file",
"register_geometry_yaml",
"load_geometry_file",
# orientation
"ub_identity",
Expand Down
81 changes: 81 additions & 0 deletions src/ad_hoc_diffractometer/geometry_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- :func:`register_geometry_file` — parse a YAML file from any path and
add it to the geometry registry under its declared name (or an
explicit ``name=`` override).
- :func:`register_geometry_yaml` — parse an in-memory YAML string and
add it to the geometry registry under a caller-supplied name (the
in-memory companion to :func:`register_geometry_file`).
- :data:`KIND_KEY` — the top-level marker key
(``"ad_hoc_diffractometer_geometry"``).
- :data:`SUPPORTED_REVISIONS` — the schema revisions this loader
Expand Down Expand Up @@ -1083,6 +1086,84 @@ def _factory(**kwargs: Any) -> AdHocDiffractometer:
return final_name


def register_geometry_yaml(
yaml_text: str,
*,
name: str,
) -> str:
"""Parse an in-memory YAML string and add it to the geometry registry.

The in-memory companion to :func:`register_geometry_file`. Use it
when the YAML definition of a geometry is already available as a
Python ``str`` (for example, persisted inside another configuration
document) and you want it discoverable via :func:`list_geometries`
and :func:`make_geometry` without round-tripping through the
filesystem.

Parameters
----------
yaml_text : str
The YAML document declaring the geometry. Must contain the
:data:`KIND_KEY` marker with a supported
:data:`SUPPORTED_REVISIONS` value. Parsed eagerly so schema
errors surface at registration time rather than at first
:func:`make_geometry` call (matching
:func:`register_geometry_file`).
name : str
Required registry key under which the geometry will be
installed. Unlike :func:`register_geometry_file` there is no
filesystem path from which to derive a default, so this
argument is mandatory.

Returns
-------
str
The registry name (the value passed in as ``name``).

Raises
------
ValueError
If the registry already contains an entry under ``name``.
GeometrySchemaError
If ``yaml_text`` is not a well-formed declarative geometry
document.

Notes
-----
The registered factory re-parses ``yaml_text`` on every
:func:`make_geometry` call so that each invocation returns a fresh
:class:`AdHocDiffractometer` instance, mirroring the re-read
semantics of :func:`register_geometry_file`.
"""
# Local import to avoid a circular dependency at module load time.
from . import factories as _factories

source_label = f"<in-memory:{name}>"
doc = yaml.safe_load(yaml_text)
# Validate eagerly so problems surface at registration time.
geom = _construct_from_doc(doc, source_label=source_label, overrides={})
registry = _factories._GEOMETRY_REGISTRY # noqa: SLF001
if name in registry:
existing = registry[name]
raise ValueError(
f"register_geometry_yaml: a geometry named {name!r} is "
f"already registered ({existing!r}); pass a different "
f"name= to register this YAML under another name."
)

# Capture yaml_text in the closure so each call re-parses the same
# source. This mirrors register_geometry_file's re-read semantics
# (every make_geometry() returns a fresh AdHocDiffractometer).
def _factory(**kwargs: Any) -> AdHocDiffractometer:
fresh = yaml.safe_load(yaml_text)
return _construct_from_doc(fresh, source_label=source_label, overrides=kwargs)

_factory.__name__ = name
_factory.__doc__ = geom.description.splitlines()[0] if geom.description else ""
registry[name] = _factory
return name


def _register_packaged_geometries() -> None:
"""Register every ``*.yml`` file shipped under
:mod:`ad_hoc_diffractometer.geometries` with the geometry registry.
Expand Down
92 changes: 92 additions & 0 deletions tests/test_geometry_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
``detector``, ``reference``)
- :func:`register_geometry_file` (registry insertion, name override,
duplicate-name guard)
- :func:`register_geometry_yaml` (in-memory companion: registry
insertion, required ``name=``, duplicate-name guard, eager schema
validation, re-parse on every call)
- module-level constants ``KIND_KEY``, ``SUPPORTED_REVISIONS``,
``CURRENT_REVISION``
"""
Expand All @@ -26,12 +29,14 @@

import os
import re
from contextlib import nullcontext as does_not_raise

import numpy as np
import pytest

from ad_hoc_diffractometer import load_geometry_file
from ad_hoc_diffractometer import register_geometry_file
from ad_hoc_diffractometer import register_geometry_yaml
from ad_hoc_diffractometer.factories import _GEOMETRY_REGISTRY
from ad_hoc_diffractometer.factories import BASIS_BL
from ad_hoc_diffractometer.factories import BASIS_DEFAULT
Expand Down Expand Up @@ -493,6 +498,93 @@ def test_register_geometry_file_missing(tmp_path, restore_registry):
register_geometry_file(tmp_path / "does_not_exist.yml")


# ---------------------------------------------------------------------------
# register_geometry_yaml() (issue #288 — in-memory companion)
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
"kwargs, expected_name, context",
[
pytest.param(
{"name": "inmem_alpha"},
"inmem_alpha",
does_not_raise(),
id="registers-under-supplied-name",
),
pytest.param(
{},
None,
pytest.raises(TypeError, match="name"),
id="name-is-required",
),
],
)
def test_register_geometry_yaml_name_argument(
kwargs, expected_name, context, restore_registry
):
"""``name`` is a required keyword-only argument."""
with context:
returned = register_geometry_yaml(_MINIMAL_YAML, **kwargs)
assert returned == expected_name
assert expected_name in _GEOMETRY_REGISTRY


def test_register_geometry_yaml_make_geometry_round_trip(restore_registry):
"""make_geometry() on a registered in-memory geometry produces an
AdHocDiffractometer consistent with load_geometry_file(yaml_text)."""
import ad_hoc_diffractometer as ahd

register_geometry_yaml(_MINIMAL_YAML, name="inmem_round_trip")
g_registry = ahd.make_geometry("inmem_round_trip")
g_direct = load_geometry_file(_MINIMAL_YAML)
assert g_registry.name == g_direct.name == "tiny"
assert list(g_registry._stages) == list(g_direct._stages)
assert list(g_registry.modes) == list(g_direct.modes)


def test_register_geometry_yaml_duplicate_raises(restore_registry):
"""Second registration under the same name raises ValueError
(same contract as register_geometry_file)."""
register_geometry_yaml(_MINIMAL_YAML, name="inmem_dup")
with pytest.raises(ValueError, match="already registered"):
register_geometry_yaml(_MINIMAL_YAML, name="inmem_dup")


def test_register_geometry_yaml_eager_schema_validation(restore_registry):
"""Schema errors surface at registration time, not at first
make_geometry() call (matches register_geometry_file behavior)."""
# Drop the required top-level 'stages' key.
bad_yaml = (
"ad_hoc_diffractometer_geometry:\n"
" schema_revision: 1\n"
"name: broken\n"
"basis: BL\n"
"modes:\n"
" m:\n"
" default: true\n"
" constraints: []\n"
" computed: []\n"
)
with pytest.raises(GeometrySchemaError, match="stages"):
register_geometry_yaml(bad_yaml, name="inmem_broken")
# Registration was rejected — nothing landed in the registry.
assert "inmem_broken" not in _GEOMETRY_REGISTRY


def test_register_geometry_yaml_factory_reparses_text(restore_registry):
"""Each make_geometry() call re-parses the captured YAML text, so
every call returns a fresh AdHocDiffractometer instance (mirroring
register_geometry_file's per-call re-read semantics)."""
import ad_hoc_diffractometer as ahd

register_geometry_yaml(_MINIMAL_YAML, name="inmem_fresh")
g1 = ahd.make_geometry("inmem_fresh")
g2 = ahd.make_geometry("inmem_fresh")
assert g1 is not g2
assert g1.name == g2.name == "tiny"


# ---------------------------------------------------------------------------
# load_geometry_file rejects unknown kwargs
# ---------------------------------------------------------------------------
Expand Down