From 11c2f87478ef3fb141ed6998bb8a360fb46b710f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 00:34:53 -0400 Subject: [PATCH 1/2] WIP: one widget class per id, one builder per output type --- src/pyqt_reactive/forms/form_init_service.py | 5 +++++ src/pyqt_reactive/forms/widget_registry.py | 17 +++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/pyqt_reactive/forms/form_init_service.py b/src/pyqt_reactive/forms/form_init_service.py index f886fc9e..60c98589 100644 --- a/src/pyqt_reactive/forms/form_init_service.py +++ b/src/pyqt_reactive/forms/form_init_service.py @@ -400,6 +400,11 @@ def __init__(self) -> None: def builder_for(self, output_type: Type, service_name: str): """Decorator to register builder function and auto-generate service class.""" def decorator(func: Callable) -> Callable: + if output_type in self._builders_by_output: + raise ValueError( + f"{output_type.__qualname__} already has a builder " + f"({self._builders_by_output[output_type].service_name})." + ) self._builders_by_output[output_type] = BuilderSpec(output_type, service_name, func) return func return decorator diff --git a/src/pyqt_reactive/forms/widget_registry.py b/src/pyqt_reactive/forms/widget_registry.py index 54fcee26..9124676e 100644 --- a/src/pyqt_reactive/forms/widget_registry.py +++ b/src/pyqt_reactive/forms/widget_registry.py @@ -64,12 +64,17 @@ def __new__(cls, name, bases, attrs): logger.debug(f"Skipping registration for {name} - no _widget_id attribute") return new_class - # Check for duplicate registration - if widget_id in WIDGET_IMPLEMENTATIONS: - existing = WIDGET_IMPLEMENTATIONS[widget_id] - logger.warning( - f"Widget ID '{widget_id}' already registered to {existing.__name__}. " - f"Overwriting with {name}." + # A widget id names one widget class. The same class statement + # run again (a reload) takes its id back. + existing = WIDGET_IMPLEMENTATIONS.get(widget_id) + if existing is not None and ( + existing.__module__ != new_class.__module__ + or existing.__qualname__ != new_class.__qualname__ + ): + raise ValueError( + f"Widget ID '{widget_id}' is already registered to " + f"{existing.__module__}.{existing.__qualname__}; " + f"{new_class.__module__}.{new_class.__qualname__} cannot use it too." ) # Auto-register in global registry From d24a53980b234939df3c3c288f9d7a190c9eb827 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 12 Oct 2026 08:50:56 -0400 Subject: [PATCH 2/2] 0.7.2: one widget class per widget id, one builder per form output type; require metaclass-registry 0.4 Raises lower bounds to objectstate 3.0.1, formbridge 0.1.3 and zmqruntime 0.6.4. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 8 ++++---- requirements-ci.txt | 3 ++- src/pyqt_reactive/__init__.py | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c0b370..94c7cf3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.7.2 + +Requires `metaclass-registry>=0.4.0`, `objectstate>=3.0.1,<4`, +`formbridge>=0.1.3,<0.2` and `zmqruntime>=0.6.4,<0.7`. + +Changed: + +- A widget id names one widget class. Registering a different class under an + id that is already taken raises `ValueError`; it used to log a warning and + replace the first class. The same class statement run again (a reload) + takes its id back. +- A form output type has one builder: `InitializationServiceCatalog` raises + `ValueError` when a second builder declares an output type that already + has one. + ## 0.7.1 Requires ObjectState 3 (`objectstate>=3.0,<4`) and `formbridge>=0.1.2,<0.2`. diff --git a/pyproject.toml b/pyproject.toml index 4a0a852c..d6709b1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,15 +25,15 @@ keywords = ["pyqt6", "forms", "reactive", "gui", "widgets", "dataclass"] dependencies = [ "annotated-types>=0.7.0", - "formbridge>=0.1.2,<0.2", + "formbridge>=0.1.3,<0.2", "magicgui>=0.7.0", - "metaclass-registry>=0.1.5", + "metaclass-registry>=0.4.0", "PyQt6>=6.4.0", - "objectstate>=3.0,<4", + "objectstate>=3.0.1,<4", "psutil>=5.9", "Pygments>=2.15", "python-introspect>=0.2.6,<0.3", - "zmqruntime>=0.6,<0.7", + "zmqruntime>=0.6.4,<0.7", ] [project.optional-dependencies] diff --git a/requirements-ci.txt b/requirements-ci.txt index 8167663e..6a1d9ea8 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -1,2 +1,3 @@ # Source candidates needed to validate this checkout before coordinated publication. -# Empty: every first-party dependency is released (objectstate 3.0.0, formbridge 0.1.2). +# Empty: every first-party dependency is released (objectstate 3.0.1, +# formbridge 0.1.3, zmqruntime 0.6.4, metaclass-registry 0.4.0). diff --git a/src/pyqt_reactive/__init__.py b/src/pyqt_reactive/__init__.py index 059a5e1f..c91af9c7 100644 --- a/src/pyqt_reactive/__init__.py +++ b/src/pyqt_reactive/__init__.py @@ -19,7 +19,7 @@ - Cross-window reactive updates """ -__version__ = "0.7.1" +__version__ = "0.7.2" # Public API will be populated as modules are added __all__ = [