Skip to content

Commit

Permalink
Make all the valued containers subclass ValueWidget (#663)
Browse files Browse the repository at this point in the history
* use ValuedContainerWidget, refactor inheritance

* typing

* rename

* remove backend EmptyWidget

* style(pre-commit.ci): auto fixes [...]

* update docs references to BaseValueWidget methods

* add missing references to docs

* fix references in TypeMap doc

---------

Co-authored-by: Talley Lambert <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 22, 2024
1 parent a5669e3 commit 4bc81e9
Show file tree
Hide file tree
Showing 23 changed files with 520 additions and 346 deletions.
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

0 comments on commit 4bc81e9

Please sign in to comment.