Skip to content

refactor: Make all the valued containers subclass ValueWidget #663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 22, 2024
5 changes: 5 additions & 0 deletions docs/api/type_map.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
magicgui.type_map.register_type
magicgui.type_map.type_registered
magicgui.type_map.type2callback
magicgui.type_map.TypeMap

::: magicgui.type_map.get_widget_class

Expand All @@ -13,3 +14,7 @@
::: magicgui.type_map.type_registered

::: magicgui.type_map.type2callback

::: magicgui.type_map.TypeMap
options:
show_signature_annotations: false
29 changes: 24 additions & 5 deletions docs/api/widgets/bases.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,33 @@ widgets. Therefore, it is worth being aware of the type of widget you are worki
magicgui.widgets.bases.Widget
magicgui.widgets.bases.ButtonWidget
magicgui.widgets.bases.CategoricalWidget
magicgui.widgets.bases.BaseContainerWidget
magicgui.widgets.bases.ContainerWidget
magicgui.widgets.bases.ValuedContainerWidget
magicgui.widgets.bases.DialogWidget
magicgui.widgets.bases.MainWindowWidget
magicgui.widgets.bases.RangedWidget
magicgui.widgets.bases.SliderWidget
magicgui.widgets.bases.ValueWidget
magicgui.widgets.bases.BaseValueWidget

## Class Hierarchy

In visual form, the widget class hierarchy looks like this:

``` mermaid
classDiagram
Widget <|-- ValueWidget
Widget <|-- ContainerWidget
Widget <|-- BaseValueWidget
BaseValueWidget <|-- ValueWidget
Widget <|-- BaseContainerWidget
BackendWidget ..|> WidgetProtocol : implements a
ValueWidget <|-- RangedWidget
ValueWidget <|-- ButtonWidget
ValueWidget <|-- CategoricalWidget
RangedWidget <|-- SliderWidget
BaseContainerWidget <|-- ContainerWidget
BaseContainerWidget <|-- ValuedContainerWidget
BaseValueWidget <|-- ValuedContainerWidget
Widget --* WidgetProtocol : controls a
<<Interface>> WidgetProtocol
class WidgetProtocol {
Expand All @@ -53,12 +60,14 @@ classDiagram
close()
render()
}
class ValueWidget{
class BaseValueWidget{
value: Any
changed: SignalInstance
bind(value, call) Any
unbind()
}
class ValueWidget{
}
class RangedWidget{
value: float | tuple
min: float
Expand All @@ -78,7 +87,7 @@ classDiagram
class CategoricalWidget{
choices: List[Any]
}
class ContainerWidget{
class BaseContainerWidget{
widgets: List[Widget]
labels: bool
layout: str
Expand All @@ -89,12 +98,13 @@ classDiagram
}

click Widget href "#magicgui.widgets.bases.Widget"
click BaseValueWidget href "#magicgui.widgets.bases.BaseValueWidget"
click ValueWidget href "#magicgui.widgets.bases.ValueWidget"
click RangedWidget href "#magicgui.widgets.bases.RangedWidget"
click SliderWidget href "#magicgui.widgets.bases.SliderWidget"
click ButtonWidget href "#magicgui.widgets.bases.ButtonWidget"
click CategoricalWidget href "#magicgui.widgets.bases.CategoricalWidget"
click ContainerWidget href "#magicgui.widgets.bases.ContainerWidget"
click BaseContainerWidget href "#magicgui.widgets.bases.BaseContainerWidget"

```

Expand All @@ -109,9 +119,15 @@ classDiagram
::: magicgui.widgets.bases.CategoricalWidget
options:
heading_level: 3
::: magicgui.widgets.bases.BaseContainerWidget
options:
heading_level: 3
::: magicgui.widgets.bases.ContainerWidget
options:
heading_level: 3
::: magicgui.widgets.bases.ValuedContainerWidget
options:
heading_level: 3
::: magicgui.widgets.bases.DialogWidget
options:
heading_level: 3
Expand All @@ -127,3 +143,6 @@ classDiagram
::: magicgui.widgets.bases.ValueWidget
options:
heading_level: 3
::: magicgui.widgets.bases.BaseValueWidget
options:
heading_level: 3
2 changes: 2 additions & 0 deletions docs/scripts/_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ def _replace_autosummary(md: str) -> str:
if name:
module, _name = name.rsplit(".", 1)
obj = getattr(import_module(module), _name)
if obj.__doc__ is None:
raise ValueError(f"Missing docstring for {obj}")
table.append(f"| [`{_name}`][{name}] | {obj.__doc__.splitlines()[0]} |")
lines[start:last_line] = table
return "\n".join(lines)
Expand Down
2 changes: 1 addition & 1 deletion docs/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ following `ValueWidgets` track some `value`:
|-----------|------|-------------|
| `value` | `Any` | The current value of the widget. |
| `changed` | [`psygnal.SignalInstance`][psygnal.SignalInstance] | A [`psygnal.SignalInstance`][psygnal.SignalInstance] that will emit an event when the `value` has changed. Connect callbacks to the change event using `widget.changed.connect(callback)` |
| `bind` | `Any, optional` | A value or callback to bind this widget. If bound, whenever `widget.value` is accessed, the value provided here will be returned. The bound value can be a callable, in which case `bound_value(self)` will be returned (i.e. your callback must accept a single parameter, which is this widget instance.). see [`ValueWidget.bind`][magicgui.widgets.bases.ValueWidget.bind] for details. |
| `bind` | `Any, optional` | A value or callback to bind this widget. If bound, whenever `widget.value` is accessed, the value provided here will be returned. The bound value can be a callable, in which case `bound_value(self)` will be returned (i.e. your callback must accept a single parameter, which is this widget instance.). see [`ValueWidget.bind`][magicgui.widgets.bases.BaseValueWidget.bind] for details. |

Here is a demonstration of all these:

Expand Down
2 changes: 0 additions & 2 deletions src/magicgui/backends/_ipynb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
Container,
DateEdit,
DateTimeEdit,
EmptyWidget,
FloatSlider,
FloatSpinBox,
Label,
Expand Down Expand Up @@ -34,7 +33,6 @@
"DateEdit",
"TimeEdit",
"DateTimeEdit",
"EmptyWidget",
"FloatSlider",
"FloatSpinBox",
"Label",
Expand Down
13 changes: 0 additions & 13 deletions src/magicgui/backends/_ipynb/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,6 @@ def _mgui_render(self):
pass


class EmptyWidget(_IPyWidget):
_ipywidget: ipywdg.Widget

def _mgui_get_value(self) -> Any:
raise NotImplementedError()

def _mgui_set_value(self, value: Any) -> None:
raise NotImplementedError()

def _mgui_bind_change_callback(self, callback: Callable):
pass


class _IPyValueWidget(_IPyWidget, protocols.ValueWidgetProtocol):
def _mgui_get_value(self) -> float:
return self._ipywidget.value
Expand Down
2 changes: 0 additions & 2 deletions src/magicgui/backends/_qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
DateEdit,
DateTimeEdit,
Dialog,
EmptyWidget,
FloatRangeSlider,
FloatSlider,
FloatSpinBox,
Expand Down Expand Up @@ -41,7 +40,6 @@
"DateEdit",
"DateTimeEdit",
"Dialog",
"EmptyWidget",
"FloatRangeSlider",
"FloatSlider",
"FloatSpinBox",
Expand Down
17 changes: 0 additions & 17 deletions src/magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,23 +232,6 @@ def _pre_set_hook(self, value: Any) -> Any:
return value


# BASE WIDGET


class EmptyWidget(QBaseWidget):
def __init__(self, **kwargs: Any) -> None:
super().__init__(QtW.QWidget, **kwargs)

def _mgui_get_value(self) -> Any:
raise NotImplementedError()

def _mgui_set_value(self, value: Any) -> None:
raise NotImplementedError()

def _mgui_bind_change_callback(self, callback: Callable) -> None:
pass


# STRING WIDGETS


Expand Down
6 changes: 3 additions & 3 deletions src/magicgui/schema/_guiclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from magicgui.schema._ui_field import build_widget
from magicgui.widgets import PushButton
from magicgui.widgets.bases import ContainerWidget, ValueWidget
from magicgui.widgets.bases import BaseValueWidget, ContainerWidget

if TYPE_CHECKING:
from collections.abc import Mapping
Expand Down Expand Up @@ -206,7 +206,7 @@ def __set_name__(self, owner: type, name: str) -> None:

def __get__(
self, instance: object | None, owner: type
) -> ContainerWidget[ValueWidget]:
) -> ContainerWidget[BaseValueWidget]:
wdg = build_widget(owner if instance is None else instance)

# look for @button-decorated methods
Expand Down Expand Up @@ -317,7 +317,7 @@ def unbind_gui_from_instance(gui: ContainerWidget, instance: Any) -> None:
An instance of a `guiclass`.
"""
for widget in gui:
if isinstance(widget, ValueWidget):
if isinstance(widget, BaseValueWidget):
widget.changed.disconnect_setattr(instance, widget.name, missing_ok=True)


Expand Down
8 changes: 4 additions & 4 deletions src/magicgui/schema/_ui_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from attrs import Attribute
from pydantic.fields import FieldInfo, ModelField

from magicgui.widgets.bases import ContainerWidget, ValueWidget
from magicgui.widgets.bases import BaseValueWidget, ContainerWidget

class HasAttrs(Protocol):
"""Protocol for objects that have an ``attrs`` attribute."""
Expand Down Expand Up @@ -394,7 +394,7 @@ 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) -> BaseValueWidget[T]:
"""Create a new Widget for this field."""
from magicgui.type_map import get_widget_class

Expand Down Expand Up @@ -786,7 +786,7 @@ def _uifields_to_container(
values: Mapping[str, Any] | None = None,
*,
container_kwargs: Mapping | None = None,
) -> ContainerWidget[ValueWidget]:
) -> ContainerWidget[BaseValueWidget]:
"""Create a container widget from a sequence of UiFields.

This function is the heart of build_widget.
Expand Down Expand Up @@ -849,7 +849,7 @@ 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[BaseValueWidget]:
"""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)
8 changes: 5 additions & 3 deletions src/magicgui/type_map/_type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class MissingWidget(RuntimeError):
PathLike: widgets.FileEdit,
}

_SIMPLE_TYPES_DEFAULTS = {
_SIMPLE_TYPES_DEFAULTS: dict[type, type[widgets.Widget]] = {
bool: widgets.CheckBox,
int: widgets.SpinBox,
float: widgets.FloatSpinBox,
Expand All @@ -85,6 +85,8 @@ class MissingWidget(RuntimeError):


class TypeMap:
"""Storage for mapping from types to widgets and callbacks."""

def __init__(
self,
*,
Expand Down Expand Up @@ -404,7 +406,7 @@ def create_widget(

This factory function can be used to create a widget appropriate for the
provided `value` and/or `annotation` provided. See
[Type Mapping Docs](../../type_map.md) for details on how the widget type is
[Type Mapping Docs](../type_map.md) for details on how the widget type is
determined from type annotations.

Parameters
Expand Down Expand Up @@ -453,7 +455,7 @@ def create_widget(
------
TypeError
If the provided or autodetected `widget_type` does not implement any known
[widget protocols](../protocols.md)
[widget protocols](protocols.md)

Examples
--------
Expand Down
Loading