Realtime In-Place Logger

#codesnippet #python

Useful for analysis of data that's changing in real time. Prints properties of interest in-place to the terminal, preventing the need to keep up with quickly scrolling log lines.

See also: the same code in JS.

import sys

def clear_lines(lines):
    for _ in range(lines):
        sys.stdout.write("\x1b[2K")  # Clear the current line
        sys.stdout.write("\x1b[A")  # Move cursor up one line
    sys.stdout.write("\x1b[2K") # Clear the previous line
    sys.stdout.flush()

class SingletonMeta(type):
    """
    A metaclass for creating singleton classes.
    """
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # If an instance does not already exist, create one and store it.
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class RealtimeLogger(metaclass=SingletonMeta):
    def __init__(self, num_properties: int):
        self.num_properties = num_properties
        self.has_printed = False
        self._data = {}

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def print(self):
        keys = list(self._data.keys())
        if len(keys) != self.num_properties:
            print(f"Warning: Set num_properties to {len(keys)}")
        if self.has_printed:
            clear_lines(self.num_properties)
        
        for i in range(self.num_properties):
            k = keys[i]
            print(f'{k}: {self._data[k]}')

        self.has_printed = True

Example

viz = RealtimeLogger(2)

viz['my variable'] = 42
viz['Something else'] = 50

viz.print()

for i in range(6):
    viz['my variable'] = i
    viz.print()

Prints the following. Notice that, despite the fact that print() is invoked 6 times, there are only two log lines. This is because viz only has 2 properties, and they're printed in-place upon each invocation.

my variable: 5
Something else: 50