├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── setup.cfg ├── setup.py ├── style ├── __init__.py ├── ansi.py ├── styled_string.py └── styled_string_builder.py └── tests ├── __init__.py └── test_style.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | env/ 4 | env[23]/ 5 | *__pycache__/ 6 | *.egg-info/ 7 | *.vscode/ 8 | 9 | *.pyc 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.6 5 | 6 | cache: pip 7 | 8 | before_script: 9 | - pip install pycodestyle 10 | script: 11 | - pycodestyle style tests setup.py 12 | - python -m unittest discover 13 | - VERSION=$(python -c "import style;print(style.__version__)") 14 | - PYPI_VERSION=$(curl -s https://pypi.org/pypi/style/json | jq -r .info.version) 15 | 16 | deploy: 17 | provider: pypi 18 | distributions: sdist bdist_wheel --universal 19 | user: $PYPI_USER 20 | password: $PYPI_PASSWORD 21 | on: 22 | condition: $(printf "$VERSION\n$PYPI_VERSION" | sort -V | head -n 1) != $VERSION 23 | branch: master 24 | python: 3.6 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 lmittmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | style 2 | ===== 3 | 4 | |Build Status| |PyPI version| 5 | 6 | **style** is a simple terminal string styling package. Its API is a port of the popular 7 | `chalk `__ package for javascript. 8 | 9 | 10 | Install 11 | ------- 12 | 13 | :: 14 | 15 | $ pip install style 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | .. code:: py 22 | 23 | import style 24 | 25 | print(style.red('Hello', style.bold('world') + '!')) 26 | 27 | 28 | API 29 | --- 30 | 31 | style.\ ``style*[.style](*objects, sep=' ')`` 32 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | Chain `styles <#styles>`__ and call the last one as a method with an argument. Order doesn't matter, and later styles 35 | take precedence in case of a conflict, e.g. ``style.red.yellow.green`` is equivalent to ``style.green``. Styles can 36 | be nested. 37 | 38 | Multiple arguments will be separated by ``sep``, a space by default. 39 | 40 | style.\ ``enabled`` 41 | ~~~~~~~~~~~~~~~~~~~ 42 | 43 | Color support is automatically detected, but can also be changed manually. 44 | 45 | - set ``style.enabled`` to ``True`` or ``False`` 46 | - use the command line parameter ``--color`` or ``--no-color`` 47 | 48 | 49 | Styles 50 | ------ 51 | 52 | +---------------------+-------------------------------------+-------------------------------------------+ 53 | | Modifiers | Colors | Background colors | 54 | +=====================+===============+=====================+==================+========================+ 55 | | - ``bold`` | - ``black`` | - ``light_black`` | - ``on_black`` | - ``on_light_black`` | 56 | | - ``dim`` | - ``red`` | - ``light_red`` | - ``on_red`` | - ``on_light_red`` | 57 | | - ``italic`` | - ``green`` | - ``light_green`` | - ``on_green`` | - ``on_light_green`` | 58 | | - ``underline`` | - ``yellow`` | - ``light_yellow`` | - ``on_yellow`` | - ``on_light_yellow`` | 59 | | - ``inverse`` | - ``blue`` | - ``light_blue`` | - ``on_blue`` | - ``on_light_blue`` | 60 | | - ``hidden`` | - ``magenta`` | - ``light_magenta`` | - ``on_magenta`` | - ``on_light_magenta`` | 61 | | - ``strikethrough`` | - ``cyan`` | - ``light_cyan`` | - ``on_cyan`` | - ``on_light_cyan`` | 62 | | | - ``white`` | - ``light_white`` | - ``on_white`` | - ``on_light_white`` | 63 | +---------------------+---------------+---------------------+------------------+------------------------+ 64 | 65 | 66 | .. |Build Status| image:: https://travis-ci.com/lmittmann/style.svg?branch=master 67 | :target: https://travis-ci.com/lmittmann/style 68 | .. |PyPI version| image:: https://img.shields.io/pypi/v/style.svg 69 | :target: https://pypi.org/project/style 70 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = style 3 | version = 1.1.6 4 | description = 🌈 Terminal string styling 5 | long_description = file: README.rst 6 | keywords = style, color, ansi, terminal styling, chalk 7 | url = https://github.com/lmittmann/style 8 | author = lmittmann 9 | license = MIT 10 | classifiers = 11 | Development Status :: 5 - Production/Stable 12 | Intended Audience :: Developers 13 | License :: OSI Approved :: MIT License 14 | Natural Language :: English 15 | Programming Language :: Python :: 2.7 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.5 18 | Programming Language :: Python :: 3.6 19 | 20 | [options] 21 | zip_safe = True 22 | packages = style 23 | 24 | [pycodestyle] 25 | max_line_length = 99 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /style/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pkg_resources 3 | 4 | from style.styled_string_builder import _StyledStringBuilder 5 | 6 | try: 7 | __version__ = pkg_resources.get_distribution('style').version 8 | except Exception: 9 | __version__ = 'unknown' 10 | 11 | _enabled = sys.stdout.isatty() 12 | if '--color' in sys.argv: 13 | _enabled = True 14 | elif '--no-color' in sys.argv: 15 | _enabled = False 16 | 17 | styled_string_builder = _StyledStringBuilder([], True) 18 | styled_string_builder.enabled = _enabled 19 | styled_string_builder.__version__ = __version__ 20 | sys.modules[__name__] = styled_string_builder 21 | -------------------------------------------------------------------------------- /style/ansi.py: -------------------------------------------------------------------------------- 1 | _styles = { 2 | 'default': (0, 0), 3 | 'bold': (1, 22), 4 | 'dim': (2, 22), 5 | 'italic': (3, 23), 6 | 'underline': (4, 24), 7 | 'inverse': (7, 27), 8 | 'hidden': (8, 28), 9 | 'strikethrough': (9, 29), 10 | 11 | 'black': (30, 39), 12 | 'red': (31, 39), 13 | 'green': (32, 39), 14 | 'yellow': (33, 39), 15 | 'blue': (34, 39), 16 | 'magenta': (35, 39), 17 | 'cyan': (36, 39), 18 | 'white': (37, 39), 19 | 'light_black': (90, 39), 20 | 'light_red': (91, 39), 21 | 'light_green': (92, 39), 22 | 'light_yellow': (93, 39), 23 | 'light_blue': (94, 39), 24 | 'light_magenta': (95, 39), 25 | 'light_cyan': (96, 39), 26 | 'light_white': (97, 39), 27 | 28 | 'on_black': (40, 49), 29 | 'on_red': (41, 49), 30 | 'on_green': (42, 49), 31 | 'on_yellow': (43, 49), 32 | 'on_blue': (44, 49), 33 | 'on_magenta': (45, 49), 34 | 'on_cyan': (46, 49), 35 | 'on_white': (47, 49), 36 | 'on_light_black': (100, 49), 37 | 'on_light_red': (101, 49), 38 | 'on_light_green': (102, 49), 39 | 'on_light_yellow': (103, 49), 40 | 'on_light_blue': (104, 49), 41 | 'on_light_magenta': (105, 49), 42 | 'on_light_cyan': (106, 49), 43 | 'on_light_white': (107, 49) 44 | } 45 | -------------------------------------------------------------------------------- /style/styled_string.py: -------------------------------------------------------------------------------- 1 | import style 2 | 3 | 4 | class _StyledString(str): 5 | 6 | def __new__(cls, style_list, sep, *objects): 7 | return super(_StyledString, cls).__new__(cls, sep.join([str(obj) for obj in objects])) 8 | 9 | def __init__(self, style_list, sep, *objects): 10 | self._style_start = ';'.join([str(s[0]) for s in style_list]) 11 | self._style_end = ';'.join([str(s[1]) for s in style_list]) 12 | self._sep = sep 13 | self._objects = objects 14 | 15 | def __add__(self, other): 16 | return self.__str__() + str(other) 17 | 18 | def __str__(self): 19 | if style._StyledStringBuilder._enabled: 20 | string = '' 21 | for i, obj in enumerate(self._objects): 22 | if i > 0: 23 | string += self._sep 24 | 25 | if type(obj) is _StyledString: 26 | string += '%s\033[%sm' % (obj, self._style_start) 27 | else: 28 | string += str(obj) 29 | return '\033[%sm%s\033[%sm' % (self._style_start, string, self._style_end) 30 | return super(_StyledString, self).__str__() 31 | 32 | def rjust(self, width, fillchar=' '): 33 | n_chars = width - len(self) 34 | if n_chars > 0: 35 | string = str(self) 36 | return string.rjust(len(string) + n_chars, fillchar) 37 | return self 38 | 39 | def ljust(self, width, fillchar=' '): 40 | n_chars = width - len(self) 41 | if n_chars > 0: 42 | string = str(self) 43 | return string.ljust(len(string) + n_chars, fillchar) 44 | return self 45 | -------------------------------------------------------------------------------- /style/styled_string_builder.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from style.ansi import _styles 4 | from style.styled_string import _StyledString 5 | 6 | 7 | _module_name = __name__.split('.')[0] 8 | 9 | 10 | class _StyledStringBuilder(types.ModuleType): 11 | _enabled = True 12 | 13 | @classmethod 14 | def enabled(cls): 15 | return cls._enabled 16 | 17 | @classmethod 18 | def enabled(cls, value): 19 | cls._enabled = value 20 | 21 | def __init__(self, style_list, is_root=False): 22 | super(_StyledStringBuilder, self).__init__(_module_name) 23 | self._style_list = style_list 24 | self._is_root = is_root 25 | 26 | def __call__(self, *objects, **kwargs): 27 | if self._is_root: 28 | raise TypeError('%r object is not callable' % self.__class__.__bases__[0].__name__) 29 | 30 | sep = kwargs.get('sep', ' ') 31 | if type(sep) is not str: 32 | raise TypeError('sep must be None or a string, not %r' % sep.__class__.__name__) 33 | 34 | return _StyledString(self._style_list, sep, *objects) 35 | 36 | def __getattr__(self, attr): 37 | if attr in _styles: 38 | if self._is_root: 39 | new_style_list = self._style_list[:] 40 | new_style_list.append(_styles[attr]) 41 | return _StyledStringBuilder(new_style_list) 42 | self._style_list.append(_styles[attr]) 43 | return self 44 | raise AttributeError('%r object has no attribute %r' % (self.__class__.__name__, attr)) 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmittmann/style/4439b854fdd31958e993612c2d14d7db19be0361/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_style.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import style 4 | 5 | 6 | class StyleTestCase(unittest.TestCase): 7 | 8 | def test_argument_on_root_style_builder(self): 9 | # test if a call of the root StyleBuilder raises a TypeError 10 | with self.assertRaises(TypeError): 11 | style('test') 12 | 13 | def test_single_string(self): 14 | # test styling of single string 15 | self.assertIn('test', style.red('test')) 16 | self.assertIn('31', str(style.red('test'))) 17 | 18 | def test_multiple_strings(self): 19 | # test styling of multiple strings 20 | self.assertIn('test1 test2', style.red('test1', 'test2')) 21 | self.assertIn('31', str(style.red('test1', 'test2'))) 22 | 23 | def test_non_string_arguments(self): 24 | # test styling of multiple arguments that are not strings 25 | self.assertIn('1 True 0.1', style.red(1, True, 0.1)) 26 | self.assertIn('31', str(style.red(1, True, 0.1))) 27 | 28 | def test_seperator(self): 29 | # test custom seperator 30 | self.assertIn('test1, test2', style.red('test1', 'test2', sep=', ')) 31 | 32 | def test_non_string_seperator(self): 33 | # test if a non string seperator raises a TypeError 34 | with self.assertRaises(TypeError): 35 | style.red('test1', 'test2', sep=0) 36 | 37 | def test_style_chaining(self): 38 | # test that chaining style attributes works 39 | self.assertIn('31;47;1', str(style.red.on_white.bold('test'))) 40 | self.assertIn('47;31;1', str(style.on_white.red.bold('test'))) 41 | self.assertIn('47;1;31', str(style.on_white.bold.red('test'))) 42 | 43 | def test_len(self): 44 | # test if the lenght is independet of the style 45 | styled_string = style.red('test') 46 | 47 | self.assertEqual(len(styled_string), len('test')) 48 | self.assertTrue(len(str(styled_string)) > len(styled_string)) 49 | 50 | def test_enabling(self): 51 | # test manually enabling and disabling 52 | style.enabled = False 53 | self.assertEqual('test', style.red('test')) 54 | 55 | style.enabled = True 56 | self.assertIn('test', str(style.red('test'))) 57 | self.assertIn('31', str(style.red('test'))) 58 | --------------------------------------------------------------------------------