diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 37db4643..b9425011 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,7 +20,6 @@ repos:
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- - id: python-check-mock-methods
- id: python-no-log-warn
- id: python-use-type-annotations
- id: rst-directive-colons
diff --git a/docs/source/console.rst b/docs/source/console.rst
index 5c0aeb32..b924f648 100644
--- a/docs/source/console.rst
+++ b/docs/source/console.rst
@@ -420,11 +420,15 @@ Rich respects some standard environment variables.
Setting the environment variable ``TERM`` to ``"dumb"`` or ``"unknown"`` will disable color/style and some features that require moving the cursor, such as progress bars.
-If the environment variable ``FORCE_COLOR`` is set, then color/styles will be enabled regardless of the value of ``TERM``. This is useful on CI systems which aren't terminals but can none-the-less display ANSI escape sequences.
+If the environment variable ``FORCE_COLOR`` is set and non-empty, then color/styles will be enabled regardless of the value of ``TERM``.
-If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output. This takes precedence over ``FORCE_COLOR``. See `no_color `_ for details.
+If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output. ``NO_COLOR`` takes precedence over ``FORCE_COLOR``. See `no_color `_ for details.
.. note::
The ``NO_COLOR`` environment variable removes *color* only. Styles such as dim, bold, italic, underline etc. are preserved.
-If ``width`` / ``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS``/``LINES`` can be used to set the console width/height. ``JUPYTER_COLUMNS``/``JUPYTER_LINES`` behave similarly and are used in Jupyter.
+The environment variable ``TTY_COMPATIBLE`` is used to override Rich's auto-detection of terminal support. If ``TTY_COMPATIBLE`` is set to ``1`` then rich will assume it is writing to a terminal (or a device that can handle escape sequences). If ``TTY_COMPATIBLE`` is set to ``"0"``, then Rich will assume that it is not writing to a terminal. If the variable is not set, or any other value, then Rich will attempt to auto-detect terminal support. If you want Rich output in CI or Github Actions, then you should set ``TTY_COMPATIBLE=1``.
+
+Note that these variable set the default behavior. If you explicitly set ``force_terminal`` in the Console constructor, then this will take precedence over the environment variable.
+
+If ``width`` / ``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS`` / ``LINES`` can be used to set the console width / height. ``JUPYTER_COLUMNS`` / ``JUPYTER_LINES`` behave similarly and are used in Jupyter.
diff --git a/rich/console.py b/rich/console.py
index 3ec9a8aa..fb311593 100644
--- a/rich/console.py
+++ b/rich/console.py
@@ -500,7 +500,7 @@ def group(fit: bool = True) -> Callable[..., Callable[..., Group]]:
"""
def decorator(
- method: Callable[..., Iterable[RenderableType]]
+ method: Callable[..., Iterable[RenderableType]],
) -> Callable[..., Group]:
"""Convert a method that returns an iterable of renderables in to a Group."""
@@ -933,11 +933,13 @@ class Console:
Returns:
bool: True if the console writing to a device capable of
- understanding terminal codes, otherwise False.
+ understanding escape sequences, otherwise False.
"""
+ # If dev has explicitly set this value, return it
if self._force_terminal is not None:
return self._force_terminal
+ # Fudge for Idle
if hasattr(sys.stdin, "__module__") and sys.stdin.__module__.startswith(
"idlelib"
):
@@ -948,12 +950,22 @@ class Console:
# return False for Jupyter, which may have FORCE_COLOR set
return False
- # If FORCE_COLOR env var has any value at all, we assume a terminal.
- force_color = self._environ.get("FORCE_COLOR")
- if force_color is not None:
- self._force_terminal = True
+ environ = self._environ
+
+ tty_compatible = environ.get("TTY_COMPATIBLE", "")
+ # 0 indicates device is not tty compatible
+ if tty_compatible == "0":
+ return False
+ # 1 indicates device is tty compatible
+ if tty_compatible == "1":
return True
+ # https://force-color.org/
+ force_color = environ.get("FORCE_COLOR")
+ if force_color is not None:
+ return force_color != ""
+
+ # Any other value defaults to auto detect
isatty: Optional[Callable[[], bool]] = getattr(self.file, "isatty", None)
try:
return False if isatty is None else isatty()
diff --git a/tests/test_console.py b/tests/test_console.py
index 407a138e..206dbdd1 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -34,7 +34,7 @@ from rich.text import Text
os.get_terminal_size
-def test_dumb_terminal():
+def test_dumb_terminal() -> None:
console = Console(force_terminal=True, _environ={})
assert console.color_system is not None
@@ -45,14 +45,14 @@ def test_dumb_terminal():
assert height == 25
-def test_soft_wrap():
+def test_soft_wrap() -> None:
console = Console(file=io.StringIO(), width=20, soft_wrap=True)
console.print("foo " * 10)
assert console.file.getvalue() == "foo " * 20
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_16color_terminal():
+def test_16color_terminal() -> None:
console = Console(
force_terminal=True, _environ={"TERM": "xterm-16color"}, legacy_windows=False
)
@@ -60,7 +60,7 @@ def test_16color_terminal():
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_truecolor_terminal():
+def test_truecolor_terminal() -> None:
console = Console(
force_terminal=True,
legacy_windows=False,
@@ -70,7 +70,7 @@ def test_truecolor_terminal():
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_kitty_terminal():
+def test_kitty_terminal() -> None:
console = Console(
force_terminal=True,
legacy_windows=False,
@@ -79,7 +79,7 @@ def test_kitty_terminal():
assert console.color_system == "256"
-def test_console_options_update():
+def test_console_options_update() -> None:
options = ConsoleOptions(
ConsoleDimensions(80, 25),
max_height=25,
@@ -103,7 +103,7 @@ def test_console_options_update():
assert options_copy == options and options_copy is not options
-def test_console_options_update_height():
+def test_console_options_update_height() -> None:
options = ConsoleOptions(
ConsoleDimensions(80, 25),
max_height=25,
@@ -120,7 +120,7 @@ def test_console_options_update_height():
assert render_options.max_height == 12
-def test_init():
+def test_init() -> None:
console = Console(color_system=None)
assert console._color_system == None
console = Console(color_system="standard")
@@ -128,7 +128,7 @@ def test_init():
console = Console(color_system="auto")
-def test_size():
+def test_size() -> None:
console = Console()
w, h = console.size
assert console.width == w
@@ -180,37 +180,37 @@ def test_size_can_fall_back_to_std_descriptors(
assert (w, h) == expected_size
-def test_repr():
+def test_repr() -> None:
console = Console()
assert isinstance(repr(console), str)
assert isinstance(str(console), str)
-def test_print():
+def test_print() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print("foo")
assert console.file.getvalue() == "foo\n"
-def test_print_multiple():
+def test_print_multiple() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print("foo", "bar")
assert console.file.getvalue() == "foo bar\n"
-def test_print_text():
+def test_print_text() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print(Text("foo", style="bold"))
- assert console.file.getvalue() == "\x1B[1mfoo\x1B[0m\n"
+ assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m\n"
-def test_print_text_multiple():
+def test_print_text_multiple() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print(Text("foo", style="bold"), Text("bar"), "baz")
- assert console.file.getvalue() == "\x1B[1mfoo\x1B[0m bar baz\n"
+ assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m bar baz\n"
-def test_print_json():
+def test_print_json() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print_json('[false, true, null, "foo"]', indent=4)
result = console.file.getvalue()
@@ -219,13 +219,13 @@ def test_print_json():
assert result == expected
-def test_print_json_error():
+def test_print_json_error() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
with pytest.raises(TypeError):
console.print_json(["foo"], indent=4)
-def test_print_json_data():
+def test_print_json_data() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print_json(data=[False, True, None, "foo"], indent=4)
result = console.file.getvalue()
@@ -234,7 +234,7 @@ def test_print_json_data():
assert result == expected
-def test_print_json_ensure_ascii():
+def test_print_json_ensure_ascii() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print_json(data={"foo": "💩"}, ensure_ascii=False)
result = console.file.getvalue()
@@ -243,7 +243,7 @@ def test_print_json_ensure_ascii():
assert result == expected
-def test_print_json_with_default_ensure_ascii():
+def test_print_json_with_default_ensure_ascii() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print_json(data={"foo": "💩"})
result = console.file.getvalue()
@@ -252,7 +252,7 @@ def test_print_json_with_default_ensure_ascii():
assert result == expected
-def test_print_json_indent_none():
+def test_print_json_indent_none() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
data = {"name": "apple", "count": 1}
console.print_json(data=data, indent=None)
@@ -261,7 +261,7 @@ def test_print_json_indent_none():
assert result == expected
-def test_console_null_file(monkeypatch):
+def test_console_null_file(monkeypatch) -> None:
# When stdout and stderr are null, Console.file should be replaced with NullFile
monkeypatch.setattr("sys.stdout", None)
monkeypatch.setattr("sys.stderr", None)
@@ -270,7 +270,7 @@ def test_console_null_file(monkeypatch):
assert isinstance(console.file, NullFile)
-def test_log():
+def test_log() -> None:
console = Console(
file=io.StringIO(),
width=80,
@@ -286,7 +286,7 @@ def test_log():
assert result == expected
-def test_log_milliseconds():
+def test_log_milliseconds() -> None:
def time_formatter(timestamp: datetime) -> Text:
return Text("TIME")
@@ -298,13 +298,13 @@ def test_log_milliseconds():
assert result == "TIME foo \n"
-def test_print_empty():
+def test_print_empty() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print()
assert console.file.getvalue() == "\n"
-def test_markup_highlight():
+def test_markup_highlight() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print("'[bold]foo[/bold]'")
assert (
@@ -313,13 +313,13 @@ def test_markup_highlight():
)
-def test_print_style():
+def test_print_style() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print("foo", style="bold")
assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m\n"
-def test_show_cursor():
+def test_show_cursor() -> None:
console = Console(
file=io.StringIO(), force_terminal=True, legacy_windows=False, _environ={}
)
@@ -329,31 +329,31 @@ def test_show_cursor():
assert console.file.getvalue() == "\x1b[?25lfoo\n\x1b[?25h"
-def test_clear():
+def test_clear() -> None:
console = Console(file=io.StringIO(), force_terminal=True, _environ={})
console.clear()
console.clear(home=False)
assert console.file.getvalue() == "\033[2J\033[H" + "\033[2J"
-def test_clear_no_terminal():
+def test_clear_no_terminal() -> None:
console = Console(file=io.StringIO())
console.clear()
console.clear(home=False)
assert console.file.getvalue() == ""
-def test_get_style():
+def test_get_style() -> None:
console = Console()
console.get_style("repr.brace") == Style(bold=True)
-def test_get_style_default():
+def test_get_style_default() -> None:
console = Console()
console.get_style("foobar", default="red") == Style(color="red")
-def test_get_style_error():
+def test_get_style_error() -> None:
console = Console()
with pytest.raises(errors.MissingStyle):
console.get_style("nosuchstyle")
@@ -361,20 +361,20 @@ def test_get_style_error():
console.get_style("foo bar")
-def test_render_error():
+def test_render_error() -> None:
console = Console()
with pytest.raises(errors.NotRenderableError):
list(console.render([], console.options))
-def test_control():
+def test_control() -> None:
console = Console(file=io.StringIO(), force_terminal=True, _environ={})
console.control(Control.clear())
console.print("BAR")
assert console.file.getvalue() == "\x1b[2JBAR\n"
-def test_capture():
+def test_capture() -> None:
console = Console()
with console.capture() as capture:
with pytest.raises(CaptureError):
@@ -383,7 +383,7 @@ def test_capture():
assert capture.get() == "Hello\n"
-def test_input(monkeypatch, capsys):
+def test_input(monkeypatch, capsys) -> None:
def fake_input(prompt=""):
console.file.write(prompt)
return "bar"
@@ -395,7 +395,7 @@ def test_input(monkeypatch, capsys):
assert user_input == "bar"
-def test_input_password(monkeypatch, capsys):
+def test_input_password(monkeypatch, capsys) -> None:
def fake_input(prompt, stream=None):
console.file.write(prompt)
return "bar"
@@ -409,37 +409,37 @@ def test_input_password(monkeypatch, capsys):
assert user_input == "bar"
-def test_status():
+def test_status() -> None:
console = Console(file=io.StringIO(), force_terminal=True, width=20)
status = console.status("foo")
assert isinstance(status, Status)
-def test_justify_none():
+def test_justify_none() -> None:
console = Console(file=io.StringIO(), force_terminal=True, width=20)
console.print("FOO", justify=None)
assert console.file.getvalue() == "FOO\n"
-def test_justify_left():
+def test_justify_left() -> None:
console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
console.print("FOO", justify="left")
assert console.file.getvalue() == "FOO \n"
-def test_justify_center():
+def test_justify_center() -> None:
console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
console.print("FOO", justify="center")
assert console.file.getvalue() == " FOO \n"
-def test_justify_right():
+def test_justify_right() -> None:
console = Console(file=io.StringIO(), force_terminal=True, width=20, _environ={})
console.print("FOO", justify="right")
assert console.file.getvalue() == " FOO\n"
-def test_justify_renderable_none():
+def test_justify_renderable_none() -> None:
console = Console(
file=io.StringIO(),
force_terminal=True,
@@ -451,7 +451,7 @@ def test_justify_renderable_none():
assert console.file.getvalue() == "â•───╮\n│FOO│\n╰───╯\n"
-def test_justify_renderable_left():
+def test_justify_renderable_left() -> None:
console = Console(
file=io.StringIO(),
force_terminal=True,
@@ -463,7 +463,7 @@ def test_justify_renderable_left():
assert console.file.getvalue() == "â•───╮ \n│FOO│ \n╰───╯ \n"
-def test_justify_renderable_center():
+def test_justify_renderable_center() -> None:
console = Console(
file=io.StringIO(),
force_terminal=True,
@@ -475,7 +475,7 @@ def test_justify_renderable_center():
assert console.file.getvalue() == " â•───╮ \n │FOO│ \n ╰───╯ \n"
-def test_justify_renderable_right():
+def test_justify_renderable_right() -> None:
console = Console(
file=io.StringIO(),
force_terminal=True,
@@ -495,14 +495,14 @@ class BrokenRenderable:
pass
-def test_render_broken_renderable():
+def test_render_broken_renderable() -> None:
console = Console()
broken = BrokenRenderable()
with pytest.raises(errors.NotRenderableError):
list(console.render(broken, console.options))
-def test_export_text():
+def test_export_text() -> None:
console = Console(record=True, width=100)
console.print("[b]foo")
text = console.export_text()
@@ -510,7 +510,7 @@ def test_export_text():
assert text == expected
-def test_export_html():
+def test_export_html() -> None:
console = Console(record=True, width=100)
console.print("[b]foo