Skip to content

Commit

Permalink
Improve test coverage (#70)
Browse files Browse the repository at this point in the history
* fix parent_changed signal emission

* remove useless test

* increasing coverage, wip

* more test coverage

* fix options pop

* skip events in coverage

* Revert "Merge branch 'fix-parent-signal' into improve-coverage"

This reverts commit c39debe, reversing
changes made to f4a9521.

* skip cov on tests/*

* don't assert specific container margins
  • Loading branch information
tlambert03 committed Dec 27, 2020
1 parent c441861 commit 002f973
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
\.\.\.
omit =
magicgui/events.py
tests/*
5 changes: 3 additions & 2 deletions magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _mgui_get_parent(self):
return self._qwidget.parent()

def _mgui_set_parent(self, widget: Widget):
self._qwidget.setParent(widget.native)
self._qwidget.setParent(widget.native if widget else None)

def _mgui_get_native_widget(self) -> QtW.QWidget:
return self._qwidget
Expand Down Expand Up @@ -240,7 +240,8 @@ def __init__(self, orientation="vertical"):
self._qwidget.setLayout(self._layout)

def _mgui_get_margins(self) -> Tuple[int, int, int, int]:
return self._layout.contentsMargins()
m = self._layout.contentsMargins()
return m.left(), m.top(), m.right(), m.bottom()

def _mgui_set_margins(self, margins: Tuple[int, int, int, int]) -> None:
self._layout.setContentsMargins(*margins)
Expand Down
10 changes: 7 additions & 3 deletions magicgui/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,13 @@ def get_widget_class(
else:
widget_class = widget_type

assert isinstance(widget_class, WidgetProtocol) or _is_subclass(
widget_class, widgets._bases.Widget
)
if not (
isinstance(widget_class, WidgetProtocol)
or _is_subclass(widget_class, widgets._bases.Widget)
):
raise TypeError(
f"{widget_class!r} does not implement any known widget protocols"
)
return widget_class, _options


Expand Down
20 changes: 11 additions & 9 deletions magicgui/widgets/_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def create_widget(
_app = use_app(kwargs.pop("app"))
assert _app.native
if isinstance(widget_type, _protocols.WidgetProtocol):
wdg_class = widget_type
wdg_class = kwargs.pop("widget_type")
else:
from magicgui.type_map import get_widget_class

Expand All @@ -161,9 +161,8 @@ def create_widget(
for p in ("Categorical", "Ranged", "Button", "Value", ""):
prot = getattr(_protocols, f"{p}WidgetProtocol")
if isinstance(wdg_class, prot):
widget = globals()[f"{p}Widget"](
widget_type=wdg_class, **kwargs, **kwargs.pop("options")
)
options = kwargs.pop("options", {})
widget = globals()[f"{p}Widget"](widget_type=wdg_class, **kwargs, **options)
if _kind:
widget.param_kind = _kind
return widget
Expand Down Expand Up @@ -430,10 +429,13 @@ def value(self, value):

def __repr__(self) -> str:
"""Return representation of widget of instsance."""
return (
f"{self.widget_type}(value={self.value!r}, annotation={self.annotation!r}, "
f"name={self.name!r})"
)
if hasattr(self, "_widget"):
return (
f"{self.widget_type}(value={self.value!r}, "
f"annotation={self.annotation!r}, name={self.name!r})"
)
else:
return f"<Uninitialized {self.widget_type}>"


class ButtonWidget(ValueWidget):
Expand Down Expand Up @@ -914,7 +916,7 @@ def insert(self, key: int, widget: Widget):
def _unify_label_widths(self, event=None):
if not self._initialized:
return
if self.orientation == "vertical" and self.labels:
if self.orientation == "vertical" and self.labels and len(self):
measure = use_app().get_obj("get_text_width")
widest_label = max(
measure(w.label) for w in self if not isinstance(w, ButtonWidget)
Expand Down
49 changes: 49 additions & 0 deletions tests/test_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pytest
from typing_extensions import Annotated

from magicgui.signature import magic_signature, make_annotated, split_annotated_type


def test_make_annotated_raises():
"""Test options to annotated must be a dict."""
with pytest.raises(TypeError):
make_annotated(int, "not a dict") # type: ignore


def test_make_annotated_works_with_already_annotated():
"""Test that make_annotated merges options with Annotated types."""
annotated_type = Annotated[int, {"max": 10}] # type: ignore
assert make_annotated(annotated_type) == annotated_type
assert (
make_annotated(annotated_type, {"min": 1})
== Annotated[int, {"max": 10, "min": 1}]
)


def test_split_annotated_raises():
"""Test split_annotated raises on bad input."""
with pytest.raises(TypeError):
split_annotated_type(int)

with pytest.raises(TypeError):
split_annotated_type(Annotated[int, 1])


def _sample_func(a: int, b: str = "hi"):
pass


def test_magic_signature_raises():
"""Test that gui_options must have keys that are params in function."""
with pytest.raises(ValueError):
magic_signature(_sample_func, gui_options={"not_a_param": {"choices": []}})


def test_signature_to_container():
"""Test that a MagicSignature can make a container."""
sig = magic_signature(_sample_func, gui_options={"a": {"widget_type": "Slider"}})
container = sig.to_container()
assert len(container) == 2
assert repr(container) == "<Container (a: int = 0, b: str = 'hi')>"
assert repr(container.a) == "Slider(value=0, annotation=<class 'int'>, name='a')"
assert repr(sig.parameters["a"]) == '<MagicParameter "a: int" {}>'
159 changes: 158 additions & 1 deletion tests/test_widgets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import inspect
from unittest.mock import MagicMock

import pytest
from tests import MyInt

from magicgui import magicgui, widgets
from magicgui import magicgui, use_app, widgets
from magicgui.widgets._bases import ValueWidget


@pytest.mark.parametrize(
Expand All @@ -12,6 +16,68 @@ def test_widgets(WidgetClass):
_ = WidgetClass()


expectations = (
[{"value": 1}, widgets.SpinBox],
[{"value": 1.0}, widgets.FloatSpinBox],
[{"value": "hi"}, widgets.LineEdit],
[{"value": "a", "options": {"choices": ["a", "b"]}}, widgets.Combobox],
[{"value": 1, "widget_type": "Slider"}, widgets.Slider],
)


@pytest.mark.parametrize("kwargs, expect_type", expectations)
def test_create_widget(kwargs, expect_type):
"""Test that various values get turned into widgets."""
assert isinstance(widgets.create_widget(**kwargs), expect_type)


# fmt: off
class MyBadWidget:
"""INCOMPLETE widget implementation and will error."""

def _mgui_hide_widget(self): ... # noqa
def _mgui_get_enabled(self): ... # noqa
def _mgui_set_enabled(self, enabled): ... # noqa
def _mgui_get_parent(self): ... # noqa
def _mgui_set_parent(self, widget): ... # noqa
def _mgui_get_native_widget(self): return MagicMock() # noqa
def _mgui_bind_parent_change_callback(self, callback): ... # noqa
def _mgui_render(self): ... # noqa
def _mgui_get_width(self): ... # noqa
def _mgui_set_min_width(self, value: int): ... # noqa
def _mgui_get_value(self): ... # noqa
def _mgui_set_value(self, value): ... # noqa
def _mgui_bind_change_callback(self, callback): ... # noqa


class MyValueWidget(MyBadWidget):
"""Complete protocol implementation... should work."""

def _mgui_show_widget(self): ... # noqa
# fmt: on


def test_custom_widget():
"""Test that create_widget works with arbitrary backend implementations."""
# by implementing the ValueWidgetProtocol, magicgui will know to wrap the above
# widget with a widgets._bases.ValueWidget
assert isinstance(widgets.create_widget(1, widget_type=MyValueWidget), ValueWidget)


def test_custom_widget_fails():
"""Test that create_widget works with arbitrary backend implementations."""
with pytest.raises(TypeError) as err:
widgets.create_widget(1, widget_type=MyBadWidget) # type: ignore
assert "does not implement any known widget protocols" in str(err)


def test_extra_kwargs_warn():
"""Test that unrecognized kwargs gives a FutureWarning."""
with pytest.warns(FutureWarning) as wrn:
widgets.Label(unknown_kwarg="hi")
assert "unexpected keyword arguments" in str(wrn[0].message)


def test_autocall_no_runtime_error():
"""Make sure changing a value doesn't cause an autocall infinite loop."""

Expand All @@ -22,6 +88,97 @@ def func(input=1):
func.input.value = 2


def test_basic_widget_attributes():
"""Basic test coverage for getting/setting attributes."""
widget = widgets.create_widget(value=1, name="my_name")
container = widgets.Container(labels=False)
assert widget.enabled
widget.enabled = False
assert not widget.enabled

assert widget.visible
widget.visible = False
assert not widget.visible

assert widget.parent is None
container.append(widget)
assert widget.parent is container.native
widget.parent = None
assert widget.parent is None
assert widget.label == "my name"
widget.label = "A different label"
assert widget.label == "A different label"
assert widget.width > 200
widget.width = 150
assert widget.width == 150

assert widget.param_kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
widget.param_kind = inspect.Parameter.KEYWORD_ONLY
widget.param_kind = "positional_only"
assert widget.param_kind == inspect.Parameter.POSITIONAL_ONLY
with pytest.raises(KeyError):
widget.param_kind = "not a proper param type"
with pytest.raises(TypeError):
widget.param_kind = 1

assert repr(widget) == "SpinBox(value=1, annotation=None, name='my_name')"
assert widget.options == {"max": 100, "min": 0, "step": 1, "visible": False}


def test_container_widget():
"""Test basic container functionality."""
container = widgets.Container(labels=False)
labela = widgets.Label(value="hi", name="labela")
labelb = widgets.Label(value="hi", name="labelb")
container.append(labela)
container.extend([labelb])
# different ways to index
assert container[0] == labela
assert container["labelb"] == labelb
assert container[:1] == [labela]
assert container[-1] == labelb

with pytest.raises(NotImplementedError):
container[0] = "something"

assert container.orientation == "horizontal"
with pytest.raises(NotImplementedError):
container.orientation = "vertical"

assert all(x in dir(container) for x in ["labela", "labelb"])

assert container.margins
container.margins = (8, 8, 8, 8)
assert container.margins == (8, 8, 8, 8)

del container[1:]
del container[-1]
assert not container

if use_app().backend_name == "qt":
assert container.native_layout.__class__.__name__ == "QHBoxLayout"


def test_container_label_widths():
"""Test basic container functionality."""
container = widgets.Container(orientation="vertical")
labela = widgets.Label(value="hi", name="labela")
labelb = widgets.Label(value="hi", name="I have a very long label")

def _label_width():
measure = use_app().get_obj("get_text_width")
return max(
measure(w.label)
for w in container
if not isinstance(w, widgets._bases.ButtonWidget)
)

container.append(labela)
before = _label_width()
container.append(labelb)
assert _label_width() > before


def test_delete_widget():
"""We can delete widgets from containers."""
a = widgets.Label(name="a")
Expand Down

0 comments on commit 002f973

Please sign in to comment.