Skip to content

Instantly share code, notes, and snippets.

@wolph
Last active June 25, 2023 21:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wolph/ddaaa4fe3dc454d38e381a30e0843a74 to your computer and use it in GitHub Desktop.
Save wolph/ddaaa4fe3dc454d38e381a30e0843a74 to your computer and use it in GitHub Desktop.
Little python script to wrap around pyright to add indenting, reformatting and making it easier to read. It also adds pretty colours
#!/usr/bin/env python -u
import os
import re
import sys
import itertools
import contextlib
import subprocess
sys.stdout.reconfigure(line_buffering=True)
def fg(color: int):
def _fg(*args: str, end: str = '\n', write: bool = True):
text = ''.join(args) + end
text = f'\033[{color}m{text}\033[0m'
if write:
sys.stdout.write(text)
return text
return _fg
black = fg(30)
red = fg(31)
green = fg(32)
yellow = fg(33)
blue = fg(34)
magenta = fg(35)
cyan = fg(36)
white = fg(37)
MAX_WIDTH = 120
MESSAGE_RE = re.compile(
r'''
^
(?P<prefix>\s+)
((?P<message_a>Keyword\sparameter\s"[^"]+".+?|.+?)\s)?
"(?P<type>.+?)"\s*
((?P<message_b>.+)\s*
"(?P<expected>.+?)")?
(\s+\((?P<message_type>.+?)\)|)
$
''',
re.VERBOSE,
)
FILENAME_RE = re.compile(
r'''
^(?P<info>(?P<prefix>\s+)
(?P<filename>[^:]+):(?P<lineno>\d+):(?P<charno>\d+))
\s-\serror:\s(?P<message>.+)
$
''',
re.VERBOSE,
)
QUOTED_RE = re.compile(r'(".+?")')
TOKEN_RE = re.compile(r'( -> |[|(),\[\]\{\}])')
TOKEN_START = set('([{')
TOKEN_END = set(')]}')
def run_pyright():
if 'test' in sys.argv:
command = ['cat', 'pyright.txt'] + sys.argv[1:]
else:
command = ['pyright'] + sys.argv[1:]
pyright_process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Clear before the output
sys.stdout.write('\033c')
clear = True
for line in pyright_process.stdout:
if clear:
# pytest --watch doesn't always show all output so clearing the
# screen makes it partially unusable
# sys.stdout.write('\033c')
clear = False
# Update terminal width at every run
terminal_width = min(os.get_terminal_size().columns, MAX_WIDTH)
if line.startswith('Watching for file changes'):
clear = True
# non-breaking spaces make debugging harder
line = line.replace('\xa0', ' ')
if line.startswith('/'):
green(line, end='')
continue
elif match := FILENAME_RE.match(line):
green(match.group('info'))
line = match.group('prefix') + match.group('message') + '\n'
if match := MESSAGE_RE.match(line):
g = match.groupdict().get
current = g('type') or ''
expected = g('expected') or ''
prefix = g('prefix') or ''
parts = [prefix]
# If we have a really short message we don't need to expand it
if len(current.split()) == 1 and len(expected.split()) == 1:
white(color_quotes(line), end='')
continue
if len(current) < terminal_width * 0.4 and len(expected) < terminal_width * 0.4:
white(color_quotes(line), end='')
continue
if not expected and len(current) < terminal_width * 0.8:
white(color_quotes(line), end='')
continue
if not current and len(expected) < terminal_width * 0.8:
white(color_quotes(line), end='')
continue
if g('message_type'):
parts.append(cyan(g('message_type'), end=': ', write=False))
if g('message_a'):
parts += [color_quotes(g('message_a').capitalize()), ' ']
if current and expected:
parts.append(red('A ', end='', write=False))
if g('message_b'):
parts += [color_quotes(g('message_b').capitalize()), ' ']
if current and expected:
parts.append(blue('B ', end='', write=False))
white(*parts)
if not expected:
expand(current, prefix)
elif len(prefix) + max(len(current), len(expected)) > terminal_width:
diff_wide(current, expected, prefix)
else:
diff_narrow(current, expected, prefix)
else:
print(color_quotes(line), end='')
continue
pyright_process.wait()
exit_code = pyright_process.returncode
sys.exit(exit_code)
def color_quotes(text: str):
def color_match(match):
return yellow(match.group(), end='', write=False)
return QUOTED_RE.sub(color_match, text)
def indent(level=0):
return ' ' * level
def get_params(text: str):
output = []
level = 0
suboutput = []
for token in TOKEN_RE.split(text):
if not token:
continue
token = token.rstrip()
# To prevent too verbose output We don't split into multiple lines for sublevels
prefix = ''
if level == 1:
prefix = indent(level)
indented_token = prefix + token
if token in TOKEN_START:
# If we are at the top level, we can add the suboutput to the output
if level <= 0:
output.append(indented_token)
else:
suboutput.append(token)
level += 1
elif token in TOKEN_END:
level -= 1
# If we are at the top level, we can add the suboutput to the output
if level == 0:
if suboutput:
output.append(prefix + ''.join(suboutput) + ',')
output.append(token.strip())
suboutput.clear()
else:
suboutput.append(token)
elif token == '->' and level == 0:
# Put the `->`` on a new line
assert not suboutput
output.append(token)
elif token == ',' and level == 1:
# Only split by `,` on the top level
output.append(prefix + ''.join(suboutput) + token)
suboutput.clear()
elif token == '|' and level == 1:
# Only split by `|` on the top level
output.append(prefix + ''.join(suboutput))
suboutput.clear()
suboutput.append(token)
else:
suboutput.append(token)
if suboutput:
output.append(''.join(suboutput))
# Replace: `)\n -> \n(` with `) -> (`
# Disable if you don't want that behaviour
i = len(output)
while i > 3:
a, b, c = output[i - 3 : i]
if a == ')' and b == '->' and c:
output[i - 3 : i] = [f'{a} {b} {c}']
i -= 2
i -= 1
return output
def diff_wide(current: str, expected: str, prefix: str):
# Get the width minus the | split in the middle per column
width = (os.get_terminal_size().columns - 3) // 2
current = get_params(current)
expected = get_params(expected)
for a, b in itertools.zip_longest(current, expected, fillvalue=''):
line = f'{(prefix + a):{width}} | {(prefix + b):{width}}'
if a == b:
green(line)
else:
red(line)
def diff_narrow(current: str, expected: str, prefix: str):
output_a = []
output_b = []
for a, b in itertools.zip_longest(
current.partition(' -> '),
expected.partition(' -> '),
fillvalue='',
):
longest = max(len(a), len(b))
output_a.append(f'{a:<{longest}}')
output_b.append(f'{b:<{longest}}')
red(prefix, 'A: ', *output_a)
blue(prefix, 'B: ', *output_b)
def expand(text: str, prefix: str = ''):
for param in get_params(text):
yellow(prefix, param)
with contextlib.suppress(KeyboardInterrupt):
run_pyright()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment