Post

Piping JSON to a pseudo-terminal in Python for guaranteed color highlighting

I usually print my Python data structures encoded as JSON to make them more readable:

1
2
3
4
import json

def print_data(value):
    print(json.dumps(value, indent=2, default=repr))

This is great, but sometimes I miss colors that one would normally have in shell scripts or Makefile targets piping output to jq or yq. There is actually a neat and easy way to get the colors in Python scripts too:

1
2
3
4
import os

os.system('echo true | yq')
os.system('echo null | jq')

that one could use in a helper function:

1
2
3
4
import subprocess

def color_print(value):
    subprocess.run(['yq', '-P'], input=json.dumps(value).encode()) 

The colors are visible almost everywhere except when executing scripts directly in PyCharm. Even then one could change the default Run/Debug configuration template to use terminal emulation when running Python scripts.

However, this is not known and used by everyone, so to ensure that the script output is always colorized, one could use pseudo-terminal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import select

UTF = 'utf-8', 'surrogatepass'


def read(fd, process):
    result = bytearray()
    while True:
        # Wait until file descriptor is ready for I/O
        r, _, _ = select.select([fd], [], [], 0.1)
        if r:
            chunk = os.read(fd, 1024)
            if not chunk:  # EOF
                break
            result += chunk
        elif process.poll() is not None:  # Process ended
            break
    return result.decode(*UTF)


def pty_pipe(string, to):
    alpha, bravo = os.openpty()
    proc = subprocess.Popen(
        to, stdin=subprocess.PIPE, stdout=bravo, stderr=subprocess.PIPE, bufsize=0, text=False,
    )
    proc.stdin.write(string.encode(*UTF))
    proc.stdin.close()
    if proc.returncode:
        raise Exception(proc.stderr.read().decode(*UTF).strip())
    result = read(alpha, proc).strip()
    proc.wait()
    os.close(alpha)
    os.close(bravo)
    return result

and our color print function becomes:

1
2
3
def color_print(value):
    text = json.dumps(value, ensure_ascii=False)
    print(pty_pipe(text, to=['yq', '-P']))

Enjoy!

Comments

This post is licensed under CC BY 4.0 by the author.