From d98d671c493bd3f9e10b26e7da32ccaf272cb6a5 Mon Sep 17 00:00:00 2001 From: Olivier Philippon Date: Mon, 4 Apr 2022 14:50:08 +0100 Subject: [PATCH] Fall back to `sys.__stderr__` on non-Windows OS to get size This fixes the "pipe" bug, where we could not determine the available size correctly when the output of Rich is piped to another process --- CHANGELOG.md | 6 ++++++ CONTRIBUTORS.md | 1 + rich/console.py | 14 +++++++++----- tests/test_console.py | 45 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af96d82f..0783de87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Fall back to `sys.__stderr__` on POSIX systems when trying to get the terminal size (fix issues when Rich is piped to another process) + ## [12.2.0] - 2022-04-05 ### Changed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ab258d1e..c24f21b1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -27,6 +27,7 @@ The following people have contributed to the development of Rich: - [Nathan Page](https://github.com/nathanrpage97) - [Avi Perl](https://github.com/avi-perl) - [Laurent Peuch](https://github.com/psycojoker) +- [Olivier Philippon](https://github.com/DrBenton) - [Kylian Point](https://github.com/p0lux) - [Kyle Pollina](https://github.com/kylepollina) - [Clément Robert](https://github.com/neutrinoceros) diff --git a/rich/console.py b/rich/console.py index 2cdab428..5daaba4e 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1102,12 +1102,16 @@ class Console: except OSError: # Probably not a terminal pass else: - try: - width, height = os.get_terminal_size(sys.__stdin__.fileno()) - except (AttributeError, ValueError, OSError): + posix_std_descriptors = ( + sys.__stdin__, # try this one first... + sys.__stdout__, # ...then that one... + sys.__stderr__, # ...and ultimately try to fall back to this one + ) + for descriptor in posix_std_descriptors: try: - width, height = os.get_terminal_size(sys.__stdout__.fileno()) - except (AttributeError, ValueError, OSError): + width, height = os.get_terminal_size(descriptor.fileno()) + break + except (AttributeError, ValueError, OSError) as err: pass columns = self._environ.get("COLUMNS") diff --git a/tests/test_console.py b/tests/test_console.py index 82339a3b..d993ba92 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -3,7 +3,8 @@ import io import os import sys import tempfile -from typing import Optional +from typing import Optional, Tuple, Type, Union +from unittest import mock import pytest @@ -122,6 +123,48 @@ def test_size(): assert w == 99 and h == 101 +@pytest.mark.parametrize( + "is_windows,no_descriptor_size,stdin_size,stdout_size,stderr_size,expected_size", + [ + # on Windows we'll use `os.get_terminal_size()` without arguments... + (True, (133, 24), ValueError, ValueError, ValueError, (133, 24)), + (False, (133, 24), ValueError, ValueError, ValueError, (80, 25)), + # ...while on other OS we'll try to pass stdin, then stdout, then stderr to it: + (False, ValueError, (133, 24), ValueError, ValueError, (133, 24)), + (False, ValueError, ValueError, (133, 24), ValueError, (133, 24)), + (False, ValueError, ValueError, ValueError, (133, 24), (133, 24)), + (False, ValueError, ValueError, ValueError, ValueError, (80, 25)), + ], +) +@mock.patch("rich.console.os.get_terminal_size") +def test_size_can_fall_back_to_std_descriptors( + get_terminal_size_mock: mock.MagicMock, + is_windows: bool, + no_descriptor_size: Union[Tuple[int, int], Type[ValueError]], + stdin_size: Union[Tuple[int, int], Type[ValueError]], + stdout_size: Union[Tuple[int, int], Type[ValueError]], + stderr_size: Union[Tuple[int, int], Type[ValueError]], + expected_size, +): + def get_terminal_size_mock_impl(fileno: int = None) -> Tuple[int, int]: + value = { + None: no_descriptor_size, + sys.__stdin__.fileno(): stdin_size, + sys.__stdout__.fileno(): stdout_size, + sys.__stderr__.fileno(): stderr_size, + }[fileno] + if value is ValueError: + raise value + return value + + get_terminal_size_mock.side_effect = get_terminal_size_mock_impl + + console = Console(legacy_windows=False) + with mock.patch("rich.console.WINDOWS", new=is_windows): + w, h = console.size + assert (w, h) == expected_size + + def test_repr(): console = Console() assert isinstance(repr(console), str)