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) 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 f6b825a19..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 @@ -441,7 +443,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 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 @@ -786,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. @@ -849,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 diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 192a6b65f..9daa0327e 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) and w.widget_type == "Container": + ret[w.label] = w.asdict() + return ret def update( self,