How can I add ANSI escape codes during formatting in Python

81 Views Asked by At

I have output lines that contain various elements that I want to display with different colors using ANSI escape codes.

  • I cannot add the escape codes before formatting because the length of the escape codes is taken in account by the Python formatter, and this won't work with numbers or conversions such as !r anyway.
  • I can format every item separately as a single string, add the escape codes, and splice all these together to create the final string but this makes for bulky code.
  • I can add the escape codes in the format string but this makes it quite unreadable and hard to maintain (where did I use the red?)
  • I can improve on this by putting the escape codes in strings and add these to the variables used in the formatting, but this pollutes the "true variables" list (and I have to add before/after sequences...)

There has got to be a better way?

1

There are 1 best solutions below

0
xenoid On

It turns out there is, by using a subclass of the Python string.Formatter.

It appears that the standard Formatter parses out the format specs in a rather lenient way, so if one adds a color specification such as the :blue in {hexNum:04x:blue}, the format spec downstream is 04x:blue, so in the overridden format_field(value,format_spec) method it can be split into the "standard" part that is passed to the default formatter, and the "extra" part that is used to post-process the string returned by the default formatter.

In practice:

# This implementation uses a dictionary of functions to define
# the post-processing. The extra format spec is a key in the dictionary
class BracketingFormatter(string.Formatter):
    def __init__(self,bracketings):
        string.Formatter.__init__(self)
        self.bracketings=bracketings

    # Override the formatting of the standard formatter
    # It turns out that the format parser is quite lenient on the format 
    # so our additions to the format specs are faithfully forwarded 
    # and the `format_spec` is still something like '04d:blue'
    def format_field(self,value,format_spec):
        # Cut out added bracket indication if any
        spec=format_spec.split(':')
        # Apply standard formatter
        formatted=string.Formatter.format_field(self,value,spec[0])
        # Add bracketing to formatted result if recognized
        if (len(spec) > 1) and spec[1] in self.bracketings:
            return self.bracketings[spec[1]](formatted)
        else:
            return formatted

Example of use:

# Some demo ANSI coloring
def ansiColor(s):
    def bracket(x):
        return  '\033[0;'+s+'m'+x+'\033[0;0m'
    return bracket

ansiBrackets={
     'red':    ansiColor('31'),  
     'green':  ansiColor('32'),
     'blue':   ansiColor('34'),
     'purple': ansiColor('35'),
     'cyan':   ansiColor('36'),
     'gray':   ansiColor('37'),
}

# We can use an empty dictionary if we don't want bracketing to occur
# for instance when output is to a non-TTY stream.
formatter=BracketingFormatter(ansiBrackets if os.isatty(sys.stdout.fileno()) else {})

# Version with ordinals
print(formatter.vformat("{0:s:purple} : {1:8.5f:blue} | {2!r:^14s:green} | {3:04x:red}",["abc",17./13.,"ABCDEF",42],None))

# Version with names
print(formatter.vformat("{s1:s:purple} : {decimal:8.5f:blue} | {s2!r:^14s:green} | {integer:04x:red}",None,{'s1':"abc",'decimal':17./13.,'s2':"ABCDEF",'integer':42}))

# Anonymous version
print(formatter.vformat("{:s:purple} : {:8.5f:blue} | {!r:^14s:green} | {:04x:red}",["abc",17./13.,"ABCDEF",42],None))

# For comparison the output without ANSI codes
print("{0:s} : {1:8.5f} | {2!r:^14s} | {3:04x}".format("abc",17./13.,"ABCDEF",42))

The rendered output:

enter image description here

Caveat: this works with my Python 3.10.12, I didn't try it with other versions