diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4aca031a6..de60455f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.165 + rev: v0.0.174 hooks: - id: ruff args: ["--fix"] diff --git a/pyproject.toml b/pyproject.toml index 893006e03..70eeb04bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,7 +153,7 @@ extend-select = [ "F", # flakes "D", # pydocstyle "I", # isort - "U", # pyupgrade + "UP", # pyupgrade # "N", # pep8-naming # "S", # bandit "C", # flake8-comprehensions @@ -190,6 +190,7 @@ filterwarnings = [ "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 ] # https://mypy.readthedocs.io/en/stable/config_file.html diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py index 6ddd46bf0..2882da81a 100644 --- a/src/magicgui/signature.py +++ b/src/magicgui/signature.py @@ -17,7 +17,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, Callable, Sequence, cast -from typing_extensions import Annotated, _AnnotatedAlias +from typing_extensions import Annotated, get_args, get_origin from magicgui.application import AppRef from magicgui.types import Undefined @@ -27,9 +27,7 @@ TZ_EMPTY = "__no__default__" -def make_annotated( - annotation: Any = Any, options: dict | None = None -) -> _AnnotatedAlias: +def make_annotated(annotation: Any = Any, options: dict | None = None) -> Any: """Merge a annotation and an options dict into an Annotated type. Parameters @@ -55,27 +53,12 @@ def make_annotated( raise TypeError("'options' must be a dict") _options = (options or {}).copy() - if isinstance(annotation, _AnnotatedAlias): - hint, anno_options = split_annotated_type(annotation) + if get_origin(annotation) is Annotated: + annotation, anno_options = get_args(annotation) _options.update(anno_options) - annotation = hint return Annotated[annotation, _options] -def split_annotated_type(annotation: _AnnotatedAlias) -> tuple[Any, dict]: - """Split an Annotated type into its base type and options dict.""" - if not isinstance(annotation, _AnnotatedAlias): - raise TypeError("Type hint must be an 'Annotated' type.") - - meta = annotation.__metadata__[0] - if not isinstance(meta, dict): - raise TypeError( - "Invalid Annotated format for magicgui. First arg must be a dict" - ) - - return annotation.__args__[0], meta - - class _void: """private sentinel.""" @@ -117,7 +100,7 @@ def __init__( @property def options(self) -> dict: """Return just this options part of the annotation.""" - return split_annotated_type(self.annotation)[1] + return cast(dict, get_args(self.annotation)[1]) def __repr__(self) -> str: """Return __repr__, replacing NoneType if present.""" @@ -127,7 +110,7 @@ def __repr__(self) -> str: def __str__(self) -> str: """Return string representation of the Parameter in a signature.""" - hint, _ = split_annotated_type(self.annotation) + hint, _ = get_args(self.annotation) return str( inspect.Parameter( self.name, self.kind, default=self.default, annotation=hint @@ -139,13 +122,11 @@ def to_widget(self, app: AppRef | None = None) -> Widget: from magicgui.widgets import create_widget value = Undefined if self.default in (self.empty, TZ_EMPTY) else self.default - annotation, options = split_annotated_type(self.annotation) widget = create_widget( name=self.name, value=value, - annotation=annotation, + annotation=self.annotation, app=app, - options=options, raise_on_unknown=self.raise_on_unknown, ) widget.param_kind = self.kind diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py index c2bacb08e..cf44a2b5d 100644 --- a/src/magicgui/type_map/_type_map.py +++ b/src/magicgui/type_map/_type_map.py @@ -28,7 +28,7 @@ overload, ) -from typing_extensions import get_args, get_origin +from typing_extensions import Annotated, get_args, get_origin from magicgui import widgets from magicgui._type_resolution import resolve_single_type @@ -200,7 +200,8 @@ def _pick_widget_type( raise_on_unknown: bool = True, ) -> WidgetTuple: """Pick the appropriate widget type for ``value`` with ``annotation``.""" - 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: @@ -263,6 +264,21 @@ def _pick_widget_type( return widgets.EmptyWidget, {"visible": False} +def _split_annotated_type(annotation: Any) -> tuple[Any, dict]: + """Split an Annotated type into its base type and options dict.""" + if get_origin(annotation) is not Annotated: + return annotation, {} + + type_, meta_, *_ = get_args(annotation) + + try: + meta = dict(meta_) + except TypeError: + meta = {} + + return type_, meta + + def get_widget_class( value: Any = Undefined, annotation: Any = Undefined, diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py index 4806da940..57c43829c 100644 --- a/src/magicgui/widgets/bases/_widget.py +++ b/src/magicgui/widgets/bases/_widget.py @@ -14,7 +14,7 @@ BUILDING_DOCS = sys.argv[-2:] == ["build", "docs"] if BUILDING_DOCS: - import numpy as np + pass if TYPE_CHECKING: from weakref import ReferenceType diff --git a/tests/test_signature.py b/tests/test_signature.py index db217b4a0..7c018a5d5 100644 --- a/tests/test_signature.py +++ b/tests/test_signature.py @@ -1,7 +1,7 @@ import pytest from typing_extensions import Annotated -from magicgui.signature import magic_signature, make_annotated, split_annotated_type +from magicgui.signature import magic_signature, make_annotated def test_make_annotated_raises(): @@ -20,15 +20,6 @@ def test_make_annotated_works_with_already_annotated(): ) -def test_split_annotated_raises(): - """Test split_annotated raises on bad input.""" - with pytest.raises(TypeError): - split_annotated_type(int) - - with pytest.raises(TypeError): - split_annotated_type(Annotated[int, 1]) - - def _sample_func(a: int, b: str = "hi"): pass diff --git a/tests/test_types.py b/tests/test_types.py index 41b13cd96..62b7fd587 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -4,6 +4,7 @@ from unittest.mock import Mock import pytest +from typing_extensions import Annotated, get_args from magicgui import magicgui, register_type, type_map, type_registered, types, widgets from magicgui._type_resolution import resolve_single_type @@ -43,6 +44,24 @@ def test_pick_widget_builtins_forward_refs(cls, string): assert wdg.__name__ == cls +@pytest.mark.parametrize( + "hint, expected_wdg", + [ + (Annotated[int, {"min": 8, "max": 9}], widgets.SpinBox), + ( + Annotated[float, {"widget_type": "FloatSlider", "step": 9}], + widgets.FloatSlider, + ), + ], +) +def test_annotated_types(hint, expected_wdg): + wdg, options = type_map.get_widget_class(annotation=hint) + assert wdg is expected_wdg + for k, v in get_args(hint)[1].items(): + if k != "widget_type": + assert options[k] == v + + def test_forward_refs_return_annotation(): """Test that forward refs return annotations get resolved."""