Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion audb/core/shimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import contextlib
import math
import os
import shutil
import sys
import threading
Expand All @@ -26,6 +27,33 @@
_active_shimmer: Shimmer | None = None


def animations_enabled() -> bool:
r"""If the environment allows terminal animations.

Terminal animations rely on ANSI escape sequences,
which end up as garbage in log files
when the output is captured,
e.g. by ``screen -L`` or ``script``.
Since a capturing pseudo-terminal is indistinguishable
from an interactive one,
users can disable animations explicitly
through their environment.
Animations are disabled if

* ``NO_COLOR`` is set to a non-empty value
* ``TERM`` is set to ``dumb``

Returns:
``True`` if terminal animations are allowed

"""
if os.environ.get("NO_COLOR", ""):
return False
if os.environ.get("TERM", "") == "dumb":
return False
return True


class Shimmer:
"""Animate text with a bright shimmer sweeping across.

Expand Down Expand Up @@ -80,14 +108,25 @@ def start(self):

* ``sys.stdout`` is not a TTY
(e.g. redirected output, Jupyter, CI logs).
* The environment disables animations
via ``NO_COLOR`` or ``TERM=dumb``,
see :func:`animations_enabled`.
* Another ``Shimmer`` instance is already active
(only one may run at a time).

In all those cases the static text
is still printed once.

"""
global _active_shimmer

# Skip animation in non-interactive environments
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
# and when disabled via NO_COLOR or TERM=dumb
if (
not hasattr(sys.stdout, "isatty")
or not sys.stdout.isatty()
or not animations_enabled()
):
sys.stdout.write(f"{self._prefix}{self._text}{self._suffix}\n")
sys.stdout.flush()
self._noop = True
Expand Down Expand Up @@ -286,6 +325,12 @@ def shimmer(
``start()`` / ``try`` / ``finally`` / ``stop()`` dance.
The animation is always stopped on exit,
including when the block raises.
Independent of ``enabled``,
the animation is skipped
(but the static text still printed)
in non-interactive environments
and when disabled via ``NO_COLOR`` or ``TERM=dumb``,
see :meth:`Shimmer.start`.

Args:
prefix: static text before the animated portion
Expand Down
73 changes: 73 additions & 0 deletions tests/test_shimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys

import numpy as np
import pytest

import audeer
import audformat
Expand All @@ -14,6 +15,22 @@
from audb.core.shimmer import NORMAL
from audb.core.shimmer import RESET
from audb.core.shimmer import Shimmer
from audb.core.shimmer import animations_enabled


@pytest.fixture(autouse=True)
def enable_animations(monkeypatch):
r"""Neutralize animation-disabling environment variables.

The tests below control animation behavior explicitly
(by monkey-patching ``isatty`` or setting environment variables),
so the environment the test runner happens to execute in
(e.g. CI with ``NO_COLOR`` or ``TERM=dumb``)
must not interfere.

"""
monkeypatch.delenv("NO_COLOR", raising=False)
monkeypatch.setenv("TERM", "xterm-256color")


def test_stdout_write_hook():
Expand Down Expand Up @@ -127,6 +144,62 @@ def test_noop_when_not_a_tty(monkeypatch):
shimmer.stop()


def test_animations_enabled(monkeypatch):
"""Environment variables controlling terminal animations."""
# Clean environment (see enable_animations fixture): allowed
assert animations_enabled() is True
Comment thread
hagenw marked this conversation as resolved.

# NO_COLOR set to an empty string counts as unset,
# following the https://no-color.org convention
monkeypatch.setenv("NO_COLOR", "")
assert animations_enabled() is True

# NO_COLOR set to any non-empty value disables animations
monkeypatch.setenv("NO_COLOR", "1")
assert animations_enabled() is False

# TERM=dumb disables animations
monkeypatch.delenv("NO_COLOR")
monkeypatch.setenv("TERM", "dumb")
assert animations_enabled() is False

# Other TERM values keep animations enabled
monkeypatch.setenv("TERM", "screen-256color")
assert animations_enabled() is True


@pytest.mark.parametrize(
"env_var, value",
[
("NO_COLOR", "1"),
("TERM", "dumb"),
],
)
def test_noop_when_animations_disabled(capsys, monkeypatch, env_var, value):
"""Shimmer becomes a no-op when the environment disables animations.

Even on a real TTY (e.g. a pseudo-terminal created by ``screen -L``,
whose log file would otherwise capture the escape sequences),
setting ``NO_COLOR`` or ``TERM=dumb`` must skip the animation,
while the static text is still printed once.

"""
monkeypatch.setattr(sys.stdout, "isatty", lambda: True)
monkeypatch.setenv(env_var, value)
original_write = sys.stdout.write

shimmer = Shimmer("Get: ", "db v1.0.0")
shimmer.start()
# Should not have patched stdout: no instance-level override installed.
assert "write" not in vars(sys.stdout)
assert sys.stdout.write == original_write
assert shimmer._noop is True
shimmer.stop()

# The static line is printed exactly once, without escape codes
assert capsys.readouterr().out == "Get: db v1.0.0\n"


def test_restores_pre_existing_instance_write(monkeypatch):
"""A pre-existing instance-level ``write`` is restored, not deleted.

Expand Down
Loading