terminalwriter: auto-format

This commit is contained in:
Ran Benita 2020-04-29 15:00:58 +03:00
parent 276405a039
commit 3014d9a3f7
1 changed files with 130 additions and 88 deletions

View File

@ -1,15 +1,16 @@
""" """Helper functions for writing to terminals and files."""
import sys
import os
import unicodedata
Helper functions for writing to terminals and files.
"""
import sys, os, unicodedata
import py import py
from py.builtin import text, bytes
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
py3k = sys.version_info[0] >= 3 py3k = sys.version_info[0] >= 3
py33 = sys.version_info >= (3, 3) py33 = sys.version_info >= (3, 3)
from py.builtin import text, bytes
win32_and_ctypes = False win32_and_ctypes = False
colorama = None colorama = None
@ -19,6 +20,7 @@ if sys.platform == "win32":
except ImportError: except ImportError:
try: try:
import ctypes import ctypes
win32_and_ctypes = True win32_and_ctypes = True
except ImportError: except ImportError:
pass pass
@ -27,10 +29,14 @@ if sys.platform == "win32":
def _getdimensions(): def _getdimensions():
if py33: if py33:
import shutil import shutil
size = shutil.get_terminal_size() size = shutil.get_terminal_size()
return size.lines, size.columns return size.lines, size.columns
else: else:
import termios, fcntl, struct import termios
import fcntl
import struct
call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8)
height, width = struct.unpack("hhhh", call)[:2] height, width = struct.unpack("hhhh", call)[:2]
return height, width return height, width
@ -50,27 +56,28 @@ def get_terminal_width():
# FALLBACK: # FALLBACK:
# * some exception happened # * some exception happened
# * or this is emacs terminal which reports (0,0) # * or this is emacs terminal which reports (0,0)
width = int(os.environ.get('COLUMNS', 80)) width = int(os.environ.get("COLUMNS", 80))
# XXX the windows getdimensions may be bogus, let's sanify a bit # XXX the windows getdimensions may be bogus, let's sanify a bit
if width < 40: if width < 40:
width = 80 width = 80
return width return width
terminal_width = get_terminal_width() terminal_width = get_terminal_width()
char_width = { char_width = {
'A': 1, # "Ambiguous" "A": 1, # "Ambiguous"
'F': 2, # Fullwidth "F": 2, # Fullwidth
'H': 1, # Halfwidth "H": 1, # Halfwidth
'N': 1, # Neutral "N": 1, # Neutral
'Na': 1, # Narrow "Na": 1, # Narrow
'W': 2, # Wide "W": 2, # Wide
} }
def get_line_width(text): def get_line_width(text):
text = unicodedata.normalize('NFC', text) text = unicodedata.normalize("NFC", text)
return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text)
@ -82,11 +89,11 @@ def ansi_print(text, esc, file=None, newline=True, flush=False):
if esc and not isinstance(esc, tuple): if esc and not isinstance(esc, tuple):
esc = (esc,) esc = (esc,)
if esc and sys.platform != "win32" and file.isatty(): if esc and sys.platform != "win32" and file.isatty():
text = (''.join(['\x1b[%sm' % cod for cod in esc]) + text = (
text + "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m"
'\x1b[0m') # ANSI color code "reset" ) # ANSI color code "reset"
if newline: if newline:
text += '\n' text += "\n"
if esc and win32_and_ctypes and file.isatty(): if esc and win32_and_ctypes and file.isatty():
if 1 in esc: if 1 in esc:
@ -94,16 +101,17 @@ def ansi_print(text, esc, file=None, newline=True, flush=False):
esc = tuple([x for x in esc if x != 1]) esc = tuple([x for x in esc if x != 1])
else: else:
bold = False bold = False
esctable = {() : FOREGROUND_WHITE, # normal esctable = {
(31,): FOREGROUND_RED, # red (): FOREGROUND_WHITE, # normal
(32,): FOREGROUND_GREEN, # green (31,): FOREGROUND_RED, # red
(33,): FOREGROUND_GREEN|FOREGROUND_RED, # yellow (32,): FOREGROUND_GREEN, # green
(34,): FOREGROUND_BLUE, # blue (33,): FOREGROUND_GREEN | FOREGROUND_RED, # yellow
(35,): FOREGROUND_BLUE|FOREGROUND_RED, # purple (34,): FOREGROUND_BLUE, # blue
(36,): FOREGROUND_BLUE|FOREGROUND_GREEN, # cyan (35,): FOREGROUND_BLUE | FOREGROUND_RED, # purple
(37,): FOREGROUND_WHITE, # white (36,): FOREGROUND_BLUE | FOREGROUND_GREEN, # cyan
(39,): FOREGROUND_WHITE, # reset (37,): FOREGROUND_WHITE, # white
} (39,): FOREGROUND_WHITE, # reset
}
attr = esctable.get(esc, FOREGROUND_WHITE) attr = esctable.get(esc, FOREGROUND_WHITE)
if bold: if bold:
attr |= FOREGROUND_INTENSITY attr |= FOREGROUND_INTENSITY
@ -114,7 +122,7 @@ def ansi_print(text, esc, file=None, newline=True, flush=False):
else: else:
handle = GetStdHandle(STD_OUTPUT_HANDLE) handle = GetStdHandle(STD_OUTPUT_HANDLE)
oldcolors = GetConsoleInfo(handle).wAttributes oldcolors = GetConsoleInfo(handle).wAttributes
attr |= (oldcolors & 0x0f0) attr |= oldcolors & 0x0F0
SetConsoleTextAttribute(handle, attr) SetConsoleTextAttribute(handle, attr)
while len(text) > 32768: while len(text) > 32768:
file.write(text[:32768]) file.write(text[:32768])
@ -128,21 +136,43 @@ def ansi_print(text, esc, file=None, newline=True, flush=False):
if flush: if flush:
file.flush() file.flush()
def should_do_markup(file): def should_do_markup(file):
if os.environ.get('PY_COLORS') == '1': if os.environ.get("PY_COLORS") == "1":
return True return True
if os.environ.get('PY_COLORS') == '0': if os.environ.get("PY_COLORS") == "0":
return False return False
return hasattr(file, 'isatty') and file.isatty() \ return (
and os.environ.get('TERM') != 'dumb' \ hasattr(file, "isatty")
and not (sys.platform.startswith('java') and os._name == 'nt') and file.isatty()
and os.environ.get("TERM") != "dumb"
and not (sys.platform.startswith("java") and os._name == "nt")
)
class TerminalWriter(object): class TerminalWriter(object):
_esctable = dict(black=30, red=31, green=32, yellow=33, _esctable = dict(
blue=34, purple=35, cyan=36, white=37, black=30,
Black=40, Red=41, Green=42, Yellow=43, red=31,
Blue=44, Purple=45, Cyan=46, White=47, green=32,
bold=1, light=2, blink=5, invert=7) 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,
)
# XXX deprecate stringio argument # XXX deprecate stringio argument
def __init__(self, file=None, stringio=False, encoding=None): def __init__(self, file=None, stringio=False, encoding=None):
@ -152,11 +182,12 @@ class TerminalWriter(object):
else: else:
from sys import stdout as file from sys import stdout as file
elif py.builtin.callable(file) and not ( elif py.builtin.callable(file) and not (
hasattr(file, "write") and hasattr(file, "flush")): hasattr(file, "write") and hasattr(file, "flush")
):
file = WriteFile(file, encoding=encoding) file = WriteFile(file, encoding=encoding)
if hasattr(file, "isatty") and file.isatty() and colorama: if hasattr(file, "isatty") and file.isatty() and colorama:
file = colorama.AnsiToWin32(file).stream file = colorama.AnsiToWin32(file).stream
self.encoding = encoding or getattr(file, 'encoding', "utf-8") self.encoding = encoding or getattr(file, "encoding", "utf-8")
self._file = file self._file = file
self.hasmarkup = should_do_markup(file) self.hasmarkup = should_do_markup(file)
self._lastlen = 0 self._lastlen = 0
@ -165,7 +196,7 @@ class TerminalWriter(object):
@property @property
def fullwidth(self): def fullwidth(self):
if hasattr(self, '_terminal_width'): if hasattr(self, "_terminal_width"):
return self._terminal_width return self._terminal_width
return get_terminal_width() return get_terminal_width()
@ -198,15 +229,14 @@ class TerminalWriter(object):
def _escaped(self, text, esc): def _escaped(self, text, esc):
if esc and self.hasmarkup: if esc and self.hasmarkup:
text = (''.join(['\x1b[%sm' % cod for cod in esc]) + text = "".join(["\x1b[%sm" % cod for cod in esc]) + text + "\x1b[0m"
text +'\x1b[0m')
return text return text
def markup(self, text, **kw): def markup(self, text, **kw):
esc = [] esc = []
for name in kw: for name in kw:
if name not in self._esctable: if name not in self._esctable:
raise ValueError("unknown markup: %r" %(name,)) raise ValueError("unknown markup: %r" % (name,))
if kw[name]: if kw[name]:
esc.append(self._esctable[name]) esc.append(self._esctable[name])
return self._escaped(text, tuple(esc)) return self._escaped(text, tuple(esc))
@ -227,7 +257,7 @@ class TerminalWriter(object):
# i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
# 2*len(sepchar)*N <= fullwidth - len(title) - 2 # 2*len(sepchar)*N <= fullwidth - len(title) - 2
# N <= (fullwidth - len(title) - 2) // (2*len(sepchar)) # N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
N = max((fullwidth - len(title) - 2) // (2*len(sepchar)), 1) N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
fill = sepchar * N fill = sepchar * N
line = "%s %s %s" % (fill, title, fill) line = "%s %s %s" % (fill, title, fill)
else: else:
@ -256,10 +286,10 @@ class TerminalWriter(object):
write_out(self._file, markupmsg) write_out(self._file, markupmsg)
def _update_chars_on_current_line(self, text_or_bytes): def _update_chars_on_current_line(self, text_or_bytes):
newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' newline = b"\n" if isinstance(text_or_bytes, bytes) else "\n"
current_line = text_or_bytes.rsplit(newline, 1)[-1] current_line = text_or_bytes.rsplit(newline, 1)[-1]
if isinstance(current_line, bytes): if isinstance(current_line, bytes):
current_line = current_line.decode('utf-8', errors='replace') current_line = current_line.decode("utf-8", errors="replace")
if newline in text_or_bytes: if newline in text_or_bytes:
self._chars_on_current_line = len(current_line) self._chars_on_current_line = len(current_line)
self._width_of_current_line = get_line_width(current_line) self._width_of_current_line = get_line_width(current_line)
@ -267,17 +297,17 @@ class TerminalWriter(object):
self._chars_on_current_line += len(current_line) self._chars_on_current_line += len(current_line)
self._width_of_current_line += get_line_width(current_line) self._width_of_current_line += get_line_width(current_line)
def line(self, s='', **kw): def line(self, s="", **kw):
self.write(s, **kw) self.write(s, **kw)
self._checkfill(s) self._checkfill(s)
self.write('\n') self.write("\n")
def reline(self, line, **kw): def reline(self, line, **kw):
if not self.hasmarkup: if not self.hasmarkup:
raise ValueError("cannot use rewrite-line without terminal") raise ValueError("cannot use rewrite-line without terminal")
self.write(line, **kw) self.write(line, **kw)
self._checkfill(line) self._checkfill(line)
self.write('\r') self.write("\r")
self._lastlen = len(line) self._lastlen = len(line)
def _checkfill(self, line): def _checkfill(self, line):
@ -285,6 +315,7 @@ class TerminalWriter(object):
if diff2last > 0: if diff2last > 0:
self.write(" " * diff2last) self.write(" " * diff2last)
class Win32ConsoleWriter(TerminalWriter): class Win32ConsoleWriter(TerminalWriter):
def write(self, msg, **kw): def write(self, msg, **kw):
if msg: if msg:
@ -299,17 +330,17 @@ class Win32ConsoleWriter(TerminalWriter):
oldcolors = GetConsoleInfo(handle).wAttributes oldcolors = GetConsoleInfo(handle).wAttributes
default_bg = oldcolors & 0x00F0 default_bg = oldcolors & 0x00F0
attr = default_bg attr = default_bg
if kw.pop('bold', False): if kw.pop("bold", False):
attr |= FOREGROUND_INTENSITY attr |= FOREGROUND_INTENSITY
if kw.pop('red', False): if kw.pop("red", False):
attr |= FOREGROUND_RED attr |= FOREGROUND_RED
elif kw.pop('blue', False): elif kw.pop("blue", False):
attr |= FOREGROUND_BLUE attr |= FOREGROUND_BLUE
elif kw.pop('green', False): elif kw.pop("green", False):
attr |= FOREGROUND_GREEN attr |= FOREGROUND_GREEN
elif kw.pop('yellow', False): elif kw.pop("yellow", False):
attr |= FOREGROUND_GREEN|FOREGROUND_RED attr |= FOREGROUND_GREEN | FOREGROUND_RED
else: else:
attr |= oldcolors & 0x0007 attr |= oldcolors & 0x0007
@ -318,6 +349,7 @@ class Win32ConsoleWriter(TerminalWriter):
if oldcolors: if oldcolors:
SetConsoleTextAttribute(handle, oldcolors) SetConsoleTextAttribute(handle, oldcolors)
class WriteFile(object): class WriteFile(object):
def __init__(self, writemethod, encoding=None): def __init__(self, writemethod, encoding=None):
self.encoding = encoding self.encoding = encoding
@ -339,39 +371,46 @@ if win32_and_ctypes:
# ctypes access to the Windows console # ctypes access to the Windows console
STD_OUTPUT_HANDLE = -11 STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12 STD_ERROR_HANDLE = -12
FOREGROUND_BLACK = 0x0000 # black text FOREGROUND_BLACK = 0x0000 # black text
FOREGROUND_BLUE = 0x0001 # text color contains blue. FOREGROUND_BLUE = 0x0001 # text color contains blue.
FOREGROUND_GREEN = 0x0002 # text color contains green. FOREGROUND_GREEN = 0x0002 # text color contains green.
FOREGROUND_RED = 0x0004 # text color contains red. FOREGROUND_RED = 0x0004 # text color contains red.
FOREGROUND_WHITE = 0x0007 FOREGROUND_WHITE = 0x0007
FOREGROUND_INTENSITY = 0x0008 # text color is intensified. FOREGROUND_INTENSITY = 0x0008 # text color is intensified.
BACKGROUND_BLACK = 0x0000 # background color black BACKGROUND_BLACK = 0x0000 # background color black
BACKGROUND_BLUE = 0x0010 # background color contains blue. BACKGROUND_BLUE = 0x0010 # background color contains blue.
BACKGROUND_GREEN = 0x0020 # background color contains green. BACKGROUND_GREEN = 0x0020 # background color contains green.
BACKGROUND_RED = 0x0040 # background color contains red. BACKGROUND_RED = 0x0040 # background color contains red.
BACKGROUND_WHITE = 0x0070 BACKGROUND_WHITE = 0x0070
BACKGROUND_INTENSITY = 0x0080 # background color is intensified. BACKGROUND_INTENSITY = 0x0080 # background color is intensified.
SHORT = ctypes.c_short SHORT = ctypes.c_short
class COORD(ctypes.Structure): class COORD(ctypes.Structure):
_fields_ = [('X', SHORT), _fields_ = [("X", SHORT), ("Y", SHORT)]
('Y', SHORT)]
class SMALL_RECT(ctypes.Structure): class SMALL_RECT(ctypes.Structure):
_fields_ = [('Left', SHORT), _fields_ = [
('Top', SHORT), ("Left", SHORT),
('Right', SHORT), ("Top", SHORT),
('Bottom', SHORT)] ("Right", SHORT),
("Bottom", SHORT),
]
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
_fields_ = [('dwSize', COORD), _fields_ = [
('dwCursorPosition', COORD), ("dwSize", COORD),
('wAttributes', wintypes.WORD), ("dwCursorPosition", COORD),
('srWindow', SMALL_RECT), ("wAttributes", wintypes.WORD),
('dwMaximumWindowSize', COORD)] ("srWindow", SMALL_RECT),
("dwMaximumWindowSize", COORD),
]
_GetStdHandle = ctypes.windll.kernel32.GetStdHandle _GetStdHandle = ctypes.windll.kernel32.GetStdHandle
_GetStdHandle.argtypes = [wintypes.DWORD] _GetStdHandle.argtypes = [wintypes.DWORD]
_GetStdHandle.restype = wintypes.HANDLE _GetStdHandle.restype = wintypes.HANDLE
def GetStdHandle(kind): def GetStdHandle(kind):
return _GetStdHandle(kind) return _GetStdHandle(kind)
@ -379,11 +418,13 @@ if win32_and_ctypes:
SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD] SetConsoleTextAttribute.argtypes = [wintypes.HANDLE, wintypes.WORD]
SetConsoleTextAttribute.restype = wintypes.BOOL SetConsoleTextAttribute.restype = wintypes.BOOL
_GetConsoleScreenBufferInfo = \ _GetConsoleScreenBufferInfo = ctypes.windll.kernel32.GetConsoleScreenBufferInfo
ctypes.windll.kernel32.GetConsoleScreenBufferInfo _GetConsoleScreenBufferInfo.argtypes = [
_GetConsoleScreenBufferInfo.argtypes = [wintypes.HANDLE, wintypes.HANDLE,
ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
]
_GetConsoleScreenBufferInfo.restype = wintypes.BOOL _GetConsoleScreenBufferInfo.restype = wintypes.BOOL
def GetConsoleInfo(handle): def GetConsoleInfo(handle):
info = CONSOLE_SCREEN_BUFFER_INFO() info = CONSOLE_SCREEN_BUFFER_INFO()
_GetConsoleScreenBufferInfo(handle, ctypes.byref(info)) _GetConsoleScreenBufferInfo(handle, ctypes.byref(info))
@ -396,6 +437,7 @@ if win32_and_ctypes:
# and the ending \n causes an empty line to display. # and the ending \n causes an empty line to display.
return info.dwSize.Y, info.dwSize.X - 1 return info.dwSize.Y, info.dwSize.X - 1
def write_out(fil, msg): def write_out(fil, msg):
# XXX sometimes "msg" is of type bytes, sometimes text which # XXX sometimes "msg" is of type bytes, sometimes text which
# complicates the situation. Should we try to enforce unicode? # complicates the situation. Should we try to enforce unicode?