From 8faf455e1a014ecb38bc7970f80bcf16b2e37421 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:22:47 +0000 Subject: [PATCH 1/3] ci(pre-commit.ci): autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/crate-ci/typos: v1.29.9 → v1.30.0](https://github.com/crate-ci/typos/compare/v1.29.9...v1.30.0) - [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd402778..046c9455 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,12 +10,12 @@ repos: - id: validate-pyproject - repo: https://github.com/crate-ci/typos - rev: v1.29.9 + rev: v1.30.0 hooks: - id: typos - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.7 + rev: v0.9.9 hooks: - id: ruff args: ["--fix", "--unsafe-fixes"] From 26ab96ee88600c55620ca09c5decc30928d6a1bf Mon Sep 17 00:00:00 2001 From: Michael Rohdenburg Date: Thu, 19 Jun 2025 17:43:51 +0200 Subject: [PATCH 2/3] feat: enable nesting of model container widgets --- src/magicgui/schema/_ui_field.py | 15 ++++++++++++++- src/magicgui/widgets/bases/_container_widget.py | 12 +++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/magicgui/schema/_ui_field.py b/src/magicgui/schema/_ui_field.py index a14c5abd..c74519e0 100644 --- a/src/magicgui/schema/_ui_field.py +++ b/src/magicgui/schema/_ui_field.py @@ -441,7 +441,20 @@ def create_widget(self, value: T | _Undefined = Undefined) -> BaseValueWidget[T] opts["min"] = d["exclusive_minimum"] + m value = value if value is not Undefined else self.get_default() # type: ignore - cls, kwargs = get_widget_class(value=value, annotation=self.type, options=opts) + try: + cls, kwargs = get_widget_class( + value=value, annotation=self.type, options=opts + ) + except ValueError: + try: + wdg = build_widget(self.type) + wdg.label = self.name if self.name else "" + return wdg + except TypeError as e: + raise TypeError( + f"Could not create widget for field {self.name!r} ", + f"with value {value!r}", + ) from e return cls(**kwargs) # type: ignore diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 9415b2ed..fdda48a9 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -426,11 +426,13 @@ def __repr__(self) -> str: def asdict(self) -> dict[str, Any]: """Return state of widget as dict.""" - return { - w.name: getattr(w, "value", None) - for w in self._list - if w.name and not w.gui_only - } + ret = {} + for w in self._list: + if w.name and not w.gui_only: + ret[w.name] = getattr(w, "value", None) + if isinstance(w, ContainerWidget) and w.widget_type == "Container": + ret[w.label] = w.asdict() + return ret def update( self, From 0b88814bc5c249503d312d513a78be309673dff0 Mon Sep 17 00:00:00 2001 From: Michael Rohdenburg Date: Thu, 19 Jun 2025 18:12:59 +0200 Subject: [PATCH 3/3] refactor: add proper typing for nested widgets --- src/magicgui/schema/_guiclass.py | 4 +++- src/magicgui/schema/_ui_field.py | 10 ++++++---- src/magicgui/types.py | 10 +++++++++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/magicgui/schema/_guiclass.py b/src/magicgui/schema/_guiclass.py index bd2ca157..723d3adb 100644 --- a/src/magicgui/schema/_guiclass.py +++ b/src/magicgui/schema/_guiclass.py @@ -27,6 +27,8 @@ from typing_extensions import TypeGuard + from magicgui.types import NestedValueWidgets + # fmt: off class GuiClassProtocol(Protocol): """Protocol for a guiclass.""" @@ -243,7 +245,7 @@ def widget(self) -> ContainerWidget: def __get__( self, instance: object | None, owner: type - ) -> ContainerWidget[BaseValueWidget] | GuiBuilder: + ) -> ContainerWidget[NestedValueWidgets] | GuiBuilder: if instance is None: return self wdg = build_widget(instance) diff --git a/src/magicgui/schema/_ui_field.py b/src/magicgui/schema/_ui_field.py index addb7551..8ceca626 100644 --- a/src/magicgui/schema/_ui_field.py +++ b/src/magicgui/schema/_ui_field.py @@ -20,7 +20,7 @@ from typing_extensions import TypeGuard, get_args, get_origin -from magicgui.types import JsonStringFormats, Undefined, _Undefined +from magicgui.types import JsonStringFormats, NestedValueWidgets, Undefined, _Undefined if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping @@ -394,7 +394,9 @@ def parse_annotated(self) -> UiField[T]: kwargs.pop("name", None) return dc.replace(self, **kwargs) - def create_widget(self, value: T | _Undefined = Undefined) -> BaseValueWidget[T]: + def create_widget( + self, value: T | _Undefined = Undefined + ) -> BaseValueWidget[T] | NestedValueWidgets: """Create a new Widget for this field.""" from magicgui.type_map import get_widget_class @@ -799,7 +801,7 @@ def _uifields_to_container( values: Mapping[str, Any] | None = None, *, container_kwargs: Mapping | None = None, -) -> ContainerWidget[BaseValueWidget]: +) -> ContainerWidget[NestedValueWidgets]: """Create a container widget from a sequence of UiFields. This function is the heart of build_widget. @@ -862,7 +864,7 @@ def _get_values(obj: Any) -> dict | None: # TODO: unify this with magicgui -def build_widget(cls_or_instance: Any) -> ContainerWidget[BaseValueWidget]: +def build_widget(cls_or_instance: Any) -> ContainerWidget[NestedValueWidgets]: """Build a magicgui widget from a dataclass, attrs, pydantic, or function.""" values = None if isinstance(cls_or_instance, type) else _get_values(cls_or_instance) fields = get_ui_fields(cls_or_instance) diff --git a/src/magicgui/types.py b/src/magicgui/types.py index 9ebfdab9..ec844535 100644 --- a/src/magicgui/types.py +++ b/src/magicgui/types.py @@ -11,7 +11,12 @@ if TYPE_CHECKING: from magicgui.widgets import FunctionGui - from magicgui.widgets.bases import CategoricalWidget, Widget + from magicgui.widgets.bases import ( + BaseValueWidget, + CategoricalWidget, + ContainerWidget, + Widget, + ) from magicgui.widgets.protocols import WidgetProtocol @@ -29,6 +34,9 @@ class ChoicesDict(TypedDict): WidgetRef = Union[str, WidgetClass] #: A :attr:`WidgetClass` (or a string representation of one) and a dict of kwargs WidgetTuple = tuple[WidgetRef, dict[str, Any]] +#: A [`ValueWidget`][magicgui.widgets.ValueWidget] class or a +#: [`ContainerWidget`][magicgui.widgets.ContainerWidget] class for nesting those +NestedValueWidgets = Union["BaseValueWidget", "ContainerWidget[NestedValueWidgets]"] #: An iterable that can be used as a valid argument for widget ``choices`` ChoicesIterable = Union[Iterable[tuple[str, Any]], Iterable[Any]] #: An callback that can be used as a valid argument for widget ``choices``. It takes