Skip to content

Commit

Permalink
Update color support
Browse files Browse the repository at this point in the history
  • Loading branch information
avylove committed Mar 15, 2020
1 parent 721998e commit 08b6b1a
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 75 deletions.
1 change: 1 addition & 0 deletions doc/spelling_wordlist.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
downconverted
iterable
iterables
natively
Expand Down
101 changes: 61 additions & 40 deletions enlighten/_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
except ImportError: # pragma: no cover(Python 2)
from collections import Iterable

from blessed.colorspace import X11_COLORNAMES_TO_RGB

COUNTER_FMT = u'{desc}{desc_pad}{count:d} {unit}{unit_pad}' + \
u'[{elapsed}, {rate:.2f}{unit_pad}{unit}/s]{fill}'

Expand All @@ -40,9 +42,9 @@
except AttributeError: # pragma: no cover(Non-standard Terminal)
pass

COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white')
COLORS_16 = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
'bright_black', 'bright_red', 'bright_green', 'bright_yellow',
'bright_blue', 'bright_magenta', 'bright_cyan', 'bright_white')

try:
BASESTRING = basestring
Expand Down Expand Up @@ -80,36 +82,53 @@ class BaseCounter(object):
"""
Args:
manager(:py:class:`Manager`): Manager instance. Required.
color(str): Color as a string or number 0 - 255 (Default: None)
color(str): Color as a string or RGB tuple (Default: None)
Base class for counters
"""

__slots__ = ('color', '_color', 'count', 'manager', 'start_count')
__slots__ = ('_color', 'count', 'manager', 'start_count')

def __init__(self, **kwargs):

self.count = self.start_count = kwargs.get('count', 0)

self._color = None
self.color = kwargs.get('color', None)
if self.color is None:
pass
elif isinstance(self.color, BASESTRING):
if self.color not in COLORS:
raise ValueError('Unsupported color: %s' % self.color)
elif isinstance(self.color, int):
if self.color < 0 or self.color > 255:
raise ValueError('Unsupported color: %s' % self.color)
else:
raise TypeError('color must be a string or integer')

self._color = None

self.manager = kwargs.get('manager', None)
if self.manager is None:
raise TypeError('manager must be specified')

self.color = kwargs.get('color', None)

@property
def color(self):
"""
Color property
Preferred to be a string or iterable of three integers for RGB
Single integer supported for backwards compatibility
"""

color = self._color
return color if color is None else color[0]

@color.setter
def color(self, value):

if value is None:
self._color = None
elif isinstance(value, int) and 0 <= value <= 255:
self._color = (value, self.manager.term.color(value))
elif isinstance(value, BASESTRING):
if value not in COLORS_16 or value not in X11_COLORNAMES_TO_RGB:
raise AttributeError('Invalid color specified: %s' % value)
self._color = (value, getattr(self.manager.term, value))
elif isinstance(value, Iterable) and \
len(value) == 3 and \
all(isinstance(_, int) and 0 <= _ <= 255 for _ in value):
self._color = (value, self.manager.term.color_rgb(*value))
else:
raise AttributeError('Invalid color specified: %s' % repr(value))

def _colorize(self, content):
"""
Args:
Expand All @@ -121,23 +140,14 @@ def _colorize(self, content):
Format ``content`` with the color specified for this progress bar
If no color is specified for this instance, the content is returned unmodified
The color discovery within this method is caching to improve performance
"""

if self.color is None:
# No color specified
if self._color is None:
return content

if self._color and self._color[0] == self.color:
return self._color[1](content)

if self.color in COLORS:
spec = getattr(self.manager.term, self.color)
else:
spec = self.manager.term.color(self.color)

self._color = (self.color, spec)
return spec(content)
# Used spec cached by color.setter
return self._color[1](content)

def update(self, incr=1, force=False):
"""
Expand Down Expand Up @@ -176,12 +186,12 @@ class SubCounter(BaseCounter):
"""

__slots__ = ('all_fields', 'color', 'parent')
__slots__ = ('all_fields', 'parent')

def __init__(self, parent, color=None, count=0, all_fields=False):
"""
Args:
color(str): Series color as a string or integer see :ref:`Series Color <series_color>`
color(str): Series color as a string or RGB tuple see :ref:`Series Color <series_color>`
count(int): Initial count (Default: 0)
all_fields(bool): Populate ``rate`` and ``eta`` fields (Default: False)
"""
Expand Down Expand Up @@ -256,7 +266,7 @@ class Counter(BaseCounter):
bar_format(str): Progress bar format, see :ref:`Format <counter_format>` below
count(int): Initial count (Default: 0)
counter_format(str): Counter format, see :ref:`Format <counter_format>` below
color(str): Series color as a string or integer see :ref:`Series Color <series_color>` below
color(str): Series color as a string or RGB tuple see :ref:`Series Color <series_color>`
desc(str): Description
enabled(bool): Status (Default: :py:data:`True`)
leave(True): Leave progress bar after closing (Default: :py:data:`True`)
Expand Down Expand Up @@ -322,9 +332,14 @@ class can be called directly. The output stream will default to :py:data:`sys.st
The characters specified by ``series`` will be displayed in the terminal's current
foreground color. This can be overwritten with the ``color`` argument.
``color`` can be specified as :py:data:`None`, a string or an integer 0 - 255.
While most modern terminals can support 256 colors, the actual number of supported
colors will vary.
``color`` can be specified as :py:data:`None`, a :py:mod:`string` or, an :py:term:`iterable`
of three integers, 0 - 255, describing an RGB color.
For backward compatibility, a color can be expressed as an integer 0 - 255, but this
is deprecated in favor of named or RGB colors.
If a terminal is not capable of 24-bit color, and is given a color outside of its
range, the color will be downconverted to a supported color.
Valid colors for 8 color terminals:
Expand All @@ -348,6 +363,12 @@ class can be called directly. The output stream will default to :py:data:`sys.st
- bright_white
- bright_yellow
See this `chart <https://blessed.readthedocs.io/en/latest/colors.html#id3>`_
for a complete list of supported color strings.
.. note::
If an invalid color is specified, an :py:exc:`AttributeError` will be raised
.. _counter_format:
**Format**
Expand Down Expand Up @@ -476,7 +497,7 @@ class can be called directly. The output stream will default to :py:data:`sys.st
"""
# pylint: disable=too-many-instance-attributes

__slots__ = ('additional_fields', 'bar_format', 'color', 'counter_format', 'desc', 'enabled',
__slots__ = ('additional_fields', 'bar_format', 'counter_format', 'desc', 'enabled',
'last_update', 'leave', 'manager', 'min_delta', 'offset', 'series', 'start',
'total', 'unit', '_subcounters')

Expand Down Expand Up @@ -765,7 +786,7 @@ def update(self, incr=1, force=False):
def add_subcounter(self, color, count=0, all_fields=False):
"""
Args:
color(str): Series color as a string or integer see :ref:`Series Color <series_color>`
color(str): Series color as a string or RGB tuple see :ref:`Series Color <series_color>`
count(int): Initial count (Default: 0)
all_fields(bool): Populate ``rate`` and ``eta`` formatting fields (Default: False)
Expand Down
9 changes: 5 additions & 4 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,18 @@ spelling-dict=en_US
# List of comma separated words that should not be checked.
spelling-ignore-words=
Avram, ansicon, Args, assertRaisesRegexp, assertRegexpMatches, assertNotRegexpMatches, attr,
AttributeError,
BaseManager, bool,
desc, downloader,
Enlighten's,
desc, downconverted, downloader,
Enlighten's, exc,
html,
incr, IPC,
incr, IPC, iterable,
kwargs,
len, Lubkin,
meth, Mozilla, MPL,
noqa,
pragma, py,
redirector, resize, resizing,
redirector, resize, resizing, RGB,
setscroll, sphinxcontrib, ss, stdout, stderr, str, subcounter, subcounters, submodule,
subprocesses, sys,
TestCase, tty, TTY, tuple,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from setup_helpers import get_version, readme

INSTALL_REQUIRES = ['blessed>=1.16.1']
INSTALL_REQUIRES = ['blessed>=1.17.2']
TESTS_REQUIRE = ['mock; python_version < "3.3"',
'unittest2; python_version < "2.7"']

Expand Down
65 changes: 36 additions & 29 deletions tests/test_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,53 +76,60 @@ def test_no_manager(self):
with self.assertRaisesRegex(TypeError, 'manager must be specified'):
enlighten._counter.BaseCounter()

def test_color(self):
"""Color must be a valid string or int 0 - 255"""
def test_color_invalid(self):
"""Color must be a valid string, RGB, or int 0 - 255"""
# Unsupported type
with self.assertRaisesRegex(TypeError, 'color must be a string or integer'):
enlighten._counter.BaseCounter(manager=self.manager, color=[])
with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 1.0'):
enlighten._counter.BaseCounter(manager=self.manager, color=1.0)

# Color is a string
counter = enlighten._counter.BaseCounter(manager=self.manager, color='red')
self.assertEqual(counter.color, 'red')
with self.assertRaisesRegex(ValueError, 'Unsupported color: banana'):
enlighten._counter.BaseCounter(manager=self.manager, color='banana')
# Invalid String
with self.assertRaisesRegex(AttributeError, 'Invalid color specified: buggersnot'):
enlighten._counter.BaseCounter(manager=self.manager, color='buggersnot')

# Color is an integer
counter = enlighten._counter.BaseCounter(manager=self.manager, color=15)
self.assertEqual(counter.color, 15)
with self.assertRaisesRegex(ValueError, 'Unsupported color: -1'):
# Invalid integer
with self.assertRaisesRegex(AttributeError, 'Invalid color specified: -1'):
enlighten._counter.BaseCounter(manager=self.manager, color=-1)
with self.assertRaisesRegex(ValueError, 'Unsupported color: 256'):
with self.assertRaisesRegex(AttributeError, 'Invalid color specified: 256'):
enlighten._counter.BaseCounter(manager=self.manager, color=256)

# Invalid iterable
with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[\]'):
enlighten._counter.BaseCounter(manager=self.manager, color=[])
with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \[1\]'):
enlighten._counter.BaseCounter(manager=self.manager, color=[1])
with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2\)'):
enlighten._counter.BaseCounter(manager=self.manager, color=(1, 2))
with self.assertRaisesRegex(AttributeError, r'Invalid color specified: \(1, 2, 3, 4\)'):
enlighten._counter.BaseCounter(manager=self.manager, color=(1, 2, 3, 4))

def test_colorize_none(self):
"""If color is None, return content unchanged"""
counter = enlighten._counter.BaseCounter(manager=self.manager)
self.assertEqual(counter._colorize('test'), 'test')

def test_colorize(self):
"""Return string formatted with color"""
# Color is a string
def test_colorize_string(self):
"""Return string formatted with color (string)"""
counter = enlighten._counter.BaseCounter(manager=self.manager, color='red')
self.assertIsNone(counter._color)
self.assertEqual(counter.color, 'red')
self.assertEqual(counter._color, ('red', self.manager.term.red))
self.assertNotEqual(counter._colorize('test'), 'test')
cache = counter._color
self.assertEqual(counter._colorize('test'), self.manager.term.red('test'))
self.assertEqual(counter._color[0], 'red')
self.assertIs(counter._color[1], self.manager.term.red)
self.assertIs(counter._color, cache)

# Color is an integer
def test_colorize_int(self):
"""Return string formatted with color (int)"""
counter = enlighten._counter.BaseCounter(manager=self.manager, color=40)
self.assertIsNone(counter._color)
self.assertEqual(counter.color, 40)
self.assertEqual(counter._color, (40, self.manager.term.color(40)))
self.assertNotEqual(counter._colorize('test'), 'test')
cache = counter._color
self.assertEqual(counter._colorize('test'), self.manager.term.color(40)('test'))
self.assertEqual(counter._color[0], 40)
# New instance is generated each time, so just compare strings
self.assertEqual(counter._color[1], self.manager.term.color(40))
self.assertIs(counter._color, cache)

def test_colorize_rgb(self):
"""Return string formatted with color (RGB)"""
counter = enlighten._counter.BaseCounter(manager=self.manager, color=(50, 40, 60))
self.assertEqual(counter.color, (50, 40, 60))
self.assertEqual(counter._color, ((50, 40, 60), self.manager.term.color_rgb(50, 40, 60)))
self.assertNotEqual(counter._colorize('test'), 'test')
self.assertEqual(counter._colorize('test'), self.manager.term.color_rgb(50, 40, 60)('test'))

def test_call(self):
"""Returns generator when used as a function"""
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ commands =
[testenv:el7]
basepython = python2.7
deps =
blessed == 1.16.1
blessed == 1.17.2
mock == 1.0.1
# setuptools == 0.9.8 (Doesn't support PEP 508)
setuptools == 20.2.2
Expand Down

0 comments on commit 08b6b1a

Please sign in to comment.