From adf94cdc64a8d902239fce140a068fb0bf0117dc Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 18 Nov 2022 09:05:38 -0500 Subject: [PATCH 1/5] test: wip-mocking tests --- src/magicgui/application.py | 21 +- src/magicgui/widgets/bases/_value_widget.py | 3 + src/magicgui/widgets/bases/_widget.py | 14 +- tests/conftest.py | 22 +- tests/test_application.py | 29 +- tests/test_backends/__init__.py | 3 + tests/{ => test_backends}/test_backends.py | 0 .../test_magicgui copy.py} | 0 tests/test_backends/test_magicgui.py | 879 ++++++++++++++++++ tests/{ => test_backends}/test_widgets.py | 0 tests/test_widget_bases.py | 128 +++ 11 files changed, 1077 insertions(+), 22 deletions(-) create mode 100644 tests/test_backends/__init__.py rename tests/{ => test_backends}/test_backends.py (100%) rename tests/{test_magicgui.py => test_backends/test_magicgui copy.py} (100%) create mode 100644 tests/test_backends/test_magicgui.py rename tests/{ => test_backends}/test_widgets.py (100%) create mode 100644 tests/test_widget_bases.py diff --git a/src/magicgui/application.py b/src/magicgui/application.py index 0c2f3ed7d..86ca04d22 100644 --- a/src/magicgui/application.py +++ b/src/magicgui/application.py @@ -33,8 +33,12 @@ class Application: _backend: BaseApplicationBackend _instance: Application | None = None - def __init__(self, backend_name: str | None = None): - self._use(backend_name) + def __init__(self, backend_name: str | BaseApplicationBackend | None = None): + if isinstance(backend_name, str) or not backend_name: + self._use(backend_name) + else: + self._backend = backend_name + self._backend_module = import_module(backend_name.__module__) @property def backend_name(self) -> str: @@ -140,16 +144,13 @@ def _use_app(backend_name: Optional[str] = None): # If we already have a default_app, raise error or return current = Application._instance if current is not None: - if backend_name: - names = current.backend_name.lower().replace("(", " ").strip(") ") - _nm = [n for n in names.split(" ") if n] - if backend_name.lower() not in _nm: - raise RuntimeError( - f"Can only select a backend once, already using {_nm}." - ) - else: + if not backend_name: return current # Current backend matches backend_name + names = current.backend_name.lower().replace("(", " ").strip(") ") + _nm = [n for n in names.split(" ") if n] + if backend_name.lower() not in _nm: + raise RuntimeError(f"Can only select a backend once, already using {_nm}.") # Create default app Application._instance = Application(backend_name) return Application._instance diff --git a/src/magicgui/widgets/bases/_value_widget.py b/src/magicgui/widgets/bases/_value_widget.py index cb2a102d6..7ce907b1e 100644 --- a/src/magicgui/widgets/bases/_value_widget.py +++ b/src/magicgui/widgets/bases/_value_widget.py @@ -43,6 +43,7 @@ def __init__( def _post_init(self): super()._post_init() + # Note that it is the responsibility of the backend to emit the changed signal self._widget._mgui_bind_change_callback(self._on_value_change) def _on_value_change(self, value=None): @@ -79,6 +80,8 @@ def value(self): @value.setter def value(self, value): + # value_changed will be emitted indirectly by the backend calling our + # _on_value_change method, which we connected in _post_init return self._widget._mgui_set_value(value) def __repr__(self) -> str: diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py index 043b7a94d..6d57294ba 100644 --- a/src/magicgui/widgets/bases/_widget.py +++ b/src/magicgui/widgets/bases/_widget.py @@ -9,7 +9,7 @@ from psygnal import Signal from magicgui._type_resolution import resolve_single_type -from magicgui.application import use_app +from magicgui.application import Application, use_app from magicgui.widgets import protocols BUILDING_DOCS = sys.argv[-2:] == ["build", "docs"] @@ -99,7 +99,7 @@ def __init__( _prot = _prot.__name__ prot = getattr(protocols, _prot.replace("protocols.", "")) protocols.assert_protocol(widget_type, prot) - self.__magicgui_app__ = use_app() + self.__magicgui_app__: Application = use_app() assert self.__magicgui_app__.native if isinstance(parent, Widget): parent = parent.native @@ -212,8 +212,15 @@ def parent(self) -> Widget: @parent.setter def parent(self, value: Widget): + # note that it's up to the backend to actually set the parent + # which should trigger a call to _emit_parent because of the + # self._widget._mgui_bind_parent_change_callback(self._emit_parent) + # in the constructor. self._widget._mgui_set_parent(value) + def _emit_parent(self, *_): + self.parent_changed.emit(self.parent) + @property def widget_type(self) -> str: """Return type of widget.""" @@ -391,9 +398,6 @@ def _repr_png_(self) -> Optional[bytes]: return file_obj.read() return None - def _emit_parent(self, *_): - self.parent_changed.emit(self.parent) - def _ipython_display_(self, *args, **kwargs): if hasattr(self.native, "_ipython_display_"): return self.native._ipython_display_(*args, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py index df4ad5936..32220e2d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ +from unittest.mock import MagicMock, PropertyMock, create_autospec, patch + import pytest -from magicgui.application import use_app +from magicgui.application import Application, use_app +from magicgui.widgets.protocols import BaseApplicationBackend @pytest.fixture(scope="session") @@ -11,9 +14,24 @@ def qapp(): # for now, the only backend is qt, and pytest-qt's qapp provides some nice pre-post # test cleanup that prevents some segfaults. Once we start testing multiple backends # this will need to change. -@pytest.fixture(autouse=True, scope="function") +@pytest.fixture(scope="function") def always_qapp(qapp): yield qapp for w in qapp.topLevelWidgets(): w.close() w.deleteLater() + + +@pytest.fixture +def mock_app(): + MockAppBackend: MagicMock = create_autospec(BaseApplicationBackend, spec_set=True) + mock_app = Application(MockAppBackend) + + backend_module = MagicMock() + p = PropertyMock() + setattr(type(backend_module), "some name", p) + setattr(mock_app, "_prop", p) + + mock_app._backend_module = backend_module + with patch.object(Application, "_instance", mock_app): + yield mock_app diff --git a/tests/test_application.py b/tests/test_application.py index 0f3fb8ffc..aad649dc0 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,7 +1,26 @@ -from magicgui import use_app -from magicgui.application import APPLICATION_NAME +from magicgui.application import use_app -def test_app_name(): - app = use_app("qt") - assert app.native.applicationName() == APPLICATION_NAME +def test_mock_app(mock_app): + app = use_app() + backend = mock_app._backend + + assert app is mock_app + + app.backend_name + backend._mgui_get_backend_name.assert_called_once() + + app.get_obj("some name") + mock_app._prop.assert_called_once() + + with app: + backend._mgui_get_native_app.assert_called_once() + backend._mgui_start_timer.assert_called_once() + backend._mgui_run.assert_called_once() + backend._mgui_stop_timer.assert_called_once() + + app.process_events() + backend._mgui_process_events.assert_called_once() + + app.quit() + backend._mgui_quit.assert_called_once() diff --git a/tests/test_backends/__init__.py b/tests/test_backends/__init__.py new file mode 100644 index 000000000..95cb6ec6e --- /dev/null +++ b/tests/test_backends/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.skip("No backend tests implemented yet", allow_module_level=True) diff --git a/tests/test_backends.py b/tests/test_backends/test_backends.py similarity index 100% rename from tests/test_backends.py rename to tests/test_backends/test_backends.py diff --git a/tests/test_magicgui.py b/tests/test_backends/test_magicgui copy.py similarity index 100% rename from tests/test_magicgui.py rename to tests/test_backends/test_magicgui copy.py diff --git a/tests/test_backends/test_magicgui.py b/tests/test_backends/test_magicgui.py new file mode 100644 index 000000000..4130d351b --- /dev/null +++ b/tests/test_backends/test_magicgui.py @@ -0,0 +1,879 @@ +#!/usr/bin/env python + +"""Tests for `magicgui` package.""" + +import inspect +from enum import Enum + +import pytest +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QScrollArea + +from magicgui import magicgui, register_type, type_map, widgets +from magicgui.signature import MagicSignature, magic_signature + + +def func(a: str = "works", b: int = 3, c=7.1) -> str: + return a + str(b) + + +@pytest.fixture +def magic_func(): + """Test function decorated by magicgui.""" + return magicgui(func, call_button="my_button", auto_call=True, labels=False) + + +@pytest.fixture +def magic_func_defaults(): + return magicgui(func) + + +@pytest.fixture +def magic_func_autocall(): + return magicgui(func, auto_call=True) + + +def test_magicgui(magic_func): + """Test basic magicgui functionality.""" + assert magic_func() == "works3" + assert magic_func.a.value == "works" + assert magic_func.b.value == 3 + assert magic_func.c.value == 7.1 + assert isinstance(magic_func.a, widgets.LineEdit) + assert isinstance(magic_func.b, widgets.SpinBox) + assert isinstance(magic_func.c, widgets.FloatSpinBox) + + magic_func.show() + assert magic_func.visible + + a = magic_func.a # save ref + assert magic_func.index(a) == 0 + # we can delete widgets + del magic_func.a + with pytest.raises(AttributeError): + getattr(magic_func, "a") + + # they disappear from the layout + with pytest.raises(ValueError): + magic_func.index(a) + + +def test_default_call_button_behavior(magic_func_defaults, magic_func_autocall): + assert magic_func_defaults.call_button is not None + + assert magic_func_autocall.call_button is None + prior_autocall_count = magic_func_autocall.call_count + magic_func_autocall.a.value = "hello" + magic_func_autocall.b.value = 7 + assert magic_func_autocall.call_count == prior_autocall_count + 2 + + +def test_overriding_widget_type(): + """Test overriding the widget type of a parameter.""" + # a will now be a LineEdit instead of a spinbox + @magicgui(a={"widget_type": "LineEdit"}) + def func(a: int = 1): + pass + + assert isinstance(func.a, widgets.LineEdit) + assert func.a.value == "1" + + # also without type annotation + @magicgui(a={"widget_type": "LogSlider"}) + def g(a): + ... + + assert isinstance(g.a, widgets.LogSlider) + + +def test_unrecognized_types(): + """Test that arg with an unrecognized type is hidden.""" + + class Something: + pass + + # don't know how to handle Something type + @magicgui + def func(arg: Something, b: int = 1): + pass + + assert isinstance(func.arg, widgets.EmptyWidget) + + with pytest.raises(TypeError) as e: + func() + assert "missing a required argument" in str(e) + + +def test_no_type_provided(): + """Test position args with unknown type.""" + + @magicgui + def func(a): + pass + + assert isinstance(func.a, widgets.EmptyWidget) + with pytest.raises(TypeError) as e: + func() + assert "missing a required argument" in str(e) + assert "@magicgui(a={'bind': value})" in str(e) + + +def test_bind_out_of_order(): + """Test that binding a value before a non-default argument still gives message.""" + + @magicgui(a={"bind": 10}) + def func(a, x): + pass + + assert isinstance(func.a, widgets.EmptyWidget) + with pytest.raises(TypeError) as e: + func() + assert "missing a required argument" in str(e) + assert "@magicgui(x={'bind': value})" in str(e) + + +def test_call_button(): + """Test that the call button has been added, and pressing it calls the function.""" + + @magicgui(call_button="my_button", auto_call=True) + def func(a: int, b: int = 3, c=7.1): + assert a == 7 + + assert hasattr(func, "call_button") + assert isinstance(func.call_button, widgets.PushButton) + func.a.value = 7 + + +@pytest.mark.filterwarnings("ignore") +def test_auto_call(qtbot, magic_func): + """Test that changing a parameter calls the function.""" + from qtpy.QtTest import QTest + + # TODO: remove qtbot requirement so we can test other backends eventually. + # changing the widget parameter calls the function + with qtbot.waitSignal(magic_func.called, timeout=1000): + magic_func.b.value = 6 + + # changing the gui calls the function + with qtbot.waitSignal(magic_func.called, timeout=1000): + QTest.keyClick(magic_func.a.native, Qt.Key_A, Qt.ControlModifier) + QTest.keyClick(magic_func.a.native, Qt.Key_Delete) + + +def test_dropdown_list_from_enum(): + """Test that enums properly populate the dropdown menu with options.""" + + class Medium(Enum): + Glass = 1.520 + Oil = 1.515 + Water = 1.333 + Air = 1.0003 + + @magicgui + def func(arg: Medium = Medium.Water): + ... + + assert func.arg.value == Medium.Water + assert isinstance(func.arg, widgets.ComboBox) + assert list(func.arg.choices) == list(Medium.__members__.values()) + + +def test_dropdown_list_from_choices(): + """Test that providing the 'choices' argument with a list of strings works.""" + CHOICES = ["Oil", "Water", "Air"] + + @magicgui(arg={"choices": CHOICES}) + def func(arg="Water"): + ... + + assert func.arg.value == "Water" + assert isinstance(func.arg, widgets.ComboBox) + assert list(func.arg.choices) == CHOICES + + with pytest.raises(ValueError): + # the default value must be in the list + @magicgui(arg={"choices": ["Oil", "Water", "Air"]}) + def func(arg="Silicone"): + ... + + +def test_dropdown_list_from_callable(): + """Test that providing the 'choices' argument with a callable works.""" + CHOICES = ["Oil", "Water", "Air"] + + def get_choices(gui): + return CHOICES + + @magicgui(arg={"choices": get_choices}) + def func(arg="Water"): + ... + + assert func.arg.value == "Water" + assert isinstance(func.arg, widgets.ComboBox) + assert list(func.arg.choices) == CHOICES + + func.reset_choices() + + +def test_changing_widget_attr_fails(magic_func): + """Test set_widget will either update or change an existing widget.""" + assert magic_func.a.value == "works" + widget1 = magic_func.a + assert isinstance(widget1, widgets.LineEdit) + + # changing it to a different type will destroy and create a new widget + widget2 = widgets.create_widget(value=1, name="a") + with pytest.raises(AttributeError): + magic_func.a = widget2 + + assert magic_func.a == widget1 + + +def test_multiple_gui_with_same_args(): + """Test that similarly named arguments are independent of one another.""" + + @magicgui + def example1(a=2): + return a + + @magicgui + def example2(a=5): + return a + + # they get their initial values from the function sigs + assert example1.a.value == 2 + assert example2.a.value == 5 + # settings one doesn't affect the other + example1.a.value = 10 + assert example1.a.value == 10 + assert example2.a.value == 5 + # vice versa... + example2.a.value = 4 + assert example1.a.value == 10 + assert example2.a.value == 4 + # calling the original equations updates the function defaults + assert example1() == 10 + assert example2() == 4 + + +def test_multiple_gui_instance_independence(): + """Test that multiple instance of the same decorated function are independent.""" + + def example(a=2): + return a + + w1 = magicgui(example) + w2 = magicgui(example) + # they get their initial values from the function sigs + assert w1.a.value == 2 + assert w2.a.value == 2 + # settings one doesn't affect the other + w1.a.value = 10 + assert w1.a.value == 10 + assert w2.a.value == 2 + # vice versa... + w2.a.value = 4 + assert w1.a.value == 10 + assert w2.a.value == 4 + + # all instances are independent + assert example() == 2 + assert w1() == 10 + assert w2() == 4 + + +def test_invisible_param(): + """Test that the visible option works.""" + + @magicgui(a={"visible": False}) + def func(a: str = "string", b: int = 3, c=7.1) -> str: + return "works" + + assert hasattr(func, "a") + func.show() + assert not func.a.visible + assert func.b.visible + assert func.c.visible + func() + + +def test_bad_options(): + """Test that invalid parameter options raise TypeError.""" + with pytest.raises(TypeError): + + @magicgui(b=7) # type: ignore + def func(a="string", b=3, c=7.1): + return "works" + + +# @pytest.mark.xfail(reason="MagicSignatures are slightly different") +def test_signature_repr(): + """Test that the gui makes a proper signature.""" + + def func(a: str = "string", b: int = 3, c: float = 7.1): + return locals() + + magic_func = magicgui(func) + + # the STRING signature representation should be the same as the original function + assert str(inspect.signature(magic_func)) == str(inspect.signature(func)) + # however, the magic_func signature is an enhance MagicSignature object: + assert isinstance(inspect.signature(magic_func), MagicSignature) + assert isinstance(inspect.signature(func), inspect.Signature) + + # make sure it is up to date + magic_func.b.value = 0 + assert ( + str(inspect.signature(magic_func)) + == "(a: str = 'string', b: int = 0, c: float = 7.1)" + ) + + +def test_set_choices_raises(): + """Test failures on setting choices.""" + + @magicgui(mood={"choices": ["happy", "sad"]}) + def func(mood: str = "happy"): + pass + + with pytest.raises(TypeError): + func.mood.choices = None + with pytest.raises(TypeError): + func.mood.choices = 1 + + +def test_get_choices_raises(): + """Test failures on getting choices.""" + + @magicgui(mood={"choices": [1, 2, 3]}) + def func(mood: int = 1, hi: str = "hello"): + pass + + with pytest.raises(AttributeError): + func.hi.choices + + assert func.mood.choices == (1, 2, 3) + + +@pytest.mark.parametrize( + "labels", + [ + pytest.param( + True, marks=pytest.mark.xfail(reason="indexing still wrong with labels") + ), + False, + ], + ids=["with-labels", "no-labels"], +) +def test_add_at_position(labels): + """Test that adding widghet with position option puts widget in the right place.""" + + def func(a=1, b=2, c=3): + pass + + def get_layout_items(gui): + lay = gui.native.layout() + items = [lay.itemAt(i).widget()._magic_widget.name for i in range(lay.count())] + if labels: + items = list(filter(None, items)) + return items + + gui = magicgui(func, labels=labels) + assert get_layout_items(gui) == ["a", "b", "c", "call_button"] + gui.insert(1, widgets.create_widget(name="new", raise_on_unknown=False)) + assert get_layout_items(gui) == ["a", "new", "b", "c", "call_button"] + + +def test_original_function_works(magic_func): + """Test that the decorated function is still operational.""" + assert magic_func() == "works3" + assert magic_func("hi") == "hi3" + + +def test_show(magic_func): + """Test that the show option works.""" + # assert not magic_func.visible + magic_func.show() + assert magic_func.visible + + +def test_register_types_by_string(): + """Test that we can register custom widget classes for certain types.""" + # must provide a non-None choices or widget_type + with pytest.raises(ValueError): + register_type(str, choices=None) + + register_type(int, widget_type="LineEdit") + + # this works, but choices overrides widget_type, and warns the user + with pytest.warns(UserWarning): + register_type(str, choices=["works", "cool", "huh"], widget_type="LineEdit") + + class Main: + pass + + class Sub(Main): + pass + + class Main2: + pass + + class Sub2(Main2): + pass + + register_type(Main, choices=[None, 1, 2, 3]) + register_type(Main2, widget_type="LineEdit") + + @magicgui + def func(a: str = "works", b: int = 3, c: Sub = None, d: Sub2 = None): + return a + + assert isinstance(func.a, widgets.ComboBox) + assert isinstance(func.b, widgets.LineEdit) + 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] + + +def test_register_types_by_class(): + class MyLineEdit(widgets.LineEdit): + pass + + class MyStr: + pass + + register_type(MyStr, widget_type=MyLineEdit) + w = widgets.create_widget(value=MyStr()) + assert isinstance(w, MyLineEdit) + + +def test_register_return_callback(): + """Test that registering a return callback works.""" + + def check_value(gui, value, rettype): + assert value == 1 + + class Base: + pass + + class Sub(Base): + pass + + register_type(int, return_callback=check_value) + register_type(Base, return_callback=check_value) + + try: + + @magicgui + def func(a=1) -> int: + return a + + func() + with pytest.raises(AssertionError): + func(3) + + @magicgui + def func2(a=1) -> Sub: + return a + + func2() + finally: + from magicgui.type_map._type_map import _RETURN_CALLBACKS + + _RETURN_CALLBACKS.pop(int) + _RETURN_CALLBACKS.pop(Base) + + +# @pytest.mark.skip(reason="need to rethink how to test this") +# def test_parent_changed(qtbot, magic_func): +# """Test that setting MagicGui parent emits a signal.""" +# with qtbot.waitSignal(magic_func.parent_changed, timeout=1000): +# magic_func.native.setParent(None) + + +def test_function_binding(): + class MyObject: + def __init__(self, name): + self.name = name + self.counter = 0.0 + + @magicgui(call_button="callme", sigma={"max": 365}) + def method(self, sigma: float = 1): + self.counter = self.counter + sigma + return self.name, self.counter + + a = MyObject("a") + b = MyObject("b") + + assert a.method.call_button.text == "callme" # type: ignore + assert a.method.sigma.max == 365 + assert a.method() == ("a", 1) + assert b.method(sigma=4) == ("b", 4) + assert a.method() == ("a", 2) + assert b.method() == ("b", 5) + + +def test_function_binding_multiple(): + class MyObject: + def __init__(self): + pass + + @magicgui + def method_0(self, sigma: float = 1): + pass + + @magicgui + def method_1(self, sigma: float = 2): + pass + + a = MyObject() + assert MyObject.method_0 is not a.method_0 + assert a.method_0 is not a.method_1 + assert a.method_0.sigma.value == 1 + assert a.method_1.sigma.value == 2 + + +def test_call_count(): + """Test that a function gui remembers how many times it's been called.""" + + @magicgui + def func(): + pass + + assert func.call_count == 0 + func() + func() + assert func.call_count == 2 + func.reset_call_count() + assert func.call_count == 0 + + +def test_tooltips_from_numpydoc(): + """Test that numpydocs docstrings can be used for tooltips.""" + + x_tooltip = "override tooltip" + y_docstring = """A greeting, by default 'hi'. Notice how we miraculously pull +the entirety of the docstring just like that""" + + @magicgui(x={"tooltip": x_tooltip}, z={"tooltip": None}) + def func(x: int, y: str = "hi", z=None): + """Do a little thing. + + Parameters + ---------- + x : int + An integer for you to use + y : str, optional + A greeting, by default 'hi'. Notice how we miraculously pull + the entirety of the docstring just like that + z : Any, optional + No tooltip for me please. + """ + + assert func.x.tooltip == x_tooltip + assert func.y.tooltip == y_docstring + assert not func.z.tooltip + + +def test_bad_param_name_in_docstring(): + @magicgui + def func(x: int): + """Do a little thing. + + Parameters + ---------- + not_x: int + DESCRIPTION. + """ + return x + + assert not func.x.tooltip + + +def test_duplicated_and_missing_params_from_numpydoc(): + """Test that numpydocs docstrings can be used for tooltips.""" + + @magicgui + def func(x, y, z=None): + """Do a little thing. + + Parameters + ---------- + x, y : int + Integers for you to use + """ + + assert func.x.tooltip == "Integers for you to use" + assert func.y.tooltip == "Integers for you to use" + assert not func.z.tooltip + + +def test_tooltips_from_google_doc(): + """Test that google docstrings can be used for tooltips.""" + + x_docstring = "An integer for you to use" + y_docstring = """A greeting. Notice how we miraculously pull +the entirety of the docstring just like that""" + + @magicgui + def func(x: int, y: str = "hi"): + """Do a little thing. + + Args: + x (int): An integer for you to use + y (str, optional): A greeting. Notice how we miraculously pull + the entirety of the docstring just like that + """ + + assert func.x.tooltip == x_docstring + assert func.y.tooltip == y_docstring + + +def test_tooltips_from_rest_doc(): + """Test that google docstrings can be used for tooltips.""" + + x_docstring = "An integer for you to use" + y_docstring = """A greeting, by default 'hi'. Notice how we miraculously pull +the entirety of the docstring just like that""" + + @magicgui + def func(x: int, y: str = "hi", z=None): + """Do a little thing. + + :param x: An integer for you to use + :param y: A greeting, by default 'hi'. Notice how we miraculously pull + the entirety of the docstring just like that + :type x: int + :type y: str + """ + + assert func.x.tooltip == x_docstring + assert func.y.tooltip == y_docstring + + +def test_no_tooltips_from_numpydoc(): + """Test that ``tooltips=False`` hides all tooltips.""" + + @magicgui(tooltips=False) + def func(x: int, y: str = "hi"): + """Do a little thing. + + Parameters + ---------- + x : int + An integer for you to use + y : str, optional + A greeting, by default 'hi' + """ + + assert not func.x.tooltip + assert not func.y.tooltip + + +def test_only_some_tooltips_from_numpydoc(): + """Test that we can still show some tooltips with ``tooltips=False``.""" + # tooltips=False, means docstrings wont be parsed at all, but tooltips + # can still be manually provided. + @magicgui(tooltips=False, y={"tooltip": "Still want a tooltip"}) + def func(x: int, y: str = "hi"): + """Do a little thing. + + Parameters + ---------- + x : int + An integer for you to use + y : str, optional + A greeting, by default 'hi' + """ + + assert not func.x.tooltip + assert func.y.tooltip == "Still want a tooltip" + + +def test_magicgui_type_error(): + + with pytest.raises(TypeError): + magicgui("not a function") # type: ignore + + +def self_referencing_function(x: int = 1): + """Function that refers to itself, and wants the FunctionGui instance.""" + return self_referencing_function + + +def test_magicgui_self_reference(): + """Test that self-referential magicguis work in global scopes.""" + global self_referencing_function + f = magicgui(self_referencing_function) + assert isinstance(f(), widgets.FunctionGui) + assert f() is f + + +def test_local_magicgui_self_reference(): + """Test that self-referential magicguis work in local scopes.""" + + @magicgui + def local_self_referencing_function(x: int = 1): + """Function that refers to itself, and wants the FunctionGui instance.""" + return local_self_referencing_function + + assert isinstance(local_self_referencing_function(), widgets.FunctionGui) + + +def test_empty_function(): + """Test that a function with no params works.""" + + @magicgui(call_button=True) + def f(): + ... + + f.show() + + +def test_boolean_label(): + """Test that label can be used to set the text of a button widget.""" + + @magicgui(check={"label": "ABC"}) + def test(check: bool, x=1): + pass + + assert test.check.text == "ABC" + + with pytest.warns(UserWarning) as record: + + @magicgui(check={"text": "ABC", "label": "BCD"}) + def test2(check: bool, x=1): + pass + + assert "'text' and 'label' are synonymous for button widgets" in str(record[0]) + + +def test_none_defaults(): + """Make sure that an unannotated parameter with default=None is ok.""" + assert widgets.create_widget(value=None, raise_on_unknown=False).value is None + + def func(arg=None): + return 1 + + assert magicgui(func)() == 1 + + assert str(magic_signature(func)) == str(magicgui(func).__signature__) + + +def test_update_and_dict(): + @magicgui + def test(a: int = 1, y: str = "a"): + ... + + assert test.asdict() == {"a": 1, "y": "a"} + + test.update(a=10, y="b") + assert test.asdict() == {"a": 10, "y": "b"} + + test.update({"a": 1, "y": "a"}) + assert test.asdict() == {"a": 1, "y": "a"} + + test.update([("a", 10), ("y", "b")]) + assert test.asdict() == {"a": 10, "y": "b"} + + +def test_update_on_call(): + @magicgui + def test(a: int = 1, y: str = "a"): + ... + + assert test.call_count == 0 + test(a=10, y="b", update_widget=True) + assert test.a.value == 10 + assert test.y.value == "b" + assert test.call_count == 1 + + +def test_partial(): + from functools import partial + + def some_func(x: int, y: str) -> str: + return y + str(x) + + wdg = magicgui(partial(some_func, 1)) + assert len(wdg) == 2 # because of the call_button + assert isinstance(wdg.y, widgets.LineEdit) + assert not hasattr(wdg, "x") + assert wdg("sdf") == "sdf1" + assert wdg._callable_name == "some_func" + + wdg2 = magicgui(partial(some_func, y="sdf")) + assert len(wdg2) == 3 # keyword arguments don't change the partial signature + assert isinstance(wdg2.x, widgets.SpinBox) + assert isinstance(wdg.y, widgets.LineEdit) + assert wdg2.y.value == "sdf" + assert wdg2(1) == "sdf1" + + +def test_curry(): + import toolz as tz + + @tz.curry + def some_func2(x: int, y: str) -> str: + return y + str(x) + + wdg = magicgui(some_func2(1)) + assert len(wdg) == 2 # because of the call_button + assert isinstance(wdg.y, widgets.LineEdit) + assert not hasattr(wdg, "x") + assert wdg("sdf") == "sdf1" + assert wdg._callable_name == "some_func2" + + wdg2 = magicgui(some_func2(y="sdf")) + assert len(wdg2) == 3 # keyword arguments don't change the partial signature + assert isinstance(wdg2.x, widgets.SpinBox) + assert isinstance(wdg.y, widgets.LineEdit) + assert wdg2.y.value == "sdf" + assert wdg2(1) == "sdf1" + + +def test_scrollable(): + @magicgui(scrollable=True) + def test_scrollable(a: int = 1, y: str = "a"): + ... + + assert test_scrollable.native is not test_scrollable.root_native_widget + assert not isinstance(test_scrollable.native, QScrollArea) + assert isinstance(test_scrollable.root_native_widget, QScrollArea) + + @magicgui(scrollable=False) + def test_nonscrollable(a: int = 1, y: str = "a"): + ... + + assert test_nonscrollable.native is test_nonscrollable.root_native_widget + assert not isinstance(test_nonscrollable.native, QScrollArea) + + +def test_unknown_exception_magicgui(): + """Test that an unknown type is raised as a RuntimeError.""" + + class A: + pass + + with pytest.raises(ValueError, match="No widget found for type"): + + @magicgui(raise_on_unknown=True) + def func(a: A): + print(a) + + +def test_unknown_exception_create_widget(): + """Test that an unknown type is raised as a RuntimeError.""" + + class A: + pass + + with pytest.raises(ValueError, match="No widget found for type"): + widgets.create_widget(A, raise_on_unknown=True) + with pytest.raises(ValueError, match="No widget found for type"): + widgets.create_widget(A) + assert isinstance( + widgets.create_widget(A, raise_on_unknown=False), widgets.EmptyWidget + ) diff --git a/tests/test_widgets.py b/tests/test_backends/test_widgets.py similarity index 100% rename from tests/test_widgets.py rename to tests/test_backends/test_widgets.py diff --git a/tests/test_widget_bases.py b/tests/test_widget_bases.py new file mode 100644 index 000000000..bbe4a9a54 --- /dev/null +++ b/tests/test_widget_bases.py @@ -0,0 +1,128 @@ +# type: ignore +from __future__ import annotations + +from typing import ForwardRef, TypeVar +from unittest.mock import Mock, create_autospec + +from magicgui import widgets + +W = TypeVar("W", bound=widgets.Widget) + + +def _mock_widget(WidgetType: type[W], **kwargs) -> W: + """Create a mock widget with the given spec.""" + from magicgui.widgets import protocols + + _proto = WidgetType.__annotations__.get("_widget", None) + if _proto is None: + raise TypeError(f"Cannot mock {WidgetType} without a _widget annotation") + elif isinstance(_proto, (ForwardRef, str)): + if isinstance(_proto, str): + _proto = ForwardRef(_proto) + _proto = _proto._evaluate({"protocols": protocols}, None, frozenset()) + backend_mock = create_autospec(_proto, spec_set=True) + widget = WidgetType(widget_type=backend_mock, **kwargs) + backend_mock.assert_called_once_with(parent=None) + return widget + + +def test_base_widgtet_protocol(mock_app): + widget = _mock_widget(widgets.Widget) + + assert widget.__magicgui_app__ is mock_app + mock = widget._widget + + mock._mgui_get_native_widget.assert_called_once() + assert widget.native._magic_widget is widget + + mock._mgui_set_tooltip.assert_called_once_with(None) + mock._mgui_set_enabled.assert_called_once_with(True) + mock._mgui_bind_parent_change_callback.assert_called_once() + + assert {"enabled", "visible"}.issubset(set(widget.options)) + mock._mgui_get_enabled.assert_called_once() + mock._mgui_get_visible.assert_called_once() + + for attr in ( + "width", + "height", + "min_width", + "min_height", + "max_width", + "max_height", + ): + getattr(widget, attr) + getattr(mock, f"_mgui_get_{attr}").assert_called_once() + setattr(widget, attr, 1) + getattr(mock, f"_mgui_set_{attr}").assert_called_once_with(1) + + widget.show(run=True) + mock._mgui_set_visible.assert_called_once_with(True) + mock_app._backend._mgui_run.assert_called_once() + + # shown context + mock._mgui_set_visible.reset_mock() + assert mock_app._backend._mgui_get_native_app.call_count == 1 + assert mock_app._backend._mgui_run.call_count == 1 + with widget.shown(): + mock._mgui_set_visible.assert_called_with(True) + assert mock_app._backend._mgui_get_native_app.call_count == 2 + assert mock_app._backend._mgui_run.call_count == 2 + + widget.hide() + mock._mgui_set_visible.assert_called_with(False) + + widget.close() + mock._mgui_close_widget.assert_called_once() + + widget.render() + mock._mgui_render.assert_called_once() + + +def test_base_widget_events(mock_app): + widget = _mock_widget(widgets.Widget) + widget._widget._mgui_set_parent.side_effect = widget._emit_parent + + mock = Mock() + widget.label_changed.connect(mock) + widget.label = "new_label" + mock.assert_called_once_with("new_label") + + mock.reset_mock() + widget.parent_changed.connect(mock) + widget.parent = "new_parent" + mock.assert_called_once() + + +def test_value_widget_protocol(mock_app): + widget = _mock_widget(widgets.bases.ValueWidget, value=1) + widget._widget._mgui_set_value.assert_called_once_with(1) + + widget.value + assert widget._widget._mgui_get_value.call_count == 1 + widget.get_value() + assert widget._widget._mgui_get_value.call_count == 2 + + widget.value = 2 + widget._widget._mgui_set_value.assert_called_with(2) + + +def test_value_widget_bind(mock_app): + mock = Mock() + mock.return_value = 3 + widget = _mock_widget(widgets.bases.ValueWidget) + widget.bind(mock) + mock.assert_not_called() + assert widget.value == 3 + mock.assert_called_once_with(widget) + + +def test_value_widget_events(mock_app): + widget = _mock_widget(widgets.bases.ValueWidget, value=1) + widget._widget._mgui_set_value.side_effect = widget._on_value_change + + change_mock = Mock() + widget.changed.connect(change_mock) + + widget.value = 2 + change_mock.assert_called_with(2) From d43988d5a14afeb4a0aaf8b70e5d9a2e6557380a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 18 Nov 2022 09:18:20 -0500 Subject: [PATCH 2/5] remove extra file --- tests/test_backends/test_magicgui copy.py | 879 ---------------------- 1 file changed, 879 deletions(-) delete mode 100644 tests/test_backends/test_magicgui copy.py diff --git a/tests/test_backends/test_magicgui copy.py b/tests/test_backends/test_magicgui copy.py deleted file mode 100644 index 4130d351b..000000000 --- a/tests/test_backends/test_magicgui copy.py +++ /dev/null @@ -1,879 +0,0 @@ -#!/usr/bin/env python - -"""Tests for `magicgui` package.""" - -import inspect -from enum import Enum - -import pytest -from qtpy.QtCore import Qt -from qtpy.QtWidgets import QScrollArea - -from magicgui import magicgui, register_type, type_map, widgets -from magicgui.signature import MagicSignature, magic_signature - - -def func(a: str = "works", b: int = 3, c=7.1) -> str: - return a + str(b) - - -@pytest.fixture -def magic_func(): - """Test function decorated by magicgui.""" - return magicgui(func, call_button="my_button", auto_call=True, labels=False) - - -@pytest.fixture -def magic_func_defaults(): - return magicgui(func) - - -@pytest.fixture -def magic_func_autocall(): - return magicgui(func, auto_call=True) - - -def test_magicgui(magic_func): - """Test basic magicgui functionality.""" - assert magic_func() == "works3" - assert magic_func.a.value == "works" - assert magic_func.b.value == 3 - assert magic_func.c.value == 7.1 - assert isinstance(magic_func.a, widgets.LineEdit) - assert isinstance(magic_func.b, widgets.SpinBox) - assert isinstance(magic_func.c, widgets.FloatSpinBox) - - magic_func.show() - assert magic_func.visible - - a = magic_func.a # save ref - assert magic_func.index(a) == 0 - # we can delete widgets - del magic_func.a - with pytest.raises(AttributeError): - getattr(magic_func, "a") - - # they disappear from the layout - with pytest.raises(ValueError): - magic_func.index(a) - - -def test_default_call_button_behavior(magic_func_defaults, magic_func_autocall): - assert magic_func_defaults.call_button is not None - - assert magic_func_autocall.call_button is None - prior_autocall_count = magic_func_autocall.call_count - magic_func_autocall.a.value = "hello" - magic_func_autocall.b.value = 7 - assert magic_func_autocall.call_count == prior_autocall_count + 2 - - -def test_overriding_widget_type(): - """Test overriding the widget type of a parameter.""" - # a will now be a LineEdit instead of a spinbox - @magicgui(a={"widget_type": "LineEdit"}) - def func(a: int = 1): - pass - - assert isinstance(func.a, widgets.LineEdit) - assert func.a.value == "1" - - # also without type annotation - @magicgui(a={"widget_type": "LogSlider"}) - def g(a): - ... - - assert isinstance(g.a, widgets.LogSlider) - - -def test_unrecognized_types(): - """Test that arg with an unrecognized type is hidden.""" - - class Something: - pass - - # don't know how to handle Something type - @magicgui - def func(arg: Something, b: int = 1): - pass - - assert isinstance(func.arg, widgets.EmptyWidget) - - with pytest.raises(TypeError) as e: - func() - assert "missing a required argument" in str(e) - - -def test_no_type_provided(): - """Test position args with unknown type.""" - - @magicgui - def func(a): - pass - - assert isinstance(func.a, widgets.EmptyWidget) - with pytest.raises(TypeError) as e: - func() - assert "missing a required argument" in str(e) - assert "@magicgui(a={'bind': value})" in str(e) - - -def test_bind_out_of_order(): - """Test that binding a value before a non-default argument still gives message.""" - - @magicgui(a={"bind": 10}) - def func(a, x): - pass - - assert isinstance(func.a, widgets.EmptyWidget) - with pytest.raises(TypeError) as e: - func() - assert "missing a required argument" in str(e) - assert "@magicgui(x={'bind': value})" in str(e) - - -def test_call_button(): - """Test that the call button has been added, and pressing it calls the function.""" - - @magicgui(call_button="my_button", auto_call=True) - def func(a: int, b: int = 3, c=7.1): - assert a == 7 - - assert hasattr(func, "call_button") - assert isinstance(func.call_button, widgets.PushButton) - func.a.value = 7 - - -@pytest.mark.filterwarnings("ignore") -def test_auto_call(qtbot, magic_func): - """Test that changing a parameter calls the function.""" - from qtpy.QtTest import QTest - - # TODO: remove qtbot requirement so we can test other backends eventually. - # changing the widget parameter calls the function - with qtbot.waitSignal(magic_func.called, timeout=1000): - magic_func.b.value = 6 - - # changing the gui calls the function - with qtbot.waitSignal(magic_func.called, timeout=1000): - QTest.keyClick(magic_func.a.native, Qt.Key_A, Qt.ControlModifier) - QTest.keyClick(magic_func.a.native, Qt.Key_Delete) - - -def test_dropdown_list_from_enum(): - """Test that enums properly populate the dropdown menu with options.""" - - class Medium(Enum): - Glass = 1.520 - Oil = 1.515 - Water = 1.333 - Air = 1.0003 - - @magicgui - def func(arg: Medium = Medium.Water): - ... - - assert func.arg.value == Medium.Water - assert isinstance(func.arg, widgets.ComboBox) - assert list(func.arg.choices) == list(Medium.__members__.values()) - - -def test_dropdown_list_from_choices(): - """Test that providing the 'choices' argument with a list of strings works.""" - CHOICES = ["Oil", "Water", "Air"] - - @magicgui(arg={"choices": CHOICES}) - def func(arg="Water"): - ... - - assert func.arg.value == "Water" - assert isinstance(func.arg, widgets.ComboBox) - assert list(func.arg.choices) == CHOICES - - with pytest.raises(ValueError): - # the default value must be in the list - @magicgui(arg={"choices": ["Oil", "Water", "Air"]}) - def func(arg="Silicone"): - ... - - -def test_dropdown_list_from_callable(): - """Test that providing the 'choices' argument with a callable works.""" - CHOICES = ["Oil", "Water", "Air"] - - def get_choices(gui): - return CHOICES - - @magicgui(arg={"choices": get_choices}) - def func(arg="Water"): - ... - - assert func.arg.value == "Water" - assert isinstance(func.arg, widgets.ComboBox) - assert list(func.arg.choices) == CHOICES - - func.reset_choices() - - -def test_changing_widget_attr_fails(magic_func): - """Test set_widget will either update or change an existing widget.""" - assert magic_func.a.value == "works" - widget1 = magic_func.a - assert isinstance(widget1, widgets.LineEdit) - - # changing it to a different type will destroy and create a new widget - widget2 = widgets.create_widget(value=1, name="a") - with pytest.raises(AttributeError): - magic_func.a = widget2 - - assert magic_func.a == widget1 - - -def test_multiple_gui_with_same_args(): - """Test that similarly named arguments are independent of one another.""" - - @magicgui - def example1(a=2): - return a - - @magicgui - def example2(a=5): - return a - - # they get their initial values from the function sigs - assert example1.a.value == 2 - assert example2.a.value == 5 - # settings one doesn't affect the other - example1.a.value = 10 - assert example1.a.value == 10 - assert example2.a.value == 5 - # vice versa... - example2.a.value = 4 - assert example1.a.value == 10 - assert example2.a.value == 4 - # calling the original equations updates the function defaults - assert example1() == 10 - assert example2() == 4 - - -def test_multiple_gui_instance_independence(): - """Test that multiple instance of the same decorated function are independent.""" - - def example(a=2): - return a - - w1 = magicgui(example) - w2 = magicgui(example) - # they get their initial values from the function sigs - assert w1.a.value == 2 - assert w2.a.value == 2 - # settings one doesn't affect the other - w1.a.value = 10 - assert w1.a.value == 10 - assert w2.a.value == 2 - # vice versa... - w2.a.value = 4 - assert w1.a.value == 10 - assert w2.a.value == 4 - - # all instances are independent - assert example() == 2 - assert w1() == 10 - assert w2() == 4 - - -def test_invisible_param(): - """Test that the visible option works.""" - - @magicgui(a={"visible": False}) - def func(a: str = "string", b: int = 3, c=7.1) -> str: - return "works" - - assert hasattr(func, "a") - func.show() - assert not func.a.visible - assert func.b.visible - assert func.c.visible - func() - - -def test_bad_options(): - """Test that invalid parameter options raise TypeError.""" - with pytest.raises(TypeError): - - @magicgui(b=7) # type: ignore - def func(a="string", b=3, c=7.1): - return "works" - - -# @pytest.mark.xfail(reason="MagicSignatures are slightly different") -def test_signature_repr(): - """Test that the gui makes a proper signature.""" - - def func(a: str = "string", b: int = 3, c: float = 7.1): - return locals() - - magic_func = magicgui(func) - - # the STRING signature representation should be the same as the original function - assert str(inspect.signature(magic_func)) == str(inspect.signature(func)) - # however, the magic_func signature is an enhance MagicSignature object: - assert isinstance(inspect.signature(magic_func), MagicSignature) - assert isinstance(inspect.signature(func), inspect.Signature) - - # make sure it is up to date - magic_func.b.value = 0 - assert ( - str(inspect.signature(magic_func)) - == "(a: str = 'string', b: int = 0, c: float = 7.1)" - ) - - -def test_set_choices_raises(): - """Test failures on setting choices.""" - - @magicgui(mood={"choices": ["happy", "sad"]}) - def func(mood: str = "happy"): - pass - - with pytest.raises(TypeError): - func.mood.choices = None - with pytest.raises(TypeError): - func.mood.choices = 1 - - -def test_get_choices_raises(): - """Test failures on getting choices.""" - - @magicgui(mood={"choices": [1, 2, 3]}) - def func(mood: int = 1, hi: str = "hello"): - pass - - with pytest.raises(AttributeError): - func.hi.choices - - assert func.mood.choices == (1, 2, 3) - - -@pytest.mark.parametrize( - "labels", - [ - pytest.param( - True, marks=pytest.mark.xfail(reason="indexing still wrong with labels") - ), - False, - ], - ids=["with-labels", "no-labels"], -) -def test_add_at_position(labels): - """Test that adding widghet with position option puts widget in the right place.""" - - def func(a=1, b=2, c=3): - pass - - def get_layout_items(gui): - lay = gui.native.layout() - items = [lay.itemAt(i).widget()._magic_widget.name for i in range(lay.count())] - if labels: - items = list(filter(None, items)) - return items - - gui = magicgui(func, labels=labels) - assert get_layout_items(gui) == ["a", "b", "c", "call_button"] - gui.insert(1, widgets.create_widget(name="new", raise_on_unknown=False)) - assert get_layout_items(gui) == ["a", "new", "b", "c", "call_button"] - - -def test_original_function_works(magic_func): - """Test that the decorated function is still operational.""" - assert magic_func() == "works3" - assert magic_func("hi") == "hi3" - - -def test_show(magic_func): - """Test that the show option works.""" - # assert not magic_func.visible - magic_func.show() - assert magic_func.visible - - -def test_register_types_by_string(): - """Test that we can register custom widget classes for certain types.""" - # must provide a non-None choices or widget_type - with pytest.raises(ValueError): - register_type(str, choices=None) - - register_type(int, widget_type="LineEdit") - - # this works, but choices overrides widget_type, and warns the user - with pytest.warns(UserWarning): - register_type(str, choices=["works", "cool", "huh"], widget_type="LineEdit") - - class Main: - pass - - class Sub(Main): - pass - - class Main2: - pass - - class Sub2(Main2): - pass - - register_type(Main, choices=[None, 1, 2, 3]) - register_type(Main2, widget_type="LineEdit") - - @magicgui - def func(a: str = "works", b: int = 3, c: Sub = None, d: Sub2 = None): - return a - - assert isinstance(func.a, widgets.ComboBox) - assert isinstance(func.b, widgets.LineEdit) - 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] - - -def test_register_types_by_class(): - class MyLineEdit(widgets.LineEdit): - pass - - class MyStr: - pass - - register_type(MyStr, widget_type=MyLineEdit) - w = widgets.create_widget(value=MyStr()) - assert isinstance(w, MyLineEdit) - - -def test_register_return_callback(): - """Test that registering a return callback works.""" - - def check_value(gui, value, rettype): - assert value == 1 - - class Base: - pass - - class Sub(Base): - pass - - register_type(int, return_callback=check_value) - register_type(Base, return_callback=check_value) - - try: - - @magicgui - def func(a=1) -> int: - return a - - func() - with pytest.raises(AssertionError): - func(3) - - @magicgui - def func2(a=1) -> Sub: - return a - - func2() - finally: - from magicgui.type_map._type_map import _RETURN_CALLBACKS - - _RETURN_CALLBACKS.pop(int) - _RETURN_CALLBACKS.pop(Base) - - -# @pytest.mark.skip(reason="need to rethink how to test this") -# def test_parent_changed(qtbot, magic_func): -# """Test that setting MagicGui parent emits a signal.""" -# with qtbot.waitSignal(magic_func.parent_changed, timeout=1000): -# magic_func.native.setParent(None) - - -def test_function_binding(): - class MyObject: - def __init__(self, name): - self.name = name - self.counter = 0.0 - - @magicgui(call_button="callme", sigma={"max": 365}) - def method(self, sigma: float = 1): - self.counter = self.counter + sigma - return self.name, self.counter - - a = MyObject("a") - b = MyObject("b") - - assert a.method.call_button.text == "callme" # type: ignore - assert a.method.sigma.max == 365 - assert a.method() == ("a", 1) - assert b.method(sigma=4) == ("b", 4) - assert a.method() == ("a", 2) - assert b.method() == ("b", 5) - - -def test_function_binding_multiple(): - class MyObject: - def __init__(self): - pass - - @magicgui - def method_0(self, sigma: float = 1): - pass - - @magicgui - def method_1(self, sigma: float = 2): - pass - - a = MyObject() - assert MyObject.method_0 is not a.method_0 - assert a.method_0 is not a.method_1 - assert a.method_0.sigma.value == 1 - assert a.method_1.sigma.value == 2 - - -def test_call_count(): - """Test that a function gui remembers how many times it's been called.""" - - @magicgui - def func(): - pass - - assert func.call_count == 0 - func() - func() - assert func.call_count == 2 - func.reset_call_count() - assert func.call_count == 0 - - -def test_tooltips_from_numpydoc(): - """Test that numpydocs docstrings can be used for tooltips.""" - - x_tooltip = "override tooltip" - y_docstring = """A greeting, by default 'hi'. Notice how we miraculously pull -the entirety of the docstring just like that""" - - @magicgui(x={"tooltip": x_tooltip}, z={"tooltip": None}) - def func(x: int, y: str = "hi", z=None): - """Do a little thing. - - Parameters - ---------- - x : int - An integer for you to use - y : str, optional - A greeting, by default 'hi'. Notice how we miraculously pull - the entirety of the docstring just like that - z : Any, optional - No tooltip for me please. - """ - - assert func.x.tooltip == x_tooltip - assert func.y.tooltip == y_docstring - assert not func.z.tooltip - - -def test_bad_param_name_in_docstring(): - @magicgui - def func(x: int): - """Do a little thing. - - Parameters - ---------- - not_x: int - DESCRIPTION. - """ - return x - - assert not func.x.tooltip - - -def test_duplicated_and_missing_params_from_numpydoc(): - """Test that numpydocs docstrings can be used for tooltips.""" - - @magicgui - def func(x, y, z=None): - """Do a little thing. - - Parameters - ---------- - x, y : int - Integers for you to use - """ - - assert func.x.tooltip == "Integers for you to use" - assert func.y.tooltip == "Integers for you to use" - assert not func.z.tooltip - - -def test_tooltips_from_google_doc(): - """Test that google docstrings can be used for tooltips.""" - - x_docstring = "An integer for you to use" - y_docstring = """A greeting. Notice how we miraculously pull -the entirety of the docstring just like that""" - - @magicgui - def func(x: int, y: str = "hi"): - """Do a little thing. - - Args: - x (int): An integer for you to use - y (str, optional): A greeting. Notice how we miraculously pull - the entirety of the docstring just like that - """ - - assert func.x.tooltip == x_docstring - assert func.y.tooltip == y_docstring - - -def test_tooltips_from_rest_doc(): - """Test that google docstrings can be used for tooltips.""" - - x_docstring = "An integer for you to use" - y_docstring = """A greeting, by default 'hi'. Notice how we miraculously pull -the entirety of the docstring just like that""" - - @magicgui - def func(x: int, y: str = "hi", z=None): - """Do a little thing. - - :param x: An integer for you to use - :param y: A greeting, by default 'hi'. Notice how we miraculously pull - the entirety of the docstring just like that - :type x: int - :type y: str - """ - - assert func.x.tooltip == x_docstring - assert func.y.tooltip == y_docstring - - -def test_no_tooltips_from_numpydoc(): - """Test that ``tooltips=False`` hides all tooltips.""" - - @magicgui(tooltips=False) - def func(x: int, y: str = "hi"): - """Do a little thing. - - Parameters - ---------- - x : int - An integer for you to use - y : str, optional - A greeting, by default 'hi' - """ - - assert not func.x.tooltip - assert not func.y.tooltip - - -def test_only_some_tooltips_from_numpydoc(): - """Test that we can still show some tooltips with ``tooltips=False``.""" - # tooltips=False, means docstrings wont be parsed at all, but tooltips - # can still be manually provided. - @magicgui(tooltips=False, y={"tooltip": "Still want a tooltip"}) - def func(x: int, y: str = "hi"): - """Do a little thing. - - Parameters - ---------- - x : int - An integer for you to use - y : str, optional - A greeting, by default 'hi' - """ - - assert not func.x.tooltip - assert func.y.tooltip == "Still want a tooltip" - - -def test_magicgui_type_error(): - - with pytest.raises(TypeError): - magicgui("not a function") # type: ignore - - -def self_referencing_function(x: int = 1): - """Function that refers to itself, and wants the FunctionGui instance.""" - return self_referencing_function - - -def test_magicgui_self_reference(): - """Test that self-referential magicguis work in global scopes.""" - global self_referencing_function - f = magicgui(self_referencing_function) - assert isinstance(f(), widgets.FunctionGui) - assert f() is f - - -def test_local_magicgui_self_reference(): - """Test that self-referential magicguis work in local scopes.""" - - @magicgui - def local_self_referencing_function(x: int = 1): - """Function that refers to itself, and wants the FunctionGui instance.""" - return local_self_referencing_function - - assert isinstance(local_self_referencing_function(), widgets.FunctionGui) - - -def test_empty_function(): - """Test that a function with no params works.""" - - @magicgui(call_button=True) - def f(): - ... - - f.show() - - -def test_boolean_label(): - """Test that label can be used to set the text of a button widget.""" - - @magicgui(check={"label": "ABC"}) - def test(check: bool, x=1): - pass - - assert test.check.text == "ABC" - - with pytest.warns(UserWarning) as record: - - @magicgui(check={"text": "ABC", "label": "BCD"}) - def test2(check: bool, x=1): - pass - - assert "'text' and 'label' are synonymous for button widgets" in str(record[0]) - - -def test_none_defaults(): - """Make sure that an unannotated parameter with default=None is ok.""" - assert widgets.create_widget(value=None, raise_on_unknown=False).value is None - - def func(arg=None): - return 1 - - assert magicgui(func)() == 1 - - assert str(magic_signature(func)) == str(magicgui(func).__signature__) - - -def test_update_and_dict(): - @magicgui - def test(a: int = 1, y: str = "a"): - ... - - assert test.asdict() == {"a": 1, "y": "a"} - - test.update(a=10, y="b") - assert test.asdict() == {"a": 10, "y": "b"} - - test.update({"a": 1, "y": "a"}) - assert test.asdict() == {"a": 1, "y": "a"} - - test.update([("a", 10), ("y", "b")]) - assert test.asdict() == {"a": 10, "y": "b"} - - -def test_update_on_call(): - @magicgui - def test(a: int = 1, y: str = "a"): - ... - - assert test.call_count == 0 - test(a=10, y="b", update_widget=True) - assert test.a.value == 10 - assert test.y.value == "b" - assert test.call_count == 1 - - -def test_partial(): - from functools import partial - - def some_func(x: int, y: str) -> str: - return y + str(x) - - wdg = magicgui(partial(some_func, 1)) - assert len(wdg) == 2 # because of the call_button - assert isinstance(wdg.y, widgets.LineEdit) - assert not hasattr(wdg, "x") - assert wdg("sdf") == "sdf1" - assert wdg._callable_name == "some_func" - - wdg2 = magicgui(partial(some_func, y="sdf")) - assert len(wdg2) == 3 # keyword arguments don't change the partial signature - assert isinstance(wdg2.x, widgets.SpinBox) - assert isinstance(wdg.y, widgets.LineEdit) - assert wdg2.y.value == "sdf" - assert wdg2(1) == "sdf1" - - -def test_curry(): - import toolz as tz - - @tz.curry - def some_func2(x: int, y: str) -> str: - return y + str(x) - - wdg = magicgui(some_func2(1)) - assert len(wdg) == 2 # because of the call_button - assert isinstance(wdg.y, widgets.LineEdit) - assert not hasattr(wdg, "x") - assert wdg("sdf") == "sdf1" - assert wdg._callable_name == "some_func2" - - wdg2 = magicgui(some_func2(y="sdf")) - assert len(wdg2) == 3 # keyword arguments don't change the partial signature - assert isinstance(wdg2.x, widgets.SpinBox) - assert isinstance(wdg.y, widgets.LineEdit) - assert wdg2.y.value == "sdf" - assert wdg2(1) == "sdf1" - - -def test_scrollable(): - @magicgui(scrollable=True) - def test_scrollable(a: int = 1, y: str = "a"): - ... - - assert test_scrollable.native is not test_scrollable.root_native_widget - assert not isinstance(test_scrollable.native, QScrollArea) - assert isinstance(test_scrollable.root_native_widget, QScrollArea) - - @magicgui(scrollable=False) - def test_nonscrollable(a: int = 1, y: str = "a"): - ... - - assert test_nonscrollable.native is test_nonscrollable.root_native_widget - assert not isinstance(test_nonscrollable.native, QScrollArea) - - -def test_unknown_exception_magicgui(): - """Test that an unknown type is raised as a RuntimeError.""" - - class A: - pass - - with pytest.raises(ValueError, match="No widget found for type"): - - @magicgui(raise_on_unknown=True) - def func(a: A): - print(a) - - -def test_unknown_exception_create_widget(): - """Test that an unknown type is raised as a RuntimeError.""" - - class A: - pass - - with pytest.raises(ValueError, match="No widget found for type"): - widgets.create_widget(A, raise_on_unknown=True) - with pytest.raises(ValueError, match="No widget found for type"): - widgets.create_widget(A) - assert isinstance( - widgets.create_widget(A, raise_on_unknown=False), widgets.EmptyWidget - ) From fdd317b028a05f3827168dadc74dd2e780f9644e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 18 Nov 2022 10:58:09 -0500 Subject: [PATCH 3/5] bring back old tests --- tests/{test_backends => }/test_backends.py | 0 tests/test_backends/__init__.py | 3 --- tests/{test_backends => }/test_magicgui.py | 0 tests/{test_backends => }/test_widgets.py | 0 4 files changed, 3 deletions(-) rename tests/{test_backends => }/test_backends.py (100%) delete mode 100644 tests/test_backends/__init__.py rename tests/{test_backends => }/test_magicgui.py (100%) rename tests/{test_backends => }/test_widgets.py (100%) diff --git a/tests/test_backends/test_backends.py b/tests/test_backends.py similarity index 100% rename from tests/test_backends/test_backends.py rename to tests/test_backends.py diff --git a/tests/test_backends/__init__.py b/tests/test_backends/__init__.py deleted file mode 100644 index 95cb6ec6e..000000000 --- a/tests/test_backends/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest - -pytest.skip("No backend tests implemented yet", allow_module_level=True) diff --git a/tests/test_backends/test_magicgui.py b/tests/test_magicgui.py similarity index 100% rename from tests/test_backends/test_magicgui.py rename to tests/test_magicgui.py diff --git a/tests/test_backends/test_widgets.py b/tests/test_widgets.py similarity index 100% rename from tests/test_backends/test_widgets.py rename to tests/test_widgets.py From 02622d20e48318515e91280d307c04672394bc40 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 18 Nov 2022 11:10:28 -0500 Subject: [PATCH 4/5] test: add another test --- .pre-commit-config.yaml | 2 +- src/magicgui/tqdm.py | 2 +- tests/test_types.py | 2 +- tests/test_widget_bases.py | 20 +++++++++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f7b62a0b..1475ab68c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: black - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.118 + rev: v0.0.125 hooks: - id: ruff args: ["--fix"] diff --git a/src/magicgui/tqdm.py b/src/magicgui/tqdm.py index c43def391..ea7af6645 100644 --- a/src/magicgui/tqdm.py +++ b/src/magicgui/tqdm.py @@ -82,7 +82,7 @@ def __init__(self, iterable: Iterable | None = None, *args, **kwargs) -> None: return # no-op status printer, required for older tqdm compat - self.sp = lambda x: None # noqa: E731 + self.sp = lambda x: None if self.disable: return diff --git a/tests/test_types.py b/tests/test_types.py index ce82d2fd3..93445c323 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -111,7 +111,7 @@ def test_widget_options(): def test_nested_forward_refs(): - resolved = resolve_single_type(Optional['List["numpy.ndarray"]']) # noqa + resolved = resolve_single_type(Optional['List["numpy.ndarray"]']) from typing import List diff --git a/tests/test_widget_bases.py b/tests/test_widget_bases.py index bbe4a9a54..71b51f3da 100644 --- a/tests/test_widget_bases.py +++ b/tests/test_widget_bases.py @@ -1,6 +1,7 @@ # type: ignore from __future__ import annotations +import enum from typing import ForwardRef, TypeVar from unittest.mock import Mock, create_autospec @@ -108,9 +109,10 @@ def test_value_widget_protocol(mock_app): def test_value_widget_bind(mock_app): + widget = _mock_widget(widgets.bases.ValueWidget) + mock = Mock() mock.return_value = 3 - widget = _mock_widget(widgets.bases.ValueWidget) widget.bind(mock) mock.assert_not_called() assert widget.value == 3 @@ -126,3 +128,19 @@ def test_value_widget_events(mock_app): widget.value = 2 change_mock.assert_called_with(2) + + +def test_categorical_widget_events(mock_app): + class E(enum.Enum): + a = 1 + b = 2 + + widget = _mock_widget(widgets.bases.CategoricalWidget, choices=E) + widget._widget._mgui_get_choices.return_value = ("a", "b") + widget._widget._mgui_set_value.side_effect = widget._on_value_change + + change_mock = Mock() + widget.changed.connect(change_mock) + + widget.value = E.b + change_mock.assert_called_with(E.b) From d2e39dce1d99f73151d7ab191fe8a4d7e4b8e6d2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 18 Nov 2022 11:18:56 -0500 Subject: [PATCH 5/5] test: fix test --- tests/test_widget_bases.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_widget_bases.py b/tests/test_widget_bases.py index 71b51f3da..533bf8a6b 100644 --- a/tests/test_widget_bases.py +++ b/tests/test_widget_bases.py @@ -20,7 +20,10 @@ def _mock_widget(WidgetType: type[W], **kwargs) -> W: elif isinstance(_proto, (ForwardRef, str)): if isinstance(_proto, str): _proto = ForwardRef(_proto) - _proto = _proto._evaluate({"protocols": protocols}, None, frozenset()) + try: + _proto = _proto._evaluate({"protocols": protocols}, None, frozenset()) + except TypeError: + _proto = _proto._evaluate({"protocols": protocols}, None) backend_mock = create_autospec(_proto, spec_set=True) widget = WidgetType(widget_type=backend_mock, **kwargs) backend_mock.assert_called_once_with(parent=None) @@ -136,7 +139,7 @@ class E(enum.Enum): b = 2 widget = _mock_widget(widgets.bases.CategoricalWidget, choices=E) - widget._widget._mgui_get_choices.return_value = ("a", "b") + widget._widget._mgui_get_choices.return_value = (("a", E.a), ("b", E.b)) widget._widget._mgui_set_value.side_effect = widget._on_value_change change_mock = Mock()