Skip to content
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

style: use Unpack for better kwargs typing #599

Merged
merged 5 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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