From c11184c8a5d7a6a1d2c13fc78bf4c113a2db4f3a Mon Sep 17 00:00:00 2001 From: Hanjin Liu <40591297+hanjinliu@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:34:01 +0900 Subject: [PATCH] support Annotated in list/tuple (#588) --- src/magicgui/widgets/_concrete.py | 34 +++++++++++++++++++++++-------- tests/test_widgets.py | 18 ++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index bd99fc779..37c708057 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -15,9 +15,11 @@ TYPE_CHECKING, Any, Callable, + ForwardRef, Generic, Iterable, Iterator, + List, Literal, Sequence, Tuple, @@ -29,7 +31,7 @@ ) from weakref import ref -from typing_extensions import get_args, get_origin +from typing_extensions import Annotated, get_args, get_origin from magicgui._type_resolution import resolve_single_type from magicgui._util import merge_super_sigs, safe_issubclass @@ -690,17 +692,27 @@ def annotation(self, value: Any) -> None: self._args_type = None return - value = resolve_single_type(value) + value_resolved = resolve_single_type(value) + if isinstance(value, (str, ForwardRef)): + value = value_resolved + # unwrap annotated (options are not needed to normalize `annotation`) + while get_origin(value) is Annotated: + value = get_args(value)[0] arg: type | None = None - if value and value is not inspect.Parameter.empty: - orig = get_origin(value) or value + if value_resolved and value_resolved is not inspect.Parameter.empty: + orig = get_origin(value_resolved) or value_resolved if not (safe_issubclass(orig, list) or isinstance(orig, list)): raise TypeError( f"cannot set annotation {value} to {type(self).__name__}." ) args = get_args(value) arg = args[0] if len(args) > 0 else None + args_resolved = get_args(value_resolved) + if len(args_resolved) > 0: + value = List[args_resolved[0]] # type: ignore + else: + value = list self._annotation = value self._args_type = arg @@ -929,17 +941,23 @@ def annotation(self, value: Any) -> None: self._args_types = None return - value = resolve_single_type(value) + value_resolved = resolve_single_type(value) + if isinstance(value, (str, ForwardRef)): + value = value_resolved + # unwrap annotated (options are not needed to normalize `annotation`) + while get_origin(value) is Annotated: + value = get_args(value)[0] args: tuple[type, ...] | None = None - if value and value is not inspect.Parameter.empty: - orig = get_origin(value) + if value_resolved and value_resolved is not inspect.Parameter.empty: + orig = get_origin(value_resolved) if not (safe_issubclass(orig, tuple) or isinstance(orig, tuple)): raise TypeError( f"cannot set annotation {value} to {type(self).__name__}." ) args = get_args(value) - value = Tuple[args] + args_resolved = get_args(value_resolved) + value = Tuple[args_resolved] self._annotation = value self._args_types = args diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 04e5e7288..b41f8e4d2 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest +from typing_extensions import Annotated from magicgui import magicgui, types, use_app, widgets from magicgui.widgets import Container, request_values @@ -906,6 +907,15 @@ def f4(x: List[int] = ()): # type: ignore assert type(f4.x[0]) is widgets.SpinBox assert f4.x.value == [0] + @magicgui + def f5(x: List[Annotated[int, {"max": 3}]]): + pass + + assert type(f5.x) is widgets.ListEdit + assert f5.x.annotation == List[int] + f5.x.btn_plus.changed() + assert f5.x[0].max == 3 + def test_tuple_edit(): """Test TupleEdit.""" @@ -946,6 +956,14 @@ def f2(x: Tuple[int, str]): assert f2.x.annotation == Tuple[int, str] assert f2.x.value == (0, "") + @magicgui + def f3(x: Tuple[Annotated[int, {"max": 3}], str]): + pass + + assert type(f3.x) is widgets.TupleEdit + assert f2.x.annotation == Tuple[int, str] + assert f3.x[0].max == 3 + def test_request_values(monkeypatch): from unittest.mock import Mock