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
This commit is contained in:
Olivier Philippon 2022-04-04 14:50:08 +01:00
parent 41bf6372c5
commit d98d671c49
4 changed files with 60 additions and 6 deletions

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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)