-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
dependency: replace colorama with custom module
* In stretch there is a regression in colorama in conjunction with tqdm that leads to a slow down of the progress of the script proportional to the amount of data printed to stdout/err. Colorama starts having very huge stacktraces and the process is stuck at 100% CPU for an increasingly amount of time while more data is printed. * Given the very simple usage of colors that is made in Cumin as of now, it seems much more feasible to replace the colorama library (as all that cross-OS support is not needed) and add a simple module with ANSI escape sequence support. * Use a type (metaclass) to be able to override __getattr__ for the staticmethods of the classes that use it and to automatically define a method for each color in a DRY way without code duplication. * Define a Colored class that uses ColoredType as metaclass to inherit its type with the custom behaviour. * For each color defined in ColoredType.COLORS a method of Colored is defined, e.g. Colored.red(). * The Colored class has a `disabled` property that can be set to True to globally disable coloring. This could for example be integrated later into the CLI as an option to disable colors or allow to add some code to the color.py module to autodetect when not in a TTY and automatically disable all colors. Bug: T217038 Change-Id: I053972e92b7cc6cf7821de8ecff484d01be03794
- Loading branch information
Showing
7 changed files
with
145 additions
and
47 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
"""Colors module.""" | ||
from abc import ABCMeta | ||
|
||
|
||
class ColoredType(ABCMeta): | ||
"""Metaclass to define a new type that dynamically adds static methods to its classes.""" | ||
|
||
COLORS = { | ||
'red': 31, | ||
'green': 32, | ||
'yellow': 33, | ||
'blue': 34, | ||
'cyan': 36, | ||
} | ||
""":py:class:`dict`: a mapping of colors to the ANSI foreground color code.""" | ||
|
||
def __getattr__(cls, name): # noqa: N805 (Prospector reqires an older version of pep8-naming) | ||
"""Dynamically check access to members of classes of this type. | ||
:Parameters: | ||
according to Python's Data model :py:meth:`object.__getattr__`. | ||
""" | ||
color_code = ColoredType.COLORS.get(name, None) | ||
if color_code is None: | ||
raise AttributeError("'{cls}' object has no attribute '{attr}'".format(cls=cls.__name__, attr=name)) | ||
|
||
return lambda obj: cls._color(color_code, obj) | ||
|
||
|
||
class Colored(metaclass=ColoredType): | ||
"""Class to manage colored output. | ||
Available methods are dynamically added based on the keys of the :py:const:`ColoredType.COLORS` dictionary. | ||
For each color a method with the color name is available to color any object with that specific color code. | ||
""" | ||
|
||
disabled = False | ||
""":py:class:`bool`: switch to globally control the coloring. Set it to :py:const`True` to disable all coloring.""" | ||
|
||
@staticmethod | ||
def _color(color_code, obj): | ||
"""Color the given object, unless coloring is globally disabled. | ||
Arguments: | ||
color_code (int): a valid ANSI escape sequence color code. | ||
obj (mixed): the object to color. | ||
Return: | ||
str: the string representation of the object encapsulated in the red ANSI escape sequence. | ||
""" | ||
message = str(obj) | ||
|
||
if not message: | ||
return '' | ||
|
||
if Colored.disabled: | ||
return message | ||
|
||
return '\x1b[{code}m{message}\x1b[39m'.format(code=color_code, message=message) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
"""Color tests.""" | ||
from unittest import mock | ||
|
||
import pytest | ||
|
||
from cumin.color import Colored | ||
|
||
|
||
def test_red(): | ||
"""It should return the message enclosed in ASCII red color code.""" | ||
assert Colored.red('message') == '\x1b[31mmessage\x1b[39m' | ||
|
||
|
||
def test_green(): | ||
"""It should return the message enclosed in ASCII green color code.""" | ||
assert Colored.green('message') == '\x1b[32mmessage\x1b[39m' | ||
|
||
|
||
def test_yellow(): | ||
"""It should return the message enclosed in ASCII yellow color code.""" | ||
assert Colored.yellow('message') == '\x1b[33mmessage\x1b[39m' | ||
|
||
|
||
def test_blue(): | ||
"""It should return the message enclosed in ASCII blue color code.""" | ||
assert Colored.blue('message') == '\x1b[34mmessage\x1b[39m' | ||
|
||
|
||
def test_cyan(): | ||
"""It should return the message enclosed in ASCII cyan color code.""" | ||
assert Colored.cyan('message') == '\x1b[36mmessage\x1b[39m' | ||
|
||
|
||
def test_wrong_case(): | ||
"""It should raise AttributeError if called with the wrong case.""" | ||
with pytest.raises(AttributeError, match="'Colored' object has no attribute 'Red'"): | ||
Colored.Red('') | ||
|
||
|
||
def test_non_existent(): | ||
"""It should raise AttributeError if called with a non existent color.""" | ||
with pytest.raises(AttributeError, match="'Colored' object has no attribute 'missing'"): | ||
Colored.missing('') | ||
|
||
|
||
def test_emtpy(): | ||
"""It should return an empty string if the object is empty.""" | ||
assert Colored.red('') == '' | ||
|
||
|
||
@mock.patch('cumin.color.Colored.disabled', new_callable=mock.PropertyMock) | ||
def test_disabled(mocked_colored_disabled): | ||
"""It should return the message untouched if coloration is disabled.""" | ||
mocked_colored_disabled.return_value = True | ||
assert Colored.red('message') == 'message' | ||
assert mocked_colored_disabled.called |
This file contains 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
This file contains 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
This file contains 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
This file contains 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