Skip to content

Commit

Permalink
style: use Unpack for better kwargs typing (#599)
Browse files Browse the repository at this point in the history
* style: use Unpack for kwargs

* remove another deprecation

* more kwargs

* fix napari test

* cleanup table
  • Loading branch information
tlambert03 authored Oct 10, 2023
1 parent 2eaa144 commit 3aa4b81
Show file tree
Hide file tree
Showing 13 changed files with 123 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
python-version: "3.10"
- name: Install
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade pip pytest-pretty
python -m pip install -e .[testing]
python -m pip install -e ./napari-from-github[pyqt5]
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["gui", "widgets", "type annotations"]
readme = "README.md"
requires-python = ">=3.8"
license = { text = "MIT" }
authors = [{ email = "[email protected]" }, { name = "Talley Lambert" }]
authors = [{ email = "[email protected]", name = "Talley Lambert" }]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: Qt",
Expand All @@ -31,7 +31,6 @@ classifiers = [
"Topic :: Software Development :: User Interfaces",
"Topic :: Software Development :: Widget Sets",
"Topic :: Utilities",

]
dynamic = ["version"]
dependencies = [
Expand Down Expand Up @@ -230,6 +229,7 @@ disallow_any_generics = false
disallow_subclassing_any = false
show_error_codes = true
pretty = true
enable_incomplete_feature = ['Unpack']

[[tool.mypy.overrides]]
module = [
Expand Down
14 changes: 9 additions & 5 deletions src/magicgui/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
from magicgui.types import Undefined

if TYPE_CHECKING:
from typing_extensions import Unpack

from magicgui.application import AppRef
from magicgui.widgets import Container, Widget
from magicgui.widgets.bases._container_widget import ContainerKwargs

TZ_EMPTY = "__no__default__"


Expand Down Expand Up @@ -229,14 +233,14 @@ def widgets(self, app: AppRef | None = None) -> MappingProxyType:
{n: p.to_widget(app) for n, p in self.parameters.items()}
)

def to_container(self, **kwargs: Any) -> Container:
def to_container(
self, app: AppRef | None = None, **kwargs: Unpack[ContainerKwargs]
) -> Container:
"""Return a ``magicgui.widgets.Container`` for this MagicSignature."""
from magicgui.widgets import Container

return Container(
widgets=list(self.widgets(kwargs.get("app")).values()),
**kwargs,
)
kwargs["widgets"] = list(self.widgets(app).values())
return Container(**kwargs)

def replace( # type: ignore[override]
self,
Expand Down
12 changes: 8 additions & 4 deletions src/magicgui/type_map/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ def magic_factory(
)


MAGICGUI_PARAMS = inspect.signature(magicgui).parameters


# _R is the return type of the decorated function
# _T is the type of the FunctionGui instance (FunctionGui or MainFunctionGui)
class MagicFactory(partial, Generic[_R, _T]):
Expand Down Expand Up @@ -467,22 +470,23 @@ def __new__(

def __repr__(self) -> str:
"""Return string repr."""
params = inspect.signature(magicgui).parameters
args = [
f"{k}={v!r}"
for (k, v) in self.keywords.items()
if v not in (params[k].default, {})
if v not in (MAGICGUI_PARAMS[k].default, {})
]
return f"MagicFactory({', '.join(args)})"

def __call__(self, *args: Any, **kwargs: Any) -> _T:
"""Call the wrapped _magicgui and return a FunctionGui."""
if args:
raise ValueError("MagicFactory instance only accept keyword arguments")
params = inspect.signature(magicgui).parameters

factory_kwargs = self.keywords.copy()
prm_options = factory_kwargs.pop("param_options", {})
prm_options.update({k: kwargs.pop(k) for k in list(kwargs) if k not in params})
prm_options.update(
{k: kwargs.pop(k) for k in list(kwargs) if k not in MAGICGUI_PARAMS}
)
widget = self.func(param_options=prm_options, **{**factory_kwargs, **kwargs})
if self._widget_init is not None:
self._widget_init(widget)
Expand Down
60 changes: 25 additions & 35 deletions src/magicgui/widgets/_concrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@
from magicgui.widgets.bases._mixins import _OrientationMixin, _ReadOnlyMixin

if TYPE_CHECKING:
from typing_extensions import Unpack

from magicgui.widgets import protocols
from magicgui.widgets.bases._container_widget import ContainerKwargs
from magicgui.widgets.bases._widget import WidgetKwargs


WidgetVar = TypeVar("WidgetVar", bound=Widget)
WidgetTypeVar = TypeVar("WidgetTypeVar", bound=Type[Widget])
Expand Down Expand Up @@ -283,32 +288,13 @@ def __init__(
max: float = 100,
base: float = math.e,
tracking: bool = True,
**kwargs: Any,
**kwargs: Unpack[WidgetKwargs],
):
# sourcery skip: avoid-builtin-shadow
for key in ("maximum", "minimum"):
if key in kwargs:
import warnings

warnings.warn(
f"The {key!r} keyword arguments has been changed to {key[:3]!r}. "
"In the future this will raise an exception\n",
FutureWarning,
stacklevel=2,
)
if key == "maximum":
max = kwargs.pop(key) # noqa: A001
else:
min = kwargs.pop(key) # noqa: A001
self._base = base
app = use_app()
assert app.native
super().__init__(
min=min,
max=max,
widget_type=app.get_obj("Slider"),
**kwargs,
)
kwargs["widget_type"] = app.get_obj("Slider")
super().__init__(min=min, max=max, **kwargs)
self.tracking = tracking

@property
Expand Down Expand Up @@ -371,7 +357,10 @@ class RadioButtons(CategoricalWidget, _OrientationMixin): # type: ignore
"""An exclusive group of radio buttons, providing a choice from multiple choices."""

def __init__(
self, choices: ChoicesType = (), orientation: str = "vertical", **kwargs: Any
self,
choices: ChoicesType = (),
orientation: str = "vertical",
**kwargs: Unpack[WidgetKwargs],
) -> None:
app = use_app()
assert app.native
Expand Down Expand Up @@ -422,9 +411,10 @@ def __init__(
mode: FileDialogMode = FileDialogMode.EXISTING_FILE,
filter: str | None = None,
nullable: bool = False,
**kwargs: Any,
**kwargs: Unpack[ContainerKwargs],
) -> None:
value = kwargs.pop("value", None)
# use empty string as a null value
value = kwargs.pop("value", None) # type: ignore [typeddict-item]
if value is None:
value = ""
self.line_edit = LineEdit(value=value)
Expand Down Expand Up @@ -533,9 +523,9 @@ def __init__(
step: int = 1,
min: int | tuple[int, int, int] | None = None,
max: int | tuple[int, int, int] | None = None,
**kwargs: Any,
**kwargs: Unpack[ContainerKwargs],
) -> None:
value = kwargs.pop("value", None)
value = kwargs.pop("value", None) # type: ignore [typeddict-item]
if value is not None and value is not Undefined:
if not all(hasattr(value, x) for x in ("start", "stop", "step")):
raise TypeError(f"Invalid value type for {type(self)}: {type(value)}")
Expand All @@ -548,7 +538,7 @@ def __init__(
kwargs["widgets"] = [self.start, self.stop, self.step]
kwargs.setdefault("layout", "horizontal")
kwargs.setdefault("labels", True)
kwargs.pop("nullable", None)
kwargs.pop("nullable", None) # type: ignore [typeddict-item]
super().__init__(**kwargs)

@classmethod
Expand Down Expand Up @@ -631,13 +621,13 @@ class ListEdit(Container[ValueWidget[_V]]):
All additional keyword arguments are passed to `Container` constructor.
"""

def __init__(
def __init__( # type: ignore [misc] # overlap between names
self,
value: Iterable[_V] | _Undefined = Undefined,
layout: str = "horizontal",
nullable: bool = False,
options: dict | None = None,
**kwargs: Any,
**kwargs: Unpack[ContainerKwargs],
) -> None:
self._args_type: type | None = None
self._nullable = nullable
Expand Down Expand Up @@ -888,14 +878,14 @@ def __init__(
self,
value: Iterable[_V] | _Undefined = Undefined,
*,
layout: str = "horizontal",
nullable: bool = False,
options: dict | None = None,
**kwargs: Any,
**container_kwargs: Unpack[ContainerKwargs[ValueWidget]],
) -> None:
self._nullable = nullable
self._args_types: tuple[type, ...] | None = None
super().__init__(layout=layout, labels=False, **kwargs)
container_kwargs["labels"] = False
super().__init__(**container_kwargs)
self._child_options = options or {}
self.margins = (0, 0, 0, 0)

Expand Down Expand Up @@ -982,12 +972,12 @@ def value(self, vals: Sequence) -> None:
class _LabeledWidget(Container):
"""Simple container that wraps a widget and provides a label."""

def __init__(
def __init__( # type: ignore [misc] # overlap between argument names
self,
widget: Widget,
label: str | None = None,
position: str = "left",
**kwargs: Any,
**kwargs: Unpack[ContainerKwargs],
) -> None:
kwargs["layout"] = "horizontal" if position in {"left", "right"} else "vertical"
self._inner_widget = widget
Expand Down
10 changes: 7 additions & 3 deletions src/magicgui/widgets/_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
if TYPE_CHECKING:
import numpy
import pandas
from typing_extensions import TypeGuard
from typing_extensions import TypeGuard, Unpack

from magicgui.widgets.protocols import TableWidgetProtocol

from .bases._widget import WidgetKwargs

TblKey = Any
_KT = TypeVar("_KT") # Key type
_KT_co = TypeVar("_KT_co", covariant=True) # Key type covariant containers.
Expand Down Expand Up @@ -227,9 +230,10 @@ def __init__(
*,
index: Collection | None = None,
columns: Collection | None = None,
**kwargs: Any,
**kwargs: Unpack[WidgetKwargs],
) -> None:
super().__init__(widget_type=use_app().get_obj("Table"), **kwargs)
kwargs["widget_type"] = use_app().get_obj("Table")
super().__init__(**kwargs)
self._data = DataView(self)
data, _index, _columns = normalize_table_data(value)
self.value = {
Expand Down
8 changes: 6 additions & 2 deletions src/magicgui/widgets/bases/_button_widget.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Callable

from psygnal import Signal, SignalInstance

Expand All @@ -9,8 +9,12 @@
from ._value_widget import ValueWidget

if TYPE_CHECKING:
from typing_extensions import Unpack

from magicgui.widgets import protocols

from ._widget import WidgetKwargs


class ButtonWidget(ValueWidget[bool]):
"""Widget with a value, Wraps a widget implementing the ButtonWidgetProtocol.
Expand Down Expand Up @@ -50,7 +54,7 @@ def __init__(
text: str | None = None,
bind: bool | Callable[[ValueWidget], bool] | _Undefined = Undefined,
nullable: bool = False,
**base_widget_kwargs: Any,
**base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
if text and base_widget_kwargs.get("label"):
from warnings import warn
Expand Down
6 changes: 5 additions & 1 deletion src/magicgui/widgets/bases/_categorical_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
from ._value_widget import T, ValueWidget

if TYPE_CHECKING:
from typing_extensions import Unpack

from magicgui.widgets import protocols

from ._widget import WidgetKwargs


class CategoricalWidget(ValueWidget[T]):
"""Widget with a value and choices. Wraps CategoricalWidgetProtocol.
Expand Down Expand Up @@ -45,7 +49,7 @@ def __init__(
allow_multiple: bool | None = None,
bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined,
nullable: bool = False,
**base_widget_kwargs: Any,
**base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
if allow_multiple is not None:
self._allow_multiple = allow_multiple
Expand Down
28 changes: 22 additions & 6 deletions src/magicgui/widgets/bases/_container_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
TYPE_CHECKING,
Any,
Callable,
Generic,
Iterable,
Mapping,
MutableSequence,
Expand All @@ -25,12 +26,23 @@
from ._value_widget import ValueWidget
from ._widget import Widget

WidgetVar = TypeVar("WidgetVar", bound=Widget)

if TYPE_CHECKING:
import inspect
from pathlib import Path

from typing_extensions import Unpack

from magicgui.widgets import Container, protocols
WidgetVar = TypeVar("WidgetVar", bound=Widget)

from ._widget import WidgetKwargs

class ContainerKwargs(WidgetKwargs, Generic[WidgetVar], total=False):
widgets: Sequence[WidgetVar]
layout: str
scrollable: bool
labels: bool


class ContainerWidget(Widget, _OrientationMixin, MutableSequence[WidgetVar]):
Expand Down Expand Up @@ -94,14 +106,13 @@ def __init__(
layout: str = "vertical",
scrollable: bool = False,
labels: bool = True,
**base_widget_kwargs: Any,
**base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
self._list: list[WidgetVar] = []
self._labels = labels
self._layout = layout
self._scrollable = scrollable
base_widget_kwargs.setdefault("backend_kwargs", {})
base_widget_kwargs["backend_kwargs"].update(
base_widget_kwargs.setdefault("backend_kwargs", {}).update( # type: ignore
{"layout": layout, "scrollable": scrollable}
)
super().__init__(**base_widget_kwargs)
Expand Down Expand Up @@ -284,13 +295,18 @@ def __signature__(self) -> MagicSignature:
return MagicSignature(params)

@classmethod
def from_signature(cls, sig: inspect.Signature, **kwargs: Any) -> Container:
def from_signature(
cls, sig: inspect.Signature, **kwargs: Unpack[ContainerKwargs]
) -> Container:
"""Create a Container widget from an inspect.Signature object."""
return MagicSignature.from_signature(sig).to_container(**kwargs)

@classmethod
def from_callable(
cls, obj: Callable, gui_options: dict | None = None, **kwargs: Any
cls,
obj: Callable,
gui_options: dict | None = None,
**kwargs: Unpack[ContainerKwargs],
) -> Container:
"""Create a Container widget from a callable object.
Expand Down
Loading

0 comments on commit 3aa4b81

Please sign in to comment.