New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Copy over TerminalWriter from py, simplify and optimize it #7135
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
276405a
terminalwriter: vendor TerminalWriter from py
bluetech 3014d9a
terminalwriter: auto-format
bluetech 5e2d820
terminalwriter: fix lints
bluetech 1d596b2
terminalwriter: move Win32ConsoleWriter definition under win32 condit…
bluetech c749e44
terminalwriter: remove custom win32 screen width code
bluetech 6c1b6a0
terminalwriter: simplify get_terminal_width()
bluetech 9a59970
terminalwriter: optimize get_line_width() a bit
bluetech b6cc90e
terminalwriter: remove support for writing bytes directly
bluetech a681972
terminalwriter: remove unused function ansi_print
bluetech 0528307
terminalwriter: remove unused function TerminalWriter.reline
bluetech dac05cc
terminalwriter: remove support for passing callable as file in Termin…
bluetech 94a57d2
io: combine _io.TerminalWriter and _io.terminalwriter.TerminalWriter
bluetech 66ee755
terminalwriter: remove TerminalWriter's stringio argument
bluetech 8d2d1c4
terminalwriter: inline function _escaped
bluetech f6564a5
terminalwriter: remove win32 specific code in favor of relying on col…
bluetech e8fc5f9
terminalwriter: add type annotations
bluetech 8e04d35
terminalwriter: remove unneeded hasattr use
bluetech d9b4364
terminalwriter: inline function _update_chars_on_current_line
bluetech 1bc4170
terminalwriter: don't flush implicitly; add explicit flushes
bluetech dd32c72
terminalwriter: remove unused property chars_on_current_line
bluetech d5584c7
terminalwriter: compute width_of_current_line lazily
bluetech 0e36596
testing/io: port TerminalWriter tests from py
bluetech bafc9bd
testing: merge code/test_terminal_writer.py into io/test_terminalwrit…
bluetech 414a87a
config/argparsing: use our own get_terminal_width()
bluetech d8558e8
terminalwriter: clean up markup function a bit
bluetech e40bf1d
Add a changelog for TerminalWriter changes
bluetech File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
Pytest now uses its own ``TerminalWriter`` class instead of using the one from the ``py`` library. | ||
Plugins generally access this class through ``TerminalReporter.writer``, ``TerminalReporter.write()`` | ||
(and similar methods), or ``_pytest.config.create_terminal_writer()``. | ||
|
||
The following breaking changes were made: | ||
|
||
- Output (``write()`` method and others) no longer flush implicitly; the flushing behavior | ||
of the underlying file is respected. To flush explicitly (for example, if you | ||
want output to be shown before an end-of-line is printed), use ``write(flush=True)`` or | ||
``terminal_writer.flush()``. | ||
- Explicit Windows console support was removed, delegated to the colorama library. | ||
- Support for writing ``bytes`` was removed. | ||
- The ``reline`` method and ``chars_on_current_line`` property were removed. | ||
- The ``stringio`` and ``encoding`` arguments was removed. | ||
- Support for passing a callable instead of a file was removed. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,8 @@ | ||
from typing import List | ||
from typing import Sequence | ||
from .terminalwriter import get_terminal_width | ||
from .terminalwriter import TerminalWriter | ||
|
||
from py.io import TerminalWriter as BaseTerminalWriter # noqa: F401 | ||
|
||
|
||
class TerminalWriter(BaseTerminalWriter): | ||
def _write_source(self, lines: List[str], indents: Sequence[str] = ()) -> None: | ||
"""Write lines of source code possibly highlighted. | ||
|
||
Keeping this private for now because the API is clunky. We should discuss how | ||
to evolve the terminal writer so we can have more precise color support, for example | ||
being able to write part of a line in one color and the rest in another, and so on. | ||
""" | ||
if indents and len(indents) != len(lines): | ||
raise ValueError( | ||
"indents size ({}) should have same size as lines ({})".format( | ||
len(indents), len(lines) | ||
) | ||
) | ||
if not indents: | ||
indents = [""] * len(lines) | ||
source = "\n".join(lines) | ||
new_lines = self._highlight(source).splitlines() | ||
for indent, new_line in zip(indents, new_lines): | ||
self.line(indent + new_line) | ||
|
||
def _highlight(self, source): | ||
"""Highlight the given source code if we have markup support""" | ||
if not self.hasmarkup: | ||
return source | ||
try: | ||
from pygments.formatters.terminal import TerminalFormatter | ||
from pygments.lexers.python import PythonLexer | ||
from pygments import highlight | ||
except ImportError: | ||
return source | ||
else: | ||
return highlight(source, PythonLexer(), TerminalFormatter(bg="dark")) | ||
__all__ = [ | ||
"TerminalWriter", | ||
"get_terminal_width", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
"""Helper functions for writing to terminals and files.""" | ||
import os | ||
import shutil | ||
import sys | ||
import unicodedata | ||
from functools import lru_cache | ||
from typing import Optional | ||
from typing import Sequence | ||
from typing import TextIO | ||
|
||
|
||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. | ||
|
||
|
||
def get_terminal_width() -> int: | ||
width, _ = shutil.get_terminal_size(fallback=(80, 24)) | ||
|
||
# The Windows get_terminal_size may be bogus, let's sanify a bit. | ||
if width < 40: | ||
width = 80 | ||
|
||
return width | ||
|
||
|
||
@lru_cache(100) | ||
def char_width(c: str) -> int: | ||
# Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1. | ||
return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 | ||
|
||
|
||
def get_line_width(text: str) -> int: | ||
text = unicodedata.normalize("NFC", text) | ||
return sum(char_width(c) for c in text) | ||
|
||
|
||
def should_do_markup(file: TextIO) -> bool: | ||
if os.environ.get("PY_COLORS") == "1": | ||
bluetech marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return True | ||
if os.environ.get("PY_COLORS") == "0": | ||
return False | ||
return ( | ||
hasattr(file, "isatty") | ||
and file.isatty() | ||
and os.environ.get("TERM") != "dumb" | ||
and not (sys.platform.startswith("java") and os._name == "nt") | ||
) | ||
|
||
|
||
class TerminalWriter: | ||
_esctable = dict( | ||
black=30, | ||
red=31, | ||
green=32, | ||
yellow=33, | ||
blue=34, | ||
purple=35, | ||
cyan=36, | ||
white=37, | ||
Black=40, | ||
Red=41, | ||
Green=42, | ||
Yellow=43, | ||
Blue=44, | ||
Purple=45, | ||
Cyan=46, | ||
White=47, | ||
bold=1, | ||
light=2, | ||
blink=5, | ||
invert=7, | ||
) | ||
|
||
def __init__(self, file: Optional[TextIO] = None) -> None: | ||
if file is None: | ||
file = sys.stdout | ||
if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": | ||
try: | ||
import colorama | ||
except ImportError: | ||
pass | ||
else: | ||
file = colorama.AnsiToWin32(file).stream | ||
assert file is not None | ||
self._file = file | ||
self.hasmarkup = should_do_markup(file) | ||
self._current_line = "" | ||
self._terminal_width = None # type: Optional[int] | ||
|
||
@property | ||
def fullwidth(self) -> int: | ||
if self._terminal_width is not None: | ||
return self._terminal_width | ||
return get_terminal_width() | ||
|
||
@fullwidth.setter | ||
def fullwidth(self, value: int) -> None: | ||
self._terminal_width = value | ||
|
||
@property | ||
def width_of_current_line(self) -> int: | ||
"""Return an estimate of the width so far in the current line.""" | ||
return get_line_width(self._current_line) | ||
|
||
def markup(self, text: str, **markup: bool) -> str: | ||
for name in markup: | ||
if name not in self._esctable: | ||
raise ValueError("unknown markup: {!r}".format(name)) | ||
if self.hasmarkup: | ||
esc = [self._esctable[name] for name, on in markup.items() if on] | ||
if esc: | ||
text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" | ||
return text | ||
|
||
def sep( | ||
self, | ||
sepchar: str, | ||
title: Optional[str] = None, | ||
fullwidth: Optional[int] = None, | ||
**markup: bool | ||
) -> None: | ||
if fullwidth is None: | ||
fullwidth = self.fullwidth | ||
# the goal is to have the line be as long as possible | ||
# under the condition that len(line) <= fullwidth | ||
if sys.platform == "win32": | ||
# if we print in the last column on windows we are on a | ||
# new line but there is no way to verify/neutralize this | ||
# (we may not know the exact line width) | ||
# so let's be defensive to avoid empty lines in the output | ||
fullwidth -= 1 | ||
if title is not None: | ||
# we want 2 + 2*len(fill) + len(title) <= fullwidth | ||
# i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth | ||
# 2*len(sepchar)*N <= fullwidth - len(title) - 2 | ||
# N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) | ||
N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1) | ||
fill = sepchar * N | ||
line = "{} {} {}".format(fill, title, fill) | ||
else: | ||
# we want len(sepchar)*N <= fullwidth | ||
# i.e. N <= fullwidth // len(sepchar) | ||
line = sepchar * (fullwidth // len(sepchar)) | ||
# in some situations there is room for an extra sepchar at the right, | ||
# in particular if we consider that with a sepchar like "_ " the | ||
# trailing space is not important at the end of the line | ||
if len(line) + len(sepchar.rstrip()) <= fullwidth: | ||
line += sepchar.rstrip() | ||
|
||
self.line(line, **markup) | ||
|
||
def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None: | ||
if msg: | ||
current_line = msg.rsplit("\n", 1)[-1] | ||
if "\n" in msg: | ||
self._current_line = current_line | ||
else: | ||
self._current_line += current_line | ||
|
||
msg = self.markup(msg, **markup) | ||
|
||
self._file.write(msg) | ||
if flush: | ||
self.flush() | ||
|
||
def line(self, s: str = "", **markup: bool) -> None: | ||
self.write(s, **markup) | ||
self.write("\n") | ||
|
||
def flush(self) -> None: | ||
self._file.flush() | ||
|
||
def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None: | ||
"""Write lines of source code possibly highlighted. | ||
|
||
Keeping this private for now because the API is clunky. We should discuss how | ||
to evolve the terminal writer so we can have more precise color support, for example | ||
being able to write part of a line in one color and the rest in another, and so on. | ||
""" | ||
if indents and len(indents) != len(lines): | ||
raise ValueError( | ||
"indents size ({}) should have same size as lines ({})".format( | ||
len(indents), len(lines) | ||
) | ||
) | ||
if not indents: | ||
indents = [""] * len(lines) | ||
source = "\n".join(lines) | ||
new_lines = self._highlight(source).splitlines() | ||
for indent, new_line in zip(indents, new_lines): | ||
self.line(indent + new_line) | ||
|
||
def _highlight(self, source: str) -> str: | ||
"""Highlight the given source code if we have markup support.""" | ||
if not self.hasmarkup: | ||
return source | ||
try: | ||
from pygments.formatters.terminal import TerminalFormatter | ||
from pygments.lexers.python import PythonLexer | ||
from pygments import highlight | ||
except ImportError: | ||
return source | ||
else: | ||
highlighted = highlight( | ||
source, PythonLexer(), TerminalFormatter(bg="dark") | ||
) # type: str | ||
return highlighted |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great changelog. 👍