From 21ee4524c9b0ffc8cd3253f6e3cc080008c89d37 Mon Sep 17 00:00:00 2001
From: Grzegorz Bokota
Date: Wed, 4 Oct 2023 17:15:53 +0200
Subject: [PATCH 01/25] fix: prevent dupe calls, alternative (#546)
* use variant generation to simplify discover callbacks
* add test checking that order is not important
* remove obsolete file
* fix tests
* fix type_registered
---------
Co-authored-by: Talley Lambert
---
src/magicgui/type_map/_type_map.py | 137 ++++++++++++++++-------------
tests/conftest.py | 9 ++
tests/test_magicgui.py | 36 +++++++-
tests/test_types.py | 35 ++++++++
4 files changed, 157 insertions(+), 60 deletions(-)
diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py
index 33571c73d..cc422846e 100644
--- a/src/magicgui/type_map/_type_map.py
+++ b/src/magicgui/type_map/_type_map.py
@@ -3,6 +3,7 @@
import datetime
import inspect
+import itertools
import os
import pathlib
import sys
@@ -366,6 +367,65 @@ def _validate_return_callback(func: Callable) -> None:
_T = TypeVar("_T", bound=type)
+def _register_type_callback(
+ resolved_type: _T,
+ return_callback: ReturnCallback | None = None,
+) -> list[type]:
+ modified_callbacks = []
+ if return_callback is None:
+ return []
+ _validate_return_callback(return_callback)
+ # if the type is a Union, add the callback to all of the types in the union
+ # (except NoneType)
+ if get_origin(resolved_type) is Union:
+ for type_per in _generate_union_variants(resolved_type):
+ if return_callback not in _RETURN_CALLBACKS[type_per]:
+ _RETURN_CALLBACKS[type_per].append(return_callback)
+ modified_callbacks.append(type_per)
+
+ for t in get_args(resolved_type):
+ if not _is_none_type(t) and return_callback not in _RETURN_CALLBACKS[t]:
+ _RETURN_CALLBACKS[t].append(return_callback)
+ modified_callbacks.append(t)
+ elif return_callback not in _RETURN_CALLBACKS[resolved_type]:
+ _RETURN_CALLBACKS[resolved_type].append(return_callback)
+ modified_callbacks.append(resolved_type)
+ return modified_callbacks
+
+
+def _register_widget(
+ resolved_type: _T,
+ widget_type: WidgetRef | None = None,
+ **options: Any,
+) -> WidgetTuple | None:
+ _options = cast(dict, options)
+
+ previous_widget = _TYPE_DEFS.get(resolved_type)
+
+ if "choices" in _options:
+ _TYPE_DEFS[resolved_type] = (widgets.ComboBox, _options)
+ if widget_type is not None:
+ warnings.warn(
+ "Providing `choices` overrides `widget_type`. Categorical widget "
+ f"will be used for type {resolved_type}",
+ stacklevel=2,
+ )
+ elif widget_type is not None:
+ if not isinstance(widget_type, (str, WidgetProtocol)) and not (
+ inspect.isclass(widget_type) and issubclass(widget_type, widgets.Widget)
+ ):
+ raise TypeError(
+ '"widget_type" must be either a string, WidgetProtocol, or '
+ "Widget subclass"
+ )
+ _TYPE_DEFS[resolved_type] = (widget_type, _options)
+ elif "bind" in _options:
+ # if we're binding a value to this parameter, it doesn't matter what type
+ # of ValueWidget is used... it usually won't be shown
+ _TYPE_DEFS[resolved_type] = (widgets.EmptyWidget, _options)
+ return previous_widget
+
+
@overload
def register_type(
type_: _T,
@@ -435,43 +495,11 @@ def register_type(
"must be provided."
)
- def _deco(type_: _T) -> _T:
- resolved_type = resolve_single_type(type_)
- if return_callback is not None:
- _validate_return_callback(return_callback)
- # if the type is a Union, add the callback to all of the types in the union
- # (except NoneType)
- if get_origin(resolved_type) is Union:
- for t in get_args(resolved_type):
- if not _is_none_type(t):
- _RETURN_CALLBACKS[t].append(return_callback)
- else:
- _RETURN_CALLBACKS[resolved_type].append(return_callback)
-
- _options = cast(dict, options)
-
- if "choices" in _options:
- _TYPE_DEFS[resolved_type] = (widgets.ComboBox, _options)
- if widget_type is not None:
- warnings.warn(
- "Providing `choices` overrides `widget_type`. Categorical widget "
- f"will be used for type {resolved_type}",
- stacklevel=2,
- )
- elif widget_type is not None:
- if not isinstance(widget_type, (str, WidgetProtocol)) and not (
- inspect.isclass(widget_type) and issubclass(widget_type, widgets.Widget)
- ):
- raise TypeError(
- '"widget_type" must be either a string, WidgetProtocol, or '
- "Widget subclass"
- )
- _TYPE_DEFS[resolved_type] = (widget_type, _options)
- elif "bind" in _options:
- # if we're binding a value to this parameter, it doesn't matter what type
- # of ValueWidget is used... it usually won't be shown
- _TYPE_DEFS[resolved_type] = (widgets.EmptyWidget, _options)
- return type_
+ def _deco(type__: _T) -> _T:
+ resolved_type = resolve_single_type(type__)
+ _register_type_callback(resolved_type, return_callback)
+ _register_widget(resolved_type, widget_type, **options)
+ return type__
return _deco if type_ is None else _deco(type_)
@@ -507,23 +535,19 @@ def type_registered(
"""
resolved_type = resolve_single_type(type_)
- # check if return_callback is already registered
- rc_was_present = return_callback in _RETURN_CALLBACKS.get(resolved_type, [])
# store any previous widget_type and options for this type
- prev_type_def: WidgetTuple | None = _TYPE_DEFS.get(resolved_type, None)
- resolved_type = register_type(
- resolved_type,
- widget_type=widget_type,
- return_callback=return_callback,
- **options,
- )
+
+ revert_list = _register_type_callback(resolved_type, return_callback)
+ prev_type_def = _register_widget(resolved_type, widget_type, **options)
+
new_type_def: WidgetTuple | None = _TYPE_DEFS.get(resolved_type, None)
try:
yield
finally:
# restore things to before the context
- if return_callback is not None and not rc_was_present:
- _RETURN_CALLBACKS[resolved_type].remove(return_callback)
+ if return_callback is not None: # this if is only for mypy
+ for return_callback_type in revert_list:
+ _RETURN_CALLBACKS[return_callback_type].remove(return_callback)
if _TYPE_DEFS.get(resolved_type, None) is not new_type_def:
warnings.warn("Type definition changed during context", stacklevel=2)
@@ -537,9 +561,6 @@ def type_registered(
def type2callback(type_: type) -> list[ReturnCallback]:
"""Return any callbacks that have been registered for ``type_``.
- Note that if the return type is X, then the callbacks registered for Optional[X]
- will be returned also be returned.
-
Parameters
----------
type_ : type
@@ -555,7 +576,7 @@ def type2callback(type_: type) -> list[ReturnCallback]:
# look for direct hits ...
# if it's an Optional, we need to look for the type inside the Optional
- _, type_ = _is_optional(resolve_single_type(type_))
+ type_ = resolve_single_type(type_)
if type_ in _RETURN_CALLBACKS:
return _RETURN_CALLBACKS[type_]
@@ -566,10 +587,8 @@ def type2callback(type_: type) -> list[ReturnCallback]:
return []
-def _is_optional(type_: Any) -> tuple[bool, type]:
- # TODO: this function is too similar to _type_optional above... need to combine
- if get_origin(type_) is Union:
- args = get_args(type_)
- if len(args) == 2 and any(_is_none_type(i) for i in args):
- return True, next(i for i in args if not _is_none_type(i))
- return False, type_
+def _generate_union_variants(type_: Any) -> Iterator[type]:
+ type_args = get_args(type_)
+ for i in range(2, len(type_args) + 1):
+ for per in itertools.combinations(type_args, i):
+ yield cast(type, Union[per])
diff --git a/tests/conftest.py b/tests/conftest.py
index df4ad5936..c45364c53 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -17,3 +17,12 @@ def always_qapp(qapp):
for w in qapp.topLevelWidgets():
w.close()
w.deleteLater()
+
+
+@pytest.fixture(autouse=True, scope="function")
+def _clean_return_callbacks():
+ from magicgui.type_map._type_map import _RETURN_CALLBACKS
+
+ yield
+
+ _RETURN_CALLBACKS.clear()
diff --git a/tests/test_magicgui.py b/tests/test_magicgui.py
index d96cec7fd..c20d00316 100644
--- a/tests/test_magicgui.py
+++ b/tests/test_magicgui.py
@@ -4,7 +4,7 @@
import inspect
from enum import Enum
-from typing import NewType, Optional
+from typing import NewType, Optional, Union
from unittest.mock import Mock
import pytest
@@ -901,3 +901,37 @@ def func_optional(a: bool) -> ReturnType:
mock.reset_mock()
func_optional(a=False)
mock.assert_called_once_with(func_optional, None, ReturnType)
+
+
+@pytest.mark.parametrize("optional", [True, False])
+def test_no_duplication_call(optional):
+ mock = Mock()
+ mock2 = Mock()
+
+ NewInt = NewType("NewInt", int)
+ register_type(Optional[NewInt], return_callback=mock)
+ register_type(NewInt, return_callback=mock)
+ register_type(NewInt, return_callback=mock2)
+ ReturnType = Optional[NewInt] if optional else NewInt
+
+ @magicgui
+ def func() -> ReturnType:
+ return NewInt(1)
+
+ func()
+
+ mock.assert_called_once()
+ assert mock2.call_count == (not optional)
+
+
+def test_no_order():
+ mock = Mock()
+
+ register_type(Union[int, None], return_callback=mock)
+
+ @magicgui
+ def func() -> Union[None, int]:
+ return 1
+
+ func()
+ mock.assert_called_once()
diff --git a/tests/test_types.py b/tests/test_types.py
index 83d89a62b..b2d24cb05 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -189,6 +189,41 @@ def test_type_registered_warns():
assert isinstance(widgets.create_widget(annotation=Path), widgets.FileEdit)
+def test_type_registered_optional_callbacks():
+ assert not _RETURN_CALLBACKS[int]
+ assert not _RETURN_CALLBACKS[Optional[int]]
+
+ @magicgui
+ def func1(a: int) -> int:
+ return a
+
+ @magicgui
+ def func2(a: int) -> Optional[int]:
+ return a
+
+ mock1 = Mock()
+ mock2 = Mock()
+ mock3 = Mock()
+
+ register_type(int, return_callback=mock2)
+
+ with type_registered(Optional[int], return_callback=mock1):
+ func1(1)
+ mock1.assert_called_once_with(func1, 1, int)
+ mock1.reset_mock()
+ func2(2)
+ mock1.assert_called_once_with(func2, 2, Optional[int])
+ mock1.reset_mock()
+ mock2.assert_called_once_with(func1, 1, int)
+ assert _RETURN_CALLBACKS[int] == [mock2, mock1]
+ assert _RETURN_CALLBACKS[Optional[int]] == [mock1]
+ register_type(Optional[int], return_callback=mock3)
+ assert _RETURN_CALLBACKS[Optional[int]] == [mock1, mock3]
+
+ assert _RETURN_CALLBACKS[Optional[int]] == [mock3]
+ assert _RETURN_CALLBACKS[int] == [mock2, mock3]
+
+
def test_pick_widget_literal():
from typing import Literal
From c5091a3ae81e262e919d8f686fb3fd52b12cbab3 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Thu, 5 Oct 2023 10:55:45 +0200
Subject: [PATCH 02/25] chore: remove setuppy (#595)
---
setup.py | 30 ------------------------------
tests/test_docs.py | 25 -------------------------
2 files changed, 55 deletions(-)
delete mode 100644 setup.py
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 3f6e2cd46..000000000
--- a/setup.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import sys
-
-sys.stderr.write(
- """
-===============================
-Unsupported installation method
-===============================
-magicgui does not support installation with `python setup.py install`.
-Please use `python -m pip install .` instead.
-"""
-)
-sys.exit(1)
-
-
-# The below code will never execute, however GitHub is particularly
-# picky about where it finds Python packaging metadata.
-# See: https://github.com/github/feedback/discussions/6456
-#
-# To be removed once GitHub catches up.
-
-setup(
- name="magicgui",
- install_requires=[
- "docstring_parser>=0.7",
- "psygnal>=0.5.0",
- "qtpy>=1.7.0",
- "superqt>=0.5.0",
- "typing_extensions",
- ],
-)
diff --git a/tests/test_docs.py b/tests/test_docs.py
index d80e45c8f..27f57d503 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -72,28 +72,3 @@ def test_examples(fname, monkeypatch):
if "waveform" in fname:
type_map._type_map._TYPE_DEFS.pop(int, None)
type_map._type_map._TYPE_DEFS.pop(float, None)
-
-
-@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11")
-def test_setuppy():
- """Ensure that setup.py matches pyproject deps.
-
- (setup.py is only there for github)
- """
- import ast
-
- import tomllib
-
- setup = Path(__file__).parent.parent / "setup.py"
- pyproject = Path(__file__).parent.parent / "pyproject.toml"
- settxt = setup.read_text(encoding="utf-8")
- deps = ast.literal_eval(settxt.split("install_requires=")[-1].split("]")[0] + "]")
-
- with open(pyproject, "rb") as f:
- data = tomllib.load(f)
-
- projdeps = set(data["project"]["dependencies"])
- assert projdeps == set(deps)
-
- min_req = data["project"]["optional-dependencies"]["min-req"]
- assert {k.replace(">=", "==") for k in projdeps} == set(min_req)
From 2eaa144b36e7cdd96a5dd8350b7b64a1d784d492 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Sun, 8 Oct 2023 14:26:49 -0400
Subject: [PATCH 03/25] Add python version to README.md (#596)
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index e816d3b38..2f862fb17 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,9 @@
+
+
+
From 3aa4b815aedf314ad2b3229dddc9c0f5f9816d5a Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Tue, 10 Oct 2023 18:43:17 -0400
Subject: [PATCH 04/25] style: use `Unpack` for better kwargs typing (#599)
* style: use Unpack for kwargs
* remove another deprecation
* more kwargs
* fix napari test
* cleanup table
---
.github/workflows/test_and_deploy.yml | 2 +-
pyproject.toml | 4 +-
src/magicgui/signature.py | 14 +++--
src/magicgui/type_map/_magicgui.py | 12 ++--
src/magicgui/widgets/_concrete.py | 60 ++++++++-----------
src/magicgui/widgets/_table.py | 10 +++-
src/magicgui/widgets/bases/_button_widget.py | 8 ++-
.../widgets/bases/_categorical_widget.py | 6 +-
.../widgets/bases/_container_widget.py | 28 +++++++--
src/magicgui/widgets/bases/_ranged_widget.py | 25 +++-----
src/magicgui/widgets/bases/_slider_widget.py | 8 ++-
src/magicgui/widgets/bases/_value_widget.py | 6 +-
src/magicgui/widgets/bases/_widget.py | 20 ++++++-
13 files changed, 123 insertions(+), 80 deletions(-)
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index 86a25a443..f5fa85af2 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -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]
diff --git a/pyproject.toml b/pyproject.toml
index e3a292e2f..a99286a0a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ keywords = ["gui", "widgets", "type annotations"]
readme = "README.md"
requires-python = ">=3.8"
license = { text = "MIT" }
-authors = [{ email = "talley.lambert@gmail.com" }, { name = "Talley Lambert" }]
+authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: X11 Applications :: Qt",
@@ -31,7 +31,6 @@ classifiers = [
"Topic :: Software Development :: User Interfaces",
"Topic :: Software Development :: Widget Sets",
"Topic :: Utilities",
-
]
dynamic = ["version"]
dependencies = [
@@ -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 = [
diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py
index d2db0115d..3b61c9a25 100644
--- a/src/magicgui/signature.py
+++ b/src/magicgui/signature.py
@@ -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__"
@@ -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,
diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py
index c4a8d81a6..27155a002 100644
--- a/src/magicgui/type_map/_magicgui.py
+++ b/src/magicgui/type_map/_magicgui.py
@@ -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]):
@@ -467,11 +470,10 @@ 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)})"
@@ -479,10 +481,12 @@ 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)
diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py
index 820096f3f..f7ea08ba3 100644
--- a/src/magicgui/widgets/_concrete.py
+++ b/src/magicgui/widgets/_concrete.py
@@ -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])
@@ -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
@@ -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
@@ -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)
@@ -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)}")
@@ -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
@@ -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
@@ -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)
@@ -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
diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py
index e347653f1..0dc833d89 100644
--- a/src/magicgui/widgets/_table.py
+++ b/src/magicgui/widgets/_table.py
@@ -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.
@@ -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 = {
diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py
index 52c2b2e2b..7deba402b 100644
--- a/src/magicgui/widgets/bases/_button_widget.py
+++ b/src/magicgui/widgets/bases/_button_widget.py
@@ -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
@@ -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.
@@ -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
diff --git a/src/magicgui/widgets/bases/_categorical_widget.py b/src/magicgui/widgets/bases/_categorical_widget.py
index 1eaf04eb8..a91eff746 100644
--- a/src/magicgui/widgets/bases/_categorical_widget.py
+++ b/src/magicgui/widgets/bases/_categorical_widget.py
@@ -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.
@@ -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
diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py
index e595548aa..a1fc17ee9 100644
--- a/src/magicgui/widgets/bases/_container_widget.py
+++ b/src/magicgui/widgets/bases/_container_widget.py
@@ -5,6 +5,7 @@
TYPE_CHECKING,
Any,
Callable,
+ Generic,
Iterable,
Mapping,
MutableSequence,
@@ -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]):
@@ -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)
@@ -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.
diff --git a/src/magicgui/widgets/bases/_ranged_widget.py b/src/magicgui/widgets/bases/_ranged_widget.py
index 685bf2877..b6a0e0933 100644
--- a/src/magicgui/widgets/bases/_ranged_widget.py
+++ b/src/magicgui/widgets/bases/_ranged_widget.py
@@ -3,16 +3,19 @@
import builtins
from abc import ABC, abstractmethod
from math import ceil, log10
-from typing import TYPE_CHECKING, Any, Callable, Iterable, Tuple, TypeVar, Union, cast
-from warnings import warn
+from typing import TYPE_CHECKING, Callable, Iterable, Tuple, TypeVar, Union, cast
from magicgui.types import Undefined, _Undefined
from ._value_widget import ValueWidget
if TYPE_CHECKING:
+ from typing_extensions import Unpack
+
from magicgui.widgets import protocols
+ from ._widget import WidgetKwargs
+
T = TypeVar("T", int, float, Tuple[Union[int, float], ...])
DEFAULT_MIN = 0.0
DEFAULT_MAX = 1000.0
@@ -56,20 +59,8 @@ def __init__(
step: float | _Undefined | None = Undefined,
bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined,
nullable: bool = False,
- **base_widget_kwargs: Any,
- ) -> None: # sourcery skip: avoid-builtin-shadow
- for key in ("maximum", "minimum"):
- if key in base_widget_kwargs:
- 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 = base_widget_kwargs.pop(key) # noqa: A001
- else:
- min = base_widget_kwargs.pop(key) # noqa: A001
+ **base_widget_kwargs: Unpack[WidgetKwargs],
+ ) -> None:
# value should be set *after* min max is set
super().__init__(
bind=bind, # type: ignore
@@ -242,7 +233,7 @@ def __init__(
step: int = 1,
bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined,
nullable: bool = False,
- **base_widget_kwargs: Any,
+ **base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
self._min = min
self._max = max
diff --git a/src/magicgui/widgets/bases/_slider_widget.py b/src/magicgui/widgets/bases/_slider_widget.py
index bcf732a49..2198c33ee 100644
--- a/src/magicgui/widgets/bases/_slider_widget.py
+++ b/src/magicgui/widgets/bases/_slider_widget.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Callable, Tuple, Union
+from typing import TYPE_CHECKING, Callable, Tuple, Union
from magicgui.types import Undefined, _Undefined
@@ -8,8 +8,12 @@
from ._ranged_widget import MultiValueRangedWidget, RangedWidget, T
if TYPE_CHECKING:
+ from typing_extensions import Unpack
+
from magicgui.widgets import protocols
+ from ._widget import WidgetKwargs
+
class SliderWidget(RangedWidget[T], _OrientationMixin):
"""Widget with a constrained value and orientation. Wraps SliderWidgetProtocol.
@@ -61,7 +65,7 @@ def __init__(
tracking: bool = True,
bind: T | Callable[[protocols.ValueWidgetProtocol], T] | _Undefined = Undefined,
nullable: bool = False,
- **base_widget_kwargs: Any,
+ **base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
base_widget_kwargs["backend_kwargs"] = {
"readout": readout,
diff --git a/src/magicgui/widgets/bases/_value_widget.py b/src/magicgui/widgets/bases/_value_widget.py
index 2ca345913..5ce3c0545 100644
--- a/src/magicgui/widgets/bases/_value_widget.py
+++ b/src/magicgui/widgets/bases/_value_widget.py
@@ -10,8 +10,12 @@
from ._widget import Widget
if TYPE_CHECKING:
+ from typing_extensions import Unpack
+
from magicgui.widgets import protocols
+ from ._widget import WidgetKwargs
+
T = TypeVar("T")
@@ -45,7 +49,7 @@ def __init__(
*,
bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined,
nullable: bool = False,
- **base_widget_kwargs: Any,
+ **base_widget_kwargs: Unpack[WidgetKwargs],
) -> None:
self._nullable = nullable
self._bound_value = bind
diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py
index 22540515d..791e7d7ad 100644
--- a/src/magicgui/widgets/bases/_widget.py
+++ b/src/magicgui/widgets/bases/_widget.py
@@ -15,10 +15,28 @@
from weakref import ReferenceType
import numpy as np
+ from typing_extensions import TypedDict
from magicgui.widgets._concrete import _LabeledWidget
from magicgui.widgets.protocols import WidgetProtocol
+ class WidgetKwargs(TypedDict, total=False):
+ # technically, this should be Required[type[WidgetProtocol]]]
+ # but the widget_type argument is generally provided dynamically
+ # by the @backend_widget decorator. So it appears to be missing if we require it
+ widget_type: type[WidgetProtocol]
+ name: str
+ annotation: Any | None
+ label: str | None
+ tooltip: str | None
+ visible: bool | None
+ enabled: bool
+ gui_only: bool
+ parent: Any
+ backend_kwargs: dict | None
+
+ description: str # alias for label
+
class Widget:
"""Basic Widget, wrapping a class that implements WidgetProtocol.
@@ -77,7 +95,7 @@ def __init__(
gui_only: bool = False,
parent: Any | None = None,
backend_kwargs: dict | None = None,
- **extra: Any,
+ **extra: Any, # not really used
):
# for ipywidgets API compatibility
if backend_kwargs is None:
From 05c92921a5a6e7dad3757557e73d09c2f559ac01 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Wed, 11 Oct 2023 07:36:18 -0400
Subject: [PATCH 05/25] chore: preserve magicgui-decorated function parameter
hints with ParamSpec (#600)
* chore: better param typing for magicgui decorated functions
* mainfunc
---
src/magicgui/type_map/_magicgui.py | 49 ++++++++++++++++-----------
src/magicgui/types.py | 2 +-
src/magicgui/widgets/_function_gui.py | 21 +++++++++---
3 files changed, 46 insertions(+), 26 deletions(-)
diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py
index 27155a002..1cec52d04 100644
--- a/src/magicgui/type_map/_magicgui.py
+++ b/src/magicgui/type_map/_magicgui.py
@@ -16,17 +16,22 @@
from magicgui.widgets import FunctionGui, MainFunctionGui
if TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+
from magicgui.application import AppRef
+
+ _P = ParamSpec("_P")
+
__all__ = ["magicgui", "magic_factory", "MagicFactory"]
_R = TypeVar("_R")
-_T = TypeVar("_T", bound=FunctionGui)
+_FGuiVar = TypeVar("_FGuiVar", bound=FunctionGui)
@overload
def magicgui(
- function: Callable[..., _R],
+ function: Callable[_P, _R],
*,
layout: str = "horizontal",
scrollable: bool = False,
@@ -40,7 +45,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> FunctionGui[_R]:
+) -> FunctionGui[_P, _R]:
...
@@ -60,13 +65,13 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[..., _R]], FunctionGui[_R]]:
+) -> Callable[[Callable[_P, _R]], FunctionGui[_P, _R]]:
...
@overload
def magicgui(
- function: Callable[..., _R],
+ function: Callable[_P, _R],
*,
layout: str = "horizontal",
scrollable: bool = False,
@@ -80,7 +85,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MainFunctionGui[_R]:
+) -> MainFunctionGui[_P, _R]:
...
@@ -100,7 +105,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[..., _R]], MainFunctionGui[_R]]:
+) -> Callable[[Callable[_P, _R]], MainFunctionGui[_P, _R]]:
...
@@ -206,7 +211,7 @@ def magicgui(
@overload
def magic_factory(
- function: Callable[..., _R],
+ function: Callable[_P, _R],
*,
layout: str = "horizontal",
scrollable: bool = False,
@@ -221,7 +226,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MagicFactory[_R, FunctionGui]:
+) -> MagicFactory[FunctionGui[_P, _R]]:
...
@@ -242,13 +247,13 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[..., _R]], MagicFactory[_R, FunctionGui]]:
+) -> Callable[[Callable[_P, _R]], MagicFactory[FunctionGui[_P, _R]]]:
...
@overload
def magic_factory(
- function: Callable[..., _R],
+ function: Callable[_P, _R],
*,
layout: str = "horizontal",
scrollable: bool = False,
@@ -263,7 +268,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MagicFactory[_R, MainFunctionGui]:
+) -> MagicFactory[MainFunctionGui[_P, _R]]:
...
@@ -284,7 +289,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[..., _R]], MagicFactory[_R, MainFunctionGui]]:
+) -> Callable[[Callable[_P, _R]], MagicFactory[MainFunctionGui[_P, _R]]]:
...
@@ -418,7 +423,7 @@ def magic_factory(
# _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]):
+class MagicFactory(partial, Generic[_FGuiVar]):
"""Factory function that returns a FunctionGui instance.
While this can be used directly, (see example below) the preferred usage is
@@ -436,15 +441,17 @@ class MagicFactory(partial, Generic[_R, _T]):
>>> widget2 = factory(auto_call=True, labels=True)
"""
- _widget_init: Callable[[_T], None] | None = None
- func: Callable[..., _T]
+ _widget_init: Callable[[_FGuiVar], None] | None = None
+ # func here is the function that will be called to create the widget
+ # i.e. it will be either the FunctionGui or MainFunctionGui class
+ func: Callable[..., _FGuiVar]
def __new__(
cls,
- function: Callable[..., _R],
+ function: Callable,
*args: Any,
- magic_class: type[_T] = FunctionGui, # type: ignore
- widget_init: Callable[[_T], None] | None = None,
+ magic_class: type[_FGuiVar] = FunctionGui, # type: ignore
+ widget_init: Callable[[_FGuiVar], None] | None = None,
**keywords: Any,
) -> MagicFactory:
"""Create new MagicFactory."""
@@ -477,7 +484,9 @@ def __repr__(self) -> str:
]
return f"MagicFactory({', '.join(args)})"
- def __call__(self, *args: Any, **kwargs: Any) -> _T:
+ # TODO: annotate args and kwargs here so that
+ # calling a MagicFactory instance gives proper mypy hints
+ def __call__(self, *args: Any, **kwargs: Any) -> _FGuiVar:
"""Call the wrapped _magicgui and return a FunctionGui."""
if args:
raise ValueError("MagicFactory instance only accept keyword arguments")
diff --git a/src/magicgui/types.py b/src/magicgui/types.py
index 06e520225..e82473a51 100644
--- a/src/magicgui/types.py
+++ b/src/magicgui/types.py
@@ -38,7 +38,7 @@ class ChoicesDict(TypedDict):
#: be provided an instance of a
#: [~magicgui.widgets.FunctionGui][magicgui.widgets.FunctionGui],
#: the result of the function that was called, and the return annotation itself.
-ReturnCallback = Callable[["FunctionGui[Any]", Any, type], None]
+ReturnCallback = Callable[["FunctionGui", Any, type], None]
#: A valid file path type
PathLike = Union[Path, str, bytes]
diff --git a/src/magicgui/widgets/_function_gui.py b/src/magicgui/widgets/_function_gui.py
index d14ee8b0e..2cc229a81 100644
--- a/src/magicgui/widgets/_function_gui.py
+++ b/src/magicgui/widgets/_function_gui.py
@@ -30,10 +30,16 @@
if TYPE_CHECKING:
from pathlib import Path
+ from typing_extensions import ParamSpec
+
from magicgui.application import Application, AppRef # noqa: F401
from magicgui.widgets import TextEdit
from magicgui.widgets.protocols import ContainerProtocol, MainWindowProtocol
+ _P = ParamSpec("_P")
+else:
+ _P = TypeVar("_P") # easier runtime dependency than ParamSpec
+
def _inject_tooltips_from_docstrings(
docstring: str | None, sig: MagicSignature
@@ -70,7 +76,7 @@ def _inject_tooltips_from_docstrings(
_VT = TypeVar("_VT")
-class FunctionGui(Container, Generic[_R]):
+class FunctionGui(Container, Generic[_P, _R]):
"""Wrapper for a container of widgets representing a callable object.
Parameters
@@ -129,7 +135,7 @@ class FunctionGui(Container, Generic[_R]):
def __init__(
self,
- function: Callable[..., _R],
+ function: Callable[_P, _R],
call_button: bool | str | None = None,
layout: str = "vertical",
scrollable: bool = False,
@@ -276,9 +282,12 @@ def __signature__(self) -> MagicSignature:
"""Return a MagicSignature object representing the current state of the gui."""
return super().__signature__.replace(return_annotation=self.return_annotation)
- def __call__(self, *args: Any, update_widget: bool = False, **kwargs: Any) -> _R:
+ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Call the original function with the current parameter values from the Gui.
+ You may pass a `update_widget=True` keyword argument to update the widget
+ values to match the current parameter values before calling the function.
+
It is also possible to override the current parameter values from the GUI by
providing args/kwargs to the function call. Only those provided will override
the ones from the gui. A `called` signal will also be emitted with the results.
@@ -298,6 +307,8 @@ def __call__(self, *args: Any, update_widget: bool = False, **kwargs: Any) -> _R
gui() # calls the original function with the current parameters
```
"""
+ update_widget: bool = bool(kwargs.pop("update_widget", False))
+
sig = self.__signature__
try:
bound = sig.bind(*args, **kwargs)
@@ -441,12 +452,12 @@ def _load(self, path: str | Path | None = None, quiet: bool = False) -> None:
super()._load(path or self._dump_path, quiet=quiet)
-class MainFunctionGui(FunctionGui[_R], MainWindow):
+class MainFunctionGui(FunctionGui[_P, _R], MainWindow):
"""Container of widgets as a Main Application Window."""
_widget: MainWindowProtocol
- def __init__(self, function: Callable, *args: Any, **kwargs: Any) -> None:
+ def __init__(self, function: Callable[_P, _R], *args: Any, **kwargs: Any) -> None:
super().__init__(function, *args, **kwargs)
self.create_menu_item("Help", "Documentation", callback=self._show_docs)
self._help_text_edit: TextEdit | None = None
From 2fa477db7017b5360f30645077fdbdf92a97706e Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Wed, 11 Oct 2023 16:02:40 -0400
Subject: [PATCH 06/25] feat: add icons on buttons (#598)
---
pyproject.toml | 4 +-
src/magicgui/backends/_ipynb/widgets.py | 26 ++++++++-
src/magicgui/backends/_qtpy/widgets.py | 55 ++++++++++++++++++--
src/magicgui/widgets/bases/_button_widget.py | 7 +++
src/magicgui/widgets/protocols.py | 16 +++++-
tests/test_widgets.py | 11 ++++
6 files changed, 110 insertions(+), 9 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index a99286a0a..bb7db9fd4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ dependencies = [
"docstring_parser>=0.7",
"psygnal>=0.5.0",
"qtpy>=1.7.0",
- "superqt>=0.5.0",
+ "superqt[iconify]>=0.6.1",
"typing_extensions",
]
@@ -48,7 +48,7 @@ min-req = [
"docstring_parser==0.7",
"psygnal==0.5.0",
"qtpy==1.7.0",
- "superqt==0.5.0",
+ "superqt==0.6.1",
"typing_extensions",
]
diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py
index 93af80acf..58f9971db 100644
--- a/src/magicgui/backends/_ipynb/widgets.py
+++ b/src/magicgui/backends/_ipynb/widgets.py
@@ -1,3 +1,5 @@
+# from __future__ import annotations # NO
+
from typing import Any, Callable, Iterable, Optional, Tuple, Type, Union
try:
@@ -9,6 +11,7 @@
"Please run `pip install ipywidgets`"
) from e
+
from magicgui.widgets import protocols
from magicgui.widgets.bases import Widget
@@ -260,11 +263,32 @@ def _mgui_get_text(self) -> str:
return self._ipywidget.description
+class _IPySupportsIcon(protocols.SupportsIcon):
+ """Widget that can show an icon."""
+
+ _ipywidget: ipywdg.Button
+
+ def _mgui_set_icon(self, value: Optional[str], color: Optional[str]) -> None:
+ """Set icon."""
+ # only ipywdg.Button actually supports icons.
+ # but our button protocol allows it for all buttons subclasses
+ # so we need this method in the concrete subclasses, but we
+ # can't actually set the icon for anything but ipywdg.Button
+ if hasattr(self._ipywidget, "icon"):
+ # by splitting on ":" we allow for "prefix:icon-name" syntax
+ # which works for iconify icons served by qt, while still
+ # allowing for bare "icon-name" syntax which works for ipywidgets.
+ # note however... only fa4/5 icons will work for ipywidgets.
+ value = value or ""
+ self._ipywidget.icon = value.replace("fa-", "").split(":", 1)[-1]
+ self._ipywidget.style.text_color = color
+
+
class _IPyCategoricalWidget(_IPyValueWidget, _IPySupportsChoices):
pass
-class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText):
+class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText, _IPySupportsIcon):
pass
diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py
index 71ce796b6..cf47053df 100644
--- a/src/magicgui/backends/_qtpy/widgets.py
+++ b/src/magicgui/backends/_qtpy/widgets.py
@@ -17,8 +17,10 @@
from qtpy.QtGui import (
QFont,
QFontMetrics,
+ QIcon,
QImage,
QKeyEvent,
+ QPalette,
QPixmap,
QResizeEvent,
QTextDocument,
@@ -48,10 +50,13 @@ def _signals_blocked(obj: QtW.QWidget) -> Iterator[None]:
class EventFilter(QObject):
parentChanged = Signal()
valueChanged = Signal(object)
+ paletteChanged = Signal()
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if event.type() == QEvent.Type.ParentChange:
self.parentChanged.emit()
+ if event.type() == QEvent.Type.PaletteChange:
+ self.paletteChanged.emit()
return False
@@ -419,11 +424,15 @@ def _update_precision(self, **kwargs: Any) -> None:
# BUTTONS
-class QBaseButtonWidget(QBaseValueWidget, protocols.SupportsText):
+class QBaseButtonWidget(
+ QBaseValueWidget, protocols.SupportsText, protocols.SupportsIcon
+):
_qwidget: QtW.QCheckBox | QtW.QPushButton | QtW.QRadioButton | QtW.QToolButton
- def __init__(self, qwidg: type[QtW.QWidget], **kwargs: Any) -> None:
+ def __init__(self, qwidg: type[QtW.QAbstractButton], **kwargs: Any) -> None:
super().__init__(qwidg, "isChecked", "setChecked", "toggled", **kwargs)
+ self._event_filter.paletteChanged.connect(self._update_icon)
+ self._icon: tuple[str | None, str | None] | None = None
def _mgui_set_text(self, value: str) -> None:
"""Set text."""
@@ -433,12 +442,48 @@ def _mgui_get_text(self) -> str:
"""Get text."""
return self._qwidget.text()
+ def _update_icon(self) -> None:
+ # Called when palette changes or icon is set
+ if self._icon:
+ qicon = _get_qicon(*self._icon, palette=self._qwidget.palette())
+ if qicon is None:
+ self._icon = None # an error occurred don't try again
+ self._qwidget.setIcon(QIcon())
+ else:
+ self._qwidget.setIcon(qicon)
+
+ def _mgui_set_icon(self, value: str | None, color: str | None) -> None:
+ self._icon = (value, color)
+ self._update_icon()
+
+
+def _get_qicon(key: str | None, color: str | None, palette: QPalette) -> QIcon | None:
+ """Return a QIcon from iconify, or None if it fails."""
+ if not key:
+ return QIcon()
+
+ if not color or color == "auto":
+ # use foreground color
+ color = palette.color(QPalette.ColorRole.WindowText).name()
+ # don't use full black or white
+ color = {"#000000": "#333333", "#ffffff": "#cccccc"}.get(color, color)
+
+ if ":" not in key:
+ # for parity with the other backends, assume fontawesome
+ # if no prefix is given.
+ key = f"fa:{key}"
+
+ try:
+ return superqt.QIconifyIcon(key, color=color)
+ except (OSError, ValueError) as e:
+ warnings.warn(f"Could not set iconify icon: {e}", stacklevel=2)
+ return None
+
class PushButton(QBaseButtonWidget):
def __init__(self, **kwargs: Any) -> None:
- QBaseValueWidget.__init__(
- self, QtW.QPushButton, "isChecked", "setChecked", "clicked", **kwargs
- )
+ super().__init__(QtW.QPushButton, **kwargs)
+ self._onchange_name = "clicked"
# make enter/return "click" the button when focused.
self._qwidget.setAutoDefault(True)
diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py
index 7deba402b..faed7d484 100644
--- a/src/magicgui/widgets/bases/_button_widget.py
+++ b/src/magicgui/widgets/bases/_button_widget.py
@@ -52,6 +52,8 @@ def __init__(
value: bool | _Undefined = Undefined,
*,
text: str | None = None,
+ icon: str | None = None,
+ icon_color: str | None = None,
bind: bool | Callable[[ValueWidget], bool] | _Undefined = Undefined,
nullable: bool = False,
**base_widget_kwargs: Unpack[WidgetKwargs],
@@ -72,6 +74,8 @@ def __init__(
value=value, bind=bind, nullable=nullable, **base_widget_kwargs
)
self.text = (text or self.name).replace("_", " ")
+ if icon:
+ self.set_icon(icon, icon_color)
@property
def options(self) -> dict:
@@ -93,3 +97,6 @@ def text(self, value: str) -> None:
def clicked(self) -> SignalInstance:
"""Alias for changed event."""
return self.changed
+
+ def set_icon(self, value: str | None, color: str | None = None) -> None:
+ self._widget._mgui_set_icon(value, color)
diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py
index 316528173..c730ea4d2 100644
--- a/src/magicgui/widgets/protocols.py
+++ b/src/magicgui/widgets/protocols.py
@@ -438,7 +438,21 @@ def _mgui_get_text(self) -> str:
@runtime_checkable
-class ButtonWidgetProtocol(ValueWidgetProtocol, SupportsText, Protocol):
+class SupportsIcon(Protocol):
+ """Widget that can be reoriented."""
+
+ @abstractmethod
+ def _mgui_set_icon(self, value: str | None, color: str | None) -> None:
+ """Set icon.
+
+ Value is an "prefix:name" from iconify: https://icon-sets.iconify.design
+ Color is any valid CSS color string.
+ Set value to `None` or an empty string to remove icon.
+ """
+
+
+@runtime_checkable
+class ButtonWidgetProtocol(ValueWidgetProtocol, SupportsText, SupportsIcon, Protocol):
"""The "value" in a ButtonWidget is the current (checked) state."""
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index 60abd1fc2..b12fd93a3 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -809,6 +809,17 @@ def test_pushbutton_click_signal():
mock2.assert_called_once()
+def test_pushbutton_icon(backend: str):
+ use_app(backend)
+ btn = widgets.PushButton(icon="mdi:folder")
+ btn.set_icon("play", "red")
+ btn.set_icon(None)
+
+ if backend == "qt":
+ with pytest.warns(UserWarning, match="Could not set iconify icon"):
+ btn.set_icon("bad:key")
+
+
def test_list_edit():
"""Test ListEdit."""
from typing import List
From acc9bbbba483b5a01f9b171a33918fe7311d6040 Mon Sep 17 00:00:00 2001
From: Grzegorz Bokota
Date: Fri, 20 Oct 2023 14:02:59 +0200
Subject: [PATCH 07/25] fix: Allow user overwritte default widget opts (#602)
* fix: Allow user overwritte default widget opts
* use Annotated from typing extensions
---
src/magicgui/type_map/_type_map.py | 38 ++++++++++----------
src/magicgui/widgets/bases/_create_widget.py | 16 ++++-----
tests/test_widgets.py | 11 ++++++
3 files changed, 38 insertions(+), 27 deletions(-)
diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py
index cc422846e..f15021a9e 100644
--- a/src/magicgui/type_map/_type_map.py
+++ b/src/magicgui/type_map/_type_map.py
@@ -214,8 +214,8 @@ def _pick_widget_type(
raise_on_unknown: bool = True,
) -> WidgetTuple:
"""Pick the appropriate widget type for ``value`` with ``annotation``."""
- annotation, _options = _split_annotated_type(annotation)
- options = {**_options, **(options or {})}
+ annotation, options_ = _split_annotated_type(annotation)
+ options = {**options_, **(options or {})}
choices = options.get("choices")
if is_result and annotation is inspect.Parameter.empty:
@@ -229,9 +229,9 @@ def _pick_widget_type(
):
return widgets.EmptyWidget, {"visible": False, **options}
- _type, optional = _type_optional(value, annotation)
+ type_, optional = _type_optional(value, annotation)
options.setdefault("nullable", optional)
- choices = choices or (isinstance(_type, EnumMeta) and _type)
+ choices = choices or (isinstance(type_, EnumMeta) and type_)
literal_choices, nullable = _literal_choices(annotation)
if literal_choices is not None:
choices = literal_choices
@@ -243,7 +243,7 @@ def _pick_widget_type(
if widget_type == "RadioButton":
widget_type = "RadioButtons"
warnings.warn(
- f"widget_type of 'RadioButton' (with dtype {_type}) is"
+ f"widget_type of 'RadioButton' (with dtype {type_}) is"
" being coerced to 'RadioButtons' due to choices or Enum type.",
stacklevel=2,
)
@@ -252,15 +252,15 @@ def _pick_widget_type(
# look for subclasses
for registered_type in _TYPE_DEFS:
- if _type == registered_type or safe_issubclass(_type, registered_type):
- _cls, opts = _TYPE_DEFS[registered_type]
- return _cls, {**options, **opts}
+ if type_ == registered_type or safe_issubclass(type_, registered_type):
+ cls_, opts = _TYPE_DEFS[registered_type]
+ return cls_, {**options, **opts}
if is_result:
- _widget_type = match_return_type(_type)
- if _widget_type:
- _cls, opts = _widget_type
- return _cls, {**options, **opts}
+ widget_type_ = match_return_type(type_)
+ if widget_type_:
+ cls_, opts = widget_type_
+ return cls_, {**opts, **options}
# Chosen for backwards/test compatibility
return widgets.LineEdit, {"gui_only": True}
@@ -269,14 +269,14 @@ def _pick_widget_type(
wdg = widgets.Select if options.get("allow_multiple") else widgets.ComboBox
return wdg, options
- _widget_type = match_type(_type, value)
- if _widget_type:
- _cls, opts = _widget_type
- return _cls, {**options, **opts}
+ widget_type_ = match_type(type_, value)
+ if widget_type_:
+ cls_, opts = widget_type_
+ return cls_, {**opts, **options}
if raise_on_unknown:
raise ValueError(
- f"No widget found for type {_type} and annotation {annotation!r}"
+ f"No widget found for type {type_} and annotation {annotation!r}"
)
options["visible"] = False
@@ -328,7 +328,7 @@ def get_widget_class(
The WidgetClass, and dict that can be used for params. dict
may be different than the options passed in.
"""
- widget_type, _options = _pick_widget_type(
+ widget_type, options_ = _pick_widget_type(
value, annotation, options, is_result, raise_on_unknown
)
@@ -340,7 +340,7 @@ def get_widget_class(
if not safe_issubclass(widget_class, widgets.bases.Widget):
assert_protocol(widget_class, WidgetProtocol)
- return widget_class, _options
+ return widget_class, options_
def _import_wdg_class(class_name: str) -> WidgetClass:
diff --git a/src/magicgui/widgets/bases/_create_widget.py b/src/magicgui/widgets/bases/_create_widget.py
index 00546cbd6..fd6935eb3 100644
--- a/src/magicgui/widgets/bases/_create_widget.py
+++ b/src/magicgui/widgets/bases/_create_widget.py
@@ -93,7 +93,7 @@ def create_widget(
assert wdg.value == ""
```
"""
- _options = options.copy() if options is not None else {}
+ options_ = options.copy() if options is not None else {}
kwargs = {
"value": value,
"annotation": annotation,
@@ -109,21 +109,21 @@ def create_widget(
from magicgui.type_map import get_widget_class
if widget_type:
- _options["widget_type"] = widget_type
+ options_["widget_type"] = widget_type
# special case parameters named "password" with annotation of str
if (
- not _options.get("widget_type")
+ not options_.get("widget_type")
and (name or "").lower() == "password"
and annotation is str
):
- _options["widget_type"] = "Password"
+ options_["widget_type"] = "Password"
wdg_class, opts = get_widget_class(
- value, annotation, _options, is_result, raise_on_unknown
+ value, annotation, options_, is_result, raise_on_unknown
)
if issubclass(wdg_class, Widget):
- widget = wdg_class(**{**kwargs, **opts, **_options})
+ widget = wdg_class(**{**kwargs, **opts, **options_})
if param_kind:
widget.param_kind = param_kind # type: ignore
return widget
@@ -133,9 +133,9 @@ def create_widget(
for p in ("Categorical", "Ranged", "Button", "Value", ""):
prot = getattr(protocols, f"{p}WidgetProtocol")
if isinstance(wdg_class, prot):
- _options = kwargs.pop("options", None)
+ options_ = kwargs.pop("options", None)
cls = getattr(bases, f"{p}Widget")
- widget = cls(**{**kwargs, **(_options or {}), "widget_type": wdg_class})
+ widget = cls(**{**kwargs, **(options_ or {}), "widget_type": wdg_class})
if param_kind:
widget.param_kind = param_kind # type: ignore
return widget
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index b12fd93a3..3d94e18fe 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -110,6 +110,17 @@ def test_create_widget_annotation(annotation, expected_type):
wdg.close()
+def test_create_widget_annotation_overwritte_parrams():
+ wdg1 = widgets.create_widget(annotation=widgets.ProgressBar)
+ assert isinstance(wdg1, widgets.ProgressBar)
+ assert wdg1.visible
+ wdg2 = widgets.create_widget(
+ annotation=Annotated[widgets.ProgressBar, {"visible": False}]
+ )
+ assert isinstance(wdg2, widgets.ProgressBar)
+ assert not wdg2.visible
+
+
# fmt: off
class MyBadWidget:
"""INCOMPLETE widget implementation and will error."""
From b2a23e538f2a800482093b509f4a83f66934f1a5 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Fri, 20 Oct 2023 08:25:49 -0400
Subject: [PATCH 08/25] docs: Fix docs warning (#603)
* docs: fix docs
* pin
---
mkdocs.yml | 4 ++--
pyproject.toml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/mkdocs.yml b/mkdocs.yml
index 248d713ef..c9ada6d15 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -91,8 +91,8 @@ markdown_extensions:
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.emoji:
- emoji_index: !!python/name:materialx.emoji.twemoji
- emoji_generator: !!python/name:materialx.emoji.to_svg
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
plugins:
- search
diff --git a/pyproject.toml b/pyproject.toml
index bb7db9fd4..ba78dd716 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -112,7 +112,7 @@ dev = [
]
docs = [
"mkdocs",
- "mkdocs-material ~=9.2",
+ "mkdocs-material ~=9.4",
"mkdocstrings ==0.22.0",
"mkdocstrings-python ==1.6.2",
"mkdocs-gen-files",
From b28c4990cfedbdeeb9d5fb95ec688a8af766d44b Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Fri, 20 Oct 2023 13:45:38 -0400
Subject: [PATCH 09/25] chore: changelog v0.8.0 (#605)
---
CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b64a56178..e862f96c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,42 @@
# Changelog
+## [v0.8.0](https://github.com/pyapp-kit/magicgui/tree/v0.8.0) (2023-10-20)
+
+[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.7.3...v0.8.0)
+
+**Implemented enhancements:**
+
+- feat: add icons on buttons [\#598](https://github.com/pyapp-kit/magicgui/pull/598) ([tlambert03](https://github.com/tlambert03))
+- feat: support python3.12 [\#590](https://github.com/pyapp-kit/magicgui/pull/590) ([tlambert03](https://github.com/tlambert03))
+
+**Fixed bugs:**
+
+- fix: Allow user to overwrite default widget opts [\#602](https://github.com/pyapp-kit/magicgui/pull/602) ([Czaki](https://github.com/Czaki))
+- chore: preserve magicgui-decorated function parameter hints with ParamSpec [\#600](https://github.com/pyapp-kit/magicgui/pull/600) ([tlambert03](https://github.com/tlambert03))
+- fix: Support Annotated types in list/tuple [\#588](https://github.com/pyapp-kit/magicgui/pull/588) ([hanjinliu](https://github.com/hanjinliu))
+- fix: fix Literal with widget\_type [\#586](https://github.com/pyapp-kit/magicgui/pull/586) ([tlambert03](https://github.com/tlambert03))
+- fix: Fix parent attribute to point to proper magicgui widget parent [\#583](https://github.com/pyapp-kit/magicgui/pull/583) ([tlambert03](https://github.com/tlambert03))
+- fix: prevent dupe calls, alternative [\#546](https://github.com/pyapp-kit/magicgui/pull/546) ([Czaki](https://github.com/Czaki))
+
+**Tests & CI:**
+
+- test: try fix napari tests [\#591](https://github.com/pyapp-kit/magicgui/pull/591) ([tlambert03](https://github.com/tlambert03))
+
+**Documentation:**
+
+- docs: Fix docs warning [\#603](https://github.com/pyapp-kit/magicgui/pull/603) ([tlambert03](https://github.com/tlambert03))
+- chore: Add python version to README.md [\#596](https://github.com/pyapp-kit/magicgui/pull/596) ([tlambert03](https://github.com/tlambert03))
+- docs: Fix broken mkdocs links [\#587](https://github.com/pyapp-kit/magicgui/pull/587) ([GenevieveBuckley](https://github.com/GenevieveBuckley))
+- docs: Example script for ineterminate progress bar with a long running computation [\#579](https://github.com/pyapp-kit/magicgui/pull/579) ([GenevieveBuckley](https://github.com/GenevieveBuckley))
+- docs: Auto-generated examples gallery [\#571](https://github.com/pyapp-kit/magicgui/pull/571) ([GenevieveBuckley](https://github.com/GenevieveBuckley))
+
+**Merged pull requests:**
+
+- style: use `Unpack` for better kwargs typing [\#599](https://github.com/pyapp-kit/magicgui/pull/599) ([tlambert03](https://github.com/tlambert03))
+- chore: remove setup.py [\#595](https://github.com/pyapp-kit/magicgui/pull/595) ([tlambert03](https://github.com/tlambert03))
+- ci\(dependabot\): bump actions/checkout from 3 to 4 [\#578](https://github.com/pyapp-kit/magicgui/pull/578) ([dependabot[bot]](https://github.com/apps/dependabot))
+- chore: Remove dangling \_version.py [\#576](https://github.com/pyapp-kit/magicgui/pull/576) ([Czaki](https://github.com/Czaki))
+
## [v0.7.3](https://github.com/pyapp-kit/magicgui/tree/v0.7.3) (2023-08-12)
[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.7.2...v0.7.3)
From 16b3658e98f9a5fe8dd5069039ad6fa332569ede Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Sun, 22 Oct 2023 13:39:07 -0400
Subject: [PATCH 10/25] feat: add toolbars [wip] (#597)
* wip
* update ipywidgets implementation
* fix hints
* add test
* feat: support button icons
* adding iconbtn
* match color to palette in qt
* update ipywidgets
* bump superqt
* add pytest-pretty
* test: add tests
* fix icon
* extract logic, fix 3.8
* update color
* change with palette
* unions
---
src/magicgui/backends/_ipynb/__init__.py | 2 +
src/magicgui/backends/_ipynb/widgets.py | 61 ++++++++++++++++++++++++
src/magicgui/backends/_qtpy/__init__.py | 2 +
src/magicgui/backends/_qtpy/widgets.py | 59 ++++++++++++++++++++++-
src/magicgui/widgets/__init__.py | 2 +
src/magicgui/widgets/_concrete.py | 6 +++
src/magicgui/widgets/bases/__init__.py | 4 +-
src/magicgui/widgets/bases/_toolbar.py | 60 +++++++++++++++++++++++
src/magicgui/widgets/protocols.py | 35 ++++++++++++++
tests/test_widgets.py | 11 +++++
10 files changed, 240 insertions(+), 2 deletions(-)
create mode 100644 src/magicgui/widgets/bases/_toolbar.py
diff --git a/src/magicgui/backends/_ipynb/__init__.py b/src/magicgui/backends/_ipynb/__init__.py
index 1c82d18f2..b9f98e29a 100644
--- a/src/magicgui/backends/_ipynb/__init__.py
+++ b/src/magicgui/backends/_ipynb/__init__.py
@@ -19,6 +19,7 @@
SpinBox,
TextEdit,
TimeEdit,
+ ToolBar,
get_text_width,
)
@@ -46,6 +47,7 @@
"Slider",
"SpinBox",
"TextEdit",
+ "ToolBar",
"get_text_width",
"show_file_dialog",
]
diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py
index 58f9971db..84928691d 100644
--- a/src/magicgui/backends/_ipynb/widgets.py
+++ b/src/magicgui/backends/_ipynb/widgets.py
@@ -350,6 +350,67 @@ class TimeEdit(_IPyValueWidget):
_ipywidget: ipywdg.TimePicker
+class ToolBar(_IPyWidget):
+ _ipywidget: ipywidgets.HBox
+
+ def __init__(self, **kwargs):
+ super().__init__(ipywidgets.HBox, **kwargs)
+ self._icon_sz: Optional[Tuple[int, int]] = None
+
+ def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None:
+ """Add an action to the toolbar."""
+ btn = ipywdg.Button(
+ description=text, icon=icon, layout={"width": "auto", "height": "auto"}
+ )
+ if callback:
+ btn.on_click(lambda e: callback())
+ self._add_ipywidget(btn)
+
+ def _add_ipywidget(self, widget: "ipywidgets.Widget") -> None:
+ children = list(self._ipywidget.children)
+ children.append(widget)
+ self._ipywidget.children = children
+
+ def _mgui_add_separator(self) -> None:
+ """Add a separator line to the toolbar."""
+ # Define the vertical separator
+ sep = ipywdg.Box(
+ layout=ipywdg.Layout(border_left="1px dotted gray", margin="1px 4px")
+ )
+ self._add_ipywidget(sep)
+
+ def _mgui_add_spacer(self) -> None:
+ """Add a spacer to the toolbar."""
+ self._add_ipywidget(ipywdg.Box(layout=ipywdg.Layout(flex="1")))
+
+ def _mgui_add_widget(self, widget: "Widget") -> None:
+ """Add a widget to the toolbar."""
+ self._add_ipywidget(widget.native)
+
+ def _mgui_get_icon_size(self) -> Optional[Tuple[int, int]]:
+ """Return the icon size of the toolbar."""
+ return self._icon_sz
+
+ def _mgui_set_icon_size(self, size: Union[int, Tuple[int, int], None]) -> None:
+ """Set the icon size of the toolbar."""
+ if isinstance(size, int):
+ size = (size, size)
+ elif size is None:
+ size = (0, 0)
+ elif not isinstance(size, tuple):
+ raise ValueError("icon size must be an int or tuple of ints")
+ sz = max(size)
+ self._icon_sz = (sz, sz)
+ for child in self._ipywidget.children:
+ if hasattr(child, "style"):
+ child.style.font_size = f"{sz}px" if sz else None
+ child.layout.min_height = f"{sz*2}px" if sz else None
+
+ def _mgui_clear(self) -> None:
+ """Clear the toolbar."""
+ self._ipywidget.children = ()
+
+
class PushButton(_IPyButtonWidget):
_ipywidget: ipywdg.Button
diff --git a/src/magicgui/backends/_qtpy/__init__.py b/src/magicgui/backends/_qtpy/__init__.py
index 046859544..a42b100c3 100644
--- a/src/magicgui/backends/_qtpy/__init__.py
+++ b/src/magicgui/backends/_qtpy/__init__.py
@@ -28,6 +28,7 @@
Table,
TextEdit,
TimeEdit,
+ ToolBar,
get_text_width,
show_file_dialog,
)
@@ -64,4 +65,5 @@
"Table",
"TextEdit",
"TimeEdit",
+ "ToolBar",
]
diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py
index cf47053df..a94bd3665 100644
--- a/src/magicgui/backends/_qtpy/widgets.py
+++ b/src/magicgui/backends/_qtpy/widgets.py
@@ -13,7 +13,7 @@
import qtpy
import superqt
from qtpy import QtWidgets as QtW
-from qtpy.QtCore import QEvent, QObject, Qt, Signal
+from qtpy.QtCore import QEvent, QObject, QSize, Qt, Signal
from qtpy.QtGui import (
QFont,
QFontMetrics,
@@ -1217,6 +1217,63 @@ def _mgui_get_value(self):
return self._qwidget.time().toPyTime()
+class ToolBar(QBaseWidget):
+ _qwidget: QtW.QToolBar
+
+ def __init__(self, **kwargs: Any) -> None:
+ super().__init__(QtW.QToolBar, **kwargs)
+ self._qwidget.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
+ self._event_filter.paletteChanged.connect(self._on_palette_change)
+
+ def _on_palette_change(self):
+ for action in self._qwidget.actions():
+ if icon := action.data():
+ if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()):
+ action.setIcon(qicon)
+
+ def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None:
+ """Add an action to the toolbar."""
+ act = self._qwidget.addAction(text, callback)
+ if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()):
+ act.setIcon(qicon)
+ act.setData(icon)
+
+ def _mgui_add_separator(self) -> None:
+ """Add a separator line to the toolbar."""
+ self._qwidget.addSeparator()
+
+ def _mgui_add_spacer(self) -> None:
+ """Add a spacer to the toolbar."""
+ empty = QtW.QWidget()
+ empty.setSizePolicy(
+ QtW.QSizePolicy.Policy.Expanding, QtW.QSizePolicy.Policy.Preferred
+ )
+ self._qwidget.addWidget(empty)
+
+ def _mgui_add_widget(self, widget: Widget) -> None:
+ """Add a widget to the toolbar."""
+ self._qwidget.addWidget(widget.native)
+
+ def _mgui_get_icon_size(self) -> tuple[int, int] | None:
+ """Return the icon size of the toolbar."""
+ sz = self._qwidget.iconSize()
+ return None if sz.isNull() else (sz.width(), sz.height())
+
+ def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None:
+ """Set the icon size of the toolbar."""
+ if isinstance(size, int):
+ _size = QSize(size, size)
+ elif isinstance(size, tuple):
+ _size = QSize(size[0], size[1])
+ else:
+ _size = QSize()
+ self._qwidget.setIconSize(_size)
+
+ def _mgui_clear(self) -> None:
+ """Clear the toolbar."""
+ self._qwidget.clear()
+
+
class Dialog(QBaseWidget, protocols.ContainerProtocol):
def __init__(
self, layout="vertical", scrollable: bool = False, **kwargs: Any
diff --git a/src/magicgui/widgets/__init__.py b/src/magicgui/widgets/__init__.py
index ffc556ee3..2478afb0d 100644
--- a/src/magicgui/widgets/__init__.py
+++ b/src/magicgui/widgets/__init__.py
@@ -43,6 +43,7 @@
SpinBox,
TextEdit,
TimeEdit,
+ ToolBar,
TupleEdit,
)
from ._dialogs import request_values, show_file_dialog
@@ -107,6 +108,7 @@
"Table",
"TextEdit",
"TimeEdit",
+ "ToolBar",
"TupleEdit",
"Widget",
"show_file_dialog",
diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py
index f7ea08ba3..e2fc1d938 100644
--- a/src/magicgui/widgets/_concrete.py
+++ b/src/magicgui/widgets/_concrete.py
@@ -46,6 +46,7 @@
MultiValuedSliderWidget,
RangedWidget,
SliderWidget,
+ ToolBarWidget,
TransformedRangedWidget,
ValueWidget,
Widget,
@@ -969,6 +970,11 @@ def value(self, vals: Sequence) -> None:
self.changed.emit(self.value)
+@backend_widget
+class ToolBar(ToolBarWidget):
+ """Toolbar that contains a set of controls."""
+
+
class _LabeledWidget(Container):
"""Simple container that wraps a widget and provides a label."""
diff --git a/src/magicgui/widgets/bases/__init__.py b/src/magicgui/widgets/bases/__init__.py
index dcda1c7de..176a12e9f 100644
--- a/src/magicgui/widgets/bases/__init__.py
+++ b/src/magicgui/widgets/bases/__init__.py
@@ -48,6 +48,7 @@ def __init__(
from ._create_widget import create_widget
from ._ranged_widget import RangedWidget, TransformedRangedWidget
from ._slider_widget import MultiValuedSliderWidget, SliderWidget
+from ._toolbar import ToolBarWidget
from ._value_widget import ValueWidget
from ._widget import Widget
@@ -55,13 +56,14 @@ def __init__(
"ButtonWidget",
"CategoricalWidget",
"ContainerWidget",
+ "create_widget",
"DialogWidget",
"MainWindowWidget",
"MultiValuedSliderWidget",
"RangedWidget",
"SliderWidget",
+ "ToolBarWidget",
"TransformedRangedWidget",
"ValueWidget",
"Widget",
- "create_widget",
]
diff --git a/src/magicgui/widgets/bases/_toolbar.py b/src/magicgui/widgets/bases/_toolbar.py
new file mode 100644
index 000000000..c8d87766e
--- /dev/null
+++ b/src/magicgui/widgets/bases/_toolbar.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
+
+from ._widget import Widget
+
+if TYPE_CHECKING:
+ from magicgui.widgets import protocols
+
+T = TypeVar("T", int, float, Tuple[Union[int, float], ...])
+DEFAULT_MIN = 0.0
+DEFAULT_MAX = 1000.0
+
+
+class ToolBarWidget(Widget):
+ """Widget with a value, Wraps ValueWidgetProtocol.
+
+ Parameters
+ ----------
+ **base_widget_kwargs : Any
+ All additional keyword arguments are passed to the base
+ [`magicgui.widgets.Widget`][magicgui.widgets.Widget] constructor.
+ """
+
+ _widget: protocols.ToolBarProtocol
+
+ def __init__(self, **base_widget_kwargs: Any) -> None:
+ super().__init__(**base_widget_kwargs)
+
+ def add_button(
+ self, text: str = "", icon: str = "", callback: Callable | None = None
+ ) -> None:
+ """Add an action to the toolbar."""
+ self._widget._mgui_add_button(text, icon, callback)
+
+ def add_separator(self) -> None:
+ """Add a separator line to the toolbar."""
+ self._widget._mgui_add_separator()
+
+ def add_spacer(self) -> None:
+ """Add a spacer to the toolbar."""
+ self._widget._mgui_add_spacer()
+
+ def add_widget(self, widget: Widget) -> None:
+ """Add a widget to the toolbar."""
+ self._widget._mgui_add_widget(widget)
+
+ @property
+ def icon_size(self) -> tuple[int, int] | None:
+ """Return the icon size of the toolbar."""
+ return self._widget._mgui_get_icon_size()
+
+ @icon_size.setter
+ def icon_size(self, size: int | tuple[int, int] | None) -> None:
+ """Set the icon size of the toolbar."""
+ self._widget._mgui_set_icon_size(size)
+
+ def clear(self) -> None:
+ """Clear the toolbar."""
+ self._widget._mgui_clear()
diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py
index c730ea4d2..952dc8fdd 100644
--- a/src/magicgui/widgets/protocols.py
+++ b/src/magicgui/widgets/protocols.py
@@ -515,6 +515,41 @@ def _mgui_set_margins(self, margins: tuple[int, int, int, int]) -> None:
raise NotImplementedError()
+@runtime_checkable
+class ToolBarProtocol(WidgetProtocol, Protocol):
+ """Toolbar that contains a set of controls."""
+
+ @abstractmethod
+ def _mgui_add_button(
+ self, text: str, icon: str, callback: Callable | None = None
+ ) -> None:
+ """Add a button to the toolbar."""
+
+ @abstractmethod
+ def _mgui_add_separator(self) -> None:
+ """Add a separator line to the toolbar."""
+
+ @abstractmethod
+ def _mgui_add_spacer(self) -> None:
+ """Add a spacer to the toolbar."""
+
+ @abstractmethod
+ def _mgui_add_widget(self, widget: Widget) -> None:
+ """Add a widget to the toolbar."""
+
+ @abstractmethod
+ def _mgui_get_icon_size(self) -> tuple[int, int] | None:
+ """Return the icon size of the toolbar."""
+
+ @abstractmethod
+ def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None:
+ """Set the icon size of the toolbar."""
+
+ @abstractmethod
+ def _mgui_clear(self) -> None:
+ """Clear the toolbar."""
+
+
class DialogProtocol(ContainerProtocol, Protocol):
"""Protocol for modal (blocking) containers."""
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index 3d94e18fe..99df22cf1 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -1067,3 +1067,14 @@ def test_float_slider_readout():
assert sld._widget._readout_widget.value() == 4
assert sld._widget._readout_widget.minimum() == 0.5
assert sld._widget._readout_widget.maximum() == 10.5
+
+
+def test_toolbar():
+ tb = widgets.ToolBar()
+ tb.add_button("test", callback=lambda: None)
+ tb.add_separator()
+ tb.add_spacer()
+ tb.add_button("test2", callback=lambda: None)
+ tb.icon_size = 26
+ assert tb.icon_size == (26, 26)
+ tb.clear()
From 9a58c9c4a3a51f509baab641e926ca6db3bcf027 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Mon, 23 Oct 2023 07:53:38 -0400
Subject: [PATCH 11/25] chore!: remove older deprecations (#607)
* chore: remove deprecations
* style(pre-commit.ci): auto fixes [...]
* try removing test ignores
* fix warnings
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
pyproject.toml | 8 +----
src/magicgui/__init__.py | 18 -----------
src/magicgui/type_map/__init__.py | 28 ----------------
src/magicgui/types.py | 14 --------
src/magicgui/widgets/_bases/__init__.py | 10 ------
src/magicgui/widgets/_bases/value_widget.py | 36 ---------------------
src/magicgui/widgets/_bases/widget.py | 10 ------
7 files changed, 1 insertion(+), 123 deletions(-)
delete mode 100644 src/magicgui/widgets/_bases/__init__.py
delete mode 100644 src/magicgui/widgets/_bases/value_widget.py
delete mode 100644 src/magicgui/widgets/_bases/widget.py
diff --git a/pyproject.toml b/pyproject.toml
index ba78dd716..e4212970b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -211,14 +211,8 @@ minversion = "6.0"
testpaths = ["tests"]
filterwarnings = [
"error",
- "ignore::DeprecationWarning:qtpy",
- "ignore:distutils Version classes are deprecated",
- "ignore:path is deprecated:DeprecationWarning",
- "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning",
- "ignore:Enum value:DeprecationWarning:matplotlib",
- "ignore:Widget([^\\s]+) is deprecated:DeprecationWarning", # ipywidgets
- "ignore::DeprecationWarning:docstring_parser",
"ignore::DeprecationWarning:tqdm",
+ "ignore::DeprecationWarning:docstring_parser",
]
# https://mypy.readthedocs.io/en/stable/config_file.html
diff --git a/src/magicgui/__init__.py b/src/magicgui/__init__.py
index c703765a0..677970e93 100644
--- a/src/magicgui/__init__.py
+++ b/src/magicgui/__init__.py
@@ -1,6 +1,5 @@
"""magicgui is a utility for generating a GUI from a python function."""
from importlib.metadata import PackageNotFoundError, version
-from typing import Any
try:
__version__ = version("magicgui")
@@ -23,20 +22,3 @@
"type_registered",
"use_app",
]
-
-
-def __getattr__(name: str) -> Any:
- if name == "FunctionGui":
- from warnings import warn
-
- from .widgets import FunctionGui
-
- warn(
- "magicgui.FunctionGui is deprecated. "
- "Please import at magicgui.widgets.FunctionGui",
- DeprecationWarning,
- stacklevel=2,
- )
-
- return FunctionGui
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/magicgui/type_map/__init__.py b/src/magicgui/type_map/__init__.py
index bfd49a42d..df8c1cf8b 100644
--- a/src/magicgui/type_map/__init__.py
+++ b/src/magicgui/type_map/__init__.py
@@ -1,7 +1,4 @@
"""Functions that map python types to widgets."""
-
-from typing import Any
-
from ._type_map import get_widget_class, register_type, type2callback, type_registered
__all__ = [
@@ -10,28 +7,3 @@
"type_registered",
"type2callback",
]
-
-
-def __getattr__(name: str) -> Any:
- """Import from magicgui.types if not found in magicgui.schema."""
- import warnings
-
- if name == "_type2callback":
- warnings.warn(
- "magicgui.type_map._type2callback is now public, "
- "use magicgui.type_map.type2callback",
- DeprecationWarning,
- stacklevel=2,
- )
- return type2callback
- if name == "pick_widget_type":
- from ._type_map import _pick_widget_type
-
- warnings.warn(
- "magicgui.type_map.pick_widget_type is deprecated, "
- "please use magicgui.type_map.get_widget_class instead",
- DeprecationWarning,
- stacklevel=2,
- )
- return _pick_widget_type
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/magicgui/types.py b/src/magicgui/types.py
index e82473a51..fdcc7b818 100644
--- a/src/magicgui/types.py
+++ b/src/magicgui/types.py
@@ -110,17 +110,3 @@ def __bool__(self) -> bool:
# regular expressions
"regex", # draft 7
]
-
-
-def __getattr__(name: str) -> Any:
- if name == "WidgetOptions":
- import warnings
-
- warnings.warn(
- "magicgui.types.WidgetOptions is being removed. Use `dict` instead, "
- "and restrict import to a TYPE_CHECKING clause.",
- DeprecationWarning,
- stacklevel=2,
- )
- return dict
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/magicgui/widgets/_bases/__init__.py b/src/magicgui/widgets/_bases/__init__.py
deleted file mode 100644
index d8af375b8..000000000
--- a/src/magicgui/widgets/_bases/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import warnings
-
-from magicgui.widgets.bases import * # noqa: F403
-
-warnings.warn(
- "magicgui.widgets._bases has been made public. "
- "Please import from magicgui.widgets.bases instead.",
- DeprecationWarning,
- stacklevel=2,
-)
diff --git a/src/magicgui/widgets/_bases/value_widget.py b/src/magicgui/widgets/_bases/value_widget.py
deleted file mode 100644
index 7708cc9e6..000000000
--- a/src/magicgui/widgets/_bases/value_widget.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import warnings
-from typing import Any
-
-from magicgui.widgets.bases._value_widget import ValueWidget # noqa: F401
-
-warnings.warn(
- "magicgui.widgets._bases is now public. "
- "Please import ValueWidget from magicgui.widgets.bases instead.",
- DeprecationWarning,
- stacklevel=2,
-)
-
-
-def __getattr__(name: str) -> Any:
- if name == "_Unset":
- from magicgui.types import _Undefined
-
- warnings.warn(
- "magicgui.widgets._bases.value_widget._Unset is removed. "
- "Use magicgui.types._Undefined instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return _Undefined
- if name == "UNSET":
- from magicgui.types import Undefined
-
- warnings.warn(
- "magicgui.widgets._bases.value_widget.UNSET is removed. "
- "Use magicgui.types.Undefined instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return Undefined
-
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/magicgui/widgets/_bases/widget.py b/src/magicgui/widgets/_bases/widget.py
deleted file mode 100644
index 29800562b..000000000
--- a/src/magicgui/widgets/_bases/widget.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import warnings
-
-from magicgui.widgets.bases._widget import Widget # noqa: F401
-
-warnings.warn(
- "magicgui.widgets._bases._widget is removed. "
- "Please import Widget from magicgui.widgets.bases instead.",
- DeprecationWarning,
- stacklevel=2,
-)
From 48c94e27d107e66425d8e306b0243c4e2af388dd Mon Sep 17 00:00:00 2001
From: Hanjin Liu <40591297+hanjinliu@users.noreply.github.com>
Date: Tue, 24 Oct 2023 00:25:05 +0900
Subject: [PATCH 12/25] Make kwargs of container-like widgets consistent (#606)
---
src/magicgui/widgets/_concrete.py | 24 ++++++++----------------
1 file changed, 8 insertions(+), 16 deletions(-)
diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py
index e2fc1d938..9b60a4447 100644
--- a/src/magicgui/widgets/_concrete.py
+++ b/src/magicgui/widgets/_concrete.py
@@ -611,28 +611,24 @@ class ListEdit(Container[ValueWidget[_V]]):
----------
value : Iterable, optional
The starting value for the widget.
- layout : str, optional
- The layout for the container. must be one of ``{'horizontal',
- 'vertical'}``. by default "horizontal"
nullable : bool
If `True`, the widget will accepts `None` as a valid value, by default `False`.
options: dict, optional
Widget options of child widgets.
- **kwargs : Any
- All additional keyword arguments are passed to `Container` constructor.
"""
- def __init__( # type: ignore [misc] # overlap between names
+ def __init__(
self,
value: Iterable[_V] | _Undefined = Undefined,
- layout: str = "horizontal",
nullable: bool = False,
options: dict | None = None,
- **kwargs: Unpack[ContainerKwargs],
+ **container_kwargs: Unpack[ContainerKwargs],
) -> None:
self._args_type: type | None = None
self._nullable = nullable
- super().__init__(layout=layout, labels=False, **kwargs)
+ container_kwargs.setdefault("layout", "horizontal")
+ container_kwargs.setdefault("labels", False)
+ super().__init__(**container_kwargs)
self.margins = (0, 0, 0, 0)
if not isinstance(value, _Undefined):
@@ -651,7 +647,7 @@ def __init__( # type: ignore [misc] # overlap between names
button_plus = PushButton(text="+", name="plus")
button_minus = PushButton(text="-", name="minus")
- if layout == "horizontal":
+ if self.layout == "horizontal":
button_plus.max_width = 40
button_minus.max_width = 40
@@ -864,15 +860,10 @@ class TupleEdit(Container[ValueWidget]):
----------
value : Iterable, optional
The starting value for the widget.
- layout : str, optional
- The layout for the container. must be one of ``{'horizontal',
- 'vertical'}``. by default "horizontal"
nullable : bool
If `True`, the widget will accepts `None` as a valid value, by default `False`.
options: dict, optional
Widget options of child widgets.
- **kwargs : Any
- All additional keyword arguments are passed to `Container` constructor.
"""
def __init__(
@@ -885,7 +876,8 @@ def __init__(
) -> None:
self._nullable = nullable
self._args_types: tuple[type, ...] | None = None
- container_kwargs["labels"] = False
+ container_kwargs.setdefault("labels", False)
+ container_kwargs.setdefault("layout", "horizontal")
super().__init__(**container_kwargs)
self._child_options = options or {}
self.margins = (0, 0, 0, 0)
From aa386303810a88df121b09b91e0a8dbd158d2733 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Tue, 24 Oct 2023 19:26:02 -0400
Subject: [PATCH 13/25] fix: allow future annotations in ipywidgets backend
(#609)
---
src/magicgui/backends/_ipynb/widgets.py | 56 +++++++++++++------------
1 file changed, 29 insertions(+), 27 deletions(-)
diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py
index 84928691d..96cb06ea9 100644
--- a/src/magicgui/backends/_ipynb/widgets.py
+++ b/src/magicgui/backends/_ipynb/widgets.py
@@ -1,6 +1,6 @@
-# from __future__ import annotations # NO
+from __future__ import annotations
-from typing import Any, Callable, Iterable, Optional, Tuple, Type, Union
+from typing import TYPE_CHECKING, Any, Callable, Iterable, get_type_hints
try:
import ipywidgets
@@ -13,10 +13,12 @@
from magicgui.widgets import protocols
-from magicgui.widgets.bases import Widget
+if TYPE_CHECKING:
+ from magicgui.widgets.bases import Widget
-def _pxstr2int(pxstr: Union[int, str]) -> int:
+
+def _pxstr2int(pxstr: int | str) -> int:
if isinstance(pxstr, int):
return pxstr
if isinstance(pxstr, str) and pxstr.endswith("px"):
@@ -24,7 +26,7 @@ def _pxstr2int(pxstr: Union[int, str]) -> int:
return int(pxstr)
-def _int2pxstr(pxint: Union[int, str]) -> str:
+def _int2pxstr(pxint: int | str) -> str:
return f"{pxint}px" if isinstance(pxint, int) else pxint
@@ -33,11 +35,11 @@ class _IPyWidget(protocols.WidgetProtocol):
def __init__(
self,
- wdg_class: Optional[Type[ipywdg.Widget]] = None,
- parent: Optional[ipywdg.Widget] = None,
+ wdg_class: type[ipywdg.Widget] | None = None,
+ parent: ipywdg.Widget | None = None,
):
if wdg_class is None:
- wdg_class = type(self).__annotations__.get("_ipywidget")
+ wdg_class = get_type_hints(self, None, globals()).get("_ipywidget")
if wdg_class is None:
raise TypeError("Must provide a valid ipywidget type")
self._ipywidget = wdg_class()
@@ -82,20 +84,20 @@ def _mgui_get_width(self) -> int:
# will this always work with our base Widget assumptions?
return _pxstr2int(self._ipywidget.layout.width)
- def _mgui_set_width(self, value: Union[int, str]) -> None:
+ def _mgui_set_width(self, value: int | str) -> None:
"""Set the current width of the widget."""
self._ipywidget.layout.width = _int2pxstr(value)
def _mgui_get_min_width(self) -> int:
return _pxstr2int(self._ipywidget.layout.min_width)
- def _mgui_set_min_width(self, value: Union[int, str]):
+ def _mgui_set_min_width(self, value: int | str):
self._ipywidget.layout.min_width = _int2pxstr(value)
def _mgui_get_max_width(self) -> int:
return _pxstr2int(self._ipywidget.layout.max_width)
- def _mgui_set_max_width(self, value: Union[int, str]):
+ def _mgui_set_max_width(self, value: int | str):
self._ipywidget.layout.max_width = _int2pxstr(value)
def _mgui_get_height(self) -> int:
@@ -125,7 +127,7 @@ def _mgui_set_max_height(self, value: int) -> None:
def _mgui_get_tooltip(self) -> str:
return self._ipywidget.tooltip
- def _mgui_set_tooltip(self, value: Optional[str]) -> None:
+ def _mgui_set_tooltip(self, value: str | None) -> None:
self._ipywidget.tooltip = value
def _ipython_display_(self, **kwargs):
@@ -211,11 +213,11 @@ def _mgui_get_orientation(self) -> str:
class _IPySupportsChoices(protocols.SupportsChoices):
_ipywidget: ipywdg.Widget
- def _mgui_get_choices(self) -> Tuple[Tuple[str, Any]]:
+ def _mgui_get_choices(self) -> tuple[tuple[str, Any]]:
"""Get available choices."""
return self._ipywidget.options
- def _mgui_set_choices(self, choices: Iterable[Tuple[str, Any]]) -> None:
+ def _mgui_set_choices(self, choices: Iterable[tuple[str, Any]]) -> None:
"""Set available choices."""
self._ipywidget.options = choices
@@ -268,7 +270,7 @@ class _IPySupportsIcon(protocols.SupportsIcon):
_ipywidget: ipywdg.Button
- def _mgui_set_icon(self, value: Optional[str], color: Optional[str]) -> None:
+ def _mgui_set_icon(self, value: str | None, color: str | None) -> None:
"""Set icon."""
# only ipywdg.Button actually supports icons.
# but our button protocol allows it for all buttons subclasses
@@ -355,7 +357,7 @@ class ToolBar(_IPyWidget):
def __init__(self, **kwargs):
super().__init__(ipywidgets.HBox, **kwargs)
- self._icon_sz: Optional[Tuple[int, int]] = None
+ self._icon_sz: tuple[int, int] | None = None
def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None:
"""Add an action to the toolbar."""
@@ -366,7 +368,7 @@ def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None:
btn.on_click(lambda e: callback())
self._add_ipywidget(btn)
- def _add_ipywidget(self, widget: "ipywidgets.Widget") -> None:
+ def _add_ipywidget(self, widget: ipywidgets.Widget) -> None:
children = list(self._ipywidget.children)
children.append(widget)
self._ipywidget.children = children
@@ -383,15 +385,15 @@ def _mgui_add_spacer(self) -> None:
"""Add a spacer to the toolbar."""
self._add_ipywidget(ipywdg.Box(layout=ipywdg.Layout(flex="1")))
- def _mgui_add_widget(self, widget: "Widget") -> None:
+ def _mgui_add_widget(self, widget: Widget) -> None:
"""Add a widget to the toolbar."""
self._add_ipywidget(widget.native)
- def _mgui_get_icon_size(self) -> Optional[Tuple[int, int]]:
+ def _mgui_get_icon_size(self) -> tuple[int, int] | None:
"""Return the icon size of the toolbar."""
return self._icon_sz
- def _mgui_set_icon_size(self, size: Union[int, Tuple[int, int], None]) -> None:
+ def _mgui_set_icon_size(self, size: int | (tuple[int, int] | None)) -> None:
"""Set the icon size of the toolbar."""
if isinstance(size, int):
size = (size, size)
@@ -465,19 +467,19 @@ def __init__(self, layout="horizontal", scrollable: bool = False, **kwargs):
wdg_class = ipywidgets.VBox if layout == "vertical" else ipywidgets.HBox
super().__init__(wdg_class, **kwargs)
- def _mgui_add_widget(self, widget: "Widget") -> None:
+ def _mgui_add_widget(self, widget: Widget) -> None:
children = list(self._ipywidget.children)
children.append(widget.native)
self._ipywidget.children = children
widget.parent = self._ipywidget
- def _mgui_insert_widget(self, position: int, widget: "Widget") -> None:
+ def _mgui_insert_widget(self, position: int, widget: Widget) -> None:
children = list(self._ipywidget.children)
children.insert(position, widget.native)
self._ipywidget.children = children
widget.parent = self._ipywidget
- def _mgui_remove_widget(self, widget: "Widget") -> None:
+ def _mgui_remove_widget(self, widget: Widget) -> None:
children = list(self._ipywidget.children)
children.remove(widget.native)
self._ipywidget.children = children
@@ -490,17 +492,17 @@ def _mgui_remove_index(self, position: int) -> None:
def _mgui_count(self) -> int:
return len(self._ipywidget.children)
- def _mgui_index(self, widget: "Widget") -> int:
+ def _mgui_index(self, widget: Widget) -> int:
return self._ipywidget.children.index(widget.native)
- def _mgui_get_index(self, index: int) -> Optional[Widget]:
+ def _mgui_get_index(self, index: int) -> Widget | None:
"""(return None instead of index error)."""
return self._ipywidget.children[index]._magic_widget
def _mgui_get_native_layout(self) -> Any:
raise self._ipywidget
- def _mgui_get_margins(self) -> Tuple[int, int, int, int]:
+ def _mgui_get_margins(self) -> tuple[int, int, int, int]:
margin = self._ipywidget.layout.margin
if margin:
try:
@@ -510,7 +512,7 @@ def _mgui_get_margins(self) -> Tuple[int, int, int, int]:
return margin
return (0, 0, 0, 0)
- def _mgui_set_margins(self, margins: Tuple[int, int, int, int]) -> None:
+ def _mgui_set_margins(self, margins: tuple[int, int, int, int]) -> None:
lft, top, rgt, bot = margins
self._ipywidget.layout.margin = f"{top}px {rgt}px {bot}px {lft}px"
From 0386cbb12cafa5e206e39b0b9d1c7b8744d52aa9 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Mon, 30 Oct 2023 06:15:32 -0700
Subject: [PATCH 14/25] feat: add `Table.delete_row` method (#610)
* feat: add table.delete_row
* lint
---
src/magicgui/widgets/_table.py | 33 +++++++++++++++++++++++++++++++++
tests/test_table.py | 9 +++++++++
2 files changed, 42 insertions(+)
diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py
index 0dc833d89..3cf5eed43 100644
--- a/src/magicgui/widgets/_table.py
+++ b/src/magicgui/widgets/_table.py
@@ -269,6 +269,39 @@ def value(self, value: TableData) -> None:
for row, d in enumerate(data):
self._set_rowi(row, d)
+ def delete_row(
+ self,
+ *,
+ index: int | Sequence[int] | None = None,
+ header: Any | Sequence[Any] | None = None,
+ ) -> None:
+ """Delete row(s) by index or header.
+
+ Parameters
+ ----------
+ index : int or Sequence[int], optional
+ Index or indices of row(s) to delete.
+ header : Any or Sequence[Any], optional
+ Header or headers of row(s) to delete.
+ """
+ indices: set[int] = set()
+ if index is not None:
+ if isinstance(index, Sequence):
+ indices.update(index)
+ else:
+ indices.add(index)
+ if header is not None:
+ if isinstance(header, str) or not isinstance(header, Sequence):
+ header = (header,)
+ row_headers = self.row_headers
+ for h in header:
+ try:
+ indices.add(row_headers.index(h))
+ except ValueError as e:
+ raise KeyError(f"{h!r} is not a valid row header") from e
+ for i in sorted(indices, reverse=True):
+ self._del_rowi(i)
+
@property
def data(self) -> DataView:
"""Return DataView object for this table."""
diff --git a/tests/test_table.py b/tests/test_table.py
index 0932a7352..0dd9831be 100644
--- a/tests/test_table.py
+++ b/tests/test_table.py
@@ -396,3 +396,12 @@ def test_item_delegate(qapp):
data = ["1.2", "1.23456789", "0.000123", "1234567", "0.0", "1", "s"]
results = [_format_number(v, 4) for v in data]
assert results == ["1.2000", "1.2346", "1.230e-04", "1.235e+06", "0.0000", "1", "s"]
+
+
+def test_row_delete(qapp) -> None:
+ table = Table(value=_TABLE_DATA["split"])
+ assert table.data.to_list() == [[1, 2, 3], [4, 5, 6]]
+ table.delete_row(index=0)
+ assert table.data.to_list() == [[4, 5, 6]]
+ table.delete_row(header="r2")
+ assert table.data.to_list() == []
From 7a9318c5fe1ec99c55880e57add5ff20fa76c1d6 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 6 Nov 2023 16:24:48 -0500
Subject: [PATCH 15/25] ci(pre-commit.ci): autoupdate (#613)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.4)
- [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1)
- [github.com/abravalheri/validate-pyproject: v0.14 → v0.15](https://github.com/abravalheri/validate-pyproject/compare/v0.14...v0.15)
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.1)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8a1535e4d..15fbd920a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-docstring-first
- id: end-of-file-fixer
@@ -14,23 +14,23 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.292
+ rev: v0.1.4
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.9.1
+ rev: 23.10.1
hooks:
- id: black
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.14
+ rev: v0.15
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.5.1
+ rev: v1.6.1
hooks:
- id: mypy
files: "^src/"
From 33c5e60ab02d5dc1e9b31e23e62303e0ae555bbe Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Tue, 7 Nov 2023 08:57:36 -0500
Subject: [PATCH 16/25] docs: unpin pyside6 when building docs (#614)
* unpin pyside6
* try pyqt6
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index e4212970b..08bac516f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -122,7 +122,7 @@ docs = [
"qtgallery",
# extras for all the widgets
"napari ==0.4.18",
- "pyside6 ==6.4.2", # 6.4.3 gives segfault for some reason
+ "pyqt6",
"pint",
"matplotlib",
"ipywidgets >=8.0.0",
From 9d6183073ef2d6ef3f6e778d637566d764a4c799 Mon Sep 17 00:00:00 2001
From: Ashley Anderson
Date: Wed, 6 Dec 2023 11:25:26 -0500
Subject: [PATCH 17/25] Ensure QImage is ARGB32 before converting to numpy
(#618)
---
src/magicgui/backends/_qtpy/widgets.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py
index a94bd3665..b99c60501 100644
--- a/src/magicgui/backends/_qtpy/widgets.py
+++ b/src/magicgui/backends/_qtpy/widgets.py
@@ -181,6 +181,8 @@ def _mgui_render(self) -> numpy.ndarray:
) from None
img = self._qwidget.grab().toImage()
+ if img.format() != QImage.Format_ARGB32:
+ img = img.convertToFormat(QImage.Format_ARGB32)
bits = img.constBits()
h, w, c = img.height(), img.width(), 4
if qtpy.API_NAME.startswith("PySide"):
From 3e82c272f3817ad6a02fa20e4218d81568c305ed Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 11:25:38 -0500
Subject: [PATCH 18/25] ci(pre-commit.ci): autoupdate (#616)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.6)
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
- [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.1)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 15fbd920a..4fe691bde 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,13 +14,13 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.4
+ rev: v0.1.6
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.10.1
+ rev: 23.11.0
hooks:
- id: black
@@ -30,7 +30,7 @@ repos:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.6.1
+ rev: v1.7.1
hooks:
- id: mypy
files: "^src/"
From 2eb915937d6001b6528139c38aecbe148e476ad0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 Dec 2023 11:25:47 -0500
Subject: [PATCH 19/25] ci(dependabot): bump tlambert03/setup-qt-libs from 1.5
to 1.6 (#615)
Bumps [tlambert03/setup-qt-libs](https://github.com/tlambert03/setup-qt-libs) from 1.5 to 1.6.
- [Release notes](https://github.com/tlambert03/setup-qt-libs/releases)
- [Commits](https://github.com/tlambert03/setup-qt-libs/compare/v1.5...v1.6)
---
updated-dependencies:
- dependency-name: tlambert03/setup-qt-libs
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/test_and_deploy.yml | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index f5fa85af2..8766e8727 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -45,7 +45,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- name: Install dependencies
run: python -m pip install --upgrade hatch
@@ -69,7 +69,7 @@ jobs:
with:
python-version: "3.11"
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- name: Install dependencies
run: |
@@ -92,7 +92,7 @@ jobs:
with:
repository: napari/napari
path: napari-from-github
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- uses: actions/setup-python@v4
with:
python-version: "3.10"
@@ -117,7 +117,7 @@ jobs:
with:
repository: hanjinliu/magic-class
path: magic-class
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- uses: actions/setup-python@v4
with:
python-version: "3.10"
@@ -145,7 +145,7 @@ jobs:
with:
repository: stardist/stardist-napari
path: stardist-napari
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- uses: actions/setup-python@v4
with:
python-version: "3.10"
@@ -173,7 +173,7 @@ jobs:
with:
repository: 4DNucleome/PartSeg
path: PartSeg
- - uses: tlambert03/setup-qt-libs@v1.5
+ - uses: tlambert03/setup-qt-libs@v1.6
- uses: actions/setup-python@v4
with:
python-version: "3.10"
From 05e2a8a268d840a34bf8939511cb437aa7d414c0 Mon Sep 17 00:00:00 2001
From: Talley Lambert
Date: Wed, 6 Dec 2023 11:36:46 -0500
Subject: [PATCH 20/25] chore: changelog v0.8.1
---
CHANGELOG.md | 35 ++++++++++++++++++++++++++++++-----
1 file changed, 30 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e862f96c5..0cb444e74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
# Changelog
+## [v0.8.1](https://github.com/pyapp-kit/magicgui/tree/v0.8.1) (2023-12-06)
+
+[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.8.0...v0.8.1)
+
+**Implemented enhancements:**
+
+- feat: add `Table.delete_row` method [\#610](https://github.com/pyapp-kit/magicgui/pull/610) ([tlambert03](https://github.com/tlambert03))
+- feat: add toolbar widget [\#597](https://github.com/pyapp-kit/magicgui/pull/597) ([tlambert03](https://github.com/tlambert03))
+
+**Fixed bugs:**
+
+- Ensure QImage is ARGB32 before converting to numpy [\#618](https://github.com/pyapp-kit/magicgui/pull/618) ([aganders3](https://github.com/aganders3))
+- fix: allow future annotations in ipywidgets backend [\#609](https://github.com/pyapp-kit/magicgui/pull/609) ([tlambert03](https://github.com/tlambert03))
+- Make kwargs of container-like widgets consistent [\#606](https://github.com/pyapp-kit/magicgui/pull/606) ([hanjinliu](https://github.com/hanjinliu))
+
+**Documentation:**
+
+- docs: unpin pyside6 when building docs [\#614](https://github.com/pyapp-kit/magicgui/pull/614) ([tlambert03](https://github.com/tlambert03))
+
+**Merged pull requests:**
+
+- ci\(dependabot\): bump tlambert03/setup-qt-libs from 1.5 to 1.6 [\#615](https://github.com/pyapp-kit/magicgui/pull/615) ([dependabot[bot]](https://github.com/apps/dependabot))
+- chore!: remove older deprecations [\#607](https://github.com/pyapp-kit/magicgui/pull/607) ([tlambert03](https://github.com/tlambert03))
+
## [v0.8.0](https://github.com/pyapp-kit/magicgui/tree/v0.8.0) (2023-10-20)
[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.7.3...v0.8.0)
@@ -32,6 +56,7 @@
**Merged pull requests:**
+- chore: changelog v0.8.0 [\#605](https://github.com/pyapp-kit/magicgui/pull/605) ([tlambert03](https://github.com/tlambert03))
- style: use `Unpack` for better kwargs typing [\#599](https://github.com/pyapp-kit/magicgui/pull/599) ([tlambert03](https://github.com/tlambert03))
- chore: remove setup.py [\#595](https://github.com/pyapp-kit/magicgui/pull/595) ([tlambert03](https://github.com/tlambert03))
- ci\(dependabot\): bump actions/checkout from 3 to 4 [\#578](https://github.com/pyapp-kit/magicgui/pull/578) ([dependabot[bot]](https://github.com/apps/dependabot))
@@ -548,7 +573,7 @@
## [v0.2.9](https://github.com/pyapp-kit/magicgui/tree/v0.2.9) (2021-04-05)
-[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.8...v0.2.9)
+[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.8rc0...v0.2.9)
**Implemented enhancements:**
@@ -574,13 +599,13 @@
- \[pre-commit.ci\] pre-commit autoupdate [\#212](https://github.com/pyapp-kit/magicgui/pull/212) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
-## [v0.2.8](https://github.com/pyapp-kit/magicgui/tree/v0.2.8) (2021-03-24)
+## [v0.2.8rc0](https://github.com/pyapp-kit/magicgui/tree/v0.2.8rc0) (2021-03-24)
-[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.8rc0...v0.2.8)
+[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.8...v0.2.8rc0)
-## [v0.2.8rc0](https://github.com/pyapp-kit/magicgui/tree/v0.2.8rc0) (2021-03-24)
+## [v0.2.8](https://github.com/pyapp-kit/magicgui/tree/v0.2.8) (2021-03-24)
-[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.7...v0.2.8rc0)
+[Full Changelog](https://github.com/pyapp-kit/magicgui/compare/v0.2.7...v0.2.8)
**Implemented enhancements:**
From 5758566e9484205398c2e250a271e8fa01847ea8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Dec 2023 09:46:02 -0500
Subject: [PATCH 21/25] ci(dependabot): bump actions/setup-python from 4 to 5
(#620)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)
---
updated-dependencies:
- dependency-name: actions/setup-python
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/deploy_docs.yml | 2 +-
.github/workflows/test_and_deploy.yml | 14 +++++++-------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index c172c3ee9..69583f508 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.x"
- run: |
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index 8766e8727..f96b35a26 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -42,7 +42,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: tlambert03/setup-qt-libs@v1.6
@@ -65,7 +65,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.11"
@@ -93,7 +93,7 @@ jobs:
repository: napari/napari
path: napari-from-github
- uses: tlambert03/setup-qt-libs@v1.6
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install
@@ -118,7 +118,7 @@ jobs:
repository: hanjinliu/magic-class
path: magic-class
- uses: tlambert03/setup-qt-libs@v1.6
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install
@@ -146,7 +146,7 @@ jobs:
repository: stardist/stardist-napari
path: stardist-napari
- uses: tlambert03/setup-qt-libs@v1.6
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install
@@ -174,7 +174,7 @@ jobs:
repository: 4DNucleome/PartSeg
path: PartSeg
- uses: tlambert03/setup-qt-libs@v1.6
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install
@@ -197,7 +197,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
From 96d2d99bfe8c16cf38d8dce450fe3a81a11d0d70 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Dec 2023 09:46:10 -0500
Subject: [PATCH 22/25] ci(dependabot): bump aganders3/headless-gui from 1 to 2
(#619)
Bumps [aganders3/headless-gui](https://github.com/aganders3/headless-gui) from 1 to 2.
- [Release notes](https://github.com/aganders3/headless-gui/releases)
- [Commits](https://github.com/aganders3/headless-gui/compare/v1...v2)
---
updated-dependencies:
- dependency-name: aganders3/headless-gui
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/test_and_deploy.yml | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index f96b35a26..5424bfe73 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -50,7 +50,7 @@ jobs:
run: python -m pip install --upgrade hatch
- name: Test
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
with:
run: hatch -v run +backend=${{ matrix.backend }} test:run
@@ -77,7 +77,7 @@ jobs:
python -m pip install pytest 'pydantic<2' attrs pytest-cov pyqt6
- name: Test
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
with:
run: pytest tests/test_ui_field.py -v --color=yes --cov=magicgui --cov-report=xml
@@ -103,7 +103,7 @@ jobs:
python -m pip install -e ./napari-from-github[pyqt5]
- name: Test napari magicgui
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
with:
working-directory: napari-from-github
run: pytest -W ignore napari/_tests/test_magicgui.py -v --color=yes
@@ -128,7 +128,7 @@ jobs:
python -m pip install ./magic-class[testing]
- name: Test magicclass
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
# magicclass is still in development, don't fail the whole build
# this makes this much less useful... but it's better than nothing?
continue-on-error: true
@@ -156,7 +156,7 @@ jobs:
python -m pip install ./stardist-napari[test]
- name: Run stardist tests
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
with:
working-directory: stardist-napari
run: python -m pytest -v --color=yes -W ignore stardist_napari
@@ -183,7 +183,7 @@ jobs:
python -m pip install ./PartSeg[test,pyqt5]
- name: Run PartSeg tests
- uses: aganders3/headless-gui@v1
+ uses: aganders3/headless-gui@v2
with:
working-directory: PartSeg
run: python -m pytest -v --color=yes -W ignore package/tests/test_PartSeg/test_napari_widgets.py
From bdda89f1e873a0ee5bd7977ae696dde35917ce4d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 9 Jan 2024 10:10:42 -0500
Subject: [PATCH 23/25] ci(pre-commit.ci): autoupdate (#622)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.9)
- [github.com/psf/black: 23.11.0 → 23.12.1](https://github.com/psf/black/compare/23.11.0...23.12.1)
- [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4fe691bde..7bd52dad5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,13 +14,13 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.6
+ rev: v0.1.9
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.11.0
+ rev: 23.12.1
hooks:
- id: black
@@ -30,7 +30,7 @@ repos:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.7.1
+ rev: v1.8.0
hooks:
- id: mypy
files: "^src/"
From 5e30d286cef4e778e7eeafc110daa18ad1b16373 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 7 Feb 2024 13:13:49 -0500
Subject: [PATCH 24/25] ci(dependabot): bump tlambert03/setup-qt-libs from 1.6
to 1.7 (#625)
Bumps [tlambert03/setup-qt-libs](https://github.com/tlambert03/setup-qt-libs) from 1.6 to 1.7.
- [Release notes](https://github.com/tlambert03/setup-qt-libs/releases)
- [Commits](https://github.com/tlambert03/setup-qt-libs/compare/v1.6...v1.7)
---
updated-dependencies:
- dependency-name: tlambert03/setup-qt-libs
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/test_and_deploy.yml | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml
index 5424bfe73..c308a41a0 100644
--- a/.github/workflows/test_and_deploy.yml
+++ b/.github/workflows/test_and_deploy.yml
@@ -45,7 +45,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- name: Install dependencies
run: python -m pip install --upgrade hatch
@@ -69,7 +69,7 @@ jobs:
with:
python-version: "3.11"
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- name: Install dependencies
run: |
@@ -92,7 +92,7 @@ jobs:
with:
repository: napari/napari
path: napari-from-github
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- uses: actions/setup-python@v5
with:
python-version: "3.10"
@@ -117,7 +117,7 @@ jobs:
with:
repository: hanjinliu/magic-class
path: magic-class
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- uses: actions/setup-python@v5
with:
python-version: "3.10"
@@ -145,7 +145,7 @@ jobs:
with:
repository: stardist/stardist-napari
path: stardist-napari
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- uses: actions/setup-python@v5
with:
python-version: "3.10"
@@ -173,7 +173,7 @@ jobs:
with:
repository: 4DNucleome/PartSeg
path: PartSeg
- - uses: tlambert03/setup-qt-libs@v1.6
+ - uses: tlambert03/setup-qt-libs@v1.7
- uses: actions/setup-python@v5
with:
python-version: "3.10"
From 94c8a3bf3f0b46f41c0eb19c61e387d0ff9802bc Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 7 Feb 2024 13:15:40 -0500
Subject: [PATCH 25/25] ci(pre-commit.ci): autoupdate (#626)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ci(pre-commit.ci): autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.2.0)
- [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1)
- [github.com/abravalheri/validate-pyproject: v0.15 → v0.16](https://github.com/abravalheri/validate-pyproject/compare/v0.15...v0.16)
* style(pre-commit.ci): auto fixes [...]
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 ++--
docs/examples/applications/callable.py | 1 +
docs/examples/applications/chaining.py | 1 +
docs/examples/applications/hotdog.py | 1 +
docs/examples/applications/pint_quantity.py | 1 +
docs/examples/basic_example.py | 1 +
docs/examples/basic_widgets_demo.py | 1 +
docs/examples/demo_widgets/change_label.py | 1 +
docs/examples/demo_widgets/choices.py | 1 +
docs/examples/demo_widgets/file_dialog.py | 1 +
docs/examples/demo_widgets/log_slider.py | 1 +
docs/examples/demo_widgets/login.py | 1 +
docs/examples/demo_widgets/optional.py | 1 +
docs/examples/demo_widgets/range_slider.py | 1 +
docs/examples/demo_widgets/selection.py | 1 +
docs/examples/demo_widgets/table.py | 1 +
docs/examples/matplotlib/waveform.py | 1 +
docs/examples/napari/napari_combine_qt.py | 1 +
docs/examples/napari/napari_forward_refs.py | 1 +
docs/examples/progress_bars/progress.py | 1 +
.../progress_bars/progress_indeterminate.py | 1 +
.../examples/progress_bars/progress_manual.py | 1 +
docs/examples/under_the_hood/class_method.py | 1 +
.../examples/under_the_hood/self_reference.py | 1 +
docs/scripts/_hooks.py | 1 +
src/magicgui/__init__.py | 1 +
src/magicgui/_util.py | 8 ++---
src/magicgui/application.py | 1 +
src/magicgui/backends/__init__.py | 1 +
src/magicgui/experimental.py | 1 +
src/magicgui/schema/_guiclass.py | 16 ++++------
src/magicgui/signature.py | 1 +
src/magicgui/tqdm.py | 1 +
src/magicgui/type_map/__init__.py | 1 +
src/magicgui/type_map/_magicgui.py | 24 +++++----------
src/magicgui/type_map/_type_map.py | 7 ++---
src/magicgui/types.py | 1 +
src/magicgui/widgets/_concrete.py | 25 ++++++----------
src/magicgui/widgets/_function_gui.py | 1 +
src/magicgui/widgets/_table.py | 3 +-
src/magicgui/widgets/bases/__init__.py | 1 +
.../widgets/bases/_container_widget.py | 6 ++--
src/magicgui/widgets/protocols.py | 3 +-
tests/test_container.py | 3 +-
tests/test_magicgui.py | 30 +++++++------------
tests/test_persistence.py | 3 +-
tests/test_return_widgets.py | 6 ++--
tests/test_table.py | 1 +
tests/test_tqdm.py | 1 +
tests/test_types.py | 3 +-
tests/test_ui_field.py | 3 +-
tests/test_widgets.py | 6 ++--
52 files changed, 92 insertions(+), 96 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7bd52dad5..948d9accc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,18 +14,18 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.9
+ rev: v0.2.0
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.12.1
+ rev: 24.1.1
hooks:
- id: black
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.15
+ rev: v0.16
hooks:
- id: validate-pyproject
diff --git a/docs/examples/applications/callable.py b/docs/examples/applications/callable.py
index a5d3c4b9c..0aa756a81 100644
--- a/docs/examples/applications/callable.py
+++ b/docs/examples/applications/callable.py
@@ -2,6 +2,7 @@
This example demonstrates handling callable functions with magicgui.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/applications/chaining.py b/docs/examples/applications/chaining.py
index 0147a0f35..10d270828 100644
--- a/docs/examples/applications/chaining.py
+++ b/docs/examples/applications/chaining.py
@@ -2,6 +2,7 @@
This example demonstrates chaining multiple functions together.
"""
+
from magicgui import magicgui, widgets
diff --git a/docs/examples/applications/hotdog.py b/docs/examples/applications/hotdog.py
index f5329a9d6..bad1a7671 100644
--- a/docs/examples/applications/hotdog.py
+++ b/docs/examples/applications/hotdog.py
@@ -2,6 +2,7 @@
Demo app to upload an image and classify if it's an hotdog or not.
"""
+
import pathlib
from enum import Enum
diff --git a/docs/examples/applications/pint_quantity.py b/docs/examples/applications/pint_quantity.py
index 9fa811355..b4ef0f1f0 100644
--- a/docs/examples/applications/pint_quantity.py
+++ b/docs/examples/applications/pint_quantity.py
@@ -6,6 +6,7 @@
from and to different units.
https://pint.readthedocs.io/en/stable/
"""
+
from pint import Quantity
from magicgui import magicgui
diff --git a/docs/examples/basic_example.py b/docs/examples/basic_example.py
index 6e06da627..890e1cd41 100644
--- a/docs/examples/basic_example.py
+++ b/docs/examples/basic_example.py
@@ -2,6 +2,7 @@
A basic example using magicgui.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/basic_widgets_demo.py b/docs/examples/basic_widgets_demo.py
index b3ae376cf..c051c9565 100644
--- a/docs/examples/basic_widgets_demo.py
+++ b/docs/examples/basic_widgets_demo.py
@@ -5,6 +5,7 @@
This code demonstrates a few of the widget types that magicgui can create
based on the parameter types in your function.
"""
+
import datetime
from enum import Enum
from pathlib import Path
diff --git a/docs/examples/demo_widgets/change_label.py b/docs/examples/demo_widgets/change_label.py
index 4e3b660dc..ca94598fb 100644
--- a/docs/examples/demo_widgets/change_label.py
+++ b/docs/examples/demo_widgets/change_label.py
@@ -2,6 +2,7 @@
An example showing how to create custom text labels for your widgets.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/choices.py b/docs/examples/demo_widgets/choices.py
index 74b3857f3..ec309725f 100644
--- a/docs/examples/demo_widgets/choices.py
+++ b/docs/examples/demo_widgets/choices.py
@@ -2,6 +2,7 @@
Choices for dropdowns can be provided in a few different ways.
"""
+
from enum import Enum
from magicgui import magicgui, widgets
diff --git a/docs/examples/demo_widgets/file_dialog.py b/docs/examples/demo_widgets/file_dialog.py
index ff7230ebd..11093ad79 100644
--- a/docs/examples/demo_widgets/file_dialog.py
+++ b/docs/examples/demo_widgets/file_dialog.py
@@ -2,6 +2,7 @@
A file dialog widget example.
"""
+
from pathlib import Path
from typing import Sequence
diff --git a/docs/examples/demo_widgets/log_slider.py b/docs/examples/demo_widgets/log_slider.py
index bd3385fdf..cd0eb5b7e 100644
--- a/docs/examples/demo_widgets/log_slider.py
+++ b/docs/examples/demo_widgets/log_slider.py
@@ -2,6 +2,7 @@
A logarithmic scale range slider widget.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/login.py b/docs/examples/demo_widgets/login.py
index 3cefafb42..4bb253079 100644
--- a/docs/examples/demo_widgets/login.py
+++ b/docs/examples/demo_widgets/login.py
@@ -2,6 +2,7 @@
A password login field widget.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/optional.py b/docs/examples/demo_widgets/optional.py
index 8cae102c9..6fa88fd94 100644
--- a/docs/examples/demo_widgets/optional.py
+++ b/docs/examples/demo_widgets/optional.py
@@ -2,6 +2,7 @@
Optional user input using a dropdown selection widget.
"""
+
from typing import Optional
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/range_slider.py b/docs/examples/demo_widgets/range_slider.py
index 9d25d72e2..cf3c8a464 100644
--- a/docs/examples/demo_widgets/range_slider.py
+++ b/docs/examples/demo_widgets/range_slider.py
@@ -2,6 +2,7 @@
A double ended range slider widget.
"""
+
from typing import Tuple
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/selection.py b/docs/examples/demo_widgets/selection.py
index 8f92052c7..c956e33a9 100644
--- a/docs/examples/demo_widgets/selection.py
+++ b/docs/examples/demo_widgets/selection.py
@@ -2,6 +2,7 @@
A selection widget allowing multiple selections by the user.
"""
+
from magicgui import magicgui
diff --git a/docs/examples/demo_widgets/table.py b/docs/examples/demo_widgets/table.py
index 4af4b870c..64c80f867 100644
--- a/docs/examples/demo_widgets/table.py
+++ b/docs/examples/demo_widgets/table.py
@@ -2,6 +2,7 @@
Demonstrating a few ways to input tables.
"""
+
import numpy as np
from magicgui.widgets import Table
diff --git a/docs/examples/matplotlib/waveform.py b/docs/examples/matplotlib/waveform.py
index 146096232..4ea94c0a7 100644
--- a/docs/examples/matplotlib/waveform.py
+++ b/docs/examples/matplotlib/waveform.py
@@ -2,6 +2,7 @@
Simple waveform generator widget, with plotting.
"""
+
from dataclasses import dataclass, field
from enum import Enum
from functools import partial
diff --git a/docs/examples/napari/napari_combine_qt.py b/docs/examples/napari/napari_combine_qt.py
index 9a4351e27..317db73f4 100644
--- a/docs/examples/napari/napari_combine_qt.py
+++ b/docs/examples/napari/napari_combine_qt.py
@@ -10,6 +10,7 @@
This example shows how to use just that widget in the context
of a larger custom QWidget.
"""
+
import napari
from qtpy.QtWidgets import QVBoxLayout, QWidget
diff --git a/docs/examples/napari/napari_forward_refs.py b/docs/examples/napari/napari_forward_refs.py
index 1700c0d3c..1ad2995a2 100644
--- a/docs/examples/napari/napari_forward_refs.py
+++ b/docs/examples/napari/napari_forward_refs.py
@@ -8,6 +8,7 @@
a *string* representation of the type (rather than the type itself). This is called a
"forward reference": https://www.python.org/dev/peps/pep-0484/#forward-references
"""
+
# Note: if you'd like to avoid circular imports, or just want to avoid having your
# linter yell at you for an undefined type annotation, you can place the import
# inside of an `if typing.TYPE_CHECKING` conditional. This is not evaluated at runtime,
diff --git a/docs/examples/progress_bars/progress.py b/docs/examples/progress_bars/progress.py
index 6f064b5e5..4e440605b 100644
--- a/docs/examples/progress_bars/progress.py
+++ b/docs/examples/progress_bars/progress.py
@@ -2,6 +2,7 @@
A simple progress bar demo with magicgui.
"""
+
from time import sleep
from magicgui import magicgui
diff --git a/docs/examples/progress_bars/progress_indeterminate.py b/docs/examples/progress_bars/progress_indeterminate.py
index e95aae28f..43051c46a 100644
--- a/docs/examples/progress_bars/progress_indeterminate.py
+++ b/docs/examples/progress_bars/progress_indeterminate.py
@@ -4,6 +4,7 @@
of unknown time.
"""
+
import time
from superqt.utils import thread_worker
diff --git a/docs/examples/progress_bars/progress_manual.py b/docs/examples/progress_bars/progress_manual.py
index ccc907862..6848067da 100644
--- a/docs/examples/progress_bars/progress_manual.py
+++ b/docs/examples/progress_bars/progress_manual.py
@@ -3,6 +3,7 @@
Example of a progress bar being updated manually.
"""
+
from magicgui import magicgui
from magicgui.widgets import ProgressBar
diff --git a/docs/examples/under_the_hood/class_method.py b/docs/examples/under_the_hood/class_method.py
index 7ca336755..f58a93919 100644
--- a/docs/examples/under_the_hood/class_method.py
+++ b/docs/examples/under_the_hood/class_method.py
@@ -6,6 +6,7 @@
in which the instance will always be provided as the first argument (i.e. "self") when
the FunctionGui or method is called.
"""
+
from magicgui import event_loop, magicgui
from magicgui.widgets import Container
diff --git a/docs/examples/under_the_hood/self_reference.py b/docs/examples/under_the_hood/self_reference.py
index 58f2a68f6..0940856c0 100644
--- a/docs/examples/under_the_hood/self_reference.py
+++ b/docs/examples/under_the_hood/self_reference.py
@@ -2,6 +2,7 @@
Widgets created with magicgui can reference themselves, and use the widget API.
"""
+
from magicgui import magicgui
diff --git a/docs/scripts/_hooks.py b/docs/scripts/_hooks.py
index d7922e5f7..887a37311 100644
--- a/docs/scripts/_hooks.py
+++ b/docs/scripts/_hooks.py
@@ -1,4 +1,5 @@
"""https://www.mkdocs.org/dev-guide/plugins/#events ."""
+
from __future__ import annotations
import importlib.abc
diff --git a/src/magicgui/__init__.py b/src/magicgui/__init__.py
index 677970e93..2f6f1a269 100644
--- a/src/magicgui/__init__.py
+++ b/src/magicgui/__init__.py
@@ -1,4 +1,5 @@
"""magicgui is a utility for generating a GUI from a python function."""
+
from importlib.metadata import PackageNotFoundError, version
try:
diff --git a/src/magicgui/_util.py b/src/magicgui/_util.py
index 014aabdcd..85c8dde93 100644
--- a/src/magicgui/_util.py
+++ b/src/magicgui/_util.py
@@ -21,13 +21,13 @@
@overload
-def debounce(function: Callable[P, T]) -> Callable[P, T | None]:
- ...
+def debounce(function: Callable[P, T]) -> Callable[P, T | None]: ...
@overload
-def debounce(*, wait: float = 0.2) -> Callable[[Callable[P, T]], Callable[P, T | None]]:
- ...
+def debounce(
+ *, wait: float = 0.2
+) -> Callable[[Callable[P, T]], Callable[P, T | None]]: ...
def debounce(function: Callable[P, T] | None = None, wait: float = 0.2) -> Callable:
diff --git a/src/magicgui/application.py b/src/magicgui/application.py
index 9df1e3e91..2792ad824 100644
--- a/src/magicgui/application.py
+++ b/src/magicgui/application.py
@@ -1,4 +1,5 @@
"""Magicgui application object, wrapping a backend application."""
+
from __future__ import annotations
import signal
diff --git a/src/magicgui/backends/__init__.py b/src/magicgui/backends/__init__.py
index 214876cad..9470e2924 100644
--- a/src/magicgui/backends/__init__.py
+++ b/src/magicgui/backends/__init__.py
@@ -1,4 +1,5 @@
"""Backend modules implementing applications and widgets."""
+
from __future__ import annotations
BACKENDS: dict[str, tuple[str, str]] = {
diff --git a/src/magicgui/experimental.py b/src/magicgui/experimental.py
index 586e38b59..48840b67d 100644
--- a/src/magicgui/experimental.py
+++ b/src/magicgui/experimental.py
@@ -3,6 +3,7 @@
Note: these are not guaranteed to be stable. Names and parameters may change or be
removed in future releases. Use at your own risk.
"""
+
from .schema._guiclass import button, guiclass, is_guiclass
__all__ = ["guiclass", "button", "is_guiclass"]
diff --git a/src/magicgui/schema/_guiclass.py b/src/magicgui/schema/_guiclass.py
index ae522f8a4..567b878a3 100644
--- a/src/magicgui/schema/_guiclass.py
+++ b/src/magicgui/schema/_guiclass.py
@@ -6,6 +6,7 @@
2. Adds a `gui` property to the class that will return a `magicgui` widget, bound to
the values of the dataclass instance.
"""
+
from __future__ import annotations
import contextlib
@@ -63,8 +64,7 @@ def __dataclass_transform__(
@__dataclass_transform__(field_specifiers=(Field, field))
@overload
-def guiclass(cls: T) -> T:
- ...
+def guiclass(cls: T) -> T: ...
@__dataclass_transform__(field_specifiers=(Field, field))
@@ -75,8 +75,7 @@ def guiclass(
events_namespace: str = "events",
follow_changes: bool = True,
**dataclass_kwargs: Any,
-) -> Callable[[T], T]:
- ...
+) -> Callable[[T], T]: ...
def guiclass(
@@ -168,13 +167,11 @@ def is_guiclass(obj: object) -> TypeGuard[GuiClassProtocol]:
@overload
-def button(func: F) -> F:
- ...
+def button(func: F) -> F: ...
@overload
-def button(**kwargs: Any) -> Callable[[F], F]:
- ...
+def button(**kwargs: Any) -> Callable[[F], F]: ...
def button(func: F | None = None, **button_kwargs: Any) -> F | Callable[[F], F]:
@@ -339,5 +336,4 @@ class GuiClass(metaclass=GuiClassMeta):
# the mypy dataclass magic doesn't work without the literal decorator
# it WILL work with pyright due to the __dataclass_transform__ above
# here we just avoid a false error in mypy
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- ...
+ def __init__(self, *args: Any, **kwargs: Any) -> None: ...
diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py
index 3b61c9a25..835dd0df5 100644
--- a/src/magicgui/signature.py
+++ b/src/magicgui/signature.py
@@ -11,6 +11,7 @@
(it returns a ``MagicSignature``, which is a subclass of ``inspect.Signature``)
"""
+
from __future__ import annotations
import inspect
diff --git a/src/magicgui/tqdm.py b/src/magicgui/tqdm.py
index 065c41b8f..231ece857 100644
--- a/src/magicgui/tqdm.py
+++ b/src/magicgui/tqdm.py
@@ -1,4 +1,5 @@
"""A wrapper around the tqdm.tqdm iterator that adds a ProgressBar to a magicgui."""
+
from __future__ import annotations
import contextlib
diff --git a/src/magicgui/type_map/__init__.py b/src/magicgui/type_map/__init__.py
index df8c1cf8b..3e32bc615 100644
--- a/src/magicgui/type_map/__init__.py
+++ b/src/magicgui/type_map/__init__.py
@@ -1,4 +1,5 @@
"""Functions that map python types to widgets."""
+
from ._type_map import get_widget_class, register_type, type2callback, type_registered
__all__ = [
diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py
index 1cec52d04..dec1a00ca 100644
--- a/src/magicgui/type_map/_magicgui.py
+++ b/src/magicgui/type_map/_magicgui.py
@@ -45,8 +45,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> FunctionGui[_P, _R]:
- ...
+) -> FunctionGui[_P, _R]: ...
@overload
@@ -65,8 +64,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[_P, _R]], FunctionGui[_P, _R]]:
- ...
+) -> Callable[[Callable[_P, _R]], FunctionGui[_P, _R]]: ...
@overload
@@ -85,8 +83,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MainFunctionGui[_P, _R]:
- ...
+) -> MainFunctionGui[_P, _R]: ...
@overload
@@ -105,8 +102,7 @@ def magicgui(
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[_P, _R]], MainFunctionGui[_P, _R]]:
- ...
+) -> Callable[[Callable[_P, _R]], MainFunctionGui[_P, _R]]: ...
def magicgui(
@@ -226,8 +222,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MagicFactory[FunctionGui[_P, _R]]:
- ...
+) -> MagicFactory[FunctionGui[_P, _R]]: ...
@overload
@@ -247,8 +242,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[_P, _R]], MagicFactory[FunctionGui[_P, _R]]]:
- ...
+) -> Callable[[Callable[_P, _R]], MagicFactory[FunctionGui[_P, _R]]]: ...
@overload
@@ -268,8 +262,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> MagicFactory[MainFunctionGui[_P, _R]]:
- ...
+) -> MagicFactory[MainFunctionGui[_P, _R]]: ...
@overload
@@ -289,8 +282,7 @@ def magic_factory(
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
-) -> Callable[[Callable[_P, _R]], MagicFactory[MainFunctionGui[_P, _R]]]:
- ...
+) -> Callable[[Callable[_P, _R]], MagicFactory[MainFunctionGui[_P, _R]]]: ...
def magic_factory(
diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py
index f15021a9e..308a13660 100644
--- a/src/magicgui/type_map/_type_map.py
+++ b/src/magicgui/type_map/_type_map.py
@@ -1,4 +1,5 @@
"""Functions in this module are responsible for mapping type annotations to widgets."""
+
from __future__ import annotations
import datetime
@@ -433,8 +434,7 @@ def register_type(
widget_type: WidgetRef | None = None,
return_callback: ReturnCallback | None = None,
**options: Any,
-) -> _T:
- ...
+) -> _T: ...
@overload
@@ -444,8 +444,7 @@ def register_type(
widget_type: WidgetRef | None = None,
return_callback: ReturnCallback | None = None,
**options: Any,
-) -> Callable[[_T], _T]:
- ...
+) -> Callable[[_T], _T]: ...
def register_type(
diff --git a/src/magicgui/types.py b/src/magicgui/types.py
index fdcc7b818..d00b7fb56 100644
--- a/src/magicgui/types.py
+++ b/src/magicgui/types.py
@@ -1,4 +1,5 @@
"""Types used internally in magicgui."""
+
from __future__ import annotations
from enum import Enum, EnumMeta
diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py
index 9b60a4447..d89d95787 100644
--- a/src/magicgui/widgets/_concrete.py
+++ b/src/magicgui/widgets/_concrete.py
@@ -3,6 +3,7 @@
All of these widgets should provide the `widget_type` argument to their
super().__init__ calls.
"""
+
from __future__ import annotations
import contextlib
@@ -68,8 +69,7 @@
@overload
-def backend_widget(cls: WidgetTypeVar) -> WidgetTypeVar:
- ...
+def backend_widget(cls: WidgetTypeVar) -> WidgetTypeVar: ...
@overload
@@ -78,8 +78,7 @@ def backend_widget(
*,
widget_name: str | None = ...,
transform: Callable[[type], type] | None = ...,
-) -> Callable[[WidgetTypeVar], WidgetTypeVar]:
- ...
+) -> Callable[[WidgetTypeVar], WidgetTypeVar]: ...
def backend_widget(
@@ -785,12 +784,10 @@ def __eq__(self, other: object) -> bool:
return list(self) == other
@overload
- def __getitem__(self, i: int) -> _V:
- ...
+ def __getitem__(self, i: int) -> _V: ...
@overload
- def __getitem__(self, key: slice) -> list[_V]:
- ...
+ def __getitem__(self, key: slice) -> list[_V]: ...
def __getitem__(self, key: int | slice) -> _V | list[_V]:
"""Slice as a list."""
@@ -804,12 +801,10 @@ def __getitem__(self, key: int | slice) -> _V | list[_V]:
)
@overload
- def __setitem__(self, key: int, value: _V) -> None:
- ...
+ def __setitem__(self, key: int, value: _V) -> None: ...
@overload
- def __setitem__(self, key: slice, value: _V | Iterable[_V]) -> None:
- ...
+ def __setitem__(self, key: slice, value: _V | Iterable[_V]) -> None: ...
def __setitem__(self, key: int | slice, value: _V | Iterable[_V]) -> None:
"""Update widget value."""
@@ -830,12 +825,10 @@ def __setitem__(self, key: int | slice, value: _V | Iterable[_V]) -> None:
)
@overload
- def __delitem__(self, key: int) -> None:
- ...
+ def __delitem__(self, key: int) -> None: ...
@overload
- def __delitem__(self, key: slice) -> None:
- ...
+ def __delitem__(self, key: slice) -> None: ...
def __delitem__(self, key: int | slice) -> None:
"""Delete widget at the key(s)."""
diff --git a/src/magicgui/widgets/_function_gui.py b/src/magicgui/widgets/_function_gui.py
index 2cc229a81..9f3eb837f 100644
--- a/src/magicgui/widgets/_function_gui.py
+++ b/src/magicgui/widgets/_function_gui.py
@@ -2,6 +2,7 @@
The core `magicgui` decorator returns an instance of a FunctionGui widget.
"""
+
from __future__ import annotations
import inspect
diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py
index 3cf5eed43..71d7f95f7 100644
--- a/src/magicgui/widgets/_table.py
+++ b/src/magicgui/widgets/_table.py
@@ -15,7 +15,6 @@
Literal,
Mapping,
MutableMapping,
- NoReturn,
Sequence,
TypeVar,
Union,
@@ -835,7 +834,7 @@ def _from_records(data: list[dict[TblKey, Any]]) -> tuple[list[list], list, list
def _validate_table_data(
data: Collection, index: Sequence | None, column: Sequence | None
-) -> None | NoReturn:
+) -> None:
"""Make sure data matches shape of index and column."""
nr = len(data)
if not nr:
diff --git a/src/magicgui/widgets/bases/__init__.py b/src/magicgui/widgets/bases/__init__.py
index 176a12e9f..506eda56a 100644
--- a/src/magicgui/widgets/bases/__init__.py
+++ b/src/magicgui/widgets/bases/__init__.py
@@ -42,6 +42,7 @@ def __init__(
"annotation").
"""
+
from ._button_widget import ButtonWidget
from ._categorical_widget import CategoricalWidget
from ._container_widget import ContainerWidget, DialogWidget, MainWindowWidget
diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py
index a1fc17ee9..302ed2f2b 100644
--- a/src/magicgui/widgets/bases/_container_widget.py
+++ b/src/magicgui/widgets/bases/_container_widget.py
@@ -141,12 +141,10 @@ def __setattr__(self, name: str, value: Any) -> None:
object.__setattr__(self, name, value)
@overload
- def __getitem__(self, key: int | str) -> WidgetVar:
- ...
+ def __getitem__(self, key: int | str) -> WidgetVar: ...
@overload
- def __getitem__(self, key: slice) -> MutableSequence[WidgetVar]:
- ...
+ def __getitem__(self, key: slice) -> MutableSequence[WidgetVar]: ...
def __getitem__(
self, key: int | str | slice
diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py
index 952dc8fdd..348d5be5d 100644
--- a/src/magicgui/widgets/protocols.py
+++ b/src/magicgui/widgets/protocols.py
@@ -7,6 +7,7 @@
For an example backend implementation, see ``magicgui.backends._qtpy.widgets``
"""
+
from __future__ import annotations
from abc import ABC, abstractmethod
@@ -27,7 +28,7 @@
from magicgui.widgets.bases import Widget
-def assert_protocol(widget_class: type, protocol: type) -> None | NoReturn:
+def assert_protocol(widget_class: type, protocol: type) -> None:
"""Ensure that widget_class implements protocol, or raise helpful error."""
if not isinstance(widget_class, protocol):
_raise_protocol_error(widget_class, protocol)
diff --git a/tests/test_container.py b/tests/test_container.py
index 15a2b205f..15a85880f 100644
--- a/tests/test_container.py
+++ b/tests/test_container.py
@@ -199,8 +199,7 @@ def __init__(self) -> None:
btn.changed.connect(self._on_clicked)
super().__init__(widgets=[btn])
- def _on_clicked(self):
- ...
+ def _on_clicked(self): ...
assert isinstance(C(), widgets.Container)
diff --git a/tests/test_magicgui.py b/tests/test_magicgui.py
index c20d00316..3fac94e7a 100644
--- a/tests/test_magicgui.py
+++ b/tests/test_magicgui.py
@@ -83,8 +83,7 @@ def func(a: int = 1):
# also without type annotation
@magicgui(a={"widget_type": "LogSlider"})
- def g(a):
- ...
+ def g(a): ...
assert isinstance(g.a, widgets.LogSlider)
@@ -173,8 +172,7 @@ class Medium(Enum):
Air = 1.0003
@magicgui
- def func(arg: Medium = Medium.Water):
- ...
+ def func(arg: Medium = Medium.Water): ...
assert func.arg.value == Medium.Water
assert isinstance(func.arg, widgets.ComboBox)
@@ -186,8 +184,7 @@ def test_dropdown_list_from_choices():
CHOICES = ["Oil", "Water", "Air"]
@magicgui(arg={"choices": CHOICES})
- def func(arg="Water"):
- ...
+ def func(arg="Water"): ...
assert func.arg.value == "Water"
assert isinstance(func.arg, widgets.ComboBox)
@@ -196,8 +193,7 @@ def func(arg="Water"):
with pytest.raises(ValueError):
# the default value must be in the list
@magicgui(arg={"choices": ["Oil", "Water", "Air"]})
- def func(arg="Silicone"):
- ...
+ def func(arg="Silicone"): ...
def test_dropdown_list_from_callable():
@@ -208,8 +204,7 @@ def get_choices(gui):
return CHOICES
@magicgui(arg={"choices": get_choices})
- def func(arg="Water"):
- ...
+ def func(arg="Water"): ...
assert func.arg.value == "Water"
assert isinstance(func.arg, widgets.ComboBox)
@@ -729,8 +724,7 @@ def test_empty_function():
"""Test that a function with no params works."""
@magicgui(call_button=True)
- def f():
- ...
+ def f(): ...
f.show()
@@ -767,8 +761,7 @@ def func(arg=None):
def test_update_and_dict():
@magicgui
- def test(a: int = 1, y: str = "a"):
- ...
+ def test(a: int = 1, y: str = "a"): ...
assert test.asdict() == {"a": 1, "y": "a"}
@@ -784,8 +777,7 @@ def test(a: int = 1, y: str = "a"):
def test_update_on_call():
@magicgui
- def test(a: int = 1, y: str = "a"):
- ...
+ def test(a: int = 1, y: str = "a"): ...
assert test.call_count == 0
test(a=10, y="b", update_widget=True)
@@ -839,16 +831,14 @@ def some_func2(x: int, y: str) -> str:
def test_scrollable():
@magicgui(scrollable=True)
- def test_scrollable(a: int = 1, y: str = "a"):
- ...
+ def test_scrollable(a: int = 1, y: str = "a"): ...
assert test_scrollable.native is not test_scrollable.root_native_widget
assert not isinstance(test_scrollable.native, QScrollArea)
assert isinstance(test_scrollable.root_native_widget, QScrollArea)
@magicgui(scrollable=False)
- def test_nonscrollable(a: int = 1, y: str = "a"):
- ...
+ def test_nonscrollable(a: int = 1, y: str = "a"): ...
assert test_nonscrollable.native is test_nonscrollable.root_native_widget
assert not isinstance(test_nonscrollable.native, QScrollArea)
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index f22f0e98e..2f4aa564f 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -30,8 +30,7 @@ def test_user_cache_dir():
def test_persistence(tmp_path):
"""Test that we can persist values across instances."""
- def _my_func(x: int, y="hello"):
- ...
+ def _my_func(x: int, y="hello"): ...
with patch("magicgui._util.user_cache_dir", lambda: tmp_path):
fg = FunctionGui(_my_func, persist=True)
diff --git a/tests/test_return_widgets.py b/tests/test_return_widgets.py
index 6daaa5032..776c5f87f 100644
--- a/tests/test_return_widgets.py
+++ b/tests/test_return_widgets.py
@@ -94,12 +94,10 @@ def test_return_widget_for_type(data, expected_type, equality_check):
def test_table_return_annotation():
@magicgui.magicgui(result_widget=True)
- def f() -> "magicgui.widgets.Table":
- ...
+ def f() -> "magicgui.widgets.Table": ...
@magicgui.magicgui(result_widget=True)
- def f2() -> widgets.Table:
- ...
+ def f2() -> widgets.Table: ...
assert isinstance(f._result_widget, widgets.Table)
assert isinstance(f2._result_widget, widgets.Table)
diff --git a/tests/test_table.py b/tests/test_table.py
index 0dd9831be..6a9ef2cb0 100644
--- a/tests/test_table.py
+++ b/tests/test_table.py
@@ -1,4 +1,5 @@
"""Tests for the Table widget."""
+
import os
import sys
diff --git a/tests/test_tqdm.py b/tests/test_tqdm.py
index a362338b7..28f7d8e7b 100644
--- a/tests/test_tqdm.py
+++ b/tests/test_tqdm.py
@@ -1,4 +1,5 @@
"""Tests for the tqdm wrapper."""
+
from time import sleep
import pytest
diff --git a/tests/test_types.py b/tests/test_types.py
index b2d24cb05..01ab55321 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -117,8 +117,7 @@ def widget2(fn: Union[bytes, pathlib.Path, str]):
def test_optional_type():
@magicgui(x={"choices": ["a", "b"]})
- def widget(x: Optional[str] = None):
- ...
+ def widget(x: Optional[str] = None): ...
assert isinstance(widget.x, widgets.ComboBox)
assert widget.x.value is None
diff --git a/tests/test_ui_field.py b/tests/test_ui_field.py
index 45cb1e662..6e701d758 100644
--- a/tests/test_ui_field.py
+++ b/tests/test_ui_field.py
@@ -116,8 +116,7 @@ def foo(
a: Optional[int],
b: Annotated[str, UiField(description="the b")],
c: Annotated[float, UiField(widget="FloatSlider")] = 0.0,
- ):
- ...
+ ): ...
# makes to sense to instantiate a function
_assert_uifields(foo, instantiate=False)
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index 99df22cf1..f87e4d4cb 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -1045,16 +1045,14 @@ def test_literal():
Lit = Literal[None, "a", 1, True, b"bytes"]
@magicgui
- def f(x: Lit):
- ...
+ def f(x: Lit): ...
cbox = f.x
assert type(cbox) is widgets.ComboBox
assert cbox.choices == get_args(Lit)
@magicgui
- def f(x: Set[Lit]):
- ...
+ def f(x: Set[Lit]): ...
sel = f.x
assert type(sel) is widgets.Select