├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── assets └── screenshot.png ├── pastel ├── __init__.py ├── pastel.py ├── stack.py └── style.py ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_pastel.py ├── test_stack.py └── test_style.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = pastel/version.py 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | _build 9 | .cache 10 | *.so 11 | 12 | # Installer logs 13 | pip-log.txt 14 | 15 | # Unit test / coverage reports 16 | .coverage 17 | .tox 18 | nosetests.xml 19 | 20 | .DS_Store 21 | .idea/* 22 | 23 | /test.py 24 | /test_*.py 25 | profile.html 26 | /wheelhouse 27 | 28 | .python-version 29 | setup.py 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | - "3.7" 7 | - "pypy" 8 | 9 | 10 | install: 11 | - | 12 | if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then 13 | export PYENV_ROOT="$HOME/.pyenv" 14 | if [ -f "$PYENV_ROOT/bin/pyenv" ]; then 15 | pushd "$PYENV_ROOT" && git pull && popd 16 | else 17 | rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" 18 | fi 19 | export PYPY_VERSION="5.6.0" 20 | "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION" 21 | virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" 22 | source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" 23 | fi 24 | - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py 25 | - python get-poetry.py --preview -y 26 | - source $HOME/.poetry/env 27 | - poetry install 28 | 29 | script: poetry run pytest -q tests/ 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Sébastien Eustace 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pastel: Bring colors to your terminal 2 | ##################################### 3 | 4 | Pastel is a simple library to help you colorize strings in your terminal. 5 | 6 | It comes bundled with predefined styles: 7 | 8 | * ``info``: green 9 | * ``comment``: yellow 10 | * ``question``: black on cyan 11 | * ``error``: white on red 12 | 13 | .. image:: https://raw.githubusercontent.com/sdispater/pastel/master/assets/screenshot.png 14 | 15 | 16 | Features 17 | ======== 18 | 19 | * Use predefined styles or add you own. 20 | * Disable colors all together by calling ``with_colors(False)``. 21 | * Automatically disables colors if the output is not a TTY. 22 | * Used in `cleo `_. 23 | * Supports Python **2.7+**, **3.5+** and **PyPy**. 24 | 25 | 26 | Usage 27 | ===== 28 | 29 | .. code-block:: python 30 | 31 | >>> import pastel 32 | >>> print(pastel.colorize('Information')) 33 | 'Information' # Green string by default 34 | >>> print(pastel.colorize('This is bold red')) 35 | 'This is bold red' 36 | 37 | 38 | Installation 39 | ============ 40 | 41 | .. code-block:: 42 | 43 | pip install pastel 44 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdispater/pastel/760a8e52dd989dfd47c72e82776657c530c8c1b0/assets/screenshot.png -------------------------------------------------------------------------------- /pastel/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = "0.2.1" 4 | 5 | from .pastel import Pastel 6 | 7 | 8 | _PASTEL = Pastel(True) 9 | 10 | 11 | def colorize(message): 12 | """ 13 | Formats a message to a colorful string. 14 | 15 | :param message: The message to format. 16 | :type message: str 17 | 18 | :rtype: str 19 | """ 20 | with _PASTEL.colorized(): 21 | return _PASTEL.colorize(message) 22 | 23 | 24 | def with_colors(colorized): 25 | """ 26 | Enable or disable colors. 27 | 28 | :param decorated: Whether to active colors or not. 29 | :type decorated: bool 30 | 31 | :rtype: None 32 | """ 33 | _PASTEL.with_colors(colorized) 34 | 35 | 36 | def add_style(name, fg=None, bg=None, options=None): 37 | """ 38 | Adds a new style. 39 | 40 | :param name: The name of the style 41 | :type name: str 42 | 43 | :param fg: The foreground color 44 | :type fg: str or None 45 | 46 | :param bg: The background color 47 | :type bg: str or None 48 | 49 | :param options: The style options 50 | :type options: list or str or None 51 | """ 52 | _PASTEL.add_style(name, fg, bg, options) 53 | 54 | 55 | def remove_style(name): 56 | """ 57 | Removes a style. 58 | 59 | :param name: The name of the style to remove. 60 | :type name: str 61 | 62 | :rtype: None 63 | """ 64 | _PASTEL.remove_style(name) 65 | 66 | 67 | def pastel(colorized=True): 68 | """ 69 | Returns a new Pastel instance. 70 | 71 | :rtype: Pastel 72 | """ 73 | return Pastel(colorized) 74 | -------------------------------------------------------------------------------- /pastel/pastel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import sys 5 | from contextlib import contextmanager 6 | 7 | from .style import Style 8 | from .stack import StyleStack 9 | 10 | 11 | class Pastel(object): 12 | 13 | TAG_REGEX = "[a-z][a-z0-9,_=;-]*" 14 | FULL_TAG_REGEX = re.compile("(?isx)<(({}) | /({})?)>".format(TAG_REGEX, TAG_REGEX)) 15 | 16 | def __init__(self, colorized=False): 17 | self._colorized = colorized 18 | self._style_stack = StyleStack() 19 | self._styles = {} 20 | 21 | self.add_style("error", "white", "red") 22 | self.add_style("info", "green") 23 | self.add_style("comment", "yellow") 24 | self.add_style("question", "black", "cyan") 25 | 26 | @classmethod 27 | def escape(cls, text): 28 | return re.sub("(?is)([^\\\\]?)<", "\\1\\<", text) 29 | 30 | @contextmanager 31 | def colorized(self, colorized=None): 32 | is_colorized = self.is_colorized() 33 | 34 | if colorized is None: 35 | colorized = sys.stdout.isatty() and is_colorized 36 | 37 | self.with_colors(colorized) 38 | 39 | yield 40 | 41 | self.with_colors(is_colorized) 42 | 43 | def with_colors(self, colorized): 44 | self._colorized = colorized 45 | 46 | def is_colorized(self): 47 | return self._colorized 48 | 49 | def add_style(self, name, fg=None, bg=None, options=None): 50 | style = Style(fg, bg, options) 51 | 52 | self._styles[name] = style 53 | 54 | def has_style(self, name): 55 | return name in self._styles 56 | 57 | def style(self, name): 58 | if self.has_style(name): 59 | return self._styles[name] 60 | 61 | def remove_style(self, name): 62 | if not self.has_style(name): 63 | raise ValueError("Invalid style {}".format(name)) 64 | 65 | del self._styles[name] 66 | 67 | def colorize(self, message): 68 | output = "" 69 | tags = [] 70 | i = 0 71 | for m in self.FULL_TAG_REGEX.finditer(message): 72 | if i > 0: 73 | p = tags[i - 1] 74 | tags[i - 1] = (p[0], p[1], p[2], p[3], m.start(0)) 75 | 76 | tags.append((m.group(0), m.end(0), m.group(1), m.group(3), None)) 77 | 78 | i += 1 79 | 80 | if not tags: 81 | return message.replace("\\<", "<") 82 | 83 | offset = 0 84 | for t in tags: 85 | prev_offset = offset 86 | offset = t[1] 87 | endpos = t[4] if t[4] else -1 88 | text = t[0] 89 | if prev_offset < offset - len(text): 90 | output += self._apply_current_style( 91 | message[prev_offset : offset - len(text)] 92 | ) 93 | 94 | if offset != 0 and "\\" == message[offset - len(text) - 1]: 95 | output += self._apply_current_style(text) 96 | continue 97 | 98 | # opening tag? 99 | open = "/" != text[1] 100 | if open: 101 | tag = t[2] 102 | else: 103 | tag = t[3] if t[3] else "" 104 | 105 | style = self._create_style_from_string(tag.lower()) 106 | if not open and not tag: 107 | # 108 | self._style_stack.pop() 109 | elif style is False: 110 | output += self._apply_current_style(text) 111 | elif open: 112 | self._style_stack.push(style) 113 | else: 114 | self._style_stack.pop(style) 115 | 116 | # add the text up to the next tag 117 | output += self._apply_current_style(message[offset:endpos]) 118 | offset += len(message[offset:endpos]) 119 | 120 | output += self._apply_current_style(message[offset:]) 121 | 122 | return output.replace("\\<", "<") 123 | 124 | def _create_style_from_string(self, string): 125 | if string in self._styles: 126 | return self._styles[string] 127 | 128 | matches = re.findall("([^=]+)=([^;]+)(;|$)", string.lower()) 129 | if not len(matches): 130 | return False 131 | 132 | style = Style() 133 | 134 | for match in matches: 135 | if match[0] == "fg": 136 | style.set_foreground(match[1]) 137 | elif match[0] == "bg": 138 | style.set_background(match[1]) 139 | else: 140 | try: 141 | for option in match[1].split(","): 142 | style.set_option(option.strip()) 143 | except ValueError: 144 | return False 145 | 146 | return style 147 | 148 | def _apply_current_style(self, text): 149 | if self.is_colorized() and len(text): 150 | return self._style_stack.get_current().apply(text) 151 | else: 152 | return text 153 | -------------------------------------------------------------------------------- /pastel/stack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .style import Style 4 | 5 | 6 | class StyleStack(object): 7 | def __init__(self, empty_style=None): 8 | self.empty_style = empty_style or Style() 9 | self.reset() 10 | 11 | def reset(self): 12 | self.styles = list() 13 | 14 | def push(self, style): 15 | self.styles.append(style) 16 | 17 | def pop(self, style=None): 18 | if not len(self.styles): 19 | return self.empty_style 20 | 21 | if not style: 22 | return self.styles.pop() 23 | 24 | for i, stacked_style in enumerate(reversed(self.styles)): 25 | if style == stacked_style: 26 | self.styles = self.styles[: len(self.styles) - 1 - i] 27 | 28 | return stacked_style 29 | 30 | raise ValueError("Incorrectly nested style tag found.") 31 | 32 | def get_current(self): 33 | if not len(self.styles): 34 | return self.empty_style 35 | 36 | return self.styles[-1] 37 | -------------------------------------------------------------------------------- /pastel/style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from collections import OrderedDict 4 | 5 | 6 | class Style(object): 7 | 8 | FOREGROUND_COLORS = { 9 | "black": 30, 10 | "red": 31, 11 | "green": 32, 12 | "yellow": 33, 13 | "blue": 34, 14 | "magenta": 35, 15 | "cyan": 36, 16 | "light_gray": 37, 17 | "default": 39, 18 | "dark_gray": 90, 19 | "light_red": 91, 20 | "light_green": 92, 21 | "light_yellow": 93, 22 | "light_blue": 94, 23 | "light_magenta": 95, 24 | "light_cyan": 96, 25 | "white": 97, 26 | } 27 | 28 | BACKGROUND_COLORS = { 29 | "black": 40, 30 | "red": 41, 31 | "green": 42, 32 | "yellow": 43, 33 | "blue": 44, 34 | "magenta": 45, 35 | "cyan": 46, 36 | "light_gray": 47, 37 | "default": 49, 38 | "dark_gray": 100, 39 | "light_red": 101, 40 | "light_green": 102, 41 | "light_yellow": 103, 42 | "light_blue": 104, 43 | "light_magenta": 105, 44 | "light_cyan": 106, 45 | "white": 107, 46 | } 47 | 48 | OPTIONS = { 49 | "bold": 1, 50 | "dark": 2, 51 | "italic": 3, 52 | "underline": 4, 53 | "blink": 5, 54 | "reverse": 7, 55 | "conceal": 8, 56 | } 57 | 58 | def __init__(self, foreground=None, background=None, options=None): 59 | self._fg = foreground 60 | self._bg = background 61 | self._foreground = None 62 | self._background = None 63 | 64 | if foreground: 65 | self.set_foreground(foreground) 66 | 67 | if background: 68 | self.set_background(background) 69 | 70 | options = options or [] 71 | if not isinstance(options, list): 72 | options = [options] 73 | 74 | self.set_options(options) 75 | 76 | @property 77 | def foreground(self): 78 | return self._fg 79 | 80 | @property 81 | def background(self): 82 | return self._bg 83 | 84 | @property 85 | def options(self): 86 | return list(self._options.values()) 87 | 88 | def set_foreground(self, foreground): 89 | if foreground not in self.FOREGROUND_COLORS: 90 | raise ValueError( 91 | 'Invalid foreground specified: "{}". Expected one of ({})'.format( 92 | foreground, ", ".join(self.FOREGROUND_COLORS.keys()) 93 | ) 94 | ) 95 | 96 | self._foreground = self.FOREGROUND_COLORS[foreground] 97 | 98 | def set_background(self, background): 99 | if background not in self.FOREGROUND_COLORS: 100 | raise ValueError( 101 | 'Invalid background specified: "{}". Expected one of ({})'.format( 102 | background, ", ".join(self.BACKGROUND_COLORS.keys()) 103 | ) 104 | ) 105 | 106 | self._background = self.BACKGROUND_COLORS[background] 107 | 108 | def set_option(self, option): 109 | if option not in self.OPTIONS: 110 | raise ValueError( 111 | 'Invalid option specified: "{}". Expected one of ({})'.format( 112 | option, ", ".join(self.OPTIONS.keys()) 113 | ) 114 | ) 115 | 116 | if option not in self._options: 117 | self._options[self.OPTIONS[option]] = option 118 | 119 | def unset_option(self, option): 120 | if not option in self.OPTIONS: 121 | raise ValueError( 122 | 'Invalid option specified: "{}". Expected one of ({})'.format( 123 | option, ", ".join(self.OPTIONS.keys()) 124 | ) 125 | ) 126 | 127 | del self._options[self.OPTIONS[option]] 128 | 129 | def set_options(self, options): 130 | self._options = OrderedDict() 131 | 132 | for option in options: 133 | self.set_option(option) 134 | 135 | def apply(self, text): 136 | codes = [] 137 | 138 | if self._foreground: 139 | codes.append(self._foreground) 140 | 141 | if self._background: 142 | codes.append(self._background) 143 | 144 | if len(self._options): 145 | codes += list(self._options.keys()) 146 | 147 | if not len(codes): 148 | return text 149 | 150 | return "\033[%sm%s\033[0m" % (";".join(map(str, codes)), text) 151 | 152 | def __eq__(self, other): 153 | return ( 154 | other._foreground == self._foreground 155 | and other._background == self._background 156 | and other._options == self._options 157 | ) 158 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "20.1.0" 20 | description = "Classes Without Boilerplate" 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 24 | 25 | [package.extras] 26 | dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] 27 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 28 | tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 29 | 30 | [[package]] 31 | name = "backports.functools-lru-cache" 32 | version = "1.6.1" 33 | description = "Backport of functools.lru_cache" 34 | category = "dev" 35 | optional = false 36 | python-versions = ">=2.6" 37 | 38 | [package.extras] 39 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 40 | testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] 41 | 42 | [[package]] 43 | name = "colorama" 44 | version = "0.4.1" 45 | description = "Cross-platform colored terminal text." 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.3" 53 | description = "Cross-platform colored terminal text." 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 57 | 58 | [[package]] 59 | name = "configparser" 60 | version = "4.0.2" 61 | description = "Updated configparser from Python 3.7 for Python 2.6+." 62 | category = "dev" 63 | optional = false 64 | python-versions = ">=2.6" 65 | 66 | [package.extras] 67 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 68 | testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] 69 | 70 | [[package]] 71 | name = "contextlib2" 72 | version = "0.6.0.post1" 73 | description = "Backports and enhancements for the contextlib module" 74 | category = "dev" 75 | optional = false 76 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 77 | 78 | [[package]] 79 | name = "coverage" 80 | version = "4.5.4" 81 | description = "Code coverage measurement for Python" 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" 85 | 86 | [[package]] 87 | name = "distlib" 88 | version = "0.3.1" 89 | description = "Distribution utilities" 90 | category = "dev" 91 | optional = false 92 | python-versions = "*" 93 | 94 | [[package]] 95 | name = "filelock" 96 | version = "3.0.12" 97 | description = "A platform independent file lock." 98 | category = "dev" 99 | optional = false 100 | python-versions = "*" 101 | 102 | [[package]] 103 | name = "funcsigs" 104 | version = "1.0.2" 105 | description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" 106 | category = "dev" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [[package]] 111 | name = "importlib-metadata" 112 | version = "0.23" 113 | description = "Read metadata from Python packages" 114 | category = "dev" 115 | optional = false 116 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 117 | 118 | [package.extras] 119 | docs = ["sphinx", "rst.linker"] 120 | testing = ["packaging", "importlib-resources"] 121 | 122 | [package.dependencies] 123 | zipp = ">=0.5" 124 | configparser = {version = ">=3.5", markers = "python_version < \"3\""} 125 | contextlib2 = {version = "*", markers = "python_version < \"3\""} 126 | 127 | [[package]] 128 | name = "importlib-resources" 129 | version = "1.0.2" 130 | description = "Read resources from Python packages" 131 | category = "dev" 132 | optional = false 133 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" 134 | 135 | [package.dependencies] 136 | pathlib2 = {version = "*", markers = "python_version < \"3\""} 137 | typing = {version = "*", markers = "python_version < \"3.5\""} 138 | 139 | [[package]] 140 | name = "mock" 141 | version = "3.0.5" 142 | description = "Rolling backport of unittest.mock for all Pythons" 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 146 | 147 | [package.extras] 148 | build = ["twine", "wheel", "blurb"] 149 | docs = ["sphinx"] 150 | test = ["pytest", "pytest-cov"] 151 | 152 | [package.dependencies] 153 | six = "*" 154 | funcsigs = {version = ">=1", markers = "python_version < \"3.3\""} 155 | 156 | [[package]] 157 | name = "more-itertools" 158 | version = "5.0.0" 159 | description = "More routines for operating on iterables, beyond itertools" 160 | category = "dev" 161 | optional = false 162 | python-versions = "*" 163 | 164 | [package.dependencies] 165 | six = ">=1.0.0,<2.0.0" 166 | 167 | [[package]] 168 | name = "more-itertools" 169 | version = "7.2.0" 170 | description = "More routines for operating on iterables, beyond itertools" 171 | category = "dev" 172 | optional = false 173 | python-versions = ">=3.4" 174 | 175 | [[package]] 176 | name = "packaging" 177 | version = "20.4" 178 | description = "Core utilities for Python packages" 179 | category = "dev" 180 | optional = false 181 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 182 | 183 | [package.dependencies] 184 | pyparsing = ">=2.0.2" 185 | six = "*" 186 | 187 | [[package]] 188 | name = "pathlib2" 189 | version = "2.3.5" 190 | description = "Object-oriented filesystem paths" 191 | category = "dev" 192 | optional = false 193 | python-versions = "*" 194 | 195 | [package.dependencies] 196 | six = "*" 197 | scandir = {version = "*", markers = "python_version < \"3.5\""} 198 | 199 | [[package]] 200 | name = "pluggy" 201 | version = "0.13.1" 202 | description = "plugin and hook calling mechanisms for python" 203 | category = "dev" 204 | optional = false 205 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 206 | 207 | [package.extras] 208 | dev = ["pre-commit", "tox"] 209 | 210 | [package.dependencies] 211 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 212 | 213 | [[package]] 214 | name = "py" 215 | version = "1.9.0" 216 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 217 | category = "dev" 218 | optional = false 219 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 220 | 221 | [[package]] 222 | name = "pyparsing" 223 | version = "2.4.7" 224 | description = "Python parsing module" 225 | category = "dev" 226 | optional = false 227 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 228 | 229 | [[package]] 230 | name = "pytest" 231 | version = "4.6.11" 232 | description = "pytest: simple powerful testing with Python" 233 | category = "dev" 234 | optional = false 235 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 236 | 237 | [package.extras] 238 | testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] 239 | 240 | [package.dependencies] 241 | atomicwrites = ">=1.0" 242 | attrs = ">=17.4.0" 243 | packaging = "*" 244 | pluggy = ">=0.12,<1.0" 245 | py = ">=1.5.0" 246 | six = ">=1.10.0" 247 | wcwidth = "*" 248 | funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} 249 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 250 | pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} 251 | 252 | [[package.dependencies.colorama]] 253 | version = "*" 254 | markers = "sys_platform == \"win32\" and python_version != \"3.4\"" 255 | 256 | [[package.dependencies.colorama]] 257 | version = "<=0.4.1" 258 | markers = "sys_platform == \"win32\" and python_version == \"3.4\"" 259 | 260 | [[package.dependencies.more-itertools]] 261 | version = ">=4.0.0,<6.0.0" 262 | markers = "python_version <= \"2.7\"" 263 | 264 | [[package.dependencies.more-itertools]] 265 | version = ">=4.0.0" 266 | markers = "python_version > \"2.7\"" 267 | 268 | [[package]] 269 | name = "pytest-cov" 270 | version = "2.8.1" 271 | description = "Pytest plugin for measuring coverage." 272 | category = "dev" 273 | optional = false 274 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 275 | 276 | [package.extras] 277 | testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] 278 | 279 | [package.dependencies] 280 | coverage = ">=4.4" 281 | pytest = ">=3.6" 282 | 283 | [[package]] 284 | name = "pytest-mock" 285 | version = "1.13.0" 286 | description = "Thin-wrapper around the mock package for easier use with py.test" 287 | category = "dev" 288 | optional = false 289 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 290 | 291 | [package.extras] 292 | dev = ["pre-commit", "tox"] 293 | 294 | [package.dependencies] 295 | pytest = ">=2.7" 296 | mock = {version = "*", markers = "python_version < \"3.0\""} 297 | 298 | [[package]] 299 | name = "scandir" 300 | version = "1.10.0" 301 | description = "scandir, a better directory iterator and faster os.walk()" 302 | category = "dev" 303 | optional = false 304 | python-versions = "*" 305 | 306 | [[package]] 307 | name = "six" 308 | version = "1.15.0" 309 | description = "Python 2 and 3 compatibility utilities" 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 313 | 314 | [[package]] 315 | name = "toml" 316 | version = "0.10.1" 317 | description = "Python Library for Tom's Obvious, Minimal Language" 318 | category = "dev" 319 | optional = false 320 | python-versions = "*" 321 | 322 | [[package]] 323 | name = "tox" 324 | version = "3.14.0" 325 | description = "tox is a generic virtualenv management and test command line tool" 326 | category = "dev" 327 | optional = false 328 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 329 | 330 | [package.extras] 331 | docs = ["sphinx (>=2.0.0,<3)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] 332 | testing = ["freezegun (>=0.3.11,<1)", "pathlib2 (>=2.3.3,<3)", "pytest (>=4.0.0,<6)", "pytest-cov (>=2.5.1,<3)", "pytest-mock (>=1.10.0,<2)", "pytest-xdist (>=1.22.2,<2)", "pytest-randomly (>=1.2.3,<2)", "flaky (>=3.4.0,<4)", "psutil (>=5.6.1,<6)"] 333 | 334 | [package.dependencies] 335 | filelock = ">=3.0.0,<4" 336 | packaging = ">=14" 337 | pluggy = ">=0.12.0,<1" 338 | py = ">=1.4.17,<2" 339 | six = ">=1.0.0,<2" 340 | toml = ">=0.9.4" 341 | virtualenv = ">=14.0.0" 342 | importlib-metadata = {version = ">=0.12,<1", markers = "python_version < \"3.8\""} 343 | 344 | [[package]] 345 | name = "typing" 346 | version = "3.7.4.3" 347 | description = "Type Hints for Python" 348 | category = "dev" 349 | optional = false 350 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 351 | 352 | [[package]] 353 | name = "virtualenv" 354 | version = "20.0.31" 355 | description = "Virtual Python Environment builder" 356 | category = "dev" 357 | optional = false 358 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 359 | 360 | [package.extras] 361 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 362 | testing = ["coverage (>=5)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] 363 | 364 | [package.dependencies] 365 | appdirs = ">=1.4.3,<2" 366 | distlib = ">=0.3.1,<1" 367 | filelock = ">=3.0.0,<4" 368 | six = ">=1.9.0,<2" 369 | importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} 370 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 371 | pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} 372 | 373 | [[package]] 374 | name = "wcwidth" 375 | version = "0.2.5" 376 | description = "Measures the displayed width of unicode strings in a terminal" 377 | category = "dev" 378 | optional = false 379 | python-versions = "*" 380 | 381 | [package.dependencies] 382 | "backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} 383 | 384 | [[package]] 385 | name = "zipp" 386 | version = "1.2.0" 387 | description = "Backport of pathlib-compatible object wrapper for zip files" 388 | category = "dev" 389 | optional = false 390 | python-versions = ">=2.7" 391 | 392 | [package.extras] 393 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 394 | testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] 395 | 396 | [package.dependencies] 397 | contextlib2 = {version = "*", markers = "python_version < \"3.4\""} 398 | 399 | [metadata] 400 | lock-version = "1.1" 401 | python-versions = "~2.7 || ^3.4" 402 | content-hash = "569e785f5e774e4b45a66c713dbcc990048f57afce8c24a7a5a46760d225c8db" 403 | 404 | [metadata.files] 405 | appdirs = [ 406 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 407 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 408 | ] 409 | atomicwrites = [ 410 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 411 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 412 | ] 413 | attrs = [ 414 | {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, 415 | {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, 416 | ] 417 | "backports.functools-lru-cache" = [ 418 | {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, 419 | {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, 420 | ] 421 | colorama = [ 422 | {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, 423 | {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, 424 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 425 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 426 | ] 427 | configparser = [ 428 | {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, 429 | {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, 430 | ] 431 | contextlib2 = [ 432 | {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, 433 | {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, 434 | ] 435 | coverage = [ 436 | {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, 437 | {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, 438 | {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, 439 | {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, 440 | {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, 441 | {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, 442 | {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, 443 | {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, 444 | {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, 445 | {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, 446 | {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, 447 | {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, 448 | {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, 449 | {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, 450 | {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, 451 | {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, 452 | {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, 453 | {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, 454 | {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, 455 | {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, 456 | {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, 457 | {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, 458 | {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, 459 | {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, 460 | {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, 461 | {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, 462 | {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, 463 | {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, 464 | {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, 465 | {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, 466 | {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, 467 | {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, 468 | ] 469 | distlib = [ 470 | {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, 471 | {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, 472 | ] 473 | filelock = [ 474 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 475 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 476 | ] 477 | funcsigs = [ 478 | {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, 479 | {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, 480 | ] 481 | importlib-metadata = [ 482 | {file = "importlib_metadata-0.23-py2.py3-none-any.whl", hash = "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"}, 483 | {file = "importlib_metadata-0.23.tar.gz", hash = "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26"}, 484 | ] 485 | importlib-resources = [ 486 | {file = "importlib_resources-1.0.2-py2.py3-none-any.whl", hash = "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b"}, 487 | {file = "importlib_resources-1.0.2.tar.gz", hash = "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078"}, 488 | ] 489 | mock = [ 490 | {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, 491 | {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, 492 | ] 493 | more-itertools = [ 494 | {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, 495 | {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, 496 | {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, 497 | {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, 498 | {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, 499 | ] 500 | packaging = [ 501 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 502 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 503 | ] 504 | pathlib2 = [ 505 | {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, 506 | {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, 507 | ] 508 | pluggy = [ 509 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 510 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 511 | ] 512 | py = [ 513 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 514 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 515 | ] 516 | pyparsing = [ 517 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 518 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 519 | ] 520 | pytest = [ 521 | {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, 522 | {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, 523 | ] 524 | pytest-cov = [ 525 | {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, 526 | {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, 527 | ] 528 | pytest-mock = [ 529 | {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, 530 | {file = "pytest_mock-1.13.0-py2.py3-none-any.whl", hash = "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d"}, 531 | ] 532 | scandir = [ 533 | {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, 534 | {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, 535 | {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, 536 | {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, 537 | {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, 538 | {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, 539 | {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, 540 | {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, 541 | {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, 542 | {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, 543 | {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, 544 | ] 545 | six = [ 546 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 547 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 548 | ] 549 | toml = [ 550 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 551 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 552 | ] 553 | tox = [ 554 | {file = "tox-3.14.0-py2.py3-none-any.whl", hash = "sha256:0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e"}, 555 | {file = "tox-3.14.0.tar.gz", hash = "sha256:c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1"}, 556 | ] 557 | typing = [ 558 | {file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"}, 559 | {file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"}, 560 | ] 561 | virtualenv = [ 562 | {file = "virtualenv-20.0.31-py2.py3-none-any.whl", hash = "sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b"}, 563 | {file = "virtualenv-20.0.31.tar.gz", hash = "sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc"}, 564 | ] 565 | wcwidth = [ 566 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 567 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 568 | ] 569 | zipp = [ 570 | {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, 571 | {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, 572 | ] 573 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pastel" 3 | version = "0.2.1" 4 | description = "Bring colors to your terminal." 5 | authors = ["Sébastien Eustace "] 6 | license = "MIT" 7 | readme = "README.rst" 8 | homepage = "https://github.com/sdispater/pastel" 9 | repository = "https://github.com/sdispater/pastel" 10 | 11 | packages = [ 12 | {include = "pastel"}, 13 | {include = "tests", format = "sdist"}, 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "~2.7 || ^3.4" 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^4.6.4" 21 | pytest-cov = "^2.7.1" 22 | pytest-mock = "^1.10.4" 23 | tox = "^3.13.2" 24 | 25 | [build-system] 26 | requires = ["poetry-core>=1.0.0a9"] 27 | build-backend = "poetry.core.masonry.api" 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pastel import Pastel 6 | from pastel.style import Style 7 | from pastel.stack import StyleStack 8 | 9 | 10 | @pytest.fixture 11 | def pastel(): 12 | return Pastel(True) 13 | 14 | 15 | @pytest.fixture 16 | def non_decorated_pastel(): 17 | return Pastel(False) 18 | 19 | 20 | @pytest.fixture 21 | def style(): 22 | return Style() 23 | 24 | 25 | @pytest.fixture 26 | def stack(): 27 | return StyleStack() 28 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import pastel 5 | 6 | from contextlib import contextmanager 7 | 8 | 9 | class PseudoTTY(object): 10 | def __init__(self, underlying): 11 | self._underlying = underlying 12 | 13 | def __getattr__(self, name): 14 | return getattr(self._underlying, name) 15 | 16 | def isatty(self): 17 | return True 18 | 19 | 20 | @contextmanager 21 | def mock_stdout(): 22 | original = sys.stdout 23 | sys.stdout = PseudoTTY(sys.stdout) 24 | 25 | yield 26 | 27 | sys.stdout = original 28 | 29 | 30 | def test_text(): 31 | with mock_stdout(): 32 | assert "\033[32msome info\033[0m" == pastel.colorize("some info") 33 | 34 | 35 | def test_colorize(): 36 | with mock_stdout(): 37 | pastel.with_colors(False) 38 | assert "some info" == pastel.colorize("some info") 39 | 40 | pastel.with_colors(True) 41 | assert "\033[32msome info\033[0m" == pastel.colorize("some info") 42 | 43 | 44 | def test_add_remove_style(): 45 | with mock_stdout(): 46 | pastel.add_style("success", "green") 47 | 48 | assert "\033[32msome info\033[0m" == pastel.colorize( 49 | "some info" 50 | ) 51 | 52 | pastel.remove_style("success") 53 | 54 | assert "some info" == pastel.colorize( 55 | "some info" 56 | ) 57 | 58 | 59 | def test_pastel(): 60 | p = pastel.pastel() 61 | assert isinstance(p, pastel.Pastel) 62 | -------------------------------------------------------------------------------- /tests/test_pastel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pastel import Pastel 6 | 7 | 8 | def test_empty_tag(pastel): 9 | assert "foo<>bar" == pastel.colorize("foo<>bar") 10 | 11 | 12 | def test_lg_char_escaping(pastel): 13 | assert "foosome info" == pastel.colorize("\\some info\\") 15 | assert "\\some info\\" == pastel.escape("some info") 16 | 17 | 18 | def test_bundled_styles(pastel): 19 | assert pastel.has_style("error") 20 | assert pastel.has_style("info") 21 | assert pastel.has_style("comment") 22 | assert pastel.has_style("question") 23 | 24 | assert "\033[97;41msome error\033[0m" == pastel.colorize( 25 | "some error" 26 | ) 27 | assert "\033[32msome info\033[0m" == pastel.colorize("some info") 28 | assert "\033[33msome comment\033[0m" == pastel.colorize( 29 | "some comment" 30 | ) 31 | assert "\033[30;46msome question\033[0m" == pastel.colorize( 32 | "some question" 33 | ) 34 | 35 | 36 | def test_nested_styles(pastel): 37 | assert ( 38 | "\033[97;41msome \033[0m\033[32msome info\033[0m\033[97;41m error\033[0m" 39 | == pastel.colorize("some some info error") 40 | ) 41 | 42 | 43 | def test_adjacent_style(pastel): 44 | assert "\033[97;41msome error\033[0m\033[32msome info\033[0m" == pastel.colorize( 45 | "some errorsome info" 46 | ) 47 | 48 | 49 | def test_style_matching_non_greedy(pastel): 50 | assert "(\033[32m>=2.0,<2.3\033[0m)" == pastel.colorize("(>=2.0,<2.3)") 51 | 52 | 53 | def test_style_escaping(pastel): 54 | assert "(\033[32mz>=2.0,%s)" % pastel.escape("z>=2.0,errorinfocommenterror" 64 | ) 65 | ) 66 | 67 | 68 | def test_new_style(pastel): 69 | pastel.add_style("test", "blue", "white") 70 | 71 | assert pastel.style("test") != pastel.style("info") 72 | 73 | pastel.add_style("b", "blue", "white") 74 | 75 | assert ( 76 | "\033[34;107msome \033[0m\033[34;107mcustom\033[0m\033[34;107m msg\033[0m" 77 | == pastel.colorize("some custom msg") 78 | ) 79 | 80 | pastel.remove_style("test") 81 | pastel.remove_style("b") 82 | 83 | assert "some custom msg" == pastel.colorize( 84 | "some custom msg" 85 | ) 86 | 87 | with pytest.raises(ValueError): 88 | pastel.remove_style("b") 89 | 90 | 91 | def test_redefined_style(pastel): 92 | pastel.add_style("info", "blue", "white") 93 | 94 | assert "\033[34;107msome custom msg\033[0m" == pastel.colorize( 95 | "some custom msg" 96 | ) 97 | 98 | 99 | def test_inline_style(pastel): 100 | assert "\033[34;41msome text\033[0m" == pastel.colorize( 101 | "some text" 102 | ) 103 | assert "\033[34;41msome text\033[0m" == pastel.colorize( 104 | "some text" 105 | ) 106 | assert "\033[34;41;1msome text\033[0m" == pastel.colorize( 107 | "some text" 108 | ) 109 | 110 | 111 | def test_non_style_tag(pastel): 112 | expected = ( 113 | "\033[32msome \033[0m\033[32m\033[0m\033[32m \033[0m\033[32m\033[0m\033[32m" 114 | " styled \033[0m\033[32m

\033[0m\033[32msingle-char tag\033[0m\033[32m

\033[0m" 115 | ) 116 | 117 | assert expected == pastel.colorize( 118 | "some styled

single-char tag

" 119 | ) 120 | 121 | 122 | def test_non_decorated_pastel(non_decorated_pastel): 123 | pastel = non_decorated_pastel 124 | 125 | assert pastel.has_style("error") 126 | assert pastel.has_style("info") 127 | assert pastel.has_style("comment") 128 | assert pastel.has_style("question") 129 | 130 | assert "some error" == pastel.colorize("some error") 131 | assert "some info" == pastel.colorize("some info") 132 | assert "some comment" == pastel.colorize("some comment") 133 | assert "some question" == pastel.colorize("some question") 134 | 135 | pastel.with_colors(True) 136 | 137 | assert "\033[97;41msome error\033[0m" == pastel.colorize( 138 | "some error" 139 | ) 140 | assert "\033[32msome info\033[0m" == pastel.colorize("some info") 141 | assert "\033[33msome comment\033[0m" == pastel.colorize( 142 | "some comment" 143 | ) 144 | assert "\033[30;46msome question\033[0m" == pastel.colorize( 145 | "some question" 146 | ) 147 | 148 | 149 | @pytest.mark.parametrize( 150 | "expected, message", 151 | [ 152 | ( 153 | """\033[32m 154 | some text\033[0m""", 155 | """ 156 | some text""", 157 | ), 158 | ( 159 | """\033[32msome text 160 | \033[0m""", 161 | """some text 162 | """, 163 | ), 164 | ( 165 | """\033[32m 166 | some text 167 | \033[0m""", 168 | """ 169 | some text 170 | """, 171 | ), 172 | ( 173 | """\033[32m 174 | some text 175 | more text 176 | \033[0m""", 177 | """ 178 | some text 179 | more text 180 | """, 181 | ), 182 | ], 183 | ) 184 | def test_content_with_line_breaks(pastel, expected, message): 185 | assert expected == pastel.colorize(message) 186 | -------------------------------------------------------------------------------- /tests/test_stack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pastel.style import Style 6 | 7 | 8 | def test_push(stack): 9 | s1 = Style("white", "black") 10 | s2 = Style("yellow", "blue") 11 | stack.push(s1) 12 | stack.push(s2) 13 | 14 | assert s2 == stack.get_current() 15 | 16 | s3 = Style("green", "red") 17 | stack.push(s3) 18 | 19 | assert s3 == stack.get_current() 20 | 21 | 22 | def test_pop(stack): 23 | s1 = Style("white", "black") 24 | s2 = Style("yellow", "blue") 25 | stack.push(s1) 26 | stack.push(s2) 27 | 28 | assert s2 == stack.pop() 29 | assert s1 == stack.pop() 30 | 31 | 32 | def test_pop_empty(stack): 33 | assert isinstance(stack.pop(), Style) 34 | 35 | 36 | def test_pop_not_last(stack): 37 | s1 = Style("white", "black") 38 | s2 = Style("yellow", "blue") 39 | s3 = Style("green", "red") 40 | stack.push(s1) 41 | stack.push(s2) 42 | stack.push(s3) 43 | 44 | assert s2 == stack.pop(s2) 45 | assert s1 == stack.pop() 46 | 47 | 48 | def test_invalid_pop(stack): 49 | s1 = Style("white", "black") 50 | s2 = Style("yellow", "blue") 51 | stack.push(s1) 52 | 53 | with pytest.raises(ValueError): 54 | stack.pop(s2) 55 | -------------------------------------------------------------------------------- /tests/test_style.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from pastel.style import Style 6 | 7 | 8 | def test_init(): 9 | style = Style("green", "black", ["bold", "underline"]) 10 | assert "\033[32;40;1;4mfoo\033[0m" == style.apply("foo") 11 | assert "green" == style.foreground 12 | assert "black" == style.background 13 | assert ["bold", "underline"] == style.options 14 | 15 | style = Style("red", None, ["blink"]) 16 | assert "\033[31;5mfoo\033[0m" == style.apply("foo") 17 | 18 | style = Style(None, "white") 19 | assert "\033[107mfoo\033[0m" == style.apply("foo") 20 | 21 | style = Style("red", None, "blink") 22 | assert "\033[31;5mfoo\033[0m" == style.apply("foo") 23 | 24 | 25 | def test_foreground(style): 26 | style.set_foreground("black") 27 | assert "\033[30mfoo\033[0m" == style.apply("foo") 28 | 29 | style.set_foreground("blue") 30 | assert "\033[34mfoo\033[0m" == style.apply("foo") 31 | 32 | with pytest.raises(ValueError): 33 | style.set_foreground("undefined-color") 34 | 35 | 36 | def test_background(style): 37 | style.set_background("black") 38 | assert "\033[40mfoo\033[0m" == style.apply("foo") 39 | 40 | style.set_background("yellow") 41 | assert "\033[43mfoo\033[0m" == style.apply("foo") 42 | 43 | with pytest.raises(ValueError): 44 | style.set_background("undefined-color") 45 | 46 | 47 | def test_options(style): 48 | style.set_options(["reverse", "conceal"]) 49 | assert "\033[7;8mfoo\033[0m" == style.apply("foo") 50 | 51 | style.set_option("bold") 52 | assert "\033[7;8;1mfoo\033[0m" == style.apply("foo") 53 | 54 | style.unset_option("reverse") 55 | assert "\033[8;1mfoo\033[0m" == style.apply("foo") 56 | 57 | style.set_option("bold") 58 | assert "\033[8;1mfoo\033[0m" == style.apply("foo") 59 | 60 | style.set_options(["bold"]) 61 | assert "\033[1mfoo\033[0m" == style.apply("foo") 62 | 63 | with pytest.raises(ValueError) as e: 64 | style.set_option("foo") 65 | 66 | assert 'Invalid option specified: "foo"' in str(e.value) 67 | 68 | with pytest.raises(ValueError) as e: 69 | style.unset_option("foo") 70 | 71 | assert 'Invalid option specified: "foo"' in str(e.value) 72 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py35, py36, pypy 3 | skipsdist=True 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | commands = 8 | poetry install -v 9 | poetry run pytest --cov pastel tests/ 10 | --------------------------------------------------------------------------------