Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix annotated in get_widget_class #525

Merged
merged 4 commits into from
Dec 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ extend-select = [
"F", # flakes
"D", # pydocstyle
"I", # isort
"U", # pyupgrade
"UP", # pyupgrade
# "N", # pep8-naming
# "S", # bandit
"C", # flake8-comprehensions
Expand Down Expand Up @@ -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
Expand Down
33 changes: 7 additions & 26 deletions src/magicgui/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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."""

Expand Down Expand Up @@ -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."""
Expand All @@ -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
Expand All @@ -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
Expand Down
20 changes: 18 additions & 2 deletions src/magicgui/type_map/_type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/magicgui/widgets/bases/_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 1 addition & 10 deletions tests/test_signature.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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

Expand Down
19 changes: 19 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""

Expand Down