From 952f991f114d981915be172f9a374f0a91337795 Mon Sep 17 00:00:00 2001 From: Hanjin Liu Date: Thu, 7 Nov 2024 11:09:31 +0900 Subject: [PATCH 1/2] implement TypeMap --- src/magicgui/signature.py | 28 +- src/magicgui/type_map/__init__.py | 9 +- src/magicgui/type_map/_magicgui.py | 447 +----- src/magicgui/type_map/_type_map.py | 1441 +++++++++++++----- src/magicgui/widgets/_function_gui.py | 18 +- src/magicgui/widgets/bases/_create_widget.py | 70 +- 6 files changed, 1096 insertions(+), 917 deletions(-) diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py index 7170d25e8..fa5ee1386 100644 --- a/src/magicgui/signature.py +++ b/src/magicgui/signature.py @@ -28,6 +28,7 @@ from typing_extensions import Unpack from magicgui.application import AppRef + from magicgui.type_map import TypeMap from magicgui.widgets import Container, Widget from magicgui.widgets.bases._container_widget import ContainerKwargs @@ -188,12 +189,18 @@ def __str__(self) -> str: ) ) - def to_widget(self, app: AppRef | None = None) -> Widget: + def to_widget( + self, + app: AppRef | None = None, + type_map: TypeMap | None = None, + ) -> Widget: """Create and return a widget for this object.""" - from magicgui.widgets import create_widget + from magicgui.type_map import TypeMap value = Undefined if self.default in (self.empty, TZ_EMPTY) else self.default - widget = create_widget( + if type_map is None: + type_map = TypeMap.global_instance() + widget = type_map.create_widget( name=self.name, value=value, annotation=self.annotation, @@ -286,19 +293,26 @@ def from_signature( raise_on_unknown=raise_on_unknown, ) - def widgets(self, app: AppRef | None = None) -> MappingProxyType: + def widgets( + self, + app: AppRef | None = None, + type_map: TypeMap | None = None, + ) -> MappingProxyType: """Return mapping from parameters to widgets for all params in Signature.""" return MappingProxyType( - {n: p.to_widget(app) for n, p in self.parameters.items()} + {n: p.to_widget(app, type_map=type_map) for n, p in self.parameters.items()} ) def to_container( - self, app: AppRef | None = None, **kwargs: Unpack[ContainerKwargs] + self, + app: AppRef | None = None, + type_map: TypeMap | None = None, + **kwargs: Unpack[ContainerKwargs], ) -> Container: """Return a ``magicgui.widgets.Container`` for this MagicSignature.""" from magicgui.widgets import Container - kwargs["widgets"] = list(self.widgets(app).values()) + kwargs["widgets"] = list(self.widgets(app, type_map=type_map).values()) return Container(**kwargs) def replace( # type: ignore[override] diff --git a/src/magicgui/type_map/__init__.py b/src/magicgui/type_map/__init__.py index 3e32bc615..47d3c39a7 100644 --- a/src/magicgui/type_map/__init__.py +++ b/src/magicgui/type_map/__init__.py @@ -1,10 +1,17 @@ """Functions that map python types to widgets.""" -from ._type_map import get_widget_class, register_type, type2callback, type_registered +from ._type_map import ( + TypeMap, + get_widget_class, + register_type, + type2callback, + type_registered, +) __all__ = [ "get_widget_class", "register_type", "type_registered", "type2callback", + "TypeMap", ] diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index cfe0b9a07..e47f0dc8b 100644 --- a/src/magicgui/type_map/_magicgui.py +++ b/src/magicgui/type_map/_magicgui.py @@ -2,411 +2,17 @@ import inspect from functools import partial -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Generic, - Literal, - TypeVar, - cast, - overload, -) +from typing import Any, Callable, Generic, TypeVar -from magicgui.widgets import FunctionGui, MainFunctionGui - -if TYPE_CHECKING: - from typing_extensions import ParamSpec - - from magicgui.application import AppRef - - _P = ParamSpec("_P") +from magicgui.type_map._type_map import TypeMap +from magicgui.widgets import FunctionGui __all__ = ["magicgui", "magic_factory", "MagicFactory"] - -_R = TypeVar("_R") _FGuiVar = TypeVar("_FGuiVar", bound=FunctionGui) - -@overload -def magicgui( - function: Callable[_P, _R], - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[False] = False, - app: AppRef | None = None, - persist: bool = False, - raise_on_unknown: bool = False, - **param_options: dict, -) -> FunctionGui[_P, _R]: ... - - -@overload -def magicgui( - function: Literal[None] | None = None, - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[False] = False, - app: AppRef | None = None, - persist: bool = False, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable[[Callable[_P, _R]], FunctionGui[_P, _R]]: ... - - -@overload -def magicgui( - function: Callable[_P, _R], - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[True], - app: AppRef | None = None, - persist: bool = False, - raise_on_unknown: bool = False, - **param_options: dict, -) -> MainFunctionGui[_P, _R]: ... - - -@overload -def magicgui( - function: Literal[None] | None = None, - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[True], - app: AppRef | None = None, - persist: bool = False, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable[[Callable[_P, _R]], MainFunctionGui[_P, _R]]: ... - - -def magicgui( - function: Callable | None = None, - *, - layout: str = "vertical", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: bool = False, - app: AppRef | None = None, - persist: bool = False, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable | FunctionGui: - """Return a [`FunctionGui`][magicgui.widgets.FunctionGui] for `function`. - - Parameters - ---------- - function : Callable, optional - The function to decorate. Optional to allow bare decorator with optional - arguments. by default ``None`` - layout : str, optional - The type of layout to use. Must be `horizontal` or `vertical` - by default "vertical". - scrollable : bool, optional - Whether to enable scroll bars or not. If enabled, scroll bars will - only appear along the layout direction, not in both directions. - labels : bool, optional - Whether labels are shown in the widget. by default True - tooltips : bool, optional - Whether tooltips are shown when hovering over widgets. by default True - call_button : bool or str, optional - If ``True``, create an additional button that calls the original - function when clicked. If a ``str``, set the button text. If None (the - default), it defaults to True when ``auto_call`` is False, and False - otherwise. - auto_call : bool, optional - If ``True``, changing any parameter in either the GUI or the widget attributes - will call the original function with the current settings. by default False - result_widget : bool, optional - Whether to display a LineEdit widget the output of the function when called, - by default False - main_window : bool - Whether this widget should be treated as the main app window, with menu bar, - by default False. - app : magicgui.Application or str, optional - A backend to use, by default ``None`` (use the default backend.) - persist : bool, optional - If `True`, when parameter values change in the widget, they will be stored to - disk and restored when the widget is loaded again with ``persist = True``. - Call ``magicgui._util.user_cache_dir()`` to get the default cache location. - By default False. - raise_on_unknown : bool, optional - If ``True``, raise an error if magicgui cannot determine widget for function - argument or return type. If ``False``, ignore unknown types. By default False. - param_options : dict[str, dict] - Any additional keyword arguments will be used as parameter-specific options. - Keywords must match the name of one of the arguments in the function signature, - and the value must be a dict of keyword arguments to pass to the widget - constructor. - - Returns - ------- - result : FunctionGui or Callable[[F], FunctionGui] - If ``function`` is not ``None`` (such as when this is used as a bare decorator), - returns a FunctionGui instance, which is a list-like container of autogenerated - widgets corresponding to each parameter in the function. - If ``function`` is ``None`` such as when arguments are provided like - ``magicgui(auto_call=True)``, then returns a function that can be used as a - decorator. - - Examples - -------- - >>> @magicgui - ... def my_function(a: int = 1, b: str = "hello"): - ... pass - >>> my_function.show() - >>> my_function.a.value == 1 # True - >>> my_function.b.value = "world" - """ - return _magicgui( - function=function, - layout=layout, - scrollable=scrollable, - labels=labels, - tooltips=tooltips, - call_button=call_button, - auto_call=auto_call, - result_widget=result_widget, - main_window=main_window, - app=app, - persist=persist, - raise_on_unknown=raise_on_unknown, - param_options=param_options, - ) - - -@overload -def magic_factory( - function: Callable[_P, _R], - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[False] = False, - app: AppRef | None = None, - persist: bool = False, - widget_init: Callable[[FunctionGui], None] | None = None, - raise_on_unknown: bool = False, - **param_options: dict, -) -> MagicFactory[FunctionGui[_P, _R]]: ... - - -@overload -def magic_factory( - function: Literal[None] | None = None, - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[False] = False, - app: AppRef | None = None, - persist: bool = False, - widget_init: Callable[[FunctionGui], None] | None = None, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable[[Callable[_P, _R]], MagicFactory[FunctionGui[_P, _R]]]: ... - - -@overload -def magic_factory( - function: Callable[_P, _R], - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[True], - app: AppRef | None = None, - persist: bool = False, - widget_init: Callable[[FunctionGui], None] | None = None, - raise_on_unknown: bool = False, - **param_options: dict, -) -> MagicFactory[MainFunctionGui[_P, _R]]: ... - - -@overload -def magic_factory( - function: Literal[None] | None = None, - *, - layout: str = "horizontal", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: Literal[True], - app: AppRef | None = None, - persist: bool = False, - widget_init: Callable[[FunctionGui], None] | None = None, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable[[Callable[_P, _R]], MagicFactory[MainFunctionGui[_P, _R]]]: ... - - -def magic_factory( - function: Callable | None = None, - *, - layout: str = "vertical", - scrollable: bool = False, - labels: bool = True, - tooltips: bool = True, - call_button: bool | str | None = None, - auto_call: bool = False, - result_widget: bool = False, - main_window: bool = False, - app: AppRef | None = None, - persist: bool = False, - widget_init: Callable[[FunctionGui], None] | None = None, - raise_on_unknown: bool = False, - **param_options: dict, -) -> Callable | MagicFactory: - """Return a [`MagicFactory`][magicgui.type_map._magicgui.MagicFactory] for function. - - `magic_factory` is nearly identical to the [`magicgui`][magicgui.magicgui] decorator - with the following differences: - - 1. Whereas `magicgui` returns a `FunctionGui` instance, `magic_factory` returns a - callable that returns a `FunctionGui` instance. (Technically, it returns an - instance of [`MagicFactory`][magicgui.type_map._magicgui.MagicFactory] which you - behaves exactly like a [`functools.partial`][functools.partial] - for a `FunctionGui` instance.) - 2. `magic_factory` adds a `widget_init` method: a callable that will be called - immediately after the `FunctionGui` instance is created. This can be used to - add additional widgets to the layout, or to connect signals to the widgets. - - !!!important - Whereas decorating a function with `magicgui` will **immediately** create a - widget instance, `magic_factory` will **not** create a widget instance until the - decorated object is called. This is often what you want in a library, whereas - `magicgui` is useful for rapid, interactive development. - - Parameters - ---------- - function : Callable, optional - The function to decorate. Optional to allow bare decorator with optional - arguments. by default ``None`` - layout : str, optional - The type of layout to use. Must be `horizontal` or `vertical` - by default "vertical". - scrollable : bool, optional - Whether to enable scroll bars or not. If enabled, scroll bars will - only appear along the layout direction, not in both directions. - labels : bool, optional - Whether labels are shown in the widget. by default True - tooltips : bool, optional - Whether tooltips are shown when hovering over widgets. by default True - call_button : bool or str, optional - If ``True``, create an additional button that calls the original - function when clicked. If a ``str``, set the button text. If None (the - default), it defaults to True when ``auto_call`` is False, and False - otherwise. - auto_call : bool, optional - If ``True``, changing any parameter in either the GUI or the widget attributes - will call the original function with the current settings. by default False - result_widget : bool, optional - Whether to display a LineEdit widget the output of the function when called, - by default False - main_window : bool - Whether this widget should be treated as the main app window, with menu bar, - by default False. - app : magicgui.Application or str, optional - A backend to use, by default ``None`` (use the default backend.) - persist : bool, optional - If `True`, when parameter values change in the widget, they will be stored to - disk and restored when the widget is loaded again with ``persist = True``. - Call ``magicgui._util.user_cache_dir()`` to get the default cache location. - By default False. - widget_init : callable, optional - A function that will be called with the newly created widget instance as its - only argument. This can be used to customize the widget after it is created. - by default ``None``. - raise_on_unknown : bool, optional - If ``True``, raise an error if magicgui cannot determine widget for function - argument or return type. If ``False``, ignore unknown types. By default False. - param_options : dict of dict - Any additional keyword arguments will be used as parameter-specific widget - options. Keywords must match the name of one of the arguments in the function - signature, and the value must be a dict of keyword arguments to pass to the - widget constructor. - - Returns - ------- - result : MagicFactory or Callable[[F], MagicFactory] - If ``function`` is not ``None`` (such as when this is used as a bare decorator), - returns a MagicFactory instance. - If ``function`` is ``None`` such as when arguments are provided like - ``magic_factory(auto_call=True)``, then returns a function that can be used as a - decorator. - - Examples - -------- - >>> @magic_factory - ... def my_function(a: int = 1, b: str = "hello"): - ... pass - >>> my_widget = my_function() - >>> my_widget.show() - >>> my_widget.a.value == 1 # True - >>> my_widget.b.value = "world" - """ - return _magicgui( - factory=True, - function=function, - layout=layout, - scrollable=scrollable, - labels=labels, - tooltips=tooltips, - call_button=call_button, - auto_call=auto_call, - result_widget=result_widget, - main_window=main_window, - app=app, - persist=persist, - widget_init=widget_init, - raise_on_unknown=raise_on_unknown, - param_options=param_options, - ) - +magicgui = TypeMap.global_instance().magicgui +magic_factory = TypeMap.global_instance().magic_factory MAGICGUI_PARAMS = inspect.signature(magicgui).parameters @@ -431,6 +37,7 @@ class MagicFactory(partial, Generic[_FGuiVar]): """ _widget_init: Callable[[_FGuiVar], None] | None = None + _type_map: TypeMap # 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] @@ -441,6 +48,7 @@ def __new__( *args: Any, magic_class: type[_FGuiVar] = FunctionGui, # type: ignore widget_init: Callable[[_FGuiVar], None] | None = None, + type_map: TypeMap | None = None, **keywords: Any, ) -> MagicFactory: """Create new MagicFactory.""" @@ -448,7 +56,8 @@ def __new__( raise TypeError( "MagicFactory missing required positional argument 'function'" ) - + if type_map is None: + type_map = TypeMap.global_instance() # we want function first for the repr keywords = {"function": function, **keywords} if widget_init is not None: @@ -462,6 +71,7 @@ def __new__( ) obj = super().__new__(cls, magic_class, *args, **keywords) obj._widget_init = widget_init + obj._type_map = type_map return obj def __repr__(self) -> str: @@ -485,7 +95,11 @@ def __call__(self, *args: Any, **kwargs: Any) -> _FGuiVar: 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}) + widget = self.func( + param_options=prm_options, + type_map=self._type_map, + **{**factory_kwargs, **kwargs}, + ) if self._widget_init is not None: self._widget_init(widget) return widget @@ -497,34 +111,3 @@ def __getattr__(self, name: str) -> Any: def __name__(self) -> str: """Pass function name.""" return getattr(self.keywords.get("function"), "__name__", "FunctionGui") - - -def _magicgui( - function: Callable | None = None, - factory: bool = False, - widget_init: Callable | None = None, - main_window: bool = False, - **kwargs: Any, -) -> Callable: - """Actual private magicui decorator. - - if factory is `True` will return a MagicFactory instance, that can be called - to return a `FunctionGui` instance. See docstring of ``magicgui`` for parameters. - Otherwise, this will return a FunctionGui instance directly. - """ - - def inner_func(func: Callable) -> FunctionGui | MagicFactory: - if not callable(func): - raise TypeError("the first argument must be callable") - - magic_class = MainFunctionGui if main_window else FunctionGui - - if factory: - return MagicFactory( - func, magic_class=magic_class, widget_init=widget_init, **kwargs - ) - # MagicFactory is unnecessary if we are immediately instantiating the widget, - # so we shortcut that and just return the FunctionGui here. - return cast(FunctionGui, magic_class(func, **kwargs)) - - return inner_func if function is None else inner_func(function) diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py index c4882839d..8431465fb 100644 --- a/src/magicgui/type_map/_type_map.py +++ b/src/magicgui/type_map/_type_map.py @@ -14,6 +14,7 @@ from contextlib import contextmanager from enum import EnumMeta from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -30,14 +31,19 @@ overload, ) -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import Annotated, ParamSpec, get_args, get_origin from magicgui import widgets from magicgui._type_resolution import resolve_single_type from magicgui._util import safe_issubclass +from magicgui.application import AppRef, use_app from magicgui.types import PathLike, ReturnCallback, Undefined, _Undefined +from magicgui.widgets import protocols from magicgui.widgets.protocols import WidgetProtocol, assert_protocol +if TYPE_CHECKING: + from magicgui.type_map._magicgui import MagicFactory + __all__: list[str] = ["register_type", "get_widget_class"] @@ -50,16 +56,15 @@ class MissingWidget(RuntimeError): """Raised when a backend widget cannot be found.""" +_T = TypeVar("_T", bound=type) +_P = ParamSpec("_P") +_R = TypeVar("_R") -_RETURN_CALLBACKS: defaultdict[type, list[ReturnCallback]] = defaultdict(list) -_TYPE_DEFS: dict[type, WidgetTuple] = {} - - -_SIMPLE_ANNOTATIONS = { +_SIMPLE_ANNOTATIONS_DEFAULTS = { PathLike: widgets.FileEdit, } -_SIMPLE_TYPES = { +_SIMPLE_TYPES_DEFAULTS = { bool: widgets.CheckBox, int: widgets.SpinBox, float: widgets.FloatSpinBox, @@ -77,79 +82,1023 @@ class MissingWidget(RuntimeError): os.PathLike: widgets.FileEdit, } -_ADDITIONAL_KWARGS: dict[type, dict[str, Any]] = { +_ADDITIONAL_KWARGS_DEFAULTS: dict[type, dict[str, Any]] = { Sequence[pathlib.Path]: {"mode": "rm"} } -def match_type(type_: Any, default: Any | None = None) -> WidgetTuple | None: - """Check simple type mappings.""" - if type_ in _SIMPLE_ANNOTATIONS: - return _SIMPLE_ANNOTATIONS[type_], {} - - if type_ is widgets.ProgressBar: - return widgets.ProgressBar, {"bind": lambda widget: widget, "visible": True} - - if type_ in _SIMPLE_TYPES: - return _SIMPLE_TYPES[type_], _ADDITIONAL_KWARGS.get(type_, {}) - for key in _SIMPLE_TYPES.keys(): - if safe_issubclass(type_, key): - return _SIMPLE_TYPES[key], _ADDITIONAL_KWARGS.get(key, {}) - - if type_ in (types.FunctionType,): - return widgets.FunctionGui, {"function": default} - - origin = get_origin(type_) or type_ - choices, nullable = _literal_choices(type_) - if choices is not None: # it's a Literal type - return widgets.ComboBox, {"choices": choices, "nullable": nullable} - - if safe_issubclass(origin, Set): - for arg in get_args(type_): - if get_origin(arg) is Literal: - return widgets.Select, {"choices": get_args(arg)} - - pint = sys.modules.get("pint") - if pint and safe_issubclass(origin, pint.Quantity): - return widgets.QuantityEdit, {} - - return None - - -_SIMPLE_RETURN_TYPES = [ - bool, - int, - float, - str, - pathlib.Path, - datetime.time, - datetime.date, - datetime.datetime, - range, - slice, -] - - -def match_return_type(type_: Any) -> WidgetTuple | None: - """Check simple type mappings for result widgets.""" - if type_ in _SIMPLE_TYPES: - return widgets.LineEdit, {"gui_only": True} - - if type_ is widgets.Table: - return widgets.Table, {} - - table_types = [ - resolve_single_type(x) for x in ("pandas.DataFrame", "numpy.ndarray") - ] - - if any( - safe_issubclass(type_, tt) - for tt in table_types - if not isinstance(tt, ForwardRef) +class TypeMap: + def __init__( + self, + *, + simple_types: dict | None = None, + simple_annotations: dict | None = None, + type_defs: dict | None = None, + return_callbacks: dict | None = None, + additional_kwargs: dict | None = None, ): - return widgets.Table, {} + if simple_types is None: + simple_types = _SIMPLE_TYPES_DEFAULTS.copy() + if simple_annotations is None: + simple_annotations = _SIMPLE_ANNOTATIONS_DEFAULTS.copy() + if additional_kwargs is None: + additional_kwargs = _ADDITIONAL_KWARGS_DEFAULTS.copy() + self._simple_types = simple_types + self._simple_annotations = simple_annotations + self._type_defs = type_defs or {} + self._return_callbacks: defaultdict[type, list[ReturnCallback]] = defaultdict( + list, return_callbacks or {} + ) + self._additional_kwargs = additional_kwargs + + @staticmethod + def global_instance() -> TypeMap: + """Get the global type map.""" + return _GLOBAL_TYPE_MAP + + def copy(self) -> TypeMap: + """Return a copy of the type map.""" + return TypeMap( + simple_types=self._simple_types, + simple_annotations=self._simple_annotations, + type_defs=self._type_defs, + return_callbacks=self._return_callbacks, + additional_kwargs=self._additional_kwargs, + ) + + def match_type(self, type_: Any, default: Any | None = None) -> WidgetTuple | None: + """Check simple type mappings.""" + if type_ in self._simple_annotations: + return self._simple_annotations[type_], {} + + if type_ is widgets.ProgressBar: + return widgets.ProgressBar, {"bind": lambda widget: widget, "visible": True} + + if type_ in self._simple_types: + return self._simple_types[type_], self._additional_kwargs.get(type_, {}) + for key in self._simple_types.keys(): + if safe_issubclass(type_, key): + return self._simple_types[key], self._additional_kwargs.get(key, {}) + + if type_ in (types.FunctionType,): + return widgets.FunctionGui, {"function": default} + + origin = get_origin(type_) or type_ + choices, nullable = _literal_choices(type_) + if choices is not None: # it's a Literal type + return widgets.ComboBox, {"choices": choices, "nullable": nullable} + + if safe_issubclass(origin, Set): + for arg in get_args(type_): + if get_origin(arg) is Literal: + return widgets.Select, {"choices": get_args(arg)} + + pint = sys.modules.get("pint") + if pint and safe_issubclass(origin, pint.Quantity): + return widgets.QuantityEdit, {} + + return None - return None + def match_return_type(self, type_: Any) -> WidgetTuple | None: + """Check simple type mappings for result widgets.""" + if type_ in self._simple_types: + return widgets.LineEdit, {"gui_only": True} + + if type_ is widgets.Table: + return widgets.Table, {} + + table_types = [ + resolve_single_type(x) for x in ("pandas.DataFrame", "numpy.ndarray") + ] + + if any( + safe_issubclass(type_, tt) + for tt in table_types + if not isinstance(tt, ForwardRef) + ): + return widgets.Table, {} + + return None + + @overload + def register_type( + self, + type_: _T, + *, + widget_type: WidgetRef | None = None, + return_callback: ReturnCallback | None = None, + **options: Any, + ) -> _T: ... + + + @overload + def register_type( + self, + type_: Literal[None] | None = None, + *, + widget_type: WidgetRef | None = None, + return_callback: ReturnCallback | None = None, + **options: Any, + ) -> Callable[[_T], _T]: ... + + + def register_type( + self, + type_: _T | None = None, + *, + widget_type: WidgetRef | None = None, + return_callback: ReturnCallback | None = None, + **options: Any, + ) -> _T | Callable[[_T], _T]: + """Register a ``widget_type`` to be used for all parameters with type `type_`. + + Note: registering a Union (or Optional) type effectively registers all types in + the union with the arguments. + + Parameters + ---------- + type_ : type + The type for which a widget class or return callback will be provided. + widget_type : WidgetRef, optional + A widget class from the current backend that should be used whenever `type_` + is used as the type annotation for an argument in a decorated function, + by default None + return_callback: callable, optional + If provided, whenever `type_` is declared as the return type of a decorated + function, `return_callback(widget, value, return_type)` will be called + whenever the decorated function is called... where `widget` is the Widget + instance, and `value` is the return value of the decorated function. + options + key value pairs where the keys are valid `dict` + + Raises + ------ + ValueError + If none of `widget_type`, `return_callback`, `bind` or `choices` are + provided. + """ + if all( + x is None + for x in [ + return_callback, + options.get("bind"), + options.get("choices"), + widget_type, + ] + ): + raise ValueError( + "At least one of `widget_type`, `return_callback`, `bind` or `choices` " + "must be provided." + ) + + def _deco(type__: _T) -> _T: + resolved_type = resolve_single_type(type__) + self._register_type_callback(resolved_type, return_callback) + self._register_widget(resolved_type, widget_type, **options) + return type__ + + return _deco if type_ is None else _deco(type_) + + + @contextmanager + def type_registered( + self, + type_: _T, + *, + widget_type: WidgetRef | None = None, + return_callback: ReturnCallback | None = None, + **options: Any, + ) -> Iterator[None]: + """Context manager that temporarily registers a widget type for a given `type_`. + + When the context is exited, the previous widget type associations for `type_` is + restored. + + Parameters + ---------- + type_ : _T + The type for which a widget class or return callback will be provided. + widget_type : Optional[WidgetRef] + A widget class from the current backend that should be used whenever `type_` + is used as the type annotation for an argument in a decorated function, + by default None + return_callback: Optional[callable] + If provided, whenever `type_` is declared as the return type of a decorated + function, `return_callback(widget, value, return_type)` will be called + whenever the decorated function is called... where `widget` is the Widget + instance, and `value` is the return value of the decorated function. + options + key value pairs where the keys are valid `dict` + """ + resolved_type = resolve_single_type(type_) + + # store any previous widget_type and options for this type + + revert_list = self._register_type_callback(resolved_type, return_callback) + prev_type_def = self._register_widget(resolved_type, widget_type, **options) + + new_type_def: WidgetTuple | None = self._type_defs.get(resolved_type, None) + try: + yield + finally: + # restore things to before the context + if return_callback is not None: # this if is only for mypy + for return_callback_type in revert_list: + self._return_callbacks[return_callback_type].remove(return_callback) + + if self._type_defs.get(resolved_type, None) is not new_type_def: + warnings.warn("Type definition changed during context", stacklevel=2) + + if prev_type_def is not None: + self._type_defs[resolved_type] = prev_type_def + else: + self._type_defs.pop(resolved_type, None) + + + def type2callback(self, type_: type) -> list[ReturnCallback]: + """Return any callbacks that have been registered for ``type_``. + + Parameters + ---------- + type_ : type + The type_ to look up. + + Returns + ------- + list of callable + Where a return callback accepts two arguments (gui, value) and does + something. + """ + if type_ is inspect.Parameter.empty: + return [] + + # look for direct hits ... + # if it's an Optional, we need to look for the type inside the Optional + type_ = resolve_single_type(type_) + if type_ in self._return_callbacks: + return self._return_callbacks[type_] + + # look for subclasses + for registered_type in self._return_callbacks: # sourcery skip: use-next + if safe_issubclass(type_, registered_type): + return self._return_callbacks[registered_type] + return [] + + + def get_widget_class( + self, + value: Any = Undefined, + annotation: Any = Undefined, + options: dict | None = None, + is_result: bool = False, + raise_on_unknown: bool = True, + ) -> tuple[WidgetClass, dict]: + """Return a [Widget][magicgui.widgets.Widget] subclass for the `value`/`annotation`. + + Parameters + ---------- + value : Any, optional + A python value. Will be used to determine the widget type if an + `annotation` is not explicitly provided by default None + annotation : Optional[Type], optional + A type annotation, by default None + options : dict, optional + Options to pass when constructing the widget, by default {} + is_result : bool, optional + Identifies whether the returned widget should be tailored to + an input or to an output. + raise_on_unknown : bool, optional + Raise exception if no widget is found for the given type, by default True + + Returns + ------- + Tuple[WidgetClass, dict] + The WidgetClass, and dict that can be used for params. dict + may be different than the options passed in. + """ # noqa: E501 + widget_type, options_ = self._pick_widget_type( + value, annotation, options, is_result, raise_on_unknown + ) + + if isinstance(widget_type, str): + widget_class = _import_wdg_class(widget_type) + else: + widget_class = widget_type + + if not safe_issubclass(widget_class, widgets.bases.Widget): + assert_protocol(widget_class, WidgetProtocol) + + return widget_class, options_ + + + def create_widget( + self, + value: Any = Undefined, + annotation: Any | None = None, + name: str = "", + param_kind: str | inspect._ParameterKind = "POSITIONAL_OR_KEYWORD", + label: str | None = None, + gui_only: bool = False, + app: AppRef = None, + widget_type: str | type[WidgetProtocol] | None = None, + options: dict | None = None, + is_result: bool = False, + raise_on_unknown: bool = True, + ) -> widgets.Widget: + """Create and return appropriate widget subclass. + + This factory function can be used to create a widget appropriate for the + provided `value` and/or `annotation` provided. See + [Type Mapping Docs](../../type_map.md) for details on how the widget type is + determined from type annotations. + + Parameters + ---------- + value : Any, optional + The starting value for the widget, by default `None` + annotation : Any, optional + The type annotation for the parameter represented by the widget, by default + `None`. + name : str, optional + The name of the parameter represented by this widget. by default `""` + param_kind : str, optional + The :attr:`inspect.Parameter.kind` represented by this widget. Used in + building signatures from multiple widgets, by default + `"POSITIONAL_OR_KEYWORD"` + label : str + A string to use for an associated Label widget (if this widget is being + shown in a [`Container`][magicgui.widgets.Container] widget, and labels are + on). By default, `name` will be used. Note: `name` refers the name of + the parameter, as might be used in a signature, whereas label is just the + label for that widget in the GUI. + gui_only : bool, optional + Whether the widget should be considered "only for the gui", or if it should + be included in any widget container signatures, by default False + app : str, optional + The backend to use, by default `None` + widget_type : str or Type[WidgetProtocol] or None + A class implementing a widget protocol or a string with the name of a + magicgui widget type (e.g. "Label", "PushButton", etc...). + If provided, this widget type will be used instead of the type + autodetermined from `value` and/or `annotation` above. + options : dict, optional + Dict of options to pass to the Widget constructor, by default dict() + is_result : boolean, optional + Whether the widget belongs to an input or an output. By default, an input + is assumed. + raise_on_unknown : bool, optional + Raise exception if no widget is found for the given type, by default True + + Returns + ------- + Widget + An instantiated widget subclass + + Raises + ------ + TypeError + If the provided or autodetected `widget_type` does not implement any known + [widget protocols](../protocols.md) + + Examples + -------- + ```python + from magicgui.widgets import create_widget + + # create a widget from a string value + wdg = create_widget(value="hello world") + assert wdg.value == "hello world" + + # create a widget from a string annotation + wdg = create_widget(annotation=str) + assert wdg.value == "" + ``` + """ + options_ = options.copy() if options is not None else {} + kwargs = { + "value": value, + "annotation": annotation, + "name": name, + "label": label, + "gui_only": gui_only, + } + + assert use_app(app).native + if isinstance(widget_type, protocols.WidgetProtocol): + wdg_class = widget_type + else: + if widget_type: + options_["widget_type"] = widget_type + # special case parameters named "password" with annotation of str + if ( + not options_.get("widget_type") + and (name or "").lower() == "password" + and annotation is str + ): + options_["widget_type"] = "Password" + + wdg_class, opts = self.get_widget_class( + value, annotation, options_, is_result, raise_on_unknown + ) + + if issubclass(wdg_class, widgets.Widget): + widget = wdg_class(**{**kwargs, **opts, **options_}) + if param_kind: + widget.param_kind = param_kind # type: ignore + return widget + + # pick the appropriate subclass for the given protocol + # order matters + for p in ("Categorical", "Ranged", "Button", "Value", ""): + prot = getattr(protocols, f"{p}WidgetProtocol") + if isinstance(wdg_class, prot): + options_ = kwargs.pop("options", None) + cls = getattr(widgets.bases, f"{p}Widget") + widget = cls( + **{**kwargs, **(options_ or {}), "widget_type": wdg_class} + ) + if param_kind: + widget.param_kind = param_kind # type: ignore + return widget + + raise TypeError(f"{wdg_class!r} does not implement any known widget protocols") + + + @overload + def magicgui( + self, + function: Callable[_P, _R], + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[False] = False, + app: AppRef | None = None, + persist: bool = False, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> widgets.FunctionGui[_P, _R]: ... + + + @overload + def magicgui( + self, + function: Literal[None] | None = None, + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[False] = False, + app: AppRef | None = None, + persist: bool = False, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable[[Callable[_P, _R]], widgets.FunctionGui[_P, _R]]: ... + + + @overload + def magicgui( + self, + function: Callable[_P, _R], + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[True], + app: AppRef | None = None, + persist: bool = False, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> widgets.MainFunctionGui[_P, _R]: ... + + + @overload + def magicgui( + self, + function: Literal[None] | None = None, + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[True], + app: AppRef | None = None, + persist: bool = False, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable[[Callable[_P, _R]], widgets.MainFunctionGui[_P, _R]]: ... + + + def magicgui( + self, + function: Callable | None = None, + *, + layout: str = "vertical", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: bool = False, + app: AppRef | None = None, + persist: bool = False, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable | widgets.FunctionGui: + """Return a [`FunctionGui`][magicgui.widgets.FunctionGui] for `function`. + + Parameters + ---------- + function : Callable, optional + The function to decorate. Optional to allow bare decorator with optional + arguments. by default `None` + layout : str, optional + The type of layout to use. Must be `horizontal` or `vertical` + by default "vertical". + scrollable : bool, optional + Whether to enable scroll bars or not. If enabled, scroll bars will + only appear along the layout direction, not in both directions. + labels : bool, optional + Whether labels are shown in the widget. by default True + tooltips : bool, optional + Whether tooltips are shown when hovering over widgets. by default True + call_button : bool or str, optional + If `True`, create an additional button that calls the original + function when clicked. If a `str`, set the button text. If None (the + default), it defaults to True when `auto_call` is False, and False + otherwise. + auto_call : bool, optional + If `True`, changing any parameter in either the GUI or the widget attributes + will call the original function with the current settings. by default False + result_widget : bool, optional + Whether to display a LineEdit widget the output of the function when called, + by default False + main_window : bool + Whether this widget should be treated as the main app window, with menu bar, + by default False. + app : magicgui.Application or str, optional + A backend to use, by default `None` (use the default backend.) + persist : bool, optional + If `True`, when parameter values change in the widget, they will be stored + to disk and restored when the widget is loaded again with `persist = True`. + Call `magicgui._util.user_cache_dir()` to get the default cache location. + By default False. + raise_on_unknown : bool, optional + If `True`, raise an error if magicgui cannot determine widget for function + argument or return type. If `False`, ignore unknown types. By default False. + param_options : dict[str, dict] + Any additional keyword arguments will be used as parameter-specific options. + Keywords must match the name of one of the arguments in the function + signature, and the value must be a dict of keyword arguments to pass to the + widget constructor. + + Returns + ------- + result : FunctionGui or Callable[[F], FunctionGui] + If `function` is not `None` (such as when this is used as a bare decorator), + returns a FunctionGui instance, which is a list-like container of + autogenerated widgets corresponding to each parameter in the function. + If `function` is `None` such as when arguments are provided like + `magicgui(auto_call=True)`, then returns a function that can be used as a + decorator. + + Examples + -------- + >>> @magicgui + ... def my_function(a: int = 1, b: str = "hello"): + ... pass + >>> my_function.show() + >>> my_function.a.value == 1 # True + >>> my_function.b.value = "world" + """ + return self._magicgui( + function=function, + layout=layout, + scrollable=scrollable, + labels=labels, + tooltips=tooltips, + call_button=call_button, + auto_call=auto_call, + result_widget=result_widget, + main_window=main_window, + app=app, + persist=persist, + raise_on_unknown=raise_on_unknown, + param_options=param_options, + ) + + + @overload + def magic_factory( + self, + function: Callable[_P, _R], + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[False] = False, + app: AppRef | None = None, + persist: bool = False, + widget_init: Callable[[widgets.FunctionGui], None] | None = None, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> MagicFactory[widgets.FunctionGui[_P, _R]]: ... + + + @overload + def magic_factory( + self, + function: Literal[None] | None = None, + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[False] = False, + app: AppRef | None = None, + persist: bool = False, + widget_init: Callable[[widgets.FunctionGui], None] | None = None, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable[[Callable[_P, _R]], MagicFactory[widgets.FunctionGui[_P, _R]]]: ... + + + @overload + def magic_factory( + self, + function: Callable[_P, _R], + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[True], + app: AppRef | None = None, + persist: bool = False, + widget_init: Callable[[widgets.FunctionGui], None] | None = None, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> MagicFactory[widgets.MainFunctionGui[_P, _R]]: ... + + + @overload + def magic_factory( + self, + function: Literal[None] | None = None, + *, + layout: str = "horizontal", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: Literal[True], + app: AppRef | None = None, + persist: bool = False, + widget_init: Callable[[widgets.FunctionGui], None] | None = None, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable[[Callable[_P, _R]], MagicFactory[widgets.MainFunctionGui[_P, _R]]]: + ... + + def magic_factory( + self, + function: Callable | None = None, + *, + layout: str = "vertical", + scrollable: bool = False, + labels: bool = True, + tooltips: bool = True, + call_button: bool | str | None = None, + auto_call: bool = False, + result_widget: bool = False, + main_window: bool = False, + app: AppRef | None = None, + persist: bool = False, + widget_init: Callable[[widgets.FunctionGui], None] | None = None, + raise_on_unknown: bool = False, + **param_options: dict, + ) -> Callable | MagicFactory: + """Return a [`MagicFactory`][magicgui.type_map._magicgui.MagicFactory] for function. + + `magic_factory` is nearly identical to the [`magicgui`][magicgui.magicgui] + decorator with the following differences: + + 1. Whereas `magicgui` returns a `FunctionGui` instance, `magic_factory` returns + a callable that returns a `FunctionGui` instance. (Technically, it returns an + instance of [`MagicFactory`][magicgui.type_map._magicgui.MagicFactory] which you + behaves exactly like a [`functools.partial`][functools.partial] + for a `FunctionGui` instance.) + 2. `magic_factory` adds a `widget_init` method: a callable that will be called + immediately after the `FunctionGui` instance is created. This can be used + to add additional widgets to the layout, or to connect signals to the + widgets. + + !!!important + Whereas decorating a function with `magicgui` will **immediately** create a + widget instance, `magic_factory` will **not** create a widget instance until + the decorated object is called. This is often what you want in a library, + whereas `magicgui` is useful for rapid, interactive development. + + Parameters + ---------- + function : Callable, optional + The function to decorate. Optional to allow bare decorator with optional + arguments. by default `None` + layout : str, optional + The type of layout to use. Must be `horizontal` or `vertical` + by default "vertical". + scrollable : bool, optional + Whether to enable scroll bars or not. If enabled, scroll bars will + only appear along the layout direction, not in both directions. + labels : bool, optional + Whether labels are shown in the widget. by default True + tooltips : bool, optional + Whether tooltips are shown when hovering over widgets. by default True + call_button : bool or str, optional + If `True`, create an additional button that calls the original + function when clicked. If a `str`, set the button text. If None (the + default), it defaults to True when `auto_call` is False, and False + otherwise. + auto_call : bool, optional + If `True`, changing any parameter in either the GUI or the widget + attributes will call the original function with the current settings. by + default False + result_widget : bool, optional + Whether to display a LineEdit widget the output of the function when called, + by default False + main_window : bool + Whether this widget should be treated as the main app window, with menu bar, + by default False. + app : magicgui.Application or str, optional + A backend to use, by default `None` (use the default backend.) + persist : bool, optional + If `True`, when parameter values change in the widget, they will be stored + to disk and restored when the widget is loaded again with `persist = True`. + Call `magicgui._util.user_cache_dir()` to get the default cache location. + By default False. + widget_init : callable, optional + A function that will be called with the newly created widget instance as its + only argument. This can be used to customize the widget after it is + created. by default `None`. + raise_on_unknown : bool, optional + If `True`, raise an error if magicgui cannot determine widget for function + argument or return type. If `False`, ignore unknown types. By default False. + param_options : dict of dict + Any additional keyword arguments will be used as parameter-specific widget + options. Keywords must match the name of one of the arguments in the + function signature, and the value must be a dict of keyword arguments to + pass to the widget constructor. + + Returns + ------- + result : MagicFactory or Callable[[F], MagicFactory] + If `function` is not `None` (such as when this is used as a bare + decorator), returns a MagicFactory instance. + If `function` is `None` such as when arguments are provided like + `magic_factory(auto_call=True)`, then returns a function that can be used + as a decorator. + + Examples + -------- + >>> @magic_factory + ... def my_function(a: int = 1, b: str = "hello"): + ... pass + >>> my_widget = my_function() + >>> my_widget.show() + >>> my_widget.a.value == 1 # True + >>> my_widget.b.value = "world" + """ # noqa: E501 + return self._magicgui( + factory=True, + function=function, + layout=layout, + scrollable=scrollable, + labels=labels, + tooltips=tooltips, + call_button=call_button, + auto_call=auto_call, + result_widget=result_widget, + main_window=main_window, + app=app, + persist=persist, + widget_init=widget_init, + raise_on_unknown=raise_on_unknown, + param_options=param_options, + ) + + def _pick_widget_type( + self, + value: Any = Undefined, + annotation: Any = Undefined, + options: dict | None = None, + is_result: bool = False, + 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 {})} + choices = options.get("choices") + + if is_result and annotation is inspect.Parameter.empty: + annotation = str + + if ( + value is Undefined + and annotation in (Undefined, inspect.Parameter.empty) + and not choices + and "widget_type" not in options + ): + return widgets.EmptyWidget, {"visible": False, **options} + + type_, optional = _type_optional(value, annotation) + options.setdefault("nullable", optional) + choices = choices or (isinstance(type_, EnumMeta) and type_) + literal_choices, nullable = _literal_choices(annotation) + if literal_choices is not None: + choices = literal_choices + options["nullable"] = nullable + + if "widget_type" in options: + widget_type = options.pop("widget_type") + if choices: + if widget_type == "RadioButton": + widget_type = "RadioButtons" + warnings.warn( + f"widget_type of 'RadioButton' (with dtype {type_}) is" + " being coerced to 'RadioButtons' due to choices or Enum type.", + stacklevel=2, + ) + options.setdefault("choices", choices) + return widget_type, options + + # look for subclasses + for registered_type in self._type_defs: + if type_ == registered_type or safe_issubclass(type_, registered_type): + cls_, opts = self._type_defs[registered_type] + return cls_, {**options, **opts} + + if is_result: + widget_type_ = self.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} + + if choices: + options["choices"] = choices + wdg = widgets.Select if options.get("allow_multiple") else widgets.ComboBox + return wdg, options + + widget_type_ = self.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}" + ) + + options["visible"] = False + return widgets.EmptyWidget, options + + def _register_type_callback( + self, + 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 self._return_callbacks[type_per]: + self._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 self._return_callbacks[t] + ): + self._return_callbacks[t].append(return_callback) + modified_callbacks.append(t) + elif return_callback not in self._return_callbacks[resolved_type]: + self._return_callbacks[resolved_type].append(return_callback) + modified_callbacks.append(resolved_type) + return modified_callbacks + + + def _register_widget( + self, + resolved_type: _T, + widget_type: WidgetRef | None = None, + **options: Any, + ) -> WidgetTuple | None: + _options = cast(dict, options) + + previous_widget = self._type_defs.get(resolved_type) + + if "choices" in _options: + self._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" + ) + self._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 + self._type_defs[resolved_type] = (widgets.EmptyWidget, _options) + return previous_widget + + + def _magicgui( + self, + function: Callable | None = None, + factory: bool = False, + widget_init: Callable | None = None, + main_window: bool = False, + **kwargs: Any, + ) -> Callable: + """Actual private magicui decorator. + + if factory is `True` will return a MagicFactory instance, that can be called + to return a `FunctionGui` instance. See docstring of ``magicgui`` for + parameters. Otherwise, this will return a FunctionGui instance directly. + """ + from magicgui.type_map._magicgui import MagicFactory + + def inner_func(func: Callable) -> widgets.FunctionGui | MagicFactory: + if not callable(func): + raise TypeError("the first argument must be callable") + + magic_class = ( + widgets.MainFunctionGui if main_window else widgets.FunctionGui + ) + + if factory: + return MagicFactory( + func, + magic_class=magic_class, + widget_init=widget_init, + type_map=self, + **kwargs, + ) + # MagicFactory is unnecessary if we are immediately instantiating the + # widget, so we shortcut that and just return the FunctionGui here. + return cast(widgets.FunctionGui, magic_class(func, type_map=self, **kwargs)) + + return inner_func if function is None else inner_func(function) + + +_GLOBAL_TYPE_MAP = TypeMap() +get_widget_class = _GLOBAL_TYPE_MAP.get_widget_class +register_type = _GLOBAL_TYPE_MAP.register_type +type2callback = _GLOBAL_TYPE_MAP.type2callback +type_registered = _GLOBAL_TYPE_MAP.type_registered def _is_none_type(type_: Any) -> bool: return any(type_ is x for x in {None, type(None), Literal[None]}) @@ -201,83 +1150,6 @@ def _literal_choices(annotation: Any) -> tuple[list | None, bool]: return choices, nullable -def _pick_widget_type( - value: Any = Undefined, - annotation: Any = Undefined, - options: dict | None = None, - is_result: bool = False, - 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 {})} - choices = options.get("choices") - - if is_result and annotation is inspect.Parameter.empty: - annotation = str - - if ( - value is Undefined - and annotation in (Undefined, inspect.Parameter.empty) - and not choices - and "widget_type" not in options - ): - return widgets.EmptyWidget, {"visible": False, **options} - - type_, optional = _type_optional(value, annotation) - options.setdefault("nullable", optional) - choices = choices or (isinstance(type_, EnumMeta) and type_) - literal_choices, nullable = _literal_choices(annotation) - if literal_choices is not None: - choices = literal_choices - options["nullable"] = nullable - - if "widget_type" in options: - widget_type = options.pop("widget_type") - if choices: - if widget_type == "RadioButton": - widget_type = "RadioButtons" - warnings.warn( - f"widget_type of 'RadioButton' (with dtype {type_}) is" - " being coerced to 'RadioButtons' due to choices or Enum type.", - stacklevel=2, - ) - options.setdefault("choices", choices) - return widget_type, options - - # 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 is_result: - 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} - - if choices: - options["choices"] = choices - 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_, {**opts, **options} - - if raise_on_unknown: - raise ValueError( - f"No widget found for type {type_} and annotation {annotation!r}" - ) - - options["visible"] = False - return widgets.EmptyWidget, options - - 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: @@ -293,51 +1165,6 @@ def _split_annotated_type(annotation: Any) -> tuple[Any, dict]: return type_, meta -def get_widget_class( - value: Any = Undefined, - annotation: Any = Undefined, - options: dict | None = None, - is_result: bool = False, - raise_on_unknown: bool = True, -) -> tuple[WidgetClass, dict]: - """Return a [Widget][magicgui.widgets.Widget] subclass for the `value`/`annotation`. - - Parameters - ---------- - value : Any, optional - A python value. Will be used to determine the widget type if an ``annotation`` - is not explicitly provided by default None - annotation : Optional[Type], optional - A type annotation, by default None - options : dict, optional - Options to pass when constructing the widget, by default {} - is_result : bool, optional - Identifies whether the returned widget should be tailored to - an input or to an output. - raise_on_unknown : bool, optional - Raise exception if no widget is found for the given type, by default True - - Returns - ------- - Tuple[WidgetClass, dict] - 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( - value, annotation, options, is_result, raise_on_unknown - ) - - if isinstance(widget_type, str): - widget_class = _import_wdg_class(widget_type) - else: - widget_class = widget_type - - if not safe_issubclass(widget_class, widgets.bases.Widget): - assert_protocol(widget_class, WidgetProtocol) - - return widget_class, options_ - - def _import_wdg_class(class_name: str) -> WidgetClass: import importlib @@ -358,228 +1185,6 @@ def _validate_return_callback(func: Callable) -> None: except TypeError as e: raise TypeError(f"object {func!r} is not a valid return callback: {e}") from e - -_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, - *, - widget_type: WidgetRef | None = None, - return_callback: ReturnCallback | None = None, - **options: Any, -) -> _T: ... - - -@overload -def register_type( - type_: Literal[None] | None = None, - *, - widget_type: WidgetRef | None = None, - return_callback: ReturnCallback | None = None, - **options: Any, -) -> Callable[[_T], _T]: ... - - -def register_type( - type_: _T | None = None, - *, - widget_type: WidgetRef | None = None, - return_callback: ReturnCallback | None = None, - **options: Any, -) -> _T | Callable[[_T], _T]: - """Register a ``widget_type`` to be used for all parameters with type ``type_``. - - Note: registering a Union (or Optional) type effectively registers all types in - the union with the arguments. - - Parameters - ---------- - type_ : type - The type for which a widget class or return callback will be provided. - widget_type : WidgetRef, optional - A widget class from the current backend that should be used whenever ``type_`` - is used as the type annotation for an argument in a decorated function, - by default None - return_callback: callable, optional - If provided, whenever ``type_`` is declared as the return type of a decorated - function, ``return_callback(widget, value, return_type)`` will be called - whenever the decorated function is called... where ``widget`` is the Widget - instance, and ``value`` is the return value of the decorated function. - options - key value pairs where the keys are valid `dict` - - Raises - ------ - ValueError - If none of `widget_type`, `return_callback`, `bind` or `choices` are provided. - """ - if all( - x is None - for x in [ - return_callback, - options.get("bind"), - options.get("choices"), - widget_type, - ] - ): - raise ValueError( - "At least one of `widget_type`, `return_callback`, `bind` or `choices` " - "must be provided." - ) - - 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_) - - -@contextmanager -def type_registered( - type_: _T, - *, - widget_type: WidgetRef | None = None, - return_callback: ReturnCallback | None = None, - **options: Any, -) -> Iterator[None]: - """Context manager that temporarily registers a widget type for a given `type_`. - - When the context is exited, the previous widget type associations for `type_` is - restored. - - Parameters - ---------- - type_ : _T - The type for which a widget class or return callback will be provided. - widget_type : Optional[WidgetRef] - A widget class from the current backend that should be used whenever ``type_`` - is used as the type annotation for an argument in a decorated function, - by default None - return_callback: Optional[callable] - If provided, whenever ``type_`` is declared as the return type of a decorated - function, ``return_callback(widget, value, return_type)`` will be called - whenever the decorated function is called... where ``widget`` is the Widget - instance, and ``value`` is the return value of the decorated function. - options - key value pairs where the keys are valid `dict` - """ - resolved_type = resolve_single_type(type_) - - # store any previous widget_type and options for this type - - 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: # 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) - - if prev_type_def is not None: - _TYPE_DEFS[resolved_type] = prev_type_def - else: - _TYPE_DEFS.pop(resolved_type, None) - - -def type2callback(type_: type) -> list[ReturnCallback]: - """Return any callbacks that have been registered for ``type_``. - - Parameters - ---------- - type_ : type - The type_ to look up. - - Returns - ------- - list of callable - Where a return callback accepts two arguments (gui, value) and does something. - """ - if type_ is inspect.Parameter.empty: - return [] - - # look for direct hits ... - # if it's an Optional, we need to look for the type inside the Optional - type_ = resolve_single_type(type_) - if type_ in _RETURN_CALLBACKS: - return _RETURN_CALLBACKS[type_] - - # look for subclasses - for registered_type in _RETURN_CALLBACKS: # sourcery skip: use-next - if safe_issubclass(type_, registered_type): - return _RETURN_CALLBACKS[registered_type] - return [] - - def _generate_union_variants(type_: Any) -> Iterator[type]: type_args = get_args(type_) for i in range(2, len(type_args) + 1): diff --git a/src/magicgui/widgets/_function_gui.py b/src/magicgui/widgets/_function_gui.py index b15455ce9..725d4befa 100644 --- a/src/magicgui/widgets/_function_gui.py +++ b/src/magicgui/widgets/_function_gui.py @@ -34,6 +34,7 @@ from typing_extensions import ParamSpec from magicgui.application import Application, AppRef # noqa: F401 + from magicgui.type_map import TypeMap from magicgui.widgets import TextEdit from magicgui.widgets.protocols import ContainerProtocol, MainWindowProtocol @@ -153,10 +154,15 @@ def __init__( name: str | None = None, persist: bool = False, raise_on_unknown: bool = False, + type_map: TypeMap | None = None, **kwargs: Any, ): + from magicgui.type_map import TypeMap + if not callable(function): raise TypeError("'function' argument to FunctionGui must be callable.") + if type_map is None: + type_map = TypeMap.global_instance() # consume extra Widget keywords extra = set(kwargs) - {"annotation", "gui_only"} @@ -198,9 +204,10 @@ def __init__( scrollable=scrollable, labels=labels, visible=visible, - widgets=list(sig.widgets(app).values()), + widgets=list(sig.widgets(app, type_map).values()), name=name or self._callable_name, ) + self._type_map = type_map self._param_options = param_options self._result_name = "" self._call_count: int = 0 @@ -233,11 +240,9 @@ def _disable_button_and_call() -> None: self._result_widget: ValueWidget | None = None if result_widget: - from magicgui.widgets.bases import create_widget - self._result_widget = cast( ValueWidget, - create_widget( + type_map.create_widget( value=None, annotation=self._return_annotation, gui_only=True, @@ -352,9 +357,7 @@ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: return_type = sig.return_annotation if return_type: - from magicgui.type_map import type2callback - - for callback in type2callback(return_type): + for callback in self._type_map.type2callback(return_type): callback(self, value, return_type) self.called.emit(value) return value @@ -389,6 +392,7 @@ def copy(self) -> FunctionGui: tooltips=self._tooltips, scrollable=self._scrollable, name=self.name, + type_map=self._type_map, ) def __get__(self, obj: object, objtype: type | None = None) -> FunctionGui: diff --git a/src/magicgui/widgets/bases/_create_widget.py b/src/magicgui/widgets/bases/_create_widget.py index fd6935eb3..48ffec27c 100644 --- a/src/magicgui/widgets/bases/_create_widget.py +++ b/src/magicgui/widgets/bases/_create_widget.py @@ -2,15 +2,14 @@ from typing import TYPE_CHECKING, Any -from magicgui.application import AppRef, use_app from magicgui.types import Undefined -from magicgui.widgets import bases, protocols - -from ._widget import Widget if TYPE_CHECKING: import inspect + from magicgui.application import AppRef + from magicgui.widgets import Widget, protocols + def create_widget( value: Any = Undefined, @@ -93,51 +92,18 @@ def create_widget( assert wdg.value == "" ``` """ - options_ = options.copy() if options is not None else {} - kwargs = { - "value": value, - "annotation": annotation, - "name": name, - "label": label, - "gui_only": gui_only, - } - - assert use_app(app).native - if isinstance(widget_type, protocols.WidgetProtocol): - wdg_class = widget_type - else: - from magicgui.type_map import get_widget_class - - if widget_type: - options_["widget_type"] = widget_type - # special case parameters named "password" with annotation of str - if ( - not options_.get("widget_type") - and (name or "").lower() == "password" - and annotation is str - ): - options_["widget_type"] = "Password" - - wdg_class, opts = get_widget_class( - value, annotation, options_, is_result, raise_on_unknown - ) - - if issubclass(wdg_class, Widget): - widget = wdg_class(**{**kwargs, **opts, **options_}) - if param_kind: - widget.param_kind = param_kind # type: ignore - return widget - - # pick the appropriate subclass for the given protocol - # order matters - for p in ("Categorical", "Ranged", "Button", "Value", ""): - prot = getattr(protocols, f"{p}WidgetProtocol") - if isinstance(wdg_class, prot): - options_ = kwargs.pop("options", None) - cls = getattr(bases, f"{p}Widget") - widget = cls(**{**kwargs, **(options_ or {}), "widget_type": wdg_class}) - if param_kind: - widget.param_kind = param_kind # type: ignore - return widget - - raise TypeError(f"{wdg_class!r} does not implement any known widget protocols") + from magicgui.type_map import TypeMap + + return TypeMap.global_instance().create_widget( + value=value, + annotation=annotation, + name=name, + param_kind=param_kind, + label=label, + gui_only=gui_only, + app=app, + widget_type=widget_type, + options=options, + is_result=is_result, + raise_on_unknown=raise_on_unknown, + ) From a562cff9215306fb9d3bc24fab5368be06c5db1d Mon Sep 17 00:00:00 2001 From: Hanjin Liu Date: Thu, 7 Nov 2024 11:29:16 +0900 Subject: [PATCH 2/2] update tests --- src/magicgui/type_map/_type_map.py | 10 +++---- tests/conftest.py | 4 +-- tests/test_docs.py | 4 +-- tests/test_magicgui.py | 10 +++---- tests/test_types.py | 44 ++++++++++++++++++++++-------- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py index 8431465fb..c837cd34c 100644 --- a/src/magicgui/type_map/_type_map.py +++ b/src/magicgui/type_map/_type_map.py @@ -118,11 +118,11 @@ def global_instance() -> TypeMap: def copy(self) -> TypeMap: """Return a copy of the type map.""" return TypeMap( - simple_types=self._simple_types, - simple_annotations=self._simple_annotations, - type_defs=self._type_defs, - return_callbacks=self._return_callbacks, - additional_kwargs=self._additional_kwargs, + simple_types=self._simple_types.copy(), + simple_annotations=self._simple_annotations.copy(), + type_defs=self._type_defs.copy(), + return_callbacks=self._return_callbacks.copy(), + additional_kwargs=self._additional_kwargs.copy(), ) def match_type(self, type_: Any, default: Any | None = None) -> WidgetTuple | None: diff --git a/tests/conftest.py b/tests/conftest.py index c45364c53..b2ed66dc1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,8 +21,8 @@ def always_qapp(qapp): @pytest.fixture(autouse=True, scope="function") def _clean_return_callbacks(): - from magicgui.type_map._type_map import _RETURN_CALLBACKS + from magicgui.type_map import TypeMap yield - _RETURN_CALLBACKS.clear() + TypeMap.global_instance()._return_callbacks.clear() diff --git a/tests/test_docs.py b/tests/test_docs.py index 27f57d503..41772ad30 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -70,5 +70,5 @@ def test_examples(fname, monkeypatch): pytest.skip("numpy unavailable: skipping image example") finally: if "waveform" in fname: - type_map._type_map._TYPE_DEFS.pop(int, None) - type_map._type_map._TYPE_DEFS.pop(float, None) + type_map.TypeMap.global_instance()._type_defs.pop(int, None) + type_map.TypeMap.global_instance()._type_defs.pop(float, None) diff --git a/tests/test_magicgui.py b/tests/test_magicgui.py index d862308b9..f113e7494 100644 --- a/tests/test_magicgui.py +++ b/tests/test_magicgui.py @@ -431,8 +431,8 @@ def func(a: str = "works", b: int = 3, c: Sub = None, d: Sub2 = None): assert isinstance(func.c, widgets.ComboBox) assert isinstance(func.d, widgets.LineEdit) - del type_map._type_map._TYPE_DEFS[str] - del type_map._type_map._TYPE_DEFS[int] + del type_map.TypeMap.global_instance()._type_defs[str] + del type_map.TypeMap.global_instance()._type_defs[int] def test_register_types_by_class(): @@ -478,10 +478,10 @@ def func2(a=1) -> Sub: func2() finally: - from magicgui.type_map._type_map import _RETURN_CALLBACKS + from magicgui.type_map import TypeMap - _RETURN_CALLBACKS.pop(int) - _RETURN_CALLBACKS.pop(Base) + TypeMap.global_instance()._return_callbacks.pop(int) + TypeMap.global_instance()._return_callbacks.pop(Base) def test_parent_changed(qtbot, magic_func: widgets.FunctionGui) -> None: diff --git a/tests/test_types.py b/tests/test_types.py index 7c538d226..990388a67 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -8,7 +8,7 @@ from magicgui import magicgui, register_type, type_map, type_registered, types, widgets from magicgui._type_resolution import resolve_single_type -from magicgui.type_map._type_map import _RETURN_CALLBACKS +from magicgui.type_map import TypeMap if TYPE_CHECKING: import numpy @@ -154,11 +154,13 @@ def test_type_registered(): def test_type_registered_callbacks(): + _return_callbacks = TypeMap.global_instance()._return_callbacks + @magicgui def func(a: int) -> int: return a - assert not _RETURN_CALLBACKS[int] + assert not _return_callbacks[int] mock = Mock() func(1) mock.assert_not_called() @@ -170,13 +172,13 @@ def func(a: int) -> int: func(2) mock.assert_called_once_with(2) mock.reset_mock() - assert _RETURN_CALLBACKS[int] == [cb] + assert _return_callbacks[int] == [cb] register_type(int, return_callback=cb2) - assert _RETURN_CALLBACKS[int] == [cb, cb2] + assert _return_callbacks[int] == [cb, cb2] func(3) mock.assert_not_called() - assert _RETURN_CALLBACKS[int] == [cb2] + assert _return_callbacks[int] == [cb2] def test_type_registered_warns(): @@ -191,8 +193,9 @@ def test_type_registered_warns(): def test_type_registered_optional_callbacks(): - assert not _RETURN_CALLBACKS[int] - assert not _RETURN_CALLBACKS[Optional[int]] + _return_callbacks = TypeMap.global_instance()._return_callbacks + assert not _return_callbacks[int] + assert not _return_callbacks[Optional[int]] @magicgui def func1(a: int) -> int: @@ -216,13 +219,13 @@ def func2(a: int) -> Optional[int]: 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] + 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]] == [mock1, mock3] - assert _RETURN_CALLBACKS[Optional[int]] == [mock3] - assert _RETURN_CALLBACKS[int] == [mock2, mock3] + assert _return_callbacks[Optional[int]] == [mock3] + assert _return_callbacks[int] == [mock2, mock3] def test_pick_widget_literal(): @@ -240,3 +243,20 @@ def test_redundant_annotation() -> None: @magicgui def f(a: Annotated[List[int], {"annotation": List[int]}]): pass + +def test_multiple_type_maps(): + typemap1 = TypeMap() + + typemap1.register_type(int, widget_type=widgets.Slider) + + def f(a: int, b: str): + pass + + fgui0 = magicgui(f) + fgui1 = typemap1.magicgui(f) + + assert isinstance(fgui0[0], widgets.SpinBox) + assert isinstance(fgui0[1], widgets.LineEdit) + assert isinstance(fgui1[0], widgets.Slider) + assert isinstance(fgui1[1], widgets.LineEdit) +