Skip to content

Commit

Permalink
Improve fallback behavior of tqdm iterator inside of *hidden* magicgu…
Browse files Browse the repository at this point in the history
…i widget (#131)
  • Loading branch information
tlambert03 committed Jan 24, 2021
1 parent 7f51aef commit 0a6961e
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 27 deletions.
2 changes: 1 addition & 1 deletion examples/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# If use inside of a magicgui-decorated function
# a progress bar widget will be added to the magicgui container
@magicgui(call_button=True)
@magicgui(call_button=True, layout="horizontal")
def long_running(steps=10, delay=0.1):
"""Long running computation with range iterator."""
# trange(steps) is a shortcut for `tqdm(range(steps))`
Expand Down
25 changes: 17 additions & 8 deletions magicgui/tqdm.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""A wrapper around the tqdm.tqdm iterator that adds a ProgressBar to a magicgui."""
import inspect
from typing import Iterable, Optional
from typing import Iterable, Optional, cast

from magicgui.application import use_app
from magicgui.widgets import FunctionGui, ProgressBar

try:
from tqdm import tqdm as tqdm_std
from tqdm import tqdm as _tqdm_std
except ImportError as e: # pragma: no cover
msg = (
f"{e}. To use magicgui with tqdm please `pip install tqdm`, "
Expand Down Expand Up @@ -35,12 +35,12 @@ def _find_calling_function_gui(max_depth=6) -> Optional[FunctionGui]:

_tqdm_kwargs = {
p.name
for p in inspect.signature(tqdm_std.__init__).parameters.values()
for p in inspect.signature(_tqdm_std.__init__).parameters.values()
if p.kind is not inspect.Parameter.VAR_KEYWORD and p.name != "self"
}


class tqdm(tqdm_std):
class tqdm(_tqdm_std):
"""magicgui version of tqdm.
See tqdm.tqdm API for valid args and kwargs: https://tqdm.github.io/docs/tqdm/
Expand Down Expand Up @@ -74,11 +74,11 @@ def __init__(self, iterable: Iterable = None, *args, **kwargs) -> None:
kwargs = kwargs.copy()
pbar_kwargs = {k: kwargs.pop(k) for k in set(kwargs) - _tqdm_kwargs}
self._mgui = _find_calling_function_gui()
if self._mgui is not None:
if self._in_visible_gui:
kwargs["gui"] = True
kwargs.setdefault("mininterval", 0.025)
super().__init__(iterable, *args, **kwargs)
if self._mgui is None:
if not self._in_visible_gui:
return

self.sp = lambda x: None # no-op status printer, required for older tqdm compat
Expand All @@ -98,6 +98,13 @@ def __init__(self, iterable: Iterable = None, *args, **kwargs) -> None:
self.progressbar.range = (0, 0)
self.progressbar.show()

@property
def _in_visible_gui(self) -> bool:
try:
return self._mgui is not None and self._mgui.visible
except RuntimeError:
return False

def _get_progressbar(self, **kwargs) -> ProgressBar:
"""Create ProgressBar or get from the parent gui `_tqdm_pbars` deque.
Expand All @@ -122,16 +129,18 @@ def _get_progressbar(self, **kwargs) -> ProgressBar:

def display(self, msg: str = None, pos: int = None) -> None:
"""Update the display."""
if self._mgui is None:
if not self._in_visible_gui:
return super().display(msg=msg, pos=pos)

self.progressbar.value = self.n
self._app.process_events()

def close(self) -> None:
"""Cleanup and (if leave=False) close the progressbar."""
if self._mgui is None:
if not self._in_visible_gui:
return super().close()
self._mgui = cast(FunctionGui, self._mgui)

if self.disable:
return

Expand Down
2 changes: 0 additions & 2 deletions magicgui/widgets/_function_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ def __init__(
name: str = None,
**kwargs,
):
print("FG, visible", visible)
if not callable(function):
raise TypeError("'function' argument to FunctionGui must be callable.")

Expand Down Expand Up @@ -362,7 +361,6 @@ class MainFunctionGui(FunctionGui[_R], MainWindow):
_widget: MainWindowProtocol

def __init__(self, function: Callable, *args, **kwargs):
print(kwargs)
super().__init__(function, *args, **kwargs)
self.create_menu_item("Help", "Documentation", callback=self._show_docs)
self._help_text_edit: Optional[TextEdit] = None
Expand Down
60 changes: 44 additions & 16 deletions tests/test_tqdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def f2():
pass
assert pbar2.progressbar.visible is False

f2.show()
f2()


Expand All @@ -60,7 +61,44 @@ def f():
# undefined range will render as a "busy" indicator
assert pbar.progressbar.range == (0, 0)

f.show()
f()


def test_tqdm_std_err(capsys):
"""Test that tqdm inside an invisible magicgui falls back to console behavior."""

# outside of a magicgui it falls back to tqdm_std behavior
with tqdm(range(10)) as t_obj:
iter(t_obj)
captured = capsys.readouterr()

assert "0%|" in str(captured.err)
assert "| 0/10" in str(captured.err)
assert not str(captured.out)


def test_tqdm_in_visible_mgui_std_err(capsys):
"""Test that tqdm inside an mgui outputs to console only when mgui not visible."""

# inside of a of a magicgui it falls back to tqdm_std behavior
@magicgui
def f():
with tqdm(range(10)) as t_obj:
iter(t_obj)

f()
captured = capsys.readouterr()
assert "0%|" in str(captured.err)
assert "| 0/10" in str(captured.err)
assert not str(captured.out)

# showing the widget disables the output to console behavior
f.show()
f()
captured = capsys.readouterr()
assert not str(captured.err)
assert not str(captured.out)


# Test various ways that tqdm might need to traverse the frame stack:
Expand All @@ -75,6 +113,7 @@ def long_func(steps=2):
sleep(0.02)

# before calling the function, we won't have any progress bars
long_func.show()
assert not long_func._tqdm_pbars
long_func()
# after calling the it, we should now have a progress bars
Expand All @@ -92,34 +131,21 @@ def long_func(steps=2):
for i in trange(4):
pass

long_func.show()
assert not long_func._tqdm_pbars
long_func()
assert len(long_func._tqdm_pbars) == 1


@magicgui
def directly_decorated(steps=2):
for i in trange(4):
pass


def test_trange_inside_of_global_magicgui():
"""Test that trange can find the magicgui within which it is called."""
assert not directly_decorated._tqdm_pbars
directly_decorated()
assert len(directly_decorated._tqdm_pbars) == 1


def _indirectly_decorated(steps=2):
for i in trange(4):
pass


indirectly_decorated = magicgui(_indirectly_decorated)


def test_trange_inside_of_indirectly_decorated_magicgui():
"""Test that trange can find the magicgui within which it is called."""
indirectly_decorated = magicgui(_indirectly_decorated)
indirectly_decorated.show()
assert not indirectly_decorated._tqdm_pbars
indirectly_decorated()
assert len(indirectly_decorated._tqdm_pbars) == 1
Expand All @@ -134,6 +160,7 @@ def long_func():
for x in trange(4):
pass

long_func.show()
# before calling the function, we won't have any progress bars
assert not long_func._tqdm_pbars
long_func()
Expand All @@ -150,6 +177,7 @@ def long_func2():
for x in trange(4):
pass

long_func2.show()
# before calling the function, we won't have any progress bars
assert not long_func2._tqdm_pbars
long_func2()
Expand Down

0 comments on commit 0a6961e

Please sign in to comment.