Skip to content

Commit

Permalink
generics for containers (#500)
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Nov 13, 2022
1 parent 964e697 commit 14d82dc
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 48 deletions.
2 changes: 1 addition & 1 deletion magicgui/widgets/_bases/container_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
Any,
Callable,
Iterable,
Mapping,
MutableSequence,
Sequence,
TypeVar,
overload,
)

from psygnal import Signal
from pyparsing import Mapping

from magicgui._util import debounce
from magicgui.application import use_app
Expand Down
75 changes: 29 additions & 46 deletions magicgui/widgets/_concrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
All of these widgets should provide the `widget_type` argument to their
super().__init__ calls.
"""

from __future__ import annotations

import contextlib
import inspect
import math
import os
Expand Down Expand Up @@ -50,6 +52,9 @@
)

BUILDING_DOCS = sys.argv[-2:] == ["build", "docs"]
WidgetVar = TypeVar("WidgetVar", bound=Widget)
C = TypeVar("C", bound=type)
_V = TypeVar("_V")


def _param_list_to_str(param_list: list[DocstringParam]) -> str:
Expand All @@ -71,7 +76,9 @@ def _param_list_to_str(param_list: list[DocstringParam]) -> str:
return "\n".join(out)


def merge_super_sigs(cls, exclude=("widget_type", "kwargs", "args", "kwds", "extra")):
def merge_super_sigs(
cls: C, exclude=("widget_type", "kwargs", "args", "kwds", "extra")
) -> C:
"""Merge the signature and kwarg docs from all superclasses, for clearer docs.
Parameters
Expand Down Expand Up @@ -109,7 +116,7 @@ def merge_super_sigs(cls, exclude=("widget_type", "kwargs", "args", "kwds", "ext
k: v.replace(annotation=inspect.Parameter.empty) for k, v in params.items()
}

cls.__init__.__signature__ = inspect.Signature(
cls.__init__.__signature__ = inspect.Signature( # type: ignore
sorted(params.values(), key=lambda x: x.kind)
)
param_docs = [p for p in param_docs if p.arg_name not in exclude]
Expand All @@ -120,15 +127,12 @@ def merge_super_sigs(cls, exclude=("widget_type", "kwargs", "args", "kwds", "ext
return cls


C = TypeVar("C")


@overload
def backend_widget( # noqa
cls: type[C],
cls: C,
widget_name: str = None,
transform: Callable[[type], type] = None,
) -> type[C]:
) -> C:
...


Expand All @@ -137,15 +141,15 @@ def backend_widget( # noqa
cls: Literal[None] = None,
widget_name: str = None,
transform: Callable[[type], type] = None,
) -> Callable[..., type[C]]:
) -> Callable[..., C]:
...


def backend_widget(
cls: type[C] = None,
cls: C = None,
widget_name: str = None,
transform: Callable[[type], type] = None,
) -> Callable | type[C]:
) -> Callable | C:
"""Decorate cls to inject the backend widget of the same name.
The purpose of this decorator is to "inject" the appropriate backend
Expand Down Expand Up @@ -434,7 +438,7 @@ def __init__(self, choices=(), orientation="vertical", **kwargs):


@backend_widget
class Container(ContainerWidget):
class Container(ContainerWidget[WidgetVar]):
"""A Widget to contain other widgets."""


Expand Down Expand Up @@ -555,7 +559,7 @@ def __repr__(self) -> str:


@merge_super_sigs
class RangeEdit(Container):
class RangeEdit(Container[SpinBox]):
"""A widget to represent a python range object, with start/stop/step.
A range object produces a sequence of integers from start (inclusive)
Expand Down Expand Up @@ -652,11 +656,8 @@ def value(self, value: slice):
self.step.value = value.step


_V = TypeVar("_V")


@merge_super_sigs
class ListEdit(Container):
class ListEdit(Container[ValueWidget]):
"""A widget to represent a list of values.
A ListEdit container can create a list with multiple objects of same type. It
Expand Down Expand Up @@ -750,20 +751,12 @@ def annotation(self, value):
self._annotation = value
self._args_type = arg

def __iter__(self) -> Iterator[ValueWidget]:
"""Just for typing."""
return super().__iter__()

def __getitem__(self, key: int) -> ValueWidget: # type: ignore[override]
"""Just for typing."""
return super().__getitem__(key)

def __delitem__(self, key: int | slice):
def __delitem__(self, key: int | slice) -> None:
"""Delete child widget(s)."""
super().__delitem__(key)
self.changed.emit(self.value)

def _append_value(self, value=Undefined):
def _append_value(self, value: _V | _Undefined = Undefined) -> None:
"""Create a new child value widget and append it."""
i = len(self) - 2

Expand All @@ -787,12 +780,10 @@ def _append_value(self, value=Undefined):
widget.changed.connect(lambda: self.changed.emit(self.value))
self.changed.emit(self.value)

def _pop_value(self):
def _pop_value(self) -> None:
"""Delete last child value widget."""
try:
with contextlib.suppress(IndexError):
self.pop(-3)
except IndexError:
pass

@property
def value(self) -> list[_V]:
Expand Down Expand Up @@ -822,7 +813,7 @@ class ListDataView(Generic[_V]):

def __init__(self, obj: ListEdit):
self._obj = obj
self._widgets: list[ValueWidget] = list(obj[:-2]) # type: ignore
self._widgets = list(obj[:-2])

def __repr__(self):
"""Return list-like representation."""
Expand All @@ -837,11 +828,11 @@ def __eq__(self, other):
return list(self) == other

@overload
def __getitem__(self, i: int) -> _V: # noqa
def __getitem__(self, i: int) -> _V: # noqa: D105
...

@overload
def __getitem__(self, key: slice) -> list[_V]: # noqa
def __getitem__(self, key: slice) -> list[_V]: # noqa: D105
...

def __getitem__(self, key):
Expand All @@ -856,11 +847,11 @@ def __getitem__(self, key):
)

@overload
def __setitem__(self, key: int, value: _V) -> None: # noqa
def __setitem__(self, key: int, value: _V) -> None: # noqa: D105
...

@overload
def __setitem__(self, key: slice, value: _V | Iterable[_V]) -> None: # noqa
def __setitem__(self, key: slice, value: _V | Iterable[_V]) -> None: # noqa: D105
...

def __setitem__(self, key, value):
Expand All @@ -882,11 +873,11 @@ def __setitem__(self, key, value):
)

@overload
def __delitem__(self, key: int) -> None: # noqa
def __delitem__(self, key: int) -> None: # noqa: D105
...

@overload
def __delitem__(self, key: slice) -> None: # noqa
def __delitem__(self, key: slice) -> None: # noqa: D105
...

def __delitem__(self, key):
Expand All @@ -900,7 +891,7 @@ def __iter__(self) -> Iterator[_V]:


@merge_super_sigs
class TupleEdit(Container):
class TupleEdit(Container[ValueWidget]):
"""A widget to represent a tuple of values.
A TupleEdit container has several child widgets of different type. Their value is
Expand Down Expand Up @@ -952,14 +943,6 @@ def __init__(
widget.changed.disconnect()
widget.changed.connect(lambda: self.changed.emit(self.value))

def __iter__(self) -> Iterator[ValueWidget]:
"""Just for typing."""
return super().__iter__()

def __getitem__(self, key: int) -> ValueWidget: # type: ignore[override]
"""Just for typing."""
return super().__getitem__(key)

@property
def annotation(self):
"""Return type annotation for the parameter represented by the widget.
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ ignore_missing_imports = True
[mypy-.examples/,magicgui._mpl_image.*,magicgui.backends._qtpy.widgets.*]
ignore_errors = True

[mypy-.examples,numpy.*,_pytest.*,packaging.*,pyparsing.*,importlib_metadata.*,docstring_parser.*,psygnal.*,qtpy.*,imageio.*,ipywidgets.*]
[mypy-.examples,numpy.*,_pytest.*,packaging.*,importlib_metadata.*,docstring_parser.*,psygnal.*,qtpy.*,imageio.*,ipywidgets.*]
ignore_errors = True

[mypy-tomli.*]
Expand Down

0 comments on commit 14d82dc

Please sign in to comment.