Skip to content

Commit 98bfb8a

Browse files
authored
Resolve ForwardRefs on return annotations (#73)
1 parent da0383e commit 98bfb8a

File tree

4 files changed

+63
-9
lines changed

4 files changed

+63
-9
lines changed

magicgui/function_gui.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def __init__(
8383
raise TypeError(f"FunctionGui got unexpected keyword argument{s}: {extra}")
8484
self._function = function
8585
sig = magic_signature(function, gui_options=param_options)
86-
self._return_annotation = sig.return_annotation
8786
super().__init__(
8887
layout=layout,
8988
labels=labels,
@@ -207,7 +206,7 @@ def __call__(self, *args: Any, **kwargs: Any):
207206
with self._result_widget.changed.blocker():
208207
self._result_widget.value = value
209208

210-
return_type = self._return_annotation
209+
return_type = self.return_annotation
211210
if return_type:
212211
for callback in _type2callback(return_type):
213212
callback(self, value, return_type)

magicgui/type_map.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ def _is_subclass(obj, superclass):
4343
return False
4444

4545

46-
def _evaluate_forwardref(type_: ForwardRef) -> Any:
46+
def _evaluate_forwardref(type_: Any) -> Any:
4747
"""Convert typing.ForwardRef into an actual object."""
48+
if isinstance(type_, str):
49+
type_ = ForwardRef(type_)
50+
51+
if not isinstance(type_, ForwardRef):
52+
return type_
53+
4854
from importlib import import_module
4955

5056
try:
@@ -146,8 +152,7 @@ def pick_widget_type(
146152
widget_type = options.pop("widget_type")
147153
return widget_type, options
148154

149-
if isinstance(annotation, ForwardRef):
150-
annotation = _evaluate_forwardref(annotation)
155+
annotation = _evaluate_forwardref(annotation)
151156

152157
dtype = _normalize_type(value, annotation)
153158

@@ -259,6 +264,8 @@ def register_type(
259264
ValueError
260265
If both ``widget_type`` and ``choices`` are None
261266
"""
267+
type_ = _evaluate_forwardref(type_)
268+
262269
if not (return_callback or options.get("choices") or widget_type):
263270
raise ValueError(
264271
"At least one of `widget_type`, `choices`, or "
@@ -305,6 +312,7 @@ def _type2callback(type_: type) -> List[ReturnCallback]:
305312
Where a return callback accepts two arguments (gui, value) and does something.
306313
"""
307314
# look for direct hits
315+
type_ = _evaluate_forwardref(type_)
308316
if type_ in _RETURN_CALLBACKS:
309317
return _RETURN_CALLBACKS[type_]
310318
# look for subclasses

magicgui/widgets/_bases.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def annotation(self):
255255

256256
@annotation.setter
257257
def annotation(self, value):
258-
if isinstance(value, ForwardRef):
258+
if isinstance(value, (str, ForwardRef)):
259259
from magicgui.type_map import _evaluate_forwardref
260260

261261
value = _evaluate_forwardref(value)
@@ -803,17 +803,34 @@ def __init__(
803803
return_annotation: Any = None,
804804
**kwargs,
805805
):
806+
self._return_annotation = None
806807
self._labels = labels
807808
self._layout = layout
808809
kwargs["backend_kwargs"] = {"layout": layout}
809810
super().__init__(**kwargs)
810811
self.changed = EventEmitter(source=self, type="changed")
811-
self._return_annotation = return_annotation
812+
self.return_annotation = return_annotation
812813
self.extend(widgets)
813814
self.parent_changed.connect(self.reset_choices)
814815
self._initialized = True
815816
self._unify_label_widths()
816817

818+
@property
819+
def return_annotation(self):
820+
"""Return annotation to use when converting to :class:`inspect.Signature`.
821+
822+
ForwardRefs will be resolve when setting the annotation.
823+
"""
824+
return self._return_annotation
825+
826+
@return_annotation.setter
827+
def return_annotation(self, value):
828+
if isinstance(value, (str, ForwardRef)):
829+
from magicgui.type_map import _evaluate_forwardref
830+
831+
value = _evaluate_forwardref(value)
832+
self._return_annotation = value
833+
817834
def __getattr__(self, name: str):
818835
"""Return attribute ``name``. Will return a widget if present."""
819836
for widget in self:
@@ -984,7 +1001,7 @@ def to_signature(self) -> MagicSignature:
9841001
params = [
9851002
MagicParameter.from_widget(w) for w in self if w.name and not w.gui_only
9861003
]
987-
return MagicSignature(params, return_annotation=self._return_annotation)
1004+
return MagicSignature(params, return_annotation=self.return_annotation)
9881005

9891006
@classmethod
9901007
def from_signature(cls, sig: inspect.Signature, **kwargs) -> Container:

tests/test_types.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import pytest
22

3-
from magicgui import magicgui, widgets
3+
from magicgui import magicgui, register_type, widgets
4+
from magicgui.function_gui import FunctionGui
45

56

67
def test_forward_refs():
8+
"""Test that forward refs parameter annotations get resolved."""
9+
710
@magicgui
811
def testA(x: "tests.MyInt" = "1"): # type: ignore # noqa
912
pass
@@ -24,3 +27,30 @@ def testA(x: "testsd.MyInt" = "1"): # type: ignore # noqa
2427
pass
2528

2629
assert "Could not resolve the magicgui forward reference" in str(err.value)
30+
31+
32+
def test_forward_refs_return_annotation():
33+
"""Test that forward refs return annotations get resolved."""
34+
35+
@magicgui
36+
def testA() -> int:
37+
return 1
38+
39+
@magicgui
40+
def testB() -> "tests.MyInt": # type: ignore # noqa
41+
return 1
42+
43+
from tests import MyInt
44+
45+
results = []
46+
register_type(MyInt, return_callback=lambda *x: results.append(x))
47+
48+
testA()
49+
assert not results
50+
51+
testB()
52+
gui, result, return_annotation = results[0]
53+
assert isinstance(gui, FunctionGui)
54+
assert result == 1
55+
# the forward ref has been resolved
56+
assert return_annotation is MyInt

0 commit comments

Comments
 (0)