Wednesday, July 22, 2015

Plumbum Color

I've been working on a color addition to Plumbum for a little while, and I'd like to share the basics of using it with you now. This library was originally built around a special str subclass, but now is built on the new Styles representation and is far more powerful than the first implementation. It safely does nothing if you do not have a color-compatible systems (posix + tty currently), but can be forced if need be. It is included with Plumbum, so you don't have to add a requirement for your scripts that is non-essential (as color often is). It is integrated with plumbum.cli, too. Also, I've managed to accelerate the color selection algorithims about 8x, allowing near game-like speeds. (see the fullcolor.py example).

Note: The plumbum.colors library was written for the terminal and ANSI escape sequences. However, the following article was written in the IPython notebook, so it cannot show ANSI escapes. Due to the fact that the plumbum.colors library uses a flexible Styles based representation, HTML is easy to implement as a Styles subclass and is available as htmlcolors (see docs for an example). With the plumbum.colorlib IPython extension, IPython loads the %%to html magic and makes plumbum.colorlib.htmlcolors available as colors. htmlcolors does not support colors.reset (and therefore using colors directly as a context manager, as well), but otherwise it is very similar. If we are constructing strings, we'll need to remember to include HTML line breaks, so we'll redefine print too.

In [2]:
%load_ext plumbum.colorlib
from functools import partial
print = partial(print, end='<br/>\n')

Plumbum.colors

Safe color manipulations made easy

This is a quick overview and tutorial on the plumbum.colors library that is being proposed for Plumbum. It allows you to do things like this:

In [3]:
%%to html
with colors.blue:
    print("This is in Blue.")
print("But this is not.")
This is in Blue.
But this is not.

It works through the COLOR object, which gives you access to styles, and the terminal colors through the forground and background objects. You can wrap a string with the | operator:

In [4]:
%%to html
print(colors.red | "This is in red", '... but not this')
print("You can change the background too!" | colors.bg.light_yellow)
This is in red ... but not this
You can change the background too!

The color can go on either side of the string you are wrapping.

Styles are available, too:

In [5]:
%%to html
with colors.green:
    print(colors.underline | "This is underlined", "and this is still green!")
This is underlined and this is still green!

You can combine styles, and the result is still a valid style:

In [6]:
%%to html
mix = colors.bold & colors.italics & colors.red & colors.bg.light_green
print(mix | "This is a muddle of styles!")
with (colors.strikeout & colors.red):
    print("Twin styles")
This is a muddle of styles!
Twin styles

All the major ANSI represetations are supported, include Basic (the first 8 colors), Simple (the first 16 colors), Full (256 colors using three parameter color codes), and True (24 bit color, using 5 parameter color codes). You can even find the closest color in a lower representation if you need to.

In [7]:
%%to html
print(colors.dark_blue | 'This is from the extended color set.')
print(colors['LIGHT_SEA_GREEN'] | 'And another one.')
print(colors.rgb(193,41,210) | 'This supports all colors!')
print(colors["#3AB227"] | 'Hex notation, too.')
This is from the extended color set.
And another one.
This supports all colors!
Hex notation, too.

The full list is on the plumbum.colors ReadTheDocs page.

As a quick shortcut, you use .print directly on color (.print_ if you are using the classic print statment in Python 2):

In [10]:
%%to html
colors.orchid.print("This is in orchid.")
colors.bg.magenta.print("This is on a magenta background.")
This is in orchid.
This is on a magenta background.

The colors can be iterated and sliced:

In [12]:
%%to html
for color in colors.fg[:16]:
    print(color | "This is color:", color.fg.name.upper())
This is color: BLACK
This is color: RED
This is color: GREEN
This is color: YELLOW
This is color: BLUE
This is color: MAGENTA
This is color: CYAN
This is color: LIGHT_GRAY
This is color: DARK_GRAY
This is color: LIGHT_RED
This is color: LIGHT_GREEN
This is color: LIGHT_YELLOW
This is color: LIGHT_BLUE
This is color: LIGHT_MAGENTA
This is color: LIGHT_CYAN
This is color: WHITE
In [14]:
%%to html
for color in colors:
    color.print("&#x25a0;", end=' ')

Finally, you can also use [] notation to wrap a color (less convenient, but similar to other methods):

In [19]:
%%to html
print(colors.blue['This is wrapped in blue'])
This is wrapped in blue

Unsafe color manipulations, too

Sometimes, you will find unsafe manipulations faster than wrapping every string. This can be done with plumbum.colors, too.

If you are planning unsafe manipulations, you can wrap your code in a context manager that restores color to your terminal. For example,

with colors:
    ...unsafe color operations...

All styles will be restored on leaving the manager. The color will automatically be reset when Python quits, as well, even on an exception, so this is not necessary, but is useful in local code.

Also, you can restore or set color instantly using the emergency restore from a terminal:

$ python -m plumbum.colorlib

This takes any string that you can use in colors, and without a string, it restores color.

The string representation of a color is the ANSI sequence that would produce the color. If you want to instantly write a color to the terminal, you can call the color without arguments. So, either of the following would change the color to blue without restoring it:

print(colors.blue, end='')
colors.blue()

To get the reset color, you can either use the .reset property on a factory or a style, or you can use ~ (inversion). So, this would be a manual color safe wrapping from unsafe components:

In [21]:
%%to html
print("Before " + colors.red + "Middle" + ~colors.red + " After")
print("Before " + colors.blue + "Middle" + ~colors.fg + " After")
print("Before " + colors.green + "Middle" + colors.fg.reset + " After")
Before Middle After
Before Middle After
Before Middle After

Details of the Style Factories:

Let's look at the contents of a colors.fg or colors.bg object:

In [22]:
{x for x in dir(colors.bg) if x[0] != '_'}
Out[22]:
{'ansi',
 'black',
 'blue',
 'cyan',
 'dark_gray',
 'full',
 'green',
 'hex',
 'light_blue',
 'light_cyan',
 'light_gray',
 'light_green',
 'light_magenta',
 'light_red',
 'light_yellow',
 'magenta',
 'red',
 'reset',
 'rgb',
 'simple',
 'white',
 'yellow'}

Notice that the extended colors are not listed, to make completion easier. Also, color access is not case sensitive and ignores underscores.

Since the colors object looks like a fg object, let's only look at the unique contents (.reset has a different meaning for colors, as it resets the terminal completly instead of just the foreground color, so let's remove it from the fg set):

In [23]:
fg = {x for x in dir(colors.bg) if x[0] != '_' and x!='reset'}
col = {x for x in dir(colors) if x[0] != '_'}
col - fg
Out[23]:
{'bg',
 'bold',
 'code',
 'contains_colors',
 'do_nothing',
 'em',
 'extract',
 'fatal',
 'fg',
 'filter',
 'from_ansi',
 'get_colors_from_string',
 'highlight',
 'info',
 'italics',
 'li',
 'load_stylesheet',
 'ol',
 'reset',
 'stdout',
 'strikeout',
 'success',
 'title',
 'underline',
 'use_color',
 'warn'}

Note that the properties are generated based on the attributes allowed for a style, so HTML has some slight differences here vs. ANSI.

Stylesheets

A recent addition to colors is stylesheets. Stylesheets allow you to use and create styles based on usage. The default sheet is the following:

default_styles = dict(
    warn="fg red",
    title="fg cyan underline bold",
    fatal="fg red bold",
    highlight="bg yellow",
    info="fg blue",
    success="fg green",
    )

You can load a new sheet or a changed sheet with colors.load_stylesheet(default_styles). The new and changed styles will be accessable just like any normal color.

Bonus

The cell magic we've been using is actually a slight upgrade on the following example:

We are going to make a quick cell magic for IPython to capture output and render html from it. It's really not hard to make a cell magic in IPython:

In [29]:
from io import StringIO
from IPython.display import display_html
from contextlib import redirect_stdout
from IPython.core.magic import register_cell_magic
In [28]:
@register_cell_magic
def output_html(line, cell):
    "Captures stdout and renders it in the notebook as html."
    out = StringIO()
    with redirect_stdout(out):
        exec(cell)
    out.seek(0)
    display_html(out.getvalue(), raw=True)

Let's test this to make sure it works:

In [30]:
%%output_html
print("<p>Wow!</p>")

Wow!


No comments:

Post a Comment