diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index c3fed16..af16211 100644 --- a/doc/release_notes/release_1.04.md +++ b/doc/release_notes/release_1.04.md @@ -12,6 +12,7 @@ * **Group UUID from the remote client**: `add_group` of the remote client (`sigima.client`) now returns the UUID of the created group, so that objects can be added to it with `group_id=...` (DataLab ≥ 1.4.0; older versions return `None`). * **Pixel grid conversions**: New `ImageObj` methods convert between physical coordinates and pixel indices consistently with ROIs: `physical_to_pixel_range` and `pixel_range_to_physical` (rectangle and range of pixels), `axis_to_float_indices` and `axis_to_physical` (fractional indices, for uniform and non-uniform coordinates) and `get_roi_crop_indices` (part of the image holding a ROI). `transformer.transform_like_image` (`sigima.proc.image`) moves a geometry result as a flip, a transposition or a 90° or 270° rotation moves the pixels of the image. +* **Removal of file formats**: The I/O registries (`SignalIORegistry`, `ImageIORegistry`) have new `unregister_format` and `register_format` methods, so that a plugin defining file formats can remove them from Sigima when it is unloaded, and register them again, without accessing the registry internals. * **NPZ object archives**: New functions `serialize_object_to_npz` and `deserialize_object_from_npz` (`sigima.io`) convert a signal or an image to and from an in-memory archive of NumPy arrays and JSON metadata, keeping labels, units, uncertainties, uniform or non-uniform image coordinates and object metadata, without ever pickling arrays. This is the format used by the Web API of DataLab to transfer objects, so that DataLab and its clients (such as DataLab-Kernel) share one implementation. The [user guide](../user_guide/overview.rst) shows how readers and connectors attach experimental-source metadata, save and restore it, and send an acquisition to a DataLab group. Source recording is opt-in and does not make the experiment reproducible. diff --git a/sigima/io/base.py b/sigima/io/base.py index 1cff805..7d49804 100644 --- a/sigima/io/base.py +++ b/sigima/io/base.py @@ -164,6 +164,49 @@ def get_formats(cls) -> list[FormatBase]: """ return cls._io_format_instances + @classmethod + def register_format(cls, format_class: type[FormatBase]) -> bool: + """Register the handler of a format class, if it is not registered + + Format classes are registered when they are defined: this method registers + again a format class removed with :meth:`unregister_format`. + + Args: + format_class: format class of this registry + + Returns: + True if the format was registered, False if it already was + + Raises: + TypeError: if format_class is not a format class of this registry + """ + if not isinstance(format_class, cls): + raise TypeError(f"{format_class!r} is not a {cls.__name__} format class") + if any(fmt.__class__ is format_class for fmt in cls._io_format_instances): + return False + cls._io_format_instances.append(format_class()) + return True + + @classmethod + def unregister_format(cls, format_class: type[FormatBase]) -> bool: + """Unregister the handler of a format class, e.g. when the plugin defining + it is unloaded + + Handlers of subclasses of format_class are kept: they are formats of their + own. + + Args: + format_class: format class of this registry + + Returns: + True if the format was unregistered, False if it was not registered + """ + instances = cls._io_format_instances + kept = [fmt for fmt in instances if fmt.__class__ is not format_class] + removed = len(kept) != len(instances) + instances[:] = kept + return removed + @classmethod def get_format_info(cls, mode: Literal["rst", "text"] = "rst") -> str: """Return I/O format info diff --git a/sigima/tests/io/addnewformat_unit_test.py b/sigima/tests/io/addnewformat_unit_test.py index 86aa03e..3a868fc 100644 --- a/sigima/tests/io/addnewformat_unit_test.py +++ b/sigima/tests/io/addnewformat_unit_test.py @@ -9,6 +9,7 @@ from typing import Type import numpy as np +import pytest from sigima.io import ImageIORegistry, SignalIORegistry from sigima.io.base import FormatInfo @@ -120,6 +121,28 @@ def test_add_signal_format() -> None: assert finfo_str in SignalIORegistry.get_format_info(mode="text") +def test_unregister_and_register_format() -> None: + """Test removing a format from the registry, then registering it again""" + signal_class = _add_signal_format() + image_class = _add_image_format() + for registry, fmt_class in ( + (SignalIORegistry, signal_class), + (ImageIORegistry, image_class), + ): + count = len(registry.get_formats()) + assert registry.unregister_format(fmt_class) + assert len(registry.get_formats()) == count - 1 + assert not any(fmt.__class__ is fmt_class for fmt in registry.get_formats()) + assert not registry.unregister_format(fmt_class) + assert registry.register_format(fmt_class) + assert not registry.register_format(fmt_class) + assert sum(fmt.__class__ is fmt_class for fmt in registry.get_formats()) == 1 + assert registry.unregister_format(fmt_class) + with pytest.raises(TypeError, match="is not a SignalIORegistry format class"): + SignalIORegistry.register_format(image_class) + + if __name__ == "__main__": test_add_image_format() test_add_signal_format() + test_unregister_and_register_format()