From 23bf77430537cdbf6035c8b40744a50a9fea03c1 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:55:04 +0100 Subject: [PATCH] feat(io): unregister and register file formats Plugins defining file formats can remove them from the I/O registries when they are unloaded, and register them again, without accessing the registry internals. Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_1.04.md | 1 + sigima/io/base.py | 43 +++++++++++++++++++++++ sigima/tests/io/addnewformat_unit_test.py | 23 ++++++++++++ 3 files changed, 67 insertions(+) diff --git a/doc/release_notes/release_1.04.md b/doc/release_notes/release_1.04.md index c3fed16a..af162118 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 1cff805a..7d498043 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 86aa03e5..3a868fc7 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()