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 @@ magicgui on conda-forge + + magicgui python version support +

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