From 700e9f96dc232a2d92209d33ae22c4d8c0f01ea5 Mon Sep 17 00:00:00 2001 From: Michael Rohdenburg Date: Thu, 5 Sep 2024 13:26:54 +0200 Subject: [PATCH 1/4] feat: enable nested pydantic models in widget creation --- 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 f6b825a19..c1f5a8a78 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) -> ValueWidget[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 + 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 192a6b65f..30ebaef90 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -335,11 +335,13 @@ def labels(self, value: bool) -> None: 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): + ret[w.label] = w.asdict() + return ret def update( self, From f1edb9a7b002d8151495049a0d8c83401d01e356 Mon Sep 17 00:00:00 2001 From: Michael Rohdenburg Date: Fri, 6 Sep 2024 13:02:31 +0200 Subject: [PATCH 2/4] refactor: add type for nested value widgets --- src/magicgui/schema/_guiclass.py | 4 +++- src/magicgui/schema/_ui_field.py | 14 +++++++++----- src/magicgui/types.py | 10 +++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/magicgui/schema/_guiclass.py b/src/magicgui/schema/_guiclass.py index fce6a30bb..7018b5095 100644 --- a/src/magicgui/schema/_guiclass.py +++ b/src/magicgui/schema/_guiclass.py @@ -26,6 +26,8 @@ from typing_extensions import TypeGuard + from magicgui.types import NestedValueWidgets + # fmt: off class GuiClassProtocol(Protocol): """Protocol for a guiclass.""" @@ -206,7 +208,7 @@ def __set_name__(self, owner: type, name: str) -> None: def __get__( self, instance: object | None, owner: type - ) -> ContainerWidget[ValueWidget]: + ) -> ContainerWidget[NestedValueWidgets]: wdg = build_widget(owner if instance is None else instance) # look for @button-decorated methods diff --git a/src/magicgui/schema/_ui_field.py b/src/magicgui/schema/_ui_field.py index c1f5a8a78..8af12d6f8 100644 --- a/src/magicgui/schema/_ui_field.py +++ b/src/magicgui/schema/_ui_field.py @@ -32,6 +32,7 @@ from attrs import Attribute from pydantic.fields import FieldInfo, ModelField + from magicgui.types import NestedValueWidgets from magicgui.widgets.bases import ContainerWidget, ValueWidget class HasAttrs(Protocol): @@ -45,7 +46,6 @@ class HasAttrs(Protocol): SLOTS = {"slots": True} if sys.version_info >= (3, 10) else {} T = TypeVar("T") - @dataclass(frozen=True, **SLOTS) class UiField(Generic[T]): """Metadata about a specific widget in a GUI.""" @@ -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) -> ValueWidget[T]: + def create_widget(self, + value: T | _Undefined = Undefined + ) -> ValueWidget[T] | NestedValueWidgets: """Create a new Widget for this field.""" from magicgui.type_map import get_widget_class @@ -448,7 +450,7 @@ def create_widget(self, value: T | _Undefined = Undefined) -> ValueWidget[T]: except ValueError: try: wdg = build_widget(self.type) - wdg.label = self.name + wdg.label = self.name if self.name else "" return wdg except TypeError as e: raise TypeError( @@ -799,7 +801,7 @@ def _uifields_to_container( values: Mapping[str, Any] | None = None, *, container_kwargs: Mapping | None = None, -) -> ContainerWidget[ValueWidget]: +) -> ContainerWidget[NestedValueWidgets]: """Create a container widget from a sequence of UiFields. This function is the heart of build_widget. @@ -862,7 +864,9 @@ def _get_values(obj: Any) -> dict | None: # TODO: unify this with magicgui -def build_widget(cls_or_instance: Any) -> ContainerWidget[ValueWidget]: +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) return _uifields_to_container(get_ui_fields(cls_or_instance), values=values) diff --git a/src/magicgui/types.py b/src/magicgui/types.py index e2c42fb9a..c102f9f77 100644 --- a/src/magicgui/types.py +++ b/src/magicgui/types.py @@ -10,7 +10,12 @@ if TYPE_CHECKING: from magicgui.widgets import FunctionGui - from magicgui.widgets.bases import CategoricalWidget, Widget + from magicgui.widgets.bases import ( + CategoricalWidget, + ContainerWidget, + ValueWidget, + Widget, + ) from magicgui.widgets.protocols import WidgetProtocol @@ -28,6 +33,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["ValueWidget", "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 From 6980abb49bb604996710d4a6d928e048f4948500 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:11:08 +0000 Subject: [PATCH 3/4] style(pre-commit.ci): auto fixes [...] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb2f048c4..bb8ecb0d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ **Fixed bugs:** -- fix: Fix checking sublcasses of Future [\#665](https://github.com/pyapp-kit/magicgui/pull/665) ([Czaki](https://github.com/Czaki)) +- fix: Fix checking subclasses of Future [\#665](https://github.com/pyapp-kit/magicgui/pull/665) ([Czaki](https://github.com/Czaki)) ## [v0.9.0](https://github.com/pyapp-kit/magicgui/tree/v0.9.0) (2024-08-05) From 15263c66942e2b82d49e3cf35a1dc1fa71b1305e Mon Sep 17 00:00:00 2001 From: Michael Rohdenburg Date: Tue, 10 Sep 2024 11:34:18 +0200 Subject: [PATCH 4/4] fix: only call `asdict()` recursively on `Container`s --- src/magicgui/widgets/bases/_container_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 30ebaef90..9daa0327e 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -339,7 +339,7 @@ def asdict(self) -> dict[str, Any]: for w in self._list: if w.name and not w.gui_only: ret[w.name] = getattr(w, "value", None) - if isinstance(w, ContainerWidget): + if isinstance(w, ContainerWidget) and w.widget_type == "Container": ret[w.label] = w.asdict() return ret