File: test_magicgui.py

package info (click to toggle)
magicgui 0.9.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 21,796 kB
  • sloc: python: 11,202; makefile: 11; sh: 9
file content (942 lines) | stat: -rw-r--r-- 25,762 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
#!/usr/bin/env python

"""Tests for `magicgui` package."""

import inspect
from enum import Enum
from typing import NewType, Optional, Union
from unittest.mock import Mock

import pytest
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QScrollArea, QWidget

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):
        _ = 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 widget 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)


def test_parent_changed(qtbot, magic_func: widgets.FunctionGui) -> None:
    """Test that setting a backend parent emits a signal."""
    wdg = QWidget()
    qtbot.addWidget(wdg)

    mock = Mock()
    magic_func.native_parent_changed.connect(mock)
    with qtbot.waitSignal(magic_func.native_parent_changed, timeout=1000):
        magic_func.native.setParent(wdg)

    # the backend parent is emitted
    mock.assert_called_once_with(wdg)

    with qtbot.waitSignal(magic_func.native_parent_changed, timeout=1000):
        magic_func.native.setParent(None)

    mock.assert_called_with(None)

    with pytest.warns(FutureWarning, match="'parent_changed' signal has been renamed"):
        magic_func.parent_changed  # noqa: B018


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 won't 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
    )


@pytest.mark.parametrize("optional", [True, False])
def test_call_union_return_type(optional: bool):
    """registering Optional[type] should imply registering"""
    mock = Mock()

    NewInt = NewType("NewInt", int)
    register_type(Optional[NewInt], return_callback=mock)

    ReturnType = Optional[NewInt] if optional else NewInt

    @magicgui
    def func_optional(a: bool) -> ReturnType:
        return NewInt(1) if a else None

    func_optional(a=True)
    mock.assert_called_once_with(func_optional, 1, ReturnType)
    mock.reset_mock()
    func_optional(a=False)
    mock.assert_called_once_with(func_optional, None, ReturnType)


@pytest.mark.parametrize("optional", [True, False])
def test_no_duplication_call(optional):
    mock = Mock()
    mock2 = Mock()

    NewInt = NewType("NewInt", int)
    register_type(Optional[NewInt], return_callback=mock)
    register_type(NewInt, return_callback=mock)
    register_type(NewInt, return_callback=mock2)
    ReturnType = Optional[NewInt] if optional else NewInt

    @magicgui
    def func() -> ReturnType:
        return NewInt(1)

    func()

    mock.assert_called_once()
    assert mock2.call_count == (not optional)


def test_no_order():
    mock = Mock()

    register_type(Union[int, None], return_callback=mock)

    @magicgui
    def func() -> Union[None, int]:
        return 1

    func()
    mock.assert_called_once()