Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use alternative to ANSI control sequences in Jupyter notebooks #195

Merged
merged 25 commits into from Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -13,7 +13,7 @@ pypi_pwd := $(shell grep password ~/.pypirc | awk -F"= " '{ print $$2 }')

flake:
@echo "$(OK_COLOR)==> Linting code ...$(NO_COLOR)"
@poetry run flake8 --ignore=F821,E501 .
@poetry run flake8 --ignore=F821,E501,W503 .
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W504 and W503 are contradictory


lint:
@echo "$(OK_COLOR)==> Linting code ...$(NO_COLOR)"
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Expand Up @@ -95,6 +95,16 @@ def reversal(request):
return request.param


@pytest.fixture(scope="session", params=[True, False], ids=["terminal", "jupyter"])
def isatty_fixture(request):
return request.param


@pytest.fixture(autouse=True)
def isatty_true(monkeypatch):
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
Comment on lines +103 to +105
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this doesn't correctly patch capsys so more monkeypatches are needed within test cases that use capsys. Do you have another idea?

However, this works fine for a bunch of tests not using capsys that would otherwise raise a warning. 👍



def color_id_func(case):
if isinstance(case, tuple):
color, _ = case
Expand Down
11 changes: 8 additions & 3 deletions tests/test_attrs.py
Expand Up @@ -5,6 +5,8 @@
Test Yaspin attributes magic hidden in __getattr__.
"""

import sys
pavdmyt marked this conversation as resolved.
Show resolved Hide resolved

import pytest

from yaspin import yaspin
Expand All @@ -19,12 +21,13 @@ def test_set_spinner_by_name(attr_name):


# Values for ``color`` argument
def test_color(color_test_cases):
def test_color(monkeypatch, color_test_cases):
color, expected = color_test_cases
# ``None`` and ``""`` are skipped
if not color:
pytest.skip("{0} - unsupported case".format(repr(color)))

monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
Comment on lines +24 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is an easier way to do this over and over?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to make this patch at the module level, like this:

@pytest.fixture(autouse=True)
def isatty_true(monkeypatch):
    monkeypatch.setattr(sys.stdout, "isatty", lambda: True)

From the docs: https://docs.pytest.org/en/6.2.x/monkeypatch.html#global-patch-example-preventing-requests-from-remote-operations

sp = yaspin()

if isinstance(expected, Exception):
Expand All @@ -37,12 +40,13 @@ def test_color(color_test_cases):


# Values for ``on_color`` argument
def test_on_color(on_color_test_cases):
def test_on_color(monkeypatch, on_color_test_cases):
on_color, expected = on_color_test_cases
# ``None`` and ``""`` are skipped
if not on_color:
pytest.skip("{0} - unsupported case".format(repr(on_color)))

monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
sp = yaspin()

if isinstance(expected, Exception):
Expand All @@ -60,7 +64,8 @@ def test_on_color(on_color_test_cases):
@pytest.mark.parametrize(
"attr", sorted([k for k, v in COLOR_MAP.items() if v == "attrs"])
)
def test_attrs(attr):
def test_attrs(monkeypatch, attr):
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
sp = yaspin()
getattr(sp, attr)
assert sp.attrs == [attr]
Expand Down
77 changes: 63 additions & 14 deletions tests/test_in_out.py
Expand Up @@ -75,22 +75,48 @@ def test_compose_out_with_color(
assert isinstance(out, str)


def test_write(capsys, text):
def test_color_jupyter(monkeypatch):
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
with pytest.warns(UserWarning):
sp = yaspin(color="red")

out = sp._compose_out(frame=u"/")
assert "\033" not in out


def test_write(monkeypatch, capsys, text, isatty_fixture):
monkeypatch.setattr(sys.stdout, "isatty", lambda: isatty_fixture)
sp = yaspin()
sp.write(text)

out, _ = capsys.readouterr()
# cleans stdout from _clear_line and \r
out = out.replace("\r\033[0K", "")
# cleans stdout from _clear_line
if isatty_fixture:
out = out.replace("\r\033[K", "")
else:
out = out.replace("\r", "")

assert isinstance(out, (str, bytes))
assert out[-1] == "\n"
if text:
assert out[:-1] == text


def test_hide_show(capsys, text, request):
def test_show_jupyter(monkeypatch, capsys):
monkeypatch.setattr(sys.stdout, "isatty", lambda: False)
with yaspin(text="12345") as sp:
sp.start()
sp.write("123")

out, _ = capsys.readouterr()
# check spinner line was correctly overridden with whitespaces
# r = \r, s = spinner char, w = space, 12345 = printed chars
assert "12345\r" + " " * len("rsw12345") + "\r123" in out


def test_hide_show(monkeypatch, capsys, text, request, isatty_fixture):
# Setup
monkeypatch.setattr(sys.stdout, "isatty", lambda: isatty_fixture)
sp = yaspin()
sp.start()

Expand All @@ -110,15 +136,21 @@ def teardown():
out, _ = capsys.readouterr()

# ensure that text was cleared with the hide method
assert out[-5:] == "\r\033[0K"
if isatty_fixture:
assert out[-4:] == "\r\033[K"
else:
assert out[-1:] == "\r"

# ``\n`` is required to flush stdout during
# the hidden state of the spinner
sys.stdout.write("{0}\n".format(text))
out, _ = capsys.readouterr()

# cleans stdout from _clear_line and \r
out = out.replace("\r\033[0K", "")
# cleans stdout from _clear_line
if isatty_fixture:
out = out.replace("\r\033[K", "")
else:
out = out.replace("\r", "")

assert isinstance(out, (str, bytes))
assert out[-1] == "\n"
Expand All @@ -132,7 +164,10 @@ def teardown():
out, _ = capsys.readouterr()

# ensure that text was cleared before resuming the spinner
assert out[:5] == "\r\033[0K"
if isatty_fixture:
assert out[:4] == "\r\033[K"
else:
assert out[:1] == "\r"


def test_spinner_write_race_condition(capsys):
Expand All @@ -154,9 +189,10 @@ def test_spinner_write_race_condition(capsys):
assert not re.search(r"aaaa[^\rb]*bbbb", out)


def test_spinner_hiding_with_context_manager(capsys):
def test_spinner_hiding_with_context_manager(monkeypatch, capsys, isatty_fixture):
HIDDEN_START = "hidden start"
HIDDEN_END = "hidden end"
monkeypatch.setattr(sys.stdout, "isatty", lambda: isatty_fixture)
sp = yaspin(text="foo")
sp.start()

Expand All @@ -174,13 +210,19 @@ def test_spinner_hiding_with_context_manager(capsys):

# make sure no spinner text was printed while the spinner was hidden
out, _ = capsys.readouterr()
out = out.replace("\r\033[0K", "")
if isatty_fixture:
out = out.replace("\r\033[K", "")
else:
out = out.replace("\r", "")
assert "{}\n{}".format(HIDDEN_START, HIDDEN_END) in out


def test_spinner_nested_hiding_with_context_manager(capsys):
def test_spinner_nested_hiding_with_context_manager(
monkeypatch, capsys, isatty_fixture
):
HIDDEN_START = "hidden start"
HIDDEN_END = "hidden end"
monkeypatch.setattr(sys.stdout, "isatty", lambda: isatty_fixture)
sp = yaspin(text="foo")
sp.start()

Expand All @@ -202,7 +244,10 @@ def test_spinner_nested_hiding_with_context_manager(capsys):

# make sure no spinner text was printed while the spinner was hidden
out, _ = capsys.readouterr()
out = out.replace("\r\033[0K", "")
if isatty_fixture:
out = out.replace("\r\033[K", "")
else:
out = out.replace("\r", "")
assert "{}\n{}".format(HIDDEN_START, HIDDEN_END) in out


Expand Down Expand Up @@ -234,9 +279,13 @@ def test_spinner_hiding_with_context_manager_and_exception():
(["foo", "bar", "'", 23], """['foo', 'bar', "'", 23]"""),
],
)
def test_write_non_str_objects(capsys, obj, obj_str):
def test_write_non_str_objects(monkeypatch, capsys, obj, obj_str, isatty_fixture):
monkeypatch.setattr(sys.stdout, "isatty", lambda: isatty_fixture)
sp = yaspin()
capsys.readouterr()
sp.write(obj)
out, _ = capsys.readouterr()
assert out == "\r\033[0K{}\n".format(obj_str)
if isatty_fixture:
assert out == "\r\033[K{}\n".format(obj_str)
else:
assert out == "\r\r{}\n".format(obj_str)
42 changes: 38 additions & 4 deletions yaspin/core.py
Expand Up @@ -16,6 +16,7 @@
import sys
import threading
import time
import warnings
from typing import List, Set, Union

from termcolor import colored
Expand Down Expand Up @@ -75,6 +76,7 @@ def __init__( # pylint: disable=too-many-arguments
self._last_frame = None
self._stdout_lock = threading.Lock()
self._hidden_level = 0
self._cur_line_len = 0

# Signals

Expand Down Expand Up @@ -308,6 +310,7 @@ def write(self, text):
assert isinstance(_text, str)

sys.stdout.write("{0}\n".format(_text))
self._cur_line_len = 0

def ok(self, text="OK"):
"""Set Ok (success) finalizer to a spinner."""
Expand All @@ -322,6 +325,13 @@ def fail(self, text="FAIL"):
#
# Protected
#
@staticmethod
def _warn_color_disabled():
warnings.warn(
"color, on_color and attrs are not supported when running in jupyter",
stacklevel=3,
)

def _freeze(self, final_text):
"""Stop spinner, compose last frame and 'freeze' it."""
text = to_unicode(final_text)
Expand All @@ -332,6 +342,7 @@ def _freeze(self, final_text):
self.stop()
with self._stdout_lock:
sys.stdout.write(self._last_frame)
self._cur_line_len = 0

def _spin(self):
while not self._stop_spin.is_set():
Expand All @@ -350,11 +361,16 @@ def _spin(self):
self._clear_line()
sys.stdout.write(out)
sys.stdout.flush()
self._cur_line_len = max(self._cur_line_len, len(out))

# Wait
self._stop_spin.wait(self._interval)

def _compose_color_func(self):
if self.is_jupyter():
# ANSI Color Control Sequences are problematic in Jupyter
return None
pavdmyt marked this conversation as resolved.
Show resolved Hide resolved

return functools.partial(
colored,
color=self._color,
Expand Down Expand Up @@ -428,8 +444,15 @@ def _reset_signal_handlers(self):
#
# Static
#
@staticmethod
def is_jupyter() -> bool:
pavdmyt marked this conversation as resolved.
Show resolved Hide resolved
return not sys.stdout.isatty()

@staticmethod
def _set_color(value: str) -> str:
if Yaspin.is_jupyter():
Yaspin._warn_color_disabled()

available_values = [k for k, v in COLOR_MAP.items() if v == "color"]
if value not in available_values:
raise ValueError(
Expand All @@ -441,6 +464,9 @@ def _set_color(value: str) -> str:

@staticmethod
def _set_on_color(value: str) -> str:
if Yaspin.is_jupyter():
Yaspin._warn_color_disabled()

available_values = [k for k, v in COLOR_MAP.items() if v == "on_color"]
if value not in available_values:
raise ValueError(
Expand All @@ -451,6 +477,9 @@ def _set_on_color(value: str) -> str:

@staticmethod
def _set_attrs(attrs: List[str]) -> Set[str]:
if Yaspin.is_jupyter():
Yaspin._warn_color_disabled()

available_values = [k for k, v in COLOR_MAP.items() if v == "attrs"]
for attr in attrs:
if attr not in available_values:
Expand Down Expand Up @@ -524,16 +553,21 @@ def _set_cycle(frames):
@staticmethod
def _hide_cursor():
if sys.stdout.isatty():
# ANSI Control Sequence DECTCEM 1 does not work in Jupyter
sys.stdout.write("\033[?25l")
sys.stdout.flush()

@staticmethod
def _show_cursor():
if sys.stdout.isatty():
# ANSI Control Sequence DECTCEM 2 does not work in Jupyter
sys.stdout.write("\033[?25h")
sys.stdout.flush()

@staticmethod
def _clear_line():
sys.stdout.write("\r")
sys.stdout.write("\033[0K")
def _clear_line(self):
if sys.stdout.isatty():
# ANSI Control Sequence EL does not work in Jupyter
sys.stdout.write("\r\033[K")
else:
fill = " " * self._cur_line_len
pavdmyt marked this conversation as resolved.
Show resolved Hide resolved
sys.stdout.write("\r{0}\r".format(fill))