From 2ef0c30905ae801ae0359b8b42f53135bc87dfe7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 14:13:00 +0100 Subject: [PATCH 01/14] Fix issue with NO_COLOR, add test for Console with no file --- rich/console.py | 9 ++++++++- tests/test_console.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/rich/console.py b/rich/console.py index 4a3ebb559..83380da14 100644 --- a/rich/console.py +++ b/rich/console.py @@ -701,7 +701,14 @@ def __init__( if force_terminal is not None: self._force_terminal = force_terminal else: - self._force_terminal = self._environ.get("FORCE_COLOR") is not None + # If FORCE_COLOR env var has any value at all, we force terminal. + force_terminal = self._environ.get("FORCE_COLOR") + if force_terminal is not None: + self._force_terminal = bool(force_terminal) + else: + self._force_terminal = None + + print(self._force_terminal) self._file = file self.quiet = quiet diff --git a/tests/test_console.py b/tests/test_console.py index 07692f198..c0c24232a 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -5,6 +5,7 @@ import tempfile from typing import Optional, Tuple, Type, Union from unittest import mock +from unittest.mock import PropertyMock import pytest @@ -897,6 +898,19 @@ def test_render_lines_height_minus_vertical_pad_is_negative(): console.render_lines(Padding("hello", pad=(1, 0)), options=options) +def test_no_stdout_file(): + # Rich should work even if there's no file available to write to. + # For example, pythonw nullifies output streams. + # Built-in print silently no-ops in pythonw. + # Related: https://github.com/Textualize/rich/issues/2400 + console = Console() + with mock.patch.object( + Console, "file", new_callable=PropertyMock + ) as mock_file_property: + mock_file_property.return_value = None + console.print("hello world") + + @mock.patch.dict(os.environ, {"FORCE_COLOR": "anything"}) def test_force_color(): # Even though we use a non-tty file, the presence of FORCE_COLOR env var From 50039b6e7e49b7442e2a9f3633929009379fd70d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 14:14:59 +0100 Subject: [PATCH 02/14] Remove a debug print statemetn --- rich/console.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index 83380da14..d8823d9d4 100644 --- a/rich/console.py +++ b/rich/console.py @@ -708,8 +708,6 @@ def __init__( else: self._force_terminal = None - print(self._force_terminal) - self._file = file self.quiet = quiet self.stderr = stderr From ffe9ae82f669b88ebf4a8312cce36121638ee43a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 15:26:05 +0100 Subject: [PATCH 03/14] Supporting no stdout, tests --- rich/console.py | 26 +++++++++++++++----------- rich/logging.py | 9 ++++++--- tests/test_cells.py | 5 +++++ tests/test_console.py | 21 +++++++++++++++------ tests/test_pretty.py | 2 +- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/rich/console.py b/rich/console.py index d8823d9d4..a9d18c582 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1997,7 +1997,8 @@ def _check_buffer(self) -> None: if self.legacy_windows: try: use_legacy_windows_render = ( - self.file.fileno() in _STD_STREAMS_OUTPUT + self.file is not None + and self.file.fileno() in _STD_STREAMS_OUTPUT ) except (ValueError, io.UnsupportedOperation): pass @@ -2014,23 +2015,26 @@ def _check_buffer(self) -> None: else: # Either a non-std stream on legacy Windows, or modern Windows. text = self._render_buffer(self._buffer[:]) - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): - try: - write(line) - except UnicodeEncodeError as error: - error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" - raise + if self.file is not None: + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + try: + write(line) + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise else: text = self._render_buffer(self._buffer[:]) try: - self.file.write(text) + if self.file is not None: + self.file.write(text) except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise - self.file.flush() + if self.file is not None: + self.file.flush() del self._buffer[:] def _render_buffer(self, buffer: Iterable[Segment]) -> str: diff --git a/rich/logging.py b/rich/logging.py index ff93531a7..2e343c907 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -3,10 +3,10 @@ from logging import Handler, LogRecord from pathlib import Path from types import ModuleType -from typing import ClassVar, List, Optional, Iterable, Type, Union +from typing import ClassVar, Iterable, List, Optional, Type, Union from . import get_console -from ._log_render import LogRender, FormatTimeCallable +from ._log_render import FormatTimeCallable, LogRender from .console import Console, ConsoleRenderable from .highlighter import Highlighter, ReprHighlighter from .text import Text @@ -159,7 +159,10 @@ def emit(self, record: LogRecord) -> None: record=record, traceback=traceback, message_renderable=message_renderable ) try: - self.console.print(log_renderable) + if self.console.file is not None: + self.console.print(log_renderable) + else: + self.handleError(record) except Exception: self.handleError(record) diff --git a/tests/test_cells.py b/tests/test_cells.py index d64317f3b..a780214a8 100644 --- a/tests/test_cells.py +++ b/tests/test_cells.py @@ -1,6 +1,11 @@ from rich import cells +def test_cell_len_long_string(): + # Long strings don't use cached cell length implementation + assert cells.cell_len("abc" * 200) == 3 * 200 + + def test_set_cell_size(): assert cells.set_cell_size("foo", 0) == "" assert cells.set_cell_size("f", 0) == "" diff --git a/tests/test_console.py b/tests/test_console.py index c0c24232a..3399a7b21 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -5,7 +5,6 @@ import tempfile from typing import Optional, Tuple, Type, Union from unittest import mock -from unittest.mock import PropertyMock import pytest @@ -860,6 +859,7 @@ def test_detect_color_system(): def test_reset_height(): """Test height is reset when rendering complex renderables.""" + # https://github.com/Textualize/rich/issues/2042 class Panels: def __rich_console__(self, console, options): @@ -898,17 +898,26 @@ def test_render_lines_height_minus_vertical_pad_is_negative(): console.render_lines(Padding("hello", pad=(1, 0)), options=options) -def test_no_stdout_file(): +def test_recording_no_stdout_and_no_stderr_files(monkeypatch): # Rich should work even if there's no file available to write to. # For example, pythonw nullifies output streams. # Built-in print silently no-ops in pythonw. # Related: https://github.com/Textualize/rich/issues/2400 + monkeypatch.setattr("sys.stdout", None) + monkeypatch.setattr("sys.stderr", None) + console = Console(record=True) + console.print("hello world") + text = console.export_text() + assert text == "hello world\n" + + +def test_capturing_no_stdout_and_no_stderr_files(monkeypatch): + monkeypatch.setattr("sys.stdout", None) + monkeypatch.setattr("sys.stderr", None) console = Console() - with mock.patch.object( - Console, "file", new_callable=PropertyMock - ) as mock_file_property: - mock_file_property.return_value = None + with console.capture() as capture: console.print("hello world") + assert capture.get() == "hello world\n" @mock.patch.dict(os.environ, {"FORCE_COLOR": "anything"}) diff --git a/tests/test_pretty.py b/tests/test_pretty.py index f57066d04..33f621134 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -56,7 +56,7 @@ def test_install_max_depth(): dh = sys.displayhook install(console, max_depth=1) sys.displayhook({"foo": {"bar": True}}) - assert console.file.getvalue() == "{'foo': ...}\n" + assert console.file.getvalue() == "{'foo': {...}}\n" assert sys.displayhook is not dh From 99ed636bea5698c309ddc713b98ded83b3b23dec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 16:27:43 +0100 Subject: [PATCH 04/14] Add NullFile, use it when stdout/stderr are None --- rich/_null_file.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++ rich/console.py | 39 +++++++++++++------------ rich/logging.py | 15 ++++++---- 3 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 rich/_null_file.py diff --git a/rich/_null_file.py b/rich/_null_file.py new file mode 100644 index 000000000..81327554d --- /dev/null +++ b/rich/_null_file.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from types import TracebackType +from typing import IO, AnyStr, Iterable, Iterator, Type + + +class NullFile(IO[str]): + def close(self) -> None: + pass + + def isatty(self) -> bool: + pass + + def read(self, __n: int = ...) -> AnyStr: + pass + + def readable(self) -> bool: + pass + + def readline(self, __limit: int = ...) -> AnyStr: + pass + + def readlines(self, __hint: int = ...) -> list[AnyStr]: + pass + + def seek(self, __offset: int, __whence: int = ...) -> int: + pass + + def seekable(self) -> bool: + pass + + def tell(self) -> int: + pass + + def truncate(self, __size: int | None = ...) -> int: + pass + + def writable(self) -> bool: + pass + + def writelines(self, __lines: Iterable[AnyStr]) -> None: + pass + + def __next__(self) -> AnyStr: + pass + + def __iter__(self) -> Iterator[AnyStr]: + pass + + def __enter__(self) -> IO[AnyStr]: + pass + + def __exit__( + self, + __t: Type[BaseException] | None, + __value: BaseException | None, + __traceback: TracebackType | None, + ) -> None: + pass + + def write(self, text: str) -> int: + return 1 + + def flush(self) -> None: + pass + + def fileno(self) -> int: + return -1 + + +NULL_FILE = NullFile() diff --git a/rich/console.py b/rich/console.py index a9d18c582..d5fbb9d62 100644 --- a/rich/console.py +++ b/rich/console.py @@ -34,6 +34,8 @@ cast, ) +from rich._null_file import NULL_FILE + if sys.version_info >= (3, 8): from typing import Literal, Protocol, runtime_checkable else: @@ -698,15 +700,14 @@ def __init__( self._color_system: Optional[ColorSystem] + self._force_terminal = None if force_terminal is not None: self._force_terminal = force_terminal else: # If FORCE_COLOR env var has any value at all, we force terminal. - force_terminal = self._environ.get("FORCE_COLOR") - if force_terminal is not None: - self._force_terminal = bool(force_terminal) - else: - self._force_terminal = None + force_color = self._environ.get("FORCE_COLOR") + if force_color is not None: + self._force_terminal = bool(force_color) self._file = file self.quiet = quiet @@ -756,6 +757,8 @@ def file(self) -> IO[str]: """Get the file object to write to.""" file = self._file or (sys.stderr if self.stderr else sys.stdout) file = getattr(file, "rich_proxied_file", file) + if file is None: + file = NULL_FILE return file @file.setter @@ -1997,8 +2000,7 @@ def _check_buffer(self) -> None: if self.legacy_windows: try: use_legacy_windows_render = ( - self.file is not None - and self.file.fileno() in _STD_STREAMS_OUTPUT + self.file.fileno() in _STD_STREAMS_OUTPUT ) except (ValueError, io.UnsupportedOperation): pass @@ -2015,26 +2017,23 @@ def _check_buffer(self) -> None: else: # Either a non-std stream on legacy Windows, or modern Windows. text = self._render_buffer(self._buffer[:]) - if self.file is not None: - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): - try: - write(line) - except UnicodeEncodeError as error: - error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" - raise + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + try: + write(line) + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise else: text = self._render_buffer(self._buffer[:]) try: - if self.file is not None: - self.file.write(text) + self.file.write(text) except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise - if self.file is not None: - self.file.flush() + self.file.flush() del self._buffer[:] def _render_buffer(self, buffer: Iterable[Segment]) -> str: diff --git a/rich/logging.py b/rich/logging.py index 2e343c907..96859934e 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -5,6 +5,8 @@ from types import ModuleType from typing import ClassVar, Iterable, List, Optional, Type, Union +from rich._null_file import NullFile + from . import get_console from ._log_render import FormatTimeCallable, LogRender from .console import Console, ConsoleRenderable @@ -158,13 +160,16 @@ def emit(self, record: LogRecord) -> None: log_renderable = self.render( record=record, traceback=traceback, message_renderable=message_renderable ) - try: - if self.console.file is not None: + if isinstance(self.console.file, NullFile): + # Handles pythonw, where stdout/stderr are null, and we return NullFile + # instance from Console.file. In this case, we still want to make a log record + # even though we won't be writing anything to a file. + self.handleError(record) + else: + try: self.console.print(log_renderable) - else: + except Exception: self.handleError(record) - except Exception: - self.handleError(record) def render_message(self, record: LogRecord, message: str) -> "ConsoleRenderable": """Render message text in to Text. From 6d0dde59061acd13b53d6e079591ac34b94137b4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 16:34:17 +0100 Subject: [PATCH 05/14] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b723ded1..4d355b82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Handle stdout/stderr being null https://github.com/Textualize/rich/pull/2513 - Fix NO_COLOR support on legacy Windows https://github.com/Textualize/rich/pull/2458 ## [12.5.2] - 2022-07-18 From cdc4767f1dc5c0e4f12af3b97dd5644d7d97134d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 16:36:35 +0100 Subject: [PATCH 06/14] Update codespell skips, fix a typo --- .github/workflows/codespell.yml | 2 +- rich/color.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index c21f7a642..624f5b888 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -7,4 +7,4 @@ jobs: - uses: actions/checkout@v3 - run: python3 -m pip install codespell - run: codespell --ignore-words-list="ba,fo,hel,revered,womens" - --skip="./README.*.md,*.svg,./benchmarks/snippets.py" + --skip="./README.*.md,*.svg,./benchmarks/snippets.py,./tests,./tools" diff --git a/rich/color.py b/rich/color.py index 9031ae378..ef2e895d7 100644 --- a/rich/color.py +++ b/rich/color.py @@ -313,7 +313,7 @@ class Color(NamedTuple): """A triplet of color components, if an RGB color.""" def __rich__(self) -> "Text": - """Dispays the actual color if Rich printed.""" + """Displays the actual color if Rich printed.""" from .style import Style from .text import Text From b4f6f9eb66d467547748c8dc1786829743b86a95 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 16:42:03 +0100 Subject: [PATCH 07/14] Remove future annotations import --- rich/_null_file.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/rich/_null_file.py b/rich/_null_file.py index 81327554d..5fa2a77a9 100644 --- a/rich/_null_file.py +++ b/rich/_null_file.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from types import TracebackType -from typing import IO, AnyStr, Iterable, Iterator, Type +from typing import IO, AnyStr, Iterable, Iterator, Optional, Type class NullFile(IO[str]): @@ -32,7 +30,7 @@ def seekable(self) -> bool: def tell(self) -> int: pass - def truncate(self, __size: int | None = ...) -> int: + def truncate(self, __size: Optional[int] = ...) -> int: pass def writable(self) -> bool: @@ -52,9 +50,9 @@ def __enter__(self) -> IO[AnyStr]: def __exit__( self, - __t: Type[BaseException] | None, - __value: BaseException | None, - __traceback: TracebackType | None, + __t: Optional[Type[BaseException]], + __value: Optional[BaseException], + __traceback: Optional[TracebackType], ) -> None: pass From 934c7743793b017080c37d40c9f9b14c9f151435 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 16:44:31 +0100 Subject: [PATCH 08/14] Use old typing List import --- rich/_null_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/_null_file.py b/rich/_null_file.py index 5fa2a77a9..af49ccc62 100644 --- a/rich/_null_file.py +++ b/rich/_null_file.py @@ -1,5 +1,5 @@ from types import TracebackType -from typing import IO, AnyStr, Iterable, Iterator, Optional, Type +from typing import IO, AnyStr, Iterable, Iterator, List, Optional, Type class NullFile(IO[str]): @@ -18,7 +18,7 @@ def readable(self) -> bool: def readline(self, __limit: int = ...) -> AnyStr: pass - def readlines(self, __hint: int = ...) -> list[AnyStr]: + def readlines(self, __hint: int = ...) -> List[AnyStr]: pass def seek(self, __offset: int, __whence: int = ...) -> int: From 46ef6283a05b0e31c5bddce92252e0b3fde7ce82 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 8 Sep 2022 10:13:11 +0100 Subject: [PATCH 09/14] Fix Python 3.6 support --- rich/_null_file.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/rich/_null_file.py b/rich/_null_file.py index af49ccc62..5128ab40b 100644 --- a/rich/_null_file.py +++ b/rich/_null_file.py @@ -3,44 +3,58 @@ class NullFile(IO[str]): + + # TODO: "mode", "name" and "closed" are only required for Python 3.6. + + @property + def mode(self) -> str: + return "" + + @property + def name(self) -> str: + return "" + + def closed(self) -> bool: + return False + def close(self) -> None: pass def isatty(self) -> bool: - pass + return False def read(self, __n: int = ...) -> AnyStr: - pass + return "" def readable(self) -> bool: - pass + return False def readline(self, __limit: int = ...) -> AnyStr: - pass + return "" def readlines(self, __hint: int = ...) -> List[AnyStr]: - pass + return [] def seek(self, __offset: int, __whence: int = ...) -> int: - pass + return 0 def seekable(self) -> bool: - pass + return False def tell(self) -> int: - pass + return 0 def truncate(self, __size: Optional[int] = ...) -> int: - pass + return 0 def writable(self) -> bool: - pass + return False def writelines(self, __lines: Iterable[AnyStr]) -> None: pass def __next__(self) -> AnyStr: - pass + return "" def __iter__(self) -> Iterator[AnyStr]: pass @@ -57,7 +71,7 @@ def __exit__( pass def write(self, text: str) -> int: - return 1 + return 0 def flush(self) -> None: pass From 9009e16b1a1a411554cbd97caa5b9259a46b05fd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 8 Sep 2022 10:23:55 +0100 Subject: [PATCH 10/14] Implementing null file --- rich/_null_file.py | 4 ++-- tests/test_null_file.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/test_null_file.py diff --git a/rich/_null_file.py b/rich/_null_file.py index 5128ab40b..d955e34d0 100644 --- a/rich/_null_file.py +++ b/rich/_null_file.py @@ -12,7 +12,7 @@ def mode(self) -> str: @property def name(self) -> str: - return "" + return "NullFile" def closed(self) -> bool: return False @@ -57,7 +57,7 @@ def __next__(self) -> AnyStr: return "" def __iter__(self) -> Iterator[AnyStr]: - pass + return iter([""]) def __enter__(self) -> IO[AnyStr]: pass diff --git a/tests/test_null_file.py b/tests/test_null_file.py new file mode 100644 index 000000000..5f11e4f91 --- /dev/null +++ b/tests/test_null_file.py @@ -0,0 +1,26 @@ +from rich._null_file import NullFile + + +def test_null_file(): + file = NullFile() + with file: + assert file.write("abc") == 0 + assert file.mode == "" + assert file.name == "NullFile" + assert not file.closed() + assert file.close() is None + assert not file.isatty() + assert file.read() == "" + assert not file.readable() + assert file.readline() == "" + assert file.readlines() == [] + assert file.seek(0, 0) == 0 + assert not file.seekable() + assert file.tell() == 0 + assert file.truncate() == 0 + assert file.writable() == False + assert file.writelines([""]) is None + assert next(file) == "" + assert next(iter(file)) == "" + assert file.fileno() == -1 + assert file.flush() is None From 1601f89bf18024c7efe7dc4efe3fb77ada63532b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 8 Sep 2022 10:38:56 +0100 Subject: [PATCH 11/14] Fix typing issue --- rich/_null_file.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rich/_null_file.py b/rich/_null_file.py index d955e34d0..49038bfcb 100644 --- a/rich/_null_file.py +++ b/rich/_null_file.py @@ -1,5 +1,5 @@ from types import TracebackType -from typing import IO, AnyStr, Iterable, Iterator, List, Optional, Type +from typing import IO, Iterable, Iterator, List, Optional, Type class NullFile(IO[str]): @@ -23,19 +23,19 @@ def close(self) -> None: def isatty(self) -> bool: return False - def read(self, __n: int = ...) -> AnyStr: + def read(self, __n: int = 1) -> str: return "" def readable(self) -> bool: return False - def readline(self, __limit: int = ...) -> AnyStr: + def readline(self, __limit: int = 1) -> str: return "" - def readlines(self, __hint: int = ...) -> List[AnyStr]: + def readlines(self, __hint: int = 1) -> List[str]: return [] - def seek(self, __offset: int, __whence: int = ...) -> int: + def seek(self, __offset: int, __whence: int = 1) -> int: return 0 def seekable(self) -> bool: @@ -44,22 +44,22 @@ def seekable(self) -> bool: def tell(self) -> int: return 0 - def truncate(self, __size: Optional[int] = ...) -> int: + def truncate(self, __size: Optional[int] = 1) -> int: return 0 def writable(self) -> bool: return False - def writelines(self, __lines: Iterable[AnyStr]) -> None: + def writelines(self, __lines: Iterable[str]) -> None: pass - def __next__(self) -> AnyStr: + def __next__(self) -> str: return "" - def __iter__(self) -> Iterator[AnyStr]: + def __iter__(self) -> Iterator[str]: return iter([""]) - def __enter__(self) -> IO[AnyStr]: + def __enter__(self) -> IO[str]: pass def __exit__( From 97bffbc7b1640dfc7bc20809dc0b9d1b536d7644 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 8 Sep 2022 11:30:05 +0100 Subject: [PATCH 12/14] Test to ensure NullFile set as Console.file when stdout null --- tests/test_console.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_console.py b/tests/test_console.py index 3399a7b21..3f95a2077 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -9,6 +9,7 @@ import pytest from rich import errors +from rich._null_file import NullFile from rich.color import ColorSystem from rich.console import ( CaptureError, @@ -238,6 +239,15 @@ def test_print_json_indent_none(): assert result == expected +def test_console_null_file(monkeypatch): + # When stdout and stderr are null, Console.file should be replaced with NullFile + monkeypatch.setattr("sys.stdout", None) + monkeypatch.setattr("sys.stderr", None) + + console = Console() + assert isinstance(console.file, NullFile) + + def test_log(): console = Console( file=io.StringIO(), From 58bbc795e63311962dd30436a013510a6fb71d35 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 8 Sep 2022 13:13:11 +0100 Subject: [PATCH 13/14] Update rich/console.py --- rich/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rich/console.py b/rich/console.py index d5fbb9d62..585221e06 100644 --- a/rich/console.py +++ b/rich/console.py @@ -707,7 +707,7 @@ def __init__( # If FORCE_COLOR env var has any value at all, we force terminal. force_color = self._environ.get("FORCE_COLOR") if force_color is not None: - self._force_terminal = bool(force_color) + self._force_terminal = True self._file = file self.quiet = quiet From f99633793a00e56dd24b81f5d5e417b0f2a055c2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 8 Sep 2022 13:33:05 +0100 Subject: [PATCH 14/14] Test to ensure FORCE_COLOR works with empty value --- tests/test_console.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_console.py b/tests/test_console.py index 3f95a2077..fa6765357 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -930,9 +930,9 @@ def test_capturing_no_stdout_and_no_stderr_files(monkeypatch): assert capture.get() == "hello world\n" -@mock.patch.dict(os.environ, {"FORCE_COLOR": "anything"}) -def test_force_color(): +@pytest.mark.parametrize("env_value", ["", "something", "0"]) +def test_force_color(env_value): # Even though we use a non-tty file, the presence of FORCE_COLOR env var # means is_terminal returns True. - console = Console(file=io.StringIO()) + console = Console(file=io.StringIO(), _environ={"FORCE_COLOR": env_value}) assert console.is_terminal