Last active
June 25, 2023 21:35
-
-
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
This file contains hidden or 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
#!/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