Skip to content

Commit

Permalink
Adopt/inherit TerminalWriter from py.io for better width (#41)
Browse files Browse the repository at this point in the history
This uses an improved version of `shutil.get_terminal_width`, mainly to look at stdin, stderr, and stdout.

Ref: https://bugs.python.org/issue14841
Ref: python/cpython#12697

Rejected/stalled in pylib: pytest-dev/py#219
Suggested to move into pytest in pytest-dev#5056.
  • Loading branch information
blueyed authored Nov 1, 2019
1 parent c947224 commit efcf61f
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 8 deletions.
4 changes: 3 additions & 1 deletion src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,10 +895,12 @@ def repr_excinfo(self, excinfo):

class TerminalRepr:
def __str__(self):
from _pytest.terminal import TerminalWriter

# FYI this is called from pytest-xdist's serialization of exception
# information.
io = StringIO()
tw = py.io.TerminalWriter(file=io)
tw = TerminalWriter(file=io)
self.toterminal(tw)
return io.getvalue().strip()

Expand Down
4 changes: 3 additions & 1 deletion src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ def pytest_report_header(config):
def cacheshow(config, session):
from pprint import pformat

tw = py.io.TerminalWriter()
from _pytest.terminal import TerminalWriter

tw = TerminalWriter()
tw.line("cachedir: " + str(config.cache._cachedir))
if not config.cache._cachedir.is_dir():
tw.line("cache is empty")
Expand Down
9 changes: 6 additions & 3 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,14 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
initialization.
"""
from _pytest.main import ExitCode
from _pytest.terminal import TerminalWriter

try:
try:
config = _prepareconfig(args, plugins)
except ConftestImportFailure as e:
exc_info = ExceptionInfo(e.excinfo)
tw = py.io.TerminalWriter(sys.stderr)
tw = TerminalWriter(sys.stderr)
tw.line(
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
)
Expand All @@ -98,7 +99,7 @@ def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]":
finally:
config._ensure_unconfigure()
except UsageError as e:
tw = py.io.TerminalWriter(sys.stderr)
tw = TerminalWriter(sys.stderr)
for msg in e.args:
tw.line("ERROR: {}\n".format(msg), red=True)
return ExitCode.USAGE_ERROR
Expand Down Expand Up @@ -1128,7 +1129,9 @@ def create_terminal_writer(config, *args, **kwargs):
in the config object. Every code which requires a TerminalWriter object
and has access to a config object should use this function.
"""
tw = py.io.TerminalWriter(*args, **kwargs)
from _pytest.terminal import TerminalWriter

tw = TerminalWriter(*args, **kwargs)
if config.option.color == "yes":
tw.hasmarkup = True
if config.option.color == "no":
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/config/argparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import py

from _pytest.config.exceptions import UsageError
from _pytest.terminal import get_terminal_width

FILE_OR_DIR = "file_or_dir"

Expand Down Expand Up @@ -417,8 +418,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):

def __init__(self, *args, **kwargs):
"""Use more accurate terminal width via pylib."""
if "width" not in kwargs:
kwargs["width"] = py.io.get_terminal_width()
kwargs.setdefault("width", get_terminal_width())
super().__init__(*args, **kwargs)

def _format_action_invocation(self, action):
Expand Down
50 changes: 50 additions & 0 deletions src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import collections
import datetime
import os
import platform
import sys
import time
Expand All @@ -30,6 +31,55 @@
REPORT_COLLECTING_RESOLUTION = 0.5


def _getdimensions():
# Improved version of shutil.get_terminal_size that looks at stdin,
# stderr, stdout. Ref: https://bugs.python.org/issue14841.
fallback = (80, 24)
# columns, lines are the working values
try:
columns = int(os.environ["COLUMNS"])
except (KeyError, ValueError):
columns = 0
try:
lines = int(os.environ["LINES"])
except (KeyError, ValueError):
lines = 0
# only query if necessary
if columns <= 0 or lines <= 0:
for check in [sys.__stdin__, sys.__stderr__, sys.__stdout__]:
try:
size = os.get_terminal_size(check.fileno())
except (AttributeError, ValueError, OSError):
# fd is None, closed, detached, or not a terminal.
continue
else:
break
else:
size = os.terminal_size(fallback)
if columns <= 0:
columns = size.columns
if lines <= 0:
lines = size.lines
return columns, lines


def get_terminal_width():
width, _ = _getdimensions()
return width


class TerminalWriter(py.io.TerminalWriter):
@property
def fullwidth(self):
if hasattr(self, "_terminal_width"):
return self._terminal_width
return get_terminal_width()

@fullwidth.setter
def fullwidth(self, value):
self._terminal_width = value


class MoreQuietAction(argparse.Action):
"""
a modified copy of the argparse count action which counts down and updates
Expand Down
2 changes: 1 addition & 1 deletion testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ def test_help_formatter_uses_py_get_terminal_width(testdir, monkeypatch):
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 90

monkeypatch.setattr("py.io.get_terminal_width", lambda: 160)
monkeypatch.setattr("_pytest.config.argparsing.get_terminal_width", lambda: 160)
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 160

Expand Down
35 changes: 35 additions & 0 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1803,3 +1803,38 @@ def test_collecterror(testdir):
"*= 1 error in *",
]
)


def test_getdimensions(monkeypatch):
from _pytest.terminal import _getdimensions, get_terminal_width

monkeypatch.setenv("COLUMNS", "30")
monkeypatch.setenv("LINES", "25")

assert _getdimensions() == (30, 25)
assert get_terminal_width() == 30

calls = []

def mocked_get_terminal_size(*args):
calls.append(args)
raise OSError()

monkeypatch.setattr(os, "get_terminal_size", mocked_get_terminal_size)

monkeypatch.delenv("COLUMNS")
monkeypatch.delenv("LINES")
assert _getdimensions() == (80, 24)
assert calls == [(0,), (2,), (1,)]

def mocked_get_terminal_size(fileno):
calls.append(fileno)
if fileno == 2:
return os.terminal_size((12, 34))
raise OSError()

monkeypatch.setattr(os, "get_terminal_size", mocked_get_terminal_size)

calls = []
assert _getdimensions() == (12, 34)
assert calls == [0, 2]

0 comments on commit efcf61f

Please sign in to comment.