1
0
Fork 0

Adding upstream version 2.2.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-07 10:18:23 +01:00
parent 56bc4cde4f
commit 12d94d2889
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
42 changed files with 4049 additions and 0 deletions

69
.gitignore vendored Normal file
View file

@ -0,0 +1,69 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
*.rpm
requirements*.txt
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints
# IntelliJ
.idea/
test*.png

25
.travis.yml Normal file
View file

@ -0,0 +1,25 @@
# Configure.
language: python
python: 3.5
sudo: false
# Run.
install: pip install appveyor-artifacts coveralls tox
script: tox -e lint,py35,py34,py33,pypy3,pypy,py27,py26
after_success:
- mv .coverage .coverage.travis
- appveyor-artifacts -mi download
- coverage combine
- coveralls
# Deploy.
deploy:
provider: pypi
user: Robpol86
password:
secure:
"JYR5ZVOHqZnr4uq8qtA9bM0+pBCfenTUApgSK2eMY3AoQ/Xi4UmcJvsGQkX70wq4twstRm\
twpb/oFkAuxLMKkK7AJOTt9lKzqjF62xm/yGilDIYMZGCWi30OcRuUSQsEaE1Bq0H1TxciV\
/ztcdwcXpTq2+oNQz9M7sbH7Czmdbw="
on:
tags: true

35
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,35 @@
# Contributing
Everyone that wants to contribute to the project should read this document.
## Getting Started
You may follow these steps if you wish to create a pull request. Fork the repo and clone it on your local machine. Then
in the project's directory:
```bash
virtualenv env # Create a virtualenv for the project's dependencies.
source env/bin/activate # Activate the virtualenv.
pip install tox # Install tox, which runs linting and tests.
tox # This runs all tests on your local machine. Make sure they pass.
```
If you don't have Python 2.6, 2.7, and 3.4 installed, you can manually run tests on one specific version by running
`tox -e lint,py27` (for Python 2.7) instead.
## Consistency and Style
Keep code style consistent with the rest of the project. Some suggestions:
1. **Write tests for your new features.** `if new_feature else` **Write tests for bug-causing scenarios.**
2. Write docstrings for all classes, functions, methods, modules, etc.
3. Document all function/method arguments and return values.
4. Document all class variables instance variables.
5. Documentation guidelines also apply to tests, though not as strict.
6. Keep code style consistent, such as the kind of quotes to use and spacing.
7. Don't use `except:` or `except Exception:` unless you have a `raise` in the block. Be specific about error handling.
8. Don't use `isinstance()` (it breaks [duck typing](https://en.wikipedia.org/wiki/Duck_typing#In_Python)).
## Thanks
Thanks for fixing bugs or adding features to the project!

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Robpol86
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

229
README.rst Normal file
View file

@ -0,0 +1,229 @@
==========
colorclass
==========
Yet another ANSI color text library for Python. Provides "auto colors" for dark/light terminals. Works on Linux, OS X,
and Windows. For Windows support you just need to call ``Windows.enable()`` in your application.
On Linux/OS X ``autocolors`` are toggled by calling ``set_light_background()`` and ``set_dark_background()``. On Windows
this can be done automatically if you call ``Windows.enable(auto_colors=True)``. Even though the latest Windows 10 does
support ANSI color codes natively, you still need to run Windows.enable() to take advantage of automatically detecting
the console's background color.
In Python2.x this library subclasses ``unicode``, while on Python3.x it subclasses ``str``.
* Python 2.6, 2.7, PyPy, PyPy3, 3.3, 3.4, and 3.5 supported on Linux and OS X.
* Python 2.6, 2.7, 3.3, 3.4, and 3.5 supported on Windows (both 32 and 64 bit versions of Python).
.. image:: https://img.shields.io/appveyor/ci/Robpol86/colorclass/master.svg?style=flat-square&label=AppVeyor%20CI
:target: https://ci.appveyor.com/project/Robpol86/colorclass
:alt: Build Status Windows
.. image:: https://img.shields.io/travis/Robpol86/colorclass/master.svg?style=flat-square&label=Travis%20CI
:target: https://travis-ci.org/Robpol86/colorclass
:alt: Build Status
.. image:: https://img.shields.io/coveralls/Robpol86/colorclass/master.svg?style=flat-square&label=Coveralls
:target: https://coveralls.io/github/Robpol86/colorclass
:alt: Coverage Status
.. image:: https://img.shields.io/pypi/v/colorclass.svg?style=flat-square&label=Latest
:target: https://pypi.python.org/pypi/colorclass
:alt: Latest Version
.. image:: https://img.shields.io/pypi/dm/colorclass.svg?style=flat-square&label=PyPI%20Downloads
:target: https://pypi.python.org/pypi/colorclass
:alt: Downloads
Quickstart
==========
Install:
.. code:: bash
pip install colorclass
Piped Command Line
==================
It is possible to pipe curly-bracket tagged (or regular ANSI coded) text to Python in the command line to produce color
text. Some examples:
.. code:: bash
echo "{red}Red{/red}" |python -m colorclass # Red colored text.
echo -e "\033[31mRed\033[0m" | COLOR_DISABLE=true python -m colorclass # Strip colors
echo -e "\033[31mRed\033[0m" | COLOR_ENABLE=true python -m colorclass &> file.txt # Force colors.
Export these environment variables as "true" to enable/disable some features:
=============== ============================================
Env Variable Description
=============== ============================================
COLOR_ENABLE Force colors even when piping to a file.
COLOR_DISABLE Strip all colors from incoming text.
COLOR_LIGHT Use light colored text for dark backgrounds.
COLOR_DARK Use dark colored text for light backgrounds.
=============== ============================================
Example Implementation
======================
.. image:: https://github.com/Robpol86/colorclass/raw/master/example.png?raw=true
:alt: Example Script Screenshot
.. image:: https://github.com/Robpol86/colorclass/raw/master/example_windows.png?raw=true
:alt: Example Windows Screenshot
Source code for the example code is: `example.py <https://github.com/Robpol86/colorclass/blob/master/example.py>`_
Usage
=====
Different colors are chosen using curly-bracket tags, such as ``{red}{/red}``. For a list of available colors, call
``colorclass.list_tags()``.
The available "auto colors" tags are:
* autoblack
* autored
* autogreen
* autoyellow
* autoblue
* automagenta
* autocyan
* autowhite
* autobgblack
* autobgred
* autobggreen
* autobgyellow
* autobgblue
* autobgmagenta
* autobgcyan
* autobgwhite
Methods of Class instances try to return sane data, such as:
.. code:: python
from colorclass import Color
color_string = Color('{red}Test{/red}')
color_string
u'\x1b[31mTest\x1b[39m'
len(color_string)
4
color_string.istitle()
True
There are also a couple of helper attributes for all Color instances:
.. code:: python
color_string.value_colors
'\x1b[31mTest\x1b[39m'
color_string.value_no_colors
'Test'
Changelog
=========
This project adheres to `Semantic Versioning <http://semver.org/>`_.
2.2.0 - 2016-05-14
------------------
Added
* ``disable_if_no_tty()`` function to conditionally disable colors when STDERR and STDOUT are not streams.
Changed
* Colors enabled by default always, like it was before v2.0.0.
2.1.1 - 2016-05-10
------------------
Fixed
* Printing box drawing characters on Windows from Python 2.6.
2.1.0 - 2016-05-07
------------------
Added
* ``keep_tags`` boolean keyword argument to Color(). Prevents colorclass from parsing curly brackets.
* Automatically skip replacing stderr/stdout streams on latest Windows 10 versions with native ANSI color support.
Changed
* Refactored most of windows.py.
* Background color determined from either stderr or stdout, instead of just one stream (e.g. piping stderr to file).
Fixed
* https://github.com/Robpol86/colorclass/issues/16
* https://github.com/Robpol86/colorclass/issues/18
2.0.0 - 2016-04-10
------------------
Added
* Python 3.5 support.
* ``enable_all_colors()``, ``is_enabled()``, and ``is_light()`` toggle functions.
* Library can be used as a script (e.g. ``echo "{red}Red{/red}" |python -m colorclass``).
* Ability to add/multiply Color instances just like str.
* Ability to iterate a Color instance and have each character keep its color codes.
Changed
* Converted library from Python module to a package.
* ``set_light_background()`` and ``set_dark_background()`` no longer enable colors. Use ``enable_all_colors()``.
* Colors are disabled by default when STDERR and STDOUT are not streams (piped to files/null). Similar to ``grep``.
* Reduce size of ANSI escape sequences by removing codes that have no effect. e.g. ``\033[31;35m`` to ``\033[35m``.
* Color methods that return strings now return Color instances instead of str instances.
Fixed
* https://github.com/Robpol86/colorclass/issues/15
* https://github.com/Robpol86/colorclass/issues/17
1.2.0 - 2015-03-19
------------------
Added
* Convenience single-color methods by `Marc Abramowitz <https://github.com/msabramo>`_.
1.1.2 - 2015-01-07
------------------
Fixed
* Maintaining ``Color`` type through ``.encode()`` and ``.decode()`` chains.
1.1.1 - 2014-11-03
------------------
Fixed
* Python 2.7 64-bit original colors bug on Windows.
* resetting colors when ``reset_atexit`` is True.
* Improved sorting of ``list_tags()``.
1.1.0 - 2014-11-01
------------------
Added
* Native Windows support and automatic background colors.
1.0.2 - 2014-10-20
------------------
Added
* Ability to disable/strip out all colors.
1.0.1 - 2014-09-11
------------------
Fixed
* ``splitlines()`` method.
1.0.0 - 2014-09-01
------------------
* Initial release.

17
appveyor.yml Normal file
View file

@ -0,0 +1,17 @@
# Configure.
artifacts:
- path: .coverage
# Run.
init: set PATH=C:\Python35-x64;C:\Python35-x64\Scripts;%PATH%
install:
- appveyor DownloadFile https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-desktop.ps1
- ps: .\enable-desktop
build_script: pip install tox
test_script: tox -e lint,py35,py34,py33,py27,py26,py35x64,py34x64,py33x64,py27x64,py26x64
# Post.
on_finish:
- appveyor PushArtifact test_example_test_windows_screenshot.png
- appveyor PushArtifact test_windows_test_enable_disable.png
- appveyor PushArtifact test_windows_test_box_characters.png

38
colorclass/__init__.py Normal file
View file

@ -0,0 +1,38 @@
"""Colorful worry-free console applications for Linux, Mac OS X, and Windows.
Supported natively on Linux and Mac OSX (Just Works), and on Windows it works the same if Windows.enable() is called.
Gives you expected and sane results from methods like len() and .capitalize().
https://github.com/Robpol86/colorclass
https://pypi.python.org/pypi/colorclass
"""
from colorclass.codes import list_tags # noqa
from colorclass.color import Color # noqa
from colorclass.toggles import disable_all_colors # noqa
from colorclass.toggles import disable_if_no_tty # noqa
from colorclass.toggles import enable_all_colors # noqa
from colorclass.toggles import is_enabled # noqa
from colorclass.toggles import is_light # noqa
from colorclass.toggles import set_dark_background # noqa
from colorclass.toggles import set_light_background # noqa
from colorclass.windows import Windows # noqa
__all__ = (
'Color',
'disable_all_colors',
'enable_all_colors',
'is_enabled',
'is_light',
'list_tags',
'set_dark_background',
'set_light_background',
'Windows',
)
__author__ = '@Robpol86'
__license__ = 'MIT'
__version__ = '2.2.0'

33
colorclass/__main__.py Normal file
View file

@ -0,0 +1,33 @@
"""Called by "python -m". Allows package to be used as a script.
Example usage:
echo "{red}Red{/red}" |python -m colorclass
"""
from __future__ import print_function
import fileinput
import os
from colorclass.color import Color
from colorclass.toggles import disable_all_colors
from colorclass.toggles import enable_all_colors
from colorclass.toggles import set_dark_background
from colorclass.toggles import set_light_background
from colorclass.windows import Windows
TRUTHY = ('true', '1', 'yes', 'on')
if __name__ == '__main__':
if os.environ.get('COLOR_ENABLE', '').lower() in TRUTHY:
enable_all_colors()
elif os.environ.get('COLOR_DISABLE', '').lower() in TRUTHY:
disable_all_colors()
if os.environ.get('COLOR_LIGHT', '').lower() in TRUTHY:
set_light_background()
elif os.environ.get('COLOR_DARK', '').lower() in TRUTHY:
set_dark_background()
Windows.enable()
for LINE in fileinput.input():
print(Color(LINE))

229
colorclass/codes.py Normal file
View file

@ -0,0 +1,229 @@
"""Handles mapping between color names and ANSI codes and determining auto color codes."""
import sys
from collections import Mapping
BASE_CODES = {
'/all': 0, 'b': 1, 'f': 2, 'i': 3, 'u': 4, 'flash': 5, 'outline': 6, 'negative': 7, 'invis': 8, 'strike': 9,
'/b': 22, '/f': 22, '/i': 23, '/u': 24, '/flash': 25, '/outline': 26, '/negative': 27, '/invis': 28,
'/strike': 29, '/fg': 39, '/bg': 49,
'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37,
'bgblack': 40, 'bgred': 41, 'bggreen': 42, 'bgyellow': 43, 'bgblue': 44, 'bgmagenta': 45, 'bgcyan': 46,
'bgwhite': 47,
'hiblack': 90, 'hired': 91, 'higreen': 92, 'hiyellow': 93, 'hiblue': 94, 'himagenta': 95, 'hicyan': 96,
'hiwhite': 97,
'hibgblack': 100, 'hibgred': 101, 'hibggreen': 102, 'hibgyellow': 103, 'hibgblue': 104, 'hibgmagenta': 105,
'hibgcyan': 106, 'hibgwhite': 107,
'autored': None, 'autoblack': None, 'automagenta': None, 'autowhite': None, 'autoblue': None, 'autoyellow': None,
'autogreen': None, 'autocyan': None,
'autobgred': None, 'autobgblack': None, 'autobgmagenta': None, 'autobgwhite': None, 'autobgblue': None,
'autobgyellow': None, 'autobggreen': None, 'autobgcyan': None,
'/black': 39, '/red': 39, '/green': 39, '/yellow': 39, '/blue': 39, '/magenta': 39, '/cyan': 39, '/white': 39,
'/hiblack': 39, '/hired': 39, '/higreen': 39, '/hiyellow': 39, '/hiblue': 39, '/himagenta': 39, '/hicyan': 39,
'/hiwhite': 39,
'/bgblack': 49, '/bgred': 49, '/bggreen': 49, '/bgyellow': 49, '/bgblue': 49, '/bgmagenta': 49, '/bgcyan': 49,
'/bgwhite': 49, '/hibgblack': 49, '/hibgred': 49, '/hibggreen': 49, '/hibgyellow': 49, '/hibgblue': 49,
'/hibgmagenta': 49, '/hibgcyan': 49, '/hibgwhite': 49,
'/autored': 39, '/autoblack': 39, '/automagenta': 39, '/autowhite': 39, '/autoblue': 39, '/autoyellow': 39,
'/autogreen': 39, '/autocyan': 39,
'/autobgred': 49, '/autobgblack': 49, '/autobgmagenta': 49, '/autobgwhite': 49, '/autobgblue': 49,
'/autobgyellow': 49, '/autobggreen': 49, '/autobgcyan': 49,
}
class ANSICodeMapping(Mapping):
"""Read-only dictionary, resolves closing tags and automatic colors. Iterates only used color tags.
:cvar bool DISABLE_COLORS: Disable colors (strip color codes).
:cvar bool LIGHT_BACKGROUND: Use low intensity color codes.
"""
DISABLE_COLORS = False
LIGHT_BACKGROUND = False
def __init__(self, value_markup):
"""Constructor.
:param str value_markup: String with {color} tags.
"""
self.whitelist = [k for k in BASE_CODES if '{' + k + '}' in value_markup]
def __getitem__(self, item):
"""Return value for key or None if colors are disabled.
:param str item: Key.
:return: Color code integer.
:rtype: int
"""
if item not in self.whitelist:
raise KeyError(item)
if self.DISABLE_COLORS:
return None
return getattr(self, item, BASE_CODES[item])
def __iter__(self):
"""Iterate dictionary."""
return iter(self.whitelist)
def __len__(self):
"""Dictionary length."""
return len(self.whitelist)
@classmethod
def disable_all_colors(cls):
"""Disable all colors. Strips any color tags or codes."""
cls.DISABLE_COLORS = True
@classmethod
def enable_all_colors(cls):
"""Enable all colors. Strips any color tags or codes."""
cls.DISABLE_COLORS = False
@classmethod
def disable_if_no_tty(cls):
"""Disable all colors only if there is no TTY available.
:return: True if colors are disabled, False if stderr or stdout is a TTY.
:rtype: bool
"""
if sys.stdout.isatty() or sys.stderr.isatty():
return False
cls.disable_all_colors()
return True
@classmethod
def set_dark_background(cls):
"""Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
cls.LIGHT_BACKGROUND = False
@classmethod
def set_light_background(cls):
"""Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
cls.LIGHT_BACKGROUND = True
@property
def autoblack(self):
"""Return automatic black foreground color depending on background color."""
return BASE_CODES['black' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblack']
@property
def autored(self):
"""Return automatic red foreground color depending on background color."""
return BASE_CODES['red' if ANSICodeMapping.LIGHT_BACKGROUND else 'hired']
@property
def autogreen(self):
"""Return automatic green foreground color depending on background color."""
return BASE_CODES['green' if ANSICodeMapping.LIGHT_BACKGROUND else 'higreen']
@property
def autoyellow(self):
"""Return automatic yellow foreground color depending on background color."""
return BASE_CODES['yellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiyellow']
@property
def autoblue(self):
"""Return automatic blue foreground color depending on background color."""
return BASE_CODES['blue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiblue']
@property
def automagenta(self):
"""Return automatic magenta foreground color depending on background color."""
return BASE_CODES['magenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'himagenta']
@property
def autocyan(self):
"""Return automatic cyan foreground color depending on background color."""
return BASE_CODES['cyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hicyan']
@property
def autowhite(self):
"""Return automatic white foreground color depending on background color."""
return BASE_CODES['white' if ANSICodeMapping.LIGHT_BACKGROUND else 'hiwhite']
@property
def autobgblack(self):
"""Return automatic black background color depending on background color."""
return BASE_CODES['bgblack' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblack']
@property
def autobgred(self):
"""Return automatic red background color depending on background color."""
return BASE_CODES['bgred' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgred']
@property
def autobggreen(self):
"""Return automatic green background color depending on background color."""
return BASE_CODES['bggreen' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibggreen']
@property
def autobgyellow(self):
"""Return automatic yellow background color depending on background color."""
return BASE_CODES['bgyellow' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgyellow']
@property
def autobgblue(self):
"""Return automatic blue background color depending on background color."""
return BASE_CODES['bgblue' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgblue']
@property
def autobgmagenta(self):
"""Return automatic magenta background color depending on background color."""
return BASE_CODES['bgmagenta' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgmagenta']
@property
def autobgcyan(self):
"""Return automatic cyan background color depending on background color."""
return BASE_CODES['bgcyan' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgcyan']
@property
def autobgwhite(self):
"""Return automatic white background color depending on background color."""
return BASE_CODES['bgwhite' if ANSICodeMapping.LIGHT_BACKGROUND else 'hibgwhite']
def list_tags():
"""List the available tags.
:return: List of 4-item tuples: opening tag, closing tag, main ansi value, closing ansi value.
:rtype: list
"""
# Build reverse dictionary. Keys are closing tags, values are [closing ansi, opening tag, opening ansi].
reverse_dict = dict()
for tag, ansi in sorted(BASE_CODES.items()):
if tag.startswith('/'):
reverse_dict[tag] = [ansi, None, None]
else:
reverse_dict['/' + tag][1:] = [tag, ansi]
# Collapse
four_item_tuples = [(v[1], k, v[2], v[0]) for k, v in reverse_dict.items()]
# Sort.
def sorter(four_item):
"""Sort /all /fg /bg first, then b i u flash, then auto colors, then dark colors, finally light colors.
:param iter four_item: [opening tag, closing tag, main ansi value, closing ansi value]
:return Sorting weight.
:rtype: int
"""
if not four_item[2]: # /all /fg /bg
return four_item[3] - 200
if four_item[2] < 10 or four_item[0].startswith('auto'): # b f i u or auto colors
return four_item[2] - 100
return four_item[2]
four_item_tuples.sort(key=sorter)
return four_item_tuples

220
colorclass/color.py Normal file
View file

@ -0,0 +1,220 @@
"""Color class used by library users."""
from colorclass.core import ColorStr
class Color(ColorStr):
"""Unicode (str in Python3) subclass with ANSI terminal text color support.
Example syntax: Color('{red}Sample Text{/red}')
Example without parsing logic: Color('{red}Sample Text{/red}', keep_tags=True)
For a list of codes, call: colorclass.list_tags()
"""
@classmethod
def colorize(cls, color, string, auto=False):
"""Color-code entire string using specified color.
:param str color: Color of string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
tag = '{0}{1}'.format('auto' if auto else '', color)
return cls('{%s}%s{/%s}' % (tag, string, tag))
@classmethod
def black(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('black', string, auto=auto)
@classmethod
def bgblack(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgblack', string, auto=auto)
@classmethod
def red(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('red', string, auto=auto)
@classmethod
def bgred(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgred', string, auto=auto)
@classmethod
def green(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('green', string, auto=auto)
@classmethod
def bggreen(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bggreen', string, auto=auto)
@classmethod
def yellow(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('yellow', string, auto=auto)
@classmethod
def bgyellow(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgyellow', string, auto=auto)
@classmethod
def blue(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('blue', string, auto=auto)
@classmethod
def bgblue(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgblue', string, auto=auto)
@classmethod
def magenta(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('magenta', string, auto=auto)
@classmethod
def bgmagenta(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgmagenta', string, auto=auto)
@classmethod
def cyan(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('cyan', string, auto=auto)
@classmethod
def bgcyan(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgcyan', string, auto=auto)
@classmethod
def white(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('white', string, auto=auto)
@classmethod
def bgwhite(cls, string, auto=False):
"""Color-code entire string.
:param str string: String to colorize.
:param bool auto: Enable auto-color (dark/light terminal).
:return: Class instance for colorized string.
:rtype: Color
"""
return cls.colorize('bgwhite', string, auto=auto)

342
colorclass/core.py Normal file
View file

@ -0,0 +1,342 @@
"""String subclass that handles ANSI color codes."""
from colorclass.codes import ANSICodeMapping
from colorclass.parse import parse_input, RE_SPLIT
from colorclass.search import build_color_index, find_char_color
PARENT_CLASS = type(u'')
def apply_text(incoming, func):
"""Call `func` on text portions of incoming color string.
:param iter incoming: Incoming string/ColorStr/string-like object to iterate.
:param func: Function to call with string portion as first and only parameter.
:return: Modified string, same class type as incoming string.
"""
split = RE_SPLIT.split(incoming)
for i, item in enumerate(split):
if not item or RE_SPLIT.match(item):
continue
split[i] = func(item)
return incoming.__class__().join(split)
class ColorBytes(bytes):
"""Str (bytes in Python3) subclass, .decode() overridden to return unicode (str in Python3) subclass instance."""
def __new__(cls, *args, **kwargs):
"""Save original class so decode() returns an instance of it."""
original_class = kwargs.pop('original_class')
combined_args = [cls] + list(args)
instance = bytes.__new__(*combined_args, **kwargs)
instance.original_class = original_class
return instance
def decode(self, encoding='utf-8', errors='strict'):
"""Decode using the codec registered for encoding. Default encoding is 'utf-8'.
errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors
raise a UnicodeDecodeError. Other possible values are 'ignore' and 'replace' as well as any other name
registered with codecs.register_error that is able to handle UnicodeDecodeErrors.
:param str encoding: Codec.
:param str errors: Error handling scheme.
"""
original_class = getattr(self, 'original_class')
return original_class(super(ColorBytes, self).decode(encoding, errors))
class ColorStr(PARENT_CLASS):
"""Core color class."""
def __new__(cls, *args, **kwargs):
"""Parse color markup and instantiate."""
keep_tags = kwargs.pop('keep_tags', False)
# Parse string.
value_markup = args[0] if args else PARENT_CLASS() # e.g. '{red}test{/red}'
value_colors, value_no_colors = parse_input(value_markup, ANSICodeMapping.DISABLE_COLORS, keep_tags)
color_index = build_color_index(value_colors)
# Instantiate.
color_args = [cls, value_colors] + list(args[1:])
instance = PARENT_CLASS.__new__(*color_args, **kwargs)
# Add additional attributes and return.
instance.value_colors = value_colors
instance.value_no_colors = value_no_colors
instance.has_colors = value_colors != value_no_colors
instance.color_index = color_index
return instance
def __add__(self, other):
"""Concatenate."""
return self.__class__(self.value_colors + other, keep_tags=True)
def __getitem__(self, item):
"""Retrieve character."""
try:
color_pos = self.color_index[int(item)]
except TypeError: # slice
return super(ColorStr, self).__getitem__(item)
return self.__class__(find_char_color(self.value_colors, color_pos), keep_tags=True)
def __iter__(self):
"""Yield one color-coded character at a time."""
for color_pos in self.color_index:
yield self.__class__(find_char_color(self.value_colors, color_pos))
def __len__(self):
"""Length of string without color codes (what users expect)."""
return self.value_no_colors.__len__()
def __mod__(self, other):
"""String substitution (like printf)."""
return self.__class__(self.value_colors % other, keep_tags=True)
def __mul__(self, other):
"""Multiply string."""
return self.__class__(self.value_colors * other, keep_tags=True)
def __repr__(self):
"""Representation of a class instance (like datetime.datetime.now())."""
return '{name}({value})'.format(name=self.__class__.__name__, value=repr(self.value_colors))
def capitalize(self):
"""Return a copy of the string with only its first character capitalized."""
return apply_text(self, lambda s: s.capitalize())
def center(self, width, fillchar=None):
"""Return centered in a string of length width. Padding is done using the specified fill character or space.
:param int width: Length of output string.
:param str fillchar: Use this character instead of spaces.
"""
if fillchar is not None:
result = self.value_no_colors.center(width, fillchar)
else:
result = self.value_no_colors.center(width)
return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True)
def count(self, sub, start=0, end=-1):
"""Return the number of non-overlapping occurrences of substring sub in string[start:end].
Optional arguments start and end are interpreted as in slice notation.
:param str sub: Substring to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.count(sub, start, end)
def endswith(self, suffix, start=0, end=None):
"""Return True if ends with the specified suffix, False otherwise.
With optional start, test beginning at that position. With optional end, stop comparing at that position.
suffix can also be a tuple of strings to try.
:param str suffix: Suffix to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
args = [suffix, start] + ([] if end is None else [end])
return self.value_no_colors.endswith(*args)
def encode(self, encoding=None, errors='strict'):
"""Encode using the codec registered for encoding. encoding defaults to the default encoding.
errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors
raise a UnicodeEncodeError. Other possible values are 'ignore', 'replace' and 'xmlcharrefreplace' as well as any
other name registered with codecs.register_error that is able to handle UnicodeEncodeErrors.
:param str encoding: Codec.
:param str errors: Error handling scheme.
"""
return ColorBytes(super(ColorStr, self).encode(encoding, errors), original_class=self.__class__)
def decode(self, encoding=None, errors='strict'):
"""Decode using the codec registered for encoding. encoding defaults to the default encoding.
errors may be given to set a different error handling scheme. Default is 'strict' meaning that encoding errors
raise a UnicodeDecodeError. Other possible values are 'ignore' and 'replace' as well as any other name
registered with codecs.register_error that is able to handle UnicodeDecodeErrors.
:param str encoding: Codec.
:param str errors: Error handling scheme.
"""
return self.__class__(super(ColorStr, self).decode(encoding, errors), keep_tags=True)
def find(self, sub, start=None, end=None):
"""Return the lowest index where substring sub is found, such that sub is contained within string[start:end].
Optional arguments start and end are interpreted as in slice notation.
:param str sub: Substring to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.find(sub, start, end)
def format(self, *args, **kwargs):
"""Return a formatted version, using substitutions from args and kwargs.
The substitutions are identified by braces ('{' and '}').
"""
return self.__class__(super(ColorStr, self).format(*args, **kwargs), keep_tags=True)
def index(self, sub, start=None, end=None):
"""Like S.find() but raise ValueError when the substring is not found.
:param str sub: Substring to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.index(sub, start, end)
def isalnum(self):
"""Return True if all characters in string are alphanumeric and there is at least one character in it."""
return self.value_no_colors.isalnum()
def isalpha(self):
"""Return True if all characters in string are alphabetic and there is at least one character in it."""
return self.value_no_colors.isalpha()
def isdecimal(self):
"""Return True if there are only decimal characters in string, False otherwise."""
return self.value_no_colors.isdecimal()
def isdigit(self):
"""Return True if all characters in string are digits and there is at least one character in it."""
return self.value_no_colors.isdigit()
def isnumeric(self):
"""Return True if there are only numeric characters in string, False otherwise."""
return self.value_no_colors.isnumeric()
def isspace(self):
"""Return True if all characters in string are whitespace and there is at least one character in it."""
return self.value_no_colors.isspace()
def istitle(self):
"""Return True if string is a titlecased string and there is at least one character in it.
That is uppercase characters may only follow uncased characters and lowercase characters only cased ones. Return
False otherwise.
"""
return self.value_no_colors.istitle()
def isupper(self):
"""Return True if all cased characters are uppercase and there is at least one cased character in it."""
return self.value_no_colors.isupper()
def join(self, iterable):
"""Return a string which is the concatenation of the strings in the iterable.
:param iterable: Join items in this iterable.
"""
return self.__class__(super(ColorStr, self).join(iterable), keep_tags=True)
def ljust(self, width, fillchar=None):
"""Return left-justified string of length width. Padding is done using the specified fill character or space.
:param int width: Length of output string.
:param str fillchar: Use this character instead of spaces.
"""
if fillchar is not None:
result = self.value_no_colors.ljust(width, fillchar)
else:
result = self.value_no_colors.ljust(width)
return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True)
def rfind(self, sub, start=None, end=None):
"""Return the highest index where substring sub is found, such that sub is contained within string[start:end].
Optional arguments start and end are interpreted as in slice notation.
:param str sub: Substring to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.rfind(sub, start, end)
def rindex(self, sub, start=None, end=None):
"""Like .rfind() but raise ValueError when the substring is not found.
:param str sub: Substring to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.rindex(sub, start, end)
def rjust(self, width, fillchar=None):
"""Return right-justified string of length width. Padding is done using the specified fill character or space.
:param int width: Length of output string.
:param str fillchar: Use this character instead of spaces.
"""
if fillchar is not None:
result = self.value_no_colors.rjust(width, fillchar)
else:
result = self.value_no_colors.rjust(width)
return self.__class__(result.replace(self.value_no_colors, self.value_colors), keep_tags=True)
def splitlines(self, keepends=False):
"""Return a list of the lines in the string, breaking at line boundaries.
Line breaks are not included in the resulting list unless keepends is given and True.
:param bool keepends: Include linebreaks.
"""
return [self.__class__(l) for l in self.value_colors.splitlines(keepends)]
def startswith(self, prefix, start=0, end=-1):
"""Return True if string starts with the specified prefix, False otherwise.
With optional start, test beginning at that position. With optional end, stop comparing at that position. prefix
can also be a tuple of strings to try.
:param str prefix: Prefix to search.
:param int start: Beginning position.
:param int end: Stop comparison at this position.
"""
return self.value_no_colors.startswith(prefix, start, end)
def swapcase(self):
"""Return a copy of the string with uppercase characters converted to lowercase and vice versa."""
return apply_text(self, lambda s: s.swapcase())
def title(self):
"""Return a titlecased version of the string.
That is words start with uppercase characters, all remaining cased characters have lowercase.
"""
return apply_text(self, lambda s: s.title())
def translate(self, table):
"""Return a copy of the string, where all characters have been mapped through the given translation table.
Table must be a mapping of Unicode ordinals to Unicode ordinals, strings, or None. Unmapped characters are left
untouched. Characters mapped to None are deleted.
:param table: Translation table.
"""
return apply_text(self, lambda s: s.translate(table))
def upper(self):
"""Return a copy of the string converted to uppercase."""
return apply_text(self, lambda s: s.upper())
def zfill(self, width):
"""Pad a numeric string with zeros on the left, to fill a field of the specified width.
The string is never truncated.
:param int width: Length of output string.
"""
if not self.value_no_colors:
result = self.value_no_colors.zfill(width)
else:
result = self.value_colors.replace(self.value_no_colors, self.value_no_colors.zfill(width))
return self.__class__(result, keep_tags=True)

96
colorclass/parse.py Normal file
View file

@ -0,0 +1,96 @@
"""Parse color markup tags into ANSI escape sequences."""
import re
from colorclass.codes import ANSICodeMapping, BASE_CODES
CODE_GROUPS = (
tuple(set(str(i) for i in BASE_CODES.values() if i and (40 <= i <= 49 or 100 <= i <= 109))), # bg colors
tuple(set(str(i) for i in BASE_CODES.values() if i and (30 <= i <= 39 or 90 <= i <= 99))), # fg colors
('1', '22'), ('2', '22'), ('3', '23'), ('4', '24'), ('5', '25'), ('6', '26'), ('7', '27'), ('8', '28'), ('9', '29'),
)
RE_ANSI = re.compile(r'(\033\[([\d;]+)m)')
RE_COMBINE = re.compile(r'\033\[([\d;]+)m\033\[([\d;]+)m')
RE_SPLIT = re.compile(r'(\033\[[\d;]+m)')
def prune_overridden(ansi_string):
"""Remove color codes that are rendered ineffective by subsequent codes in one escape sequence then sort codes.
:param str ansi_string: Incoming ansi_string with ANSI color codes.
:return: Color string with pruned color sequences.
:rtype: str
"""
multi_seqs = set(p for p in RE_ANSI.findall(ansi_string) if ';' in p[1]) # Sequences with multiple color codes.
for escape, codes in multi_seqs:
r_codes = list(reversed(codes.split(';')))
# Nuke everything before {/all}.
try:
r_codes = r_codes[:r_codes.index('0') + 1]
except ValueError:
pass
# Thin out groups.
for group in CODE_GROUPS:
for pos in reversed([i for i, n in enumerate(r_codes) if n in group][1:]):
r_codes.pop(pos)
# Done.
reduced_codes = ';'.join(sorted(r_codes, key=int))
if codes != reduced_codes:
ansi_string = ansi_string.replace(escape, '\033[' + reduced_codes + 'm')
return ansi_string
def parse_input(tagged_string, disable_colors, keep_tags):
"""Perform the actual conversion of tags to ANSI escaped codes.
Provides a version of the input without any colors for len() and other methods.
:param str tagged_string: The input unicode value.
:param bool disable_colors: Strip all colors in both outputs.
:param bool keep_tags: Skip parsing curly bracket tags into ANSI escape sequences.
:return: 2-item tuple. First item is the parsed output. Second item is a version of the input without any colors.
:rtype: tuple
"""
codes = ANSICodeMapping(tagged_string)
output_colors = getattr(tagged_string, 'value_colors', tagged_string)
# Convert: '{b}{red}' -> '\033[1m\033[31m'
if not keep_tags:
for tag, replacement in (('{' + k + '}', '' if v is None else '\033[%dm' % v) for k, v in codes.items()):
output_colors = output_colors.replace(tag, replacement)
# Strip colors.
output_no_colors = RE_ANSI.sub('', output_colors)
if disable_colors:
return output_no_colors, output_no_colors
# Combine: '\033[1m\033[31m' -> '\033[1;31m'
while True:
simplified = RE_COMBINE.sub(r'\033[\1;\2m', output_colors)
if simplified == output_colors:
break
output_colors = simplified
# Prune: '\033[31;32;33;34;35m' -> '\033[35m'
output_colors = prune_overridden(output_colors)
# Deduplicate: '\033[1;mT\033[1;mE\033[1;mS\033[1;mT' -> '\033[1;mTEST'
previous_escape = None
segments = list()
for item in (i for i in RE_SPLIT.split(output_colors) if i):
if RE_SPLIT.match(item):
if item != previous_escape:
segments.append(item)
previous_escape = item
else:
segments.append(item)
output_colors = ''.join(segments)
return output_colors, output_no_colors

49
colorclass/search.py Normal file
View file

@ -0,0 +1,49 @@
"""Determine color of characters that may or may not be adjacent to ANSI escape sequences."""
from colorclass.parse import RE_SPLIT
def build_color_index(ansi_string):
"""Build an index between visible characters and a string with invisible color codes.
:param str ansi_string: String with color codes (ANSI escape sequences).
:return: Position of visible characters in color string (indexes match non-color string).
:rtype: tuple
"""
mapping = list()
color_offset = 0
for item in (i for i in RE_SPLIT.split(ansi_string) if i):
if RE_SPLIT.match(item):
color_offset += len(item)
else:
for _ in range(len(item)):
mapping.append(color_offset)
color_offset += 1
return tuple(mapping)
def find_char_color(ansi_string, pos):
"""Determine what color a character is in the string.
:param str ansi_string: String with color codes (ANSI escape sequences).
:param int pos: Position of the character in the ansi_string.
:return: Character along with all surrounding color codes.
:rtype: str
"""
result = list()
position = 0 # Set to None when character is found.
for item in (i for i in RE_SPLIT.split(ansi_string) if i):
if RE_SPLIT.match(item):
result.append(item)
if position is not None:
position += len(item)
elif position is not None:
for char in item:
if position == pos:
result.append(char)
position = None
break
position += 1
return ''.join(result)

42
colorclass/toggles.py Normal file
View file

@ -0,0 +1,42 @@
"""Convenience functions to enable/disable features."""
from colorclass.codes import ANSICodeMapping
def disable_all_colors():
"""Disable all colors. Strip any color tags or codes."""
ANSICodeMapping.disable_all_colors()
def enable_all_colors():
"""Enable colors."""
ANSICodeMapping.enable_all_colors()
def disable_if_no_tty():
"""Disable all colors if there is no TTY available.
:return: True if colors are disabled, False if stderr or stdout is a TTY.
:rtype: bool
"""
return ANSICodeMapping.disable_if_no_tty()
def is_enabled():
"""Are colors enabled."""
return not ANSICodeMapping.DISABLE_COLORS
def set_light_background():
"""Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
ANSICodeMapping.set_light_background()
def set_dark_background():
"""Choose dark colors for all 'auto'-prefixed codes for readability on light backgrounds."""
ANSICodeMapping.set_dark_background()
def is_light():
"""Are background colors for light backgrounds."""
return ANSICodeMapping.LIGHT_BACKGROUND

388
colorclass/windows.py Normal file
View file

@ -0,0 +1,388 @@
"""Windows console screen buffer handlers."""
from __future__ import print_function
import atexit
import ctypes
import re
import sys
from colorclass.codes import ANSICodeMapping, BASE_CODES
from colorclass.core import RE_SPLIT
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
INVALID_HANDLE_VALUE = -1
IS_WINDOWS = sys.platform == 'win32'
RE_NUMBER_SEARCH = re.compile(r'\033\[([\d;]+)m')
STD_ERROR_HANDLE = -12
STD_OUTPUT_HANDLE = -11
WINDOWS_CODES = {
'/all': -33, '/fg': -39, '/bg': -49,
'black': 0, 'red': 4, 'green': 2, 'yellow': 6, 'blue': 1, 'magenta': 5, 'cyan': 3, 'white': 7,
'bgblack': -8, 'bgred': 64, 'bggreen': 32, 'bgyellow': 96, 'bgblue': 16, 'bgmagenta': 80, 'bgcyan': 48,
'bgwhite': 112,
'hiblack': 8, 'hired': 12, 'higreen': 10, 'hiyellow': 14, 'hiblue': 9, 'himagenta': 13, 'hicyan': 11, 'hiwhite': 15,
'hibgblack': 128, 'hibgred': 192, 'hibggreen': 160, 'hibgyellow': 224, 'hibgblue': 144, 'hibgmagenta': 208,
'hibgcyan': 176, 'hibgwhite': 240,
'/black': -39, '/red': -39, '/green': -39, '/yellow': -39, '/blue': -39, '/magenta': -39, '/cyan': -39,
'/white': -39, '/hiblack': -39, '/hired': -39, '/higreen': -39, '/hiyellow': -39, '/hiblue': -39, '/himagenta': -39,
'/hicyan': -39, '/hiwhite': -39,
'/bgblack': -49, '/bgred': -49, '/bggreen': -49, '/bgyellow': -49, '/bgblue': -49, '/bgmagenta': -49,
'/bgcyan': -49, '/bgwhite': -49, '/hibgblack': -49, '/hibgred': -49, '/hibggreen': -49, '/hibgyellow': -49,
'/hibgblue': -49, '/hibgmagenta': -49, '/hibgcyan': -49, '/hibgwhite': -49,
}
class COORD(ctypes.Structure):
"""COORD structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119."""
_fields_ = [
('X', ctypes.c_short),
('Y', ctypes.c_short),
]
class SmallRECT(ctypes.Structure):
"""SMALL_RECT structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311."""
_fields_ = [
('Left', ctypes.c_short),
('Top', ctypes.c_short),
('Right', ctypes.c_short),
('Bottom', ctypes.c_short),
]
class ConsoleScreenBufferInfo(ctypes.Structure):
"""CONSOLE_SCREEN_BUFFER_INFO structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093."""
_fields_ = [
('dwSize', COORD),
('dwCursorPosition', COORD),
('wAttributes', ctypes.c_ushort),
('srWindow', SmallRECT),
('dwMaximumWindowSize', COORD)
]
def init_kernel32(kernel32=None):
"""Load a unique instance of WinDLL into memory, set arg/return types, and get stdout/err handles.
1. Since we are setting DLL function argument types and return types, we need to maintain our own instance of
kernel32 to prevent overriding (or being overwritten by) user's own changes to ctypes.windll.kernel32.
2. While we're doing all this we might as well get the handles to STDOUT and STDERR streams.
3. If either stream has already been replaced set return value to INVALID_HANDLE_VALUE to indicate it shouldn't be
replaced.
:raise AttributeError: When called on a non-Windows platform.
:param kernel32: Optional mock kernel32 object. For testing.
:return: Loaded kernel32 instance, stderr handle (int), stdout handle (int).
:rtype: tuple
"""
if not kernel32:
kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 # Load our own instance. Unique memory address.
kernel32.GetStdHandle.argtypes = [ctypes.c_ulong]
kernel32.GetStdHandle.restype = ctypes.c_void_p
kernel32.GetConsoleScreenBufferInfo.argtypes = [
ctypes.c_void_p,
ctypes.POINTER(ConsoleScreenBufferInfo),
]
kernel32.GetConsoleScreenBufferInfo.restype = ctypes.c_long
# Get handles.
if hasattr(sys.stderr, '_original_stream'):
stderr = INVALID_HANDLE_VALUE
else:
stderr = kernel32.GetStdHandle(STD_ERROR_HANDLE)
if hasattr(sys.stdout, '_original_stream'):
stdout = INVALID_HANDLE_VALUE
else:
stdout = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
return kernel32, stderr, stdout
def get_console_info(kernel32, handle):
"""Get information about this current console window.
http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231
https://code.google.com/p/colorama/issues/detail?id=47
https://bitbucket.org/pytest-dev/py/src/4617fe46/py/_io/terminalwriter.py
Windows 10 Insider since around February 2016 finally introduced support for ANSI colors. No need to replace stdout
and stderr streams to intercept colors and issue multiple SetConsoleTextAttribute() calls for these consoles.
:raise OSError: When GetConsoleScreenBufferInfo or GetConsoleMode API calls fail.
:param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
:param int handle: stderr or stdout handle.
:return: Foreground and background colors (integers) as well as native ANSI support (bool).
:rtype: tuple
"""
# Query Win32 API.
csbi = ConsoleScreenBufferInfo() # Populated by GetConsoleScreenBufferInfo.
lpcsbi = ctypes.byref(csbi)
dword = ctypes.c_ulong() # Populated by GetConsoleMode.
lpdword = ctypes.byref(dword)
if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi) or not kernel32.GetConsoleMode(handle, lpdword):
raise ctypes.WinError()
# Parse data.
# buffer_width = int(csbi.dwSize.X - 1)
# buffer_height = int(csbi.dwSize.Y)
# terminal_width = int(csbi.srWindow.Right - csbi.srWindow.Left)
# terminal_height = int(csbi.srWindow.Bottom - csbi.srWindow.Top)
fg_color = csbi.wAttributes % 16
bg_color = csbi.wAttributes & 240
native_ansi = bool(dword.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
return fg_color, bg_color, native_ansi
def bg_color_native_ansi(kernel32, stderr, stdout):
"""Get background color and if console supports ANSI colors natively for both streams.
:param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
:param int stderr: stderr handle.
:param int stdout: stdout handle.
:return: Background color (int) and native ANSI support (bool).
:rtype: tuple
"""
try:
if stderr == INVALID_HANDLE_VALUE:
raise OSError
bg_color, native_ansi = get_console_info(kernel32, stderr)[1:]
except OSError:
try:
if stdout == INVALID_HANDLE_VALUE:
raise OSError
bg_color, native_ansi = get_console_info(kernel32, stdout)[1:]
except OSError:
bg_color, native_ansi = WINDOWS_CODES['black'], False
return bg_color, native_ansi
class WindowsStream(object):
"""Replacement stream which overrides sys.stdout or sys.stderr. When writing or printing, ANSI codes are converted.
ANSI (Linux/Unix) color codes are converted into win32 system calls, changing the next character's color before
printing it. Resources referenced:
https://github.com/tartley/colorama
http://www.cplusplus.com/articles/2ywTURfi/
http://thomasfischer.biz/python-and-windows-terminal-colors/
http://stackoverflow.com/questions/17125440/c-win32-console-color
http://www.tysos.org/svn/trunk/mono/corlib/System/WindowsConsoleDriver.cs
http://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088#_win32_character_attributes
:cvar list ALL_BG_CODES: List of bg Windows codes. Used to determine if requested color is foreground or background.
:cvar dict COMPILED_CODES: Translation dict. Keys are ANSI codes (values of BASE_CODES), values are Windows codes.
:ivar int default_fg: Foreground Windows color code at the time of instantiation.
:ivar int default_bg: Background Windows color code at the time of instantiation.
"""
ALL_BG_CODES = [v for k, v in WINDOWS_CODES.items() if k.startswith('bg') or k.startswith('hibg')]
COMPILED_CODES = dict((v, WINDOWS_CODES[k]) for k, v in BASE_CODES.items() if k in WINDOWS_CODES)
def __init__(self, kernel32, stream_handle, original_stream):
"""Constructor.
:param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
:param int stream_handle: stderr or stdout handle.
:param original_stream: sys.stderr or sys.stdout before being overridden by this class' instance.
"""
self._kernel32 = kernel32
self._stream_handle = stream_handle
self._original_stream = original_stream
self.default_fg, self.default_bg = self.colors
def __getattr__(self, item):
"""If an attribute/function/etc is not defined in this function, retrieve the one from the original stream.
Fixes ipython arrow key presses.
"""
return getattr(self._original_stream, item)
@property
def colors(self):
"""Return the current foreground and background colors."""
try:
return get_console_info(self._kernel32, self._stream_handle)[:2]
except OSError:
return WINDOWS_CODES['white'], WINDOWS_CODES['black']
@colors.setter
def colors(self, color_code):
"""Change the foreground and background colors for subsequently printed characters.
None resets colors to their original values (when class was instantiated).
Since setting a color requires including both foreground and background codes (merged), setting just the
foreground color resets the background color to black, and vice versa.
This function first gets the current background and foreground colors, merges in the requested color code, and
sets the result.
However if we need to remove just the foreground color but leave the background color the same (or vice versa)
such as when {/red} is used, we must merge the default foreground color with the current background color. This
is the reason for those negative values.
:param int color_code: Color code from WINDOWS_CODES.
"""
if color_code is None:
color_code = WINDOWS_CODES['/all']
# Get current color code.
current_fg, current_bg = self.colors
# Handle special negative codes. Also determine the final color code.
if color_code == WINDOWS_CODES['/fg']:
final_color_code = self.default_fg | current_bg # Reset the foreground only.
elif color_code == WINDOWS_CODES['/bg']:
final_color_code = current_fg | self.default_bg # Reset the background only.
elif color_code == WINDOWS_CODES['/all']:
final_color_code = self.default_fg | self.default_bg # Reset both.
elif color_code == WINDOWS_CODES['bgblack']:
final_color_code = current_fg # Black background.
else:
new_is_bg = color_code in self.ALL_BG_CODES
final_color_code = color_code | (current_fg if new_is_bg else current_bg)
# Set new code.
self._kernel32.SetConsoleTextAttribute(self._stream_handle, final_color_code)
def write(self, p_str):
"""Write to stream.
:param str p_str: string to print.
"""
for segment in RE_SPLIT.split(p_str):
if not segment:
# Empty string. p_str probably starts with colors so the first item is always ''.
continue
if not RE_SPLIT.match(segment):
# No color codes, print regular text.
print(segment, file=self._original_stream, end='')
self._original_stream.flush()
continue
for color_code in (int(c) for c in RE_NUMBER_SEARCH.findall(segment)[0].split(';')):
if color_code in self.COMPILED_CODES:
self.colors = self.COMPILED_CODES[color_code]
class Windows(object):
"""Enable and disable Windows support for ANSI color character codes.
Call static method Windows.enable() to enable color support for the remainder of the process' lifetime.
This class is also a context manager. You can do this:
with Windows():
print(Color('{autored}Test{/autored}'))
Or this:
with Windows(auto_colors=True):
print(Color('{autored}Test{/autored}'))
"""
@classmethod
def disable(cls):
"""Restore sys.stderr and sys.stdout to their original objects. Resets colors to their original values.
:return: If streams restored successfully.
:rtype: bool
"""
# Skip if not on Windows.
if not IS_WINDOWS:
return False
# Restore default colors.
if hasattr(sys.stderr, '_original_stream'):
getattr(sys, 'stderr').color = None
if hasattr(sys.stdout, '_original_stream'):
getattr(sys, 'stdout').color = None
# Restore original streams.
changed = False
if hasattr(sys.stderr, '_original_stream'):
changed = True
sys.stderr = getattr(sys.stderr, '_original_stream')
if hasattr(sys.stdout, '_original_stream'):
changed = True
sys.stdout = getattr(sys.stdout, '_original_stream')
return changed
@staticmethod
def is_enabled():
"""Return True if either stderr or stdout has colors enabled."""
return hasattr(sys.stderr, '_original_stream') or hasattr(sys.stdout, '_original_stream')
@classmethod
def enable(cls, auto_colors=False, reset_atexit=False):
"""Enable color text with print() or sys.stdout.write() (stderr too).
:param bool auto_colors: Automatically selects dark or light colors based on current terminal's background
color. Only works with {autored} and related tags.
:param bool reset_atexit: Resets original colors upon Python exit (in case you forget to reset it yourself with
a closing tag). Does nothing on native ANSI consoles.
:return: If streams replaced successfully.
:rtype: bool
"""
if not IS_WINDOWS:
return False # Windows only.
# Get values from init_kernel32().
kernel32, stderr, stdout = init_kernel32()
if stderr == INVALID_HANDLE_VALUE and stdout == INVALID_HANDLE_VALUE:
return False # No valid handles, nothing to do.
# Get console info.
bg_color, native_ansi = bg_color_native_ansi(kernel32, stderr, stdout)
# Set auto colors:
if auto_colors:
if bg_color in (112, 96, 240, 176, 224, 208, 160):
ANSICodeMapping.set_light_background()
else:
ANSICodeMapping.set_dark_background()
# Don't replace streams if ANSI codes are natively supported.
if native_ansi:
return False
# Reset on exit if requested.
if reset_atexit:
atexit.register(cls.disable)
# Overwrite stream references.
if stderr != INVALID_HANDLE_VALUE:
sys.stderr.flush()
sys.stderr = WindowsStream(kernel32, stderr, sys.stderr)
if stdout != INVALID_HANDLE_VALUE:
sys.stdout.flush()
sys.stdout = WindowsStream(kernel32, stdout, sys.stdout)
return True
def __init__(self, auto_colors=False):
"""Constructor."""
self.auto_colors = auto_colors
def __enter__(self):
"""Context manager, enables colors on Windows."""
self.enable(auto_colors=self.auto_colors)
def __exit__(self, *_):
"""Context manager, disabled colors on Windows."""
self.disable()

BIN
example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

229
example.py Executable file
View file

@ -0,0 +1,229 @@
#!/usr/bin/env python
"""Example usage of colorclass.
Just prints sample text and exits.
Usage:
example.py print [(-n|-c)] [(-l|-d)] [-w FILE]
example.py -h | --help
Options:
-h --help Show this screen.
-c --colors Enable colors even when piped to another program.
-d --dark-bg Autocolors for black/dark backgrounds on Linux/OSX.
-l --light-bg Autocolors for white/light backgrounds on Linux/OSX.
-n --no-colors Strip out any foreground or background colors.
-w FILE --wait=FILE Wait for user create FILE, then exit. For testing.
"""
from __future__ import print_function
import os
import sys
import time
from docopt import docopt
from colorclass import Color
from colorclass import disable_all_colors, enable_all_colors, is_enabled
from colorclass import set_dark_background, set_light_background
from colorclass import Windows
OPTIONS = docopt(__doc__) if __name__ == '__main__' else dict()
def main():
"""Main function called upon script execution."""
if OPTIONS.get('--no-colors'):
disable_all_colors()
elif OPTIONS.get('--colors'):
enable_all_colors()
if is_enabled() and os.name == 'nt':
Windows.enable(auto_colors=True, reset_atexit=True)
elif OPTIONS.get('--light-bg'):
set_light_background()
elif OPTIONS.get('--dark-bg'):
set_dark_background()
# Light or dark colors.
print('Autocolors for all backgrounds:')
print(Color(' {autoblack}Black{/black} {autored}Red{/red} {autogreen}Green{/green} '), end='')
print(Color('{autoyellow}Yellow{/yellow} {autoblue}Blue{/blue} {automagenta}Magenta{/magenta} '), end='')
print(Color('{autocyan}Cyan{/cyan} {autowhite}White{/white}'))
print(Color(' {autobgblack}{autoblack}Black{/black}{/bgblack} '), end='')
print(Color('{autobgblack}{autored}Red{/red}{/bgblack} {autobgblack}{autogreen}Green{/green}{/bgblack} '), end='')
print(Color('{autobgblack}{autoyellow}Yellow{/yellow}{/bgblack} '), end='')
print(Color('{autobgblack}{autoblue}Blue{/blue}{/bgblack} '), end='')
print(Color('{autobgblack}{automagenta}Magenta{/magenta}{/bgblack} '), end='')
print(Color('{autobgblack}{autocyan}Cyan{/cyan}{/bgblack} {autobgblack}{autowhite}White{/white}{/bgblack}'))
print(Color(' {autobgred}{autoblack}Black{/black}{/bgred} {autobgred}{autored}Red{/red}{/bgred} '), end='')
print(Color('{autobgred}{autogreen}Green{/green}{/bgred} {autobgred}{autoyellow}Yellow{/yellow}{/bgred} '), end='')
print(Color('{autobgred}{autoblue}Blue{/blue}{/bgred} {autobgred}{automagenta}Magenta{/magenta}{/bgred} '), end='')
print(Color('{autobgred}{autocyan}Cyan{/cyan}{/bgred} {autobgred}{autowhite}White{/white}{/bgred}'))
print(Color(' {autobggreen}{autoblack}Black{/black}{/bggreen} '), end='')
print(Color('{autobggreen}{autored}Red{/red}{/bggreen} {autobggreen}{autogreen}Green{/green}{/bggreen} '), end='')
print(Color('{autobggreen}{autoyellow}Yellow{/yellow}{/bggreen} '), end='')
print(Color('{autobggreen}{autoblue}Blue{/blue}{/bggreen} '), end='')
print(Color('{autobggreen}{automagenta}Magenta{/magenta}{/bggreen} '), end='')
print(Color('{autobggreen}{autocyan}Cyan{/cyan}{/bggreen} {autobggreen}{autowhite}White{/white}{/bggreen}'))
print(Color(' {autobgyellow}{autoblack}Black{/black}{/bgyellow} '), end='')
print(Color('{autobgyellow}{autored}Red{/red}{/bgyellow} '), end='')
print(Color('{autobgyellow}{autogreen}Green{/green}{/bgyellow} '), end='')
print(Color('{autobgyellow}{autoyellow}Yellow{/yellow}{/bgyellow} '), end='')
print(Color('{autobgyellow}{autoblue}Blue{/blue}{/bgyellow} '), end='')
print(Color('{autobgyellow}{automagenta}Magenta{/magenta}{/bgyellow} '), end='')
print(Color('{autobgyellow}{autocyan}Cyan{/cyan}{/bgyellow} {autobgyellow}{autowhite}White{/white}{/bgyellow}'))
print(Color(' {autobgblue}{autoblack}Black{/black}{/bgblue} {autobgblue}{autored}Red{/red}{/bgblue} '), end='')
print(Color('{autobgblue}{autogreen}Green{/green}{/bgblue} '), end='')
print(Color('{autobgblue}{autoyellow}Yellow{/yellow}{/bgblue} {autobgblue}{autoblue}Blue{/blue}{/bgblue} '), end='')
print(Color('{autobgblue}{automagenta}Magenta{/magenta}{/bgblue} '), end='')
print(Color('{autobgblue}{autocyan}Cyan{/cyan}{/bgblue} {autobgblue}{autowhite}White{/white}{/bgblue}'))
print(Color(' {autobgmagenta}{autoblack}Black{/black}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autored}Red{/red}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autogreen}Green{/green}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autoyellow}Yellow{/yellow}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autoblue}Blue{/blue}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{automagenta}Magenta{/magenta}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autocyan}Cyan{/cyan}{/bgmagenta} '), end='')
print(Color('{autobgmagenta}{autowhite}White{/white}{/bgmagenta}'))
print(Color(' {autobgcyan}{autoblack}Black{/black}{/bgcyan} {autobgcyan}{autored}Red{/red}{/bgcyan} '), end='')
print(Color('{autobgcyan}{autogreen}Green{/green}{/bgcyan} '), end='')
print(Color('{autobgcyan}{autoyellow}Yellow{/yellow}{/bgcyan} {autobgcyan}{autoblue}Blue{/blue}{/bgcyan} '), end='')
print(Color('{autobgcyan}{automagenta}Magenta{/magenta}{/bgcyan} '), end='')
print(Color('{autobgcyan}{autocyan}Cyan{/cyan}{/bgcyan} {autobgcyan}{autowhite}White{/white}{/bgcyan}'))
print(Color(' {autobgwhite}{autoblack}Black{/black}{/bgwhite} '), end='')
print(Color('{autobgwhite}{autored}Red{/red}{/bgwhite} {autobgwhite}{autogreen}Green{/green}{/bgwhite} '), end='')
print(Color('{autobgwhite}{autoyellow}Yellow{/yellow}{/bgwhite} '), end='')
print(Color('{autobgwhite}{autoblue}Blue{/blue}{/bgwhite} '), end='')
print(Color('{autobgwhite}{automagenta}Magenta{/magenta}{/bgwhite} '), end='')
print(Color('{autobgwhite}{autocyan}Cyan{/cyan}{/bgwhite} {autobgwhite}{autowhite}White{/white}{/bgwhite}'))
print()
# Light colors.
print('Light colors for dark backgrounds:')
print(Color(' {hiblack}Black{/black} {hired}Red{/red} {higreen}Green{/green} '), end='')
print(Color('{hiyellow}Yellow{/yellow} {hiblue}Blue{/blue} {himagenta}Magenta{/magenta} '), end='')
print(Color('{hicyan}Cyan{/cyan} {hiwhite}White{/white}'))
print(Color(' {hibgblack}{hiblack}Black{/black}{/bgblack} '), end='')
print(Color('{hibgblack}{hired}Red{/red}{/bgblack} {hibgblack}{higreen}Green{/green}{/bgblack} '), end='')
print(Color('{hibgblack}{hiyellow}Yellow{/yellow}{/bgblack} '), end='')
print(Color('{hibgblack}{hiblue}Blue{/blue}{/bgblack} '), end='')
print(Color('{hibgblack}{himagenta}Magenta{/magenta}{/bgblack} '), end='')
print(Color('{hibgblack}{hicyan}Cyan{/cyan}{/bgblack} {hibgblack}{hiwhite}White{/white}{/bgblack}'))
print(Color(' {hibgred}{hiblack}Black{/black}{/bgred} {hibgred}{hired}Red{/red}{/bgred} '), end='')
print(Color('{hibgred}{higreen}Green{/green}{/bgred} {hibgred}{hiyellow}Yellow{/yellow}{/bgred} '), end='')
print(Color('{hibgred}{hiblue}Blue{/blue}{/bgred} {hibgred}{himagenta}Magenta{/magenta}{/bgred} '), end='')
print(Color('{hibgred}{hicyan}Cyan{/cyan}{/bgred} {hibgred}{hiwhite}White{/white}{/bgred}'))
print(Color(' {hibggreen}{hiblack}Black{/black}{/bggreen} '), end='')
print(Color('{hibggreen}{hired}Red{/red}{/bggreen} {hibggreen}{higreen}Green{/green}{/bggreen} '), end='')
print(Color('{hibggreen}{hiyellow}Yellow{/yellow}{/bggreen} '), end='')
print(Color('{hibggreen}{hiblue}Blue{/blue}{/bggreen} '), end='')
print(Color('{hibggreen}{himagenta}Magenta{/magenta}{/bggreen} '), end='')
print(Color('{hibggreen}{hicyan}Cyan{/cyan}{/bggreen} {hibggreen}{hiwhite}White{/white}{/bggreen}'))
print(Color(' {hibgyellow}{hiblack}Black{/black}{/bgyellow} '), end='')
print(Color('{hibgyellow}{hired}Red{/red}{/bgyellow} '), end='')
print(Color('{hibgyellow}{higreen}Green{/green}{/bgyellow} '), end='')
print(Color('{hibgyellow}{hiyellow}Yellow{/yellow}{/bgyellow} '), end='')
print(Color('{hibgyellow}{hiblue}Blue{/blue}{/bgyellow} '), end='')
print(Color('{hibgyellow}{himagenta}Magenta{/magenta}{/bgyellow} '), end='')
print(Color('{hibgyellow}{hicyan}Cyan{/cyan}{/bgyellow} {hibgyellow}{hiwhite}White{/white}{/bgyellow}'))
print(Color(' {hibgblue}{hiblack}Black{/black}{/bgblue} {hibgblue}{hired}Red{/red}{/bgblue} '), end='')
print(Color('{hibgblue}{higreen}Green{/green}{/bgblue} '), end='')
print(Color('{hibgblue}{hiyellow}Yellow{/yellow}{/bgblue} {hibgblue}{hiblue}Blue{/blue}{/bgblue} '), end='')
print(Color('{hibgblue}{himagenta}Magenta{/magenta}{/bgblue} '), end='')
print(Color('{hibgblue}{hicyan}Cyan{/cyan}{/bgblue} {hibgblue}{hiwhite}White{/white}{/bgblue}'))
print(Color(' {hibgmagenta}{hiblack}Black{/black}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{hired}Red{/red}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{higreen}Green{/green}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{hiyellow}Yellow{/yellow}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{hiblue}Blue{/blue}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{himagenta}Magenta{/magenta}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{hicyan}Cyan{/cyan}{/bgmagenta} '), end='')
print(Color('{hibgmagenta}{hiwhite}White{/white}{/bgmagenta}'))
print(Color(' {hibgcyan}{hiblack}Black{/black}{/bgcyan} {hibgcyan}{hired}Red{/red}{/bgcyan} '), end='')
print(Color('{hibgcyan}{higreen}Green{/green}{/bgcyan} '), end='')
print(Color('{hibgcyan}{hiyellow}Yellow{/yellow}{/bgcyan} {hibgcyan}{hiblue}Blue{/blue}{/bgcyan} '), end='')
print(Color('{hibgcyan}{himagenta}Magenta{/magenta}{/bgcyan} '), end='')
print(Color('{hibgcyan}{hicyan}Cyan{/cyan}{/bgcyan} {hibgcyan}{hiwhite}White{/white}{/bgcyan}'))
print(Color(' {hibgwhite}{hiblack}Black{/black}{/bgwhite} '), end='')
print(Color('{hibgwhite}{hired}Red{/red}{/bgwhite} {hibgwhite}{higreen}Green{/green}{/bgwhite} '), end='')
print(Color('{hibgwhite}{hiyellow}Yellow{/yellow}{/bgwhite} '), end='')
print(Color('{hibgwhite}{hiblue}Blue{/blue}{/bgwhite} '), end='')
print(Color('{hibgwhite}{himagenta}Magenta{/magenta}{/bgwhite} '), end='')
print(Color('{hibgwhite}{hicyan}Cyan{/cyan}{/bgwhite} {hibgwhite}{hiwhite}White{/white}{/bgwhite}'))
print()
# Dark colors.
print('Dark colors for light backgrounds:')
print(Color(' {black}Black{/black} {red}Red{/red} {green}Green{/green} {yellow}Yellow{/yellow} '), end='')
print(Color('{blue}Blue{/blue} {magenta}Magenta{/magenta} {cyan}Cyan{/cyan} {white}White{/white}'))
print(Color(' {bgblack}{black}Black{/black}{/bgblack} {bgblack}{red}Red{/red}{/bgblack} '), end='')
print(Color('{bgblack}{green}Green{/green}{/bgblack} {bgblack}{yellow}Yellow{/yellow}{/bgblack} '), end='')
print(Color('{bgblack}{blue}Blue{/blue}{/bgblack} {bgblack}{magenta}Magenta{/magenta}{/bgblack} '), end='')
print(Color('{bgblack}{cyan}Cyan{/cyan}{/bgblack} {bgblack}{white}White{/white}{/bgblack}'))
print(Color(' {bgred}{black}Black{/black}{/bgred} {bgred}{red}Red{/red}{/bgred} '), end='')
print(Color('{bgred}{green}Green{/green}{/bgred} {bgred}{yellow}Yellow{/yellow}{/bgred} '), end='')
print(Color('{bgred}{blue}Blue{/blue}{/bgred} {bgred}{magenta}Magenta{/magenta}{/bgred} '), end='')
print(Color('{bgred}{cyan}Cyan{/cyan}{/bgred} {bgred}{white}White{/white}{/bgred}'))
print(Color(' {bggreen}{black}Black{/black}{/bggreen} {bggreen}{red}Red{/red}{/bggreen} '), end='')
print(Color('{bggreen}{green}Green{/green}{/bggreen} {bggreen}{yellow}Yellow{/yellow}{/bggreen} '), end='')
print(Color('{bggreen}{blue}Blue{/blue}{/bggreen} {bggreen}{magenta}Magenta{/magenta}{/bggreen} '), end='')
print(Color('{bggreen}{cyan}Cyan{/cyan}{/bggreen} {bggreen}{white}White{/white}{/bggreen}'))
print(Color(' {bgyellow}{black}Black{/black}{/bgyellow} {bgyellow}{red}Red{/red}{/bgyellow} '), end='')
print(Color('{bgyellow}{green}Green{/green}{/bgyellow} {bgyellow}{yellow}Yellow{/yellow}{/bgyellow} '), end='')
print(Color('{bgyellow}{blue}Blue{/blue}{/bgyellow} {bgyellow}{magenta}Magenta{/magenta}{/bgyellow} '), end='')
print(Color('{bgyellow}{cyan}Cyan{/cyan}{/bgyellow} {bgyellow}{white}White{/white}{/bgyellow}'))
print(Color(' {bgblue}{black}Black{/black}{/bgblue} {bgblue}{red}Red{/red}{/bgblue} '), end='')
print(Color('{bgblue}{green}Green{/green}{/bgblue} {bgblue}{yellow}Yellow{/yellow}{/bgblue} '), end='')
print(Color('{bgblue}{blue}Blue{/blue}{/bgblue} {bgblue}{magenta}Magenta{/magenta}{/bgblue} '), end='')
print(Color('{bgblue}{cyan}Cyan{/cyan}{/bgblue} {bgblue}{white}White{/white}{/bgblue}'))
print(Color(' {bgmagenta}{black}Black{/black}{/bgmagenta} {bgmagenta}{red}Red{/red}{/bgmagenta} '), end='')
print(Color('{bgmagenta}{green}Green{/green}{/bgmagenta} {bgmagenta}{yellow}Yellow{/yellow}{/bgmagenta} '), end='')
print(Color('{bgmagenta}{blue}Blue{/blue}{/bgmagenta} {bgmagenta}{magenta}Magenta{/magenta}{/bgmagenta} '), end='')
print(Color('{bgmagenta}{cyan}Cyan{/cyan}{/bgmagenta} {bgmagenta}{white}White{/white}{/bgmagenta}'))
print(Color(' {bgcyan}{black}Black{/black}{/bgcyan} {bgcyan}{red}Red{/red}{/bgcyan} '), end='')
print(Color('{bgcyan}{green}Green{/green}{/bgcyan} {bgcyan}{yellow}Yellow{/yellow}{/bgcyan} '), end='')
print(Color('{bgcyan}{blue}Blue{/blue}{/bgcyan} {bgcyan}{magenta}Magenta{/magenta}{/bgcyan} '), end='')
print(Color('{bgcyan}{cyan}Cyan{/cyan}{/bgcyan} {bgcyan}{white}White{/white}{/bgcyan}'))
print(Color(' {bgwhite}{black}Black{/black}{/bgwhite} {bgwhite}{red}Red{/red}{/bgwhite} '), end='')
print(Color('{bgwhite}{green}Green{/green}{/bgwhite} {bgwhite}{yellow}Yellow{/yellow}{/bgwhite} '), end='')
print(Color('{bgwhite}{blue}Blue{/blue}{/bgwhite} {bgwhite}{magenta}Magenta{/magenta}{/bgwhite} '), end='')
print(Color('{bgwhite}{cyan}Cyan{/cyan}{/bgwhite} {bgwhite}{white}White{/white}{/bgwhite}'))
if OPTIONS['--wait']:
print('Waiting for {0} to exist within 10 seconds...'.format(OPTIONS['--wait']), file=sys.stderr, end='')
stop_after = time.time() + 20
while not os.path.exists(OPTIONS['--wait']) and time.time() < stop_after:
print('.', file=sys.stderr, end='')
sys.stderr.flush()
time.sleep(0.5)
print(' done')
if __name__ == '__main__':
main()

BIN
example_windows.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

63
setup.py Executable file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env python
"""Setup script for the project."""
from __future__ import print_function
import codecs
import os
from setuptools import setup
def readme():
"""Try to read README.rst or return empty string if failed.
:return: File contents.
:rtype: str
"""
path = os.path.realpath(os.path.join(os.path.dirname(__file__), 'README.rst'))
handle = None
try:
handle = codecs.open(path, encoding='utf-8')
return handle.read(131072)
except IOError:
return ''
finally:
getattr(handle, 'close', lambda: None)()
setup(
author='@Robpol86',
author_email='robpol86@gmail.com',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',
'Topic :: Terminals',
'Topic :: Text Processing :: Markup',
],
description='Colorful worry-free console applications for Linux, Mac OS X, and Windows.',
install_requires=[],
keywords='Shell Bash ANSI ASCII terminal console colors automatic',
license='MIT',
long_description=readme(),
name='colorclass',
packages=['colorclass'],
url='https://github.com/Robpol86/colorclass',
version='2.2.0',
zip_safe=True,
)

1
tests/__init__.py Normal file
View file

@ -0,0 +1 @@
"""Allows importing from conftest."""

78
tests/conftest.py Normal file
View file

@ -0,0 +1,78 @@
"""Configure tests."""
import py
import pytest
from colorclass.codes import ANSICodeMapping
from colorclass.color import Color
from colorclass.core import ColorStr, PARENT_CLASS
PROJECT_ROOT = py.path.local(__file__).dirpath().join('..')
@pytest.fixture(autouse=True)
def set_defaults(monkeypatch):
"""Set ANSICodeMapping defaults before each test.
:param monkeypatch: pytest fixture.
"""
monkeypatch.setattr(ANSICodeMapping, 'DISABLE_COLORS', False)
monkeypatch.setattr(ANSICodeMapping, 'LIGHT_BACKGROUND', False)
def assert_both_values(actual, expected_plain, expected_color, kind=None):
"""Handle asserts for color and non-color strings in color and non-color tests.
:param ColorStr actual: Return value of ColorStr class method.
:param expected_plain: Expected non-color value.
:param expected_color: Expected color value.
:param str kind: Type of string to test.
"""
if kind.endswith('plain'):
assert actual.value_colors == expected_plain
assert actual.value_no_colors == expected_plain
assert actual.has_colors is False
elif kind.endswith('color'):
assert actual.value_colors == expected_color
assert actual.value_no_colors == expected_plain
if '\033' in actual.value_colors:
assert actual.has_colors is True
else:
assert actual.has_colors is False
else:
assert actual == expected_plain
if kind.startswith('ColorStr'):
assert actual.__class__ == ColorStr
elif kind.startswith('Color'):
assert actual.__class__ == Color
def get_instance(kind, sample=None, color='red'):
"""Get either a string, non-color ColorStr, or color ColorStr instance.
:param str kind: Type of string to test.
:param iter sample: Input test to derive instances from.
:param str color: Color tags to use. Default is red.
:return: Instance.
"""
# First determine which class/type to use.
if kind.startswith('ColorStr'):
cls = ColorStr
elif kind.startswith('Color'):
cls = Color
else:
cls = PARENT_CLASS
# Next handle NoneType samples.
if sample is None:
return cls()
# Finally handle non-None samples.
if kind.endswith('plain'):
return cls(sample)
elif kind.endswith('color'):
tags = '{%s}' % color, '{/%s}' % color
return cls(tags[0] + sample + tags[1])
return sample

299
tests/screenshot.py Normal file
View file

@ -0,0 +1,299 @@
"""Take screenshots and search for subimages in images."""
import ctypes
import os
import random
import struct
import subprocess
import time
try:
from itertools import izip
except ImportError:
izip = zip # Py3
from colorclass.windows import WINDOWS_CODES
from tests.conftest import PROJECT_ROOT
STARTF_USEFILLATTRIBUTE = 0x00000010
STARTF_USESHOWWINDOW = getattr(subprocess, 'STARTF_USESHOWWINDOW', 1)
STILL_ACTIVE = 259
SW_MAXIMIZE = 3
class StartupInfo(ctypes.Structure):
"""STARTUPINFO structure."""
_fields_ = [
('cb', ctypes.c_ulong),
('lpReserved', ctypes.c_char_p),
('lpDesktop', ctypes.c_char_p),
('lpTitle', ctypes.c_char_p),
('dwX', ctypes.c_ulong),
('dwY', ctypes.c_ulong),
('dwXSize', ctypes.c_ulong),
('dwYSize', ctypes.c_ulong),
('dwXCountChars', ctypes.c_ulong),
('dwYCountChars', ctypes.c_ulong),
('dwFillAttribute', ctypes.c_ulong),
('dwFlags', ctypes.c_ulong),
('wShowWindow', ctypes.c_ushort),
('cbReserved2', ctypes.c_ushort),
('lpReserved2', ctypes.c_char_p),
('hStdInput', ctypes.c_ulong),
('hStdOutput', ctypes.c_ulong),
('hStdError', ctypes.c_ulong),
]
def __init__(self, maximize=False, title=None, white_bg=False):
"""Constructor.
:param bool maximize: Start process in new console window, maximized.
:param bool white_bg: New console window will be black text on white background.
:param bytes title: Set new window title to this instead of exe path.
"""
super(StartupInfo, self).__init__()
self.cb = ctypes.sizeof(self)
if maximize:
self.dwFlags |= STARTF_USESHOWWINDOW
self.wShowWindow = SW_MAXIMIZE
if title:
self.lpTitle = ctypes.c_char_p(title)
if white_bg:
self.dwFlags |= STARTF_USEFILLATTRIBUTE
self.dwFillAttribute = WINDOWS_CODES['hibgwhite'] | WINDOWS_CODES['black']
class ProcessInfo(ctypes.Structure):
"""PROCESS_INFORMATION structure."""
_fields_ = [
('hProcess', ctypes.c_void_p),
('hThread', ctypes.c_void_p),
('dwProcessId', ctypes.c_ulong),
('dwThreadId', ctypes.c_ulong),
]
class RunNewConsole(object):
"""Run the command in a new console window. Windows only. Use in a with statement.
subprocess sucks and really limits your access to the win32 API. Its implementation is half-assed. Using this so
that STARTUPINFO.lpTitle actually works and STARTUPINFO.dwFillAttribute produce the expected result.
"""
def __init__(self, command, maximized=False, title=None, white_bg=False):
"""Constructor.
:param iter command: Command to run.
:param bool maximized: Start process in new console window, maximized.
:param bytes title: Set new window title to this. Needed by user32.FindWindow.
:param bool white_bg: New console window will be black text on white background.
"""
if title is None:
title = 'pytest-{0}-{1}'.format(os.getpid(), random.randint(1000, 9999)).encode('ascii')
self.startup_info = StartupInfo(maximize=maximized, title=title, white_bg=white_bg)
self.process_info = ProcessInfo()
self.command_str = subprocess.list2cmdline(command).encode('ascii')
self._handles = list()
self._kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32
self._kernel32.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)]
self._kernel32.GetExitCodeProcess.restype = ctypes.c_long
def __del__(self):
"""Close win32 handles."""
while self._handles:
try:
self._kernel32.CloseHandle(self._handles.pop(0)) # .pop() is thread safe.
except IndexError:
break
def __enter__(self):
"""Entering the `with` block. Runs the process."""
if not self._kernel32.CreateProcessA(
None, # lpApplicationName
self.command_str, # lpCommandLine
None, # lpProcessAttributes
None, # lpThreadAttributes
False, # bInheritHandles
subprocess.CREATE_NEW_CONSOLE, # dwCreationFlags
None, # lpEnvironment
str(PROJECT_ROOT).encode('ascii'), # lpCurrentDirectory
ctypes.byref(self.startup_info), # lpStartupInfo
ctypes.byref(self.process_info) # lpProcessInformation
):
raise ctypes.WinError()
# Add handles added by the OS.
self._handles.append(self.process_info.hProcess)
self._handles.append(self.process_info.hThread)
# Get hWnd.
self.hwnd = 0
for _ in range(int(5 / 0.1)):
# Takes time for console window to initialize.
self.hwnd = ctypes.windll.user32.FindWindowA(None, self.startup_info.lpTitle)
if self.hwnd:
break
time.sleep(0.1)
assert self.hwnd
# Return generator that yields window size/position.
return self._iter_pos()
def __exit__(self, *_):
"""Cleanup."""
try:
# Verify process exited 0.
status = ctypes.c_ulong(STILL_ACTIVE)
while status.value == STILL_ACTIVE:
time.sleep(0.1)
if not self._kernel32.GetExitCodeProcess(self.process_info.hProcess, ctypes.byref(status)):
raise ctypes.WinError()
assert status.value == 0
finally:
# Close handles.
self.__del__()
def _iter_pos(self):
"""Yield new console window's current position and dimensions.
:return: Yields region the new window is in (left, upper, right, lower).
:rtype: tuple
"""
rect = ctypes.create_string_buffer(16) # To be written to by GetWindowRect. RECT structure.
while ctypes.windll.user32.GetWindowRect(self.hwnd, rect):
left, top, right, bottom = struct.unpack('llll', rect.raw)
width, height = right - left, bottom - top
assert width > 1
assert height > 1
yield left, top, right, bottom
raise StopIteration
def iter_rows(pil_image):
"""Yield tuple of pixels for each row in the image.
itertools.izip in Python 2.x and zip in Python 3.x are writen in C. Much faster than anything else I've found
written in pure Python.
From:
http://stackoverflow.com/questions/1624883/alternative-way-to-split-a-list-into-groups-of-n/1625023#1625023
:param PIL.Image.Image pil_image: Image to read from.
:return: Yields rows.
:rtype: tuple
"""
iterator = izip(*(iter(pil_image.getdata()),) * pil_image.width)
for row in iterator:
yield row
def get_most_interesting_row(pil_image):
"""Look for a row in the image that has the most unique pixels.
:param PIL.Image.Image pil_image: Image to read from.
:return: Row (tuple of pixel tuples), row as a set, first pixel tuple, y offset from top.
:rtype: tuple
"""
final = (None, set(), None, None) # row, row_set, first_pixel, y_pos
for y_pos, row in enumerate(iter_rows(pil_image)):
row_set = set(row)
if len(row_set) > len(final[1]):
final = row, row_set, row[0], y_pos
if len(row_set) == pil_image.width:
break # Can't get bigger.
return final
def count_subimages(screenshot, subimg):
"""Check how often subimg appears in the screenshot image.
:param PIL.Image.Image screenshot: Screen shot to search through.
:param PIL.Image.Image subimg: Subimage to search for.
:return: Number of times subimg appears in the screenshot.
:rtype: int
"""
# Get row to search for.
si_pixels = list(subimg.getdata()) # Load entire subimg into memory.
si_width = subimg.width
si_height = subimg.height
si_row, si_row_set, si_pixel, si_y = get_most_interesting_row(subimg)
occurrences = 0
# Look for subimg row in screenshot, then crop and compare pixel arrays.
for y_pos, row in enumerate(iter_rows(screenshot)):
if si_row_set - set(row):
continue # Some pixels not found.
for x_pos in range(screenshot.width - si_width + 1):
if row[x_pos] != si_pixel:
continue # First pixel does not match.
if row[x_pos:x_pos + si_width] != si_row:
continue # Row does not match.
# Found match for interesting row of subimg in screenshot.
y_corrected = y_pos - si_y
with screenshot.crop((x_pos, y_corrected, x_pos + si_width, y_corrected + si_height)) as cropped:
if list(cropped.getdata()) == si_pixels:
occurrences += 1
return occurrences
def try_candidates(screenshot, subimg_candidates, expected_count):
"""Call count_subimages() for each subimage candidate until.
If you get ImportError run "pip install pillow". Only OSX and Windows is supported.
:param PIL.Image.Image screenshot: Screen shot to search through.
:param iter subimg_candidates: Subimage paths to look for. List of strings.
:param int expected_count: Try until any a subimage candidate is found this many times.
:return: Number of times subimg appears in the screenshot.
:rtype: int
"""
from PIL import Image
count_found = 0
for subimg_path in subimg_candidates:
with Image.open(subimg_path) as rgba_s:
with rgba_s.convert(mode='RGB') as subimg:
# Make sure subimage isn't too large.
assert subimg.width < 256
assert subimg.height < 256
# Count.
count_found = count_subimages(screenshot, subimg)
if count_found == expected_count:
break # No need to try other candidates.
return count_found
def screenshot_until_match(save_to, timeout, subimg_candidates, expected_count, gen):
"""Take screenshots until one of the 'done' subimages is found. Image is saved when subimage found or at timeout.
If you get ImportError run "pip install pillow". Only OSX and Windows is supported.
:param str save_to: Save screenshot to this PNG file path when expected count found or timeout.
:param int timeout: Give up after these many seconds.
:param iter subimg_candidates: Subimage paths to look for. List of strings.
:param int expected_count: Keep trying until any of subimg_candidates is found this many times.
:param iter gen: Generator yielding window position and size to crop screenshot to.
"""
from PIL import ImageGrab
assert save_to.endswith('.png')
stop_after = time.time() + timeout
# Take screenshots until subimage is found.
while True:
with ImageGrab.grab(next(gen)) as rgba:
with rgba.convert(mode='RGB') as screenshot:
count_found = try_candidates(screenshot, subimg_candidates, expected_count)
if count_found == expected_count or time.time() > stop_after:
screenshot.save(save_to)
assert count_found == expected_count
return
time.sleep(0.5)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

64
tests/test___main__.py Normal file
View file

@ -0,0 +1,64 @@
"""Test objects in module."""
import subprocess
import sys
import pytest
from colorclass.windows import IS_WINDOWS
def test_import_do_nothing():
"""Make sure importing __main__ doesn't print anything."""
command = [sys.executable, '-c', "from colorclass.__main__ import TRUTHY; assert TRUTHY"]
proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
output = proc.communicate()
assert proc.poll() == 0
assert not output[0]
assert not output[1]
@pytest.mark.parametrize('colors', [True, False, None])
@pytest.mark.parametrize('light', [True, False, None])
def test(monkeypatch, colors, light):
"""Test package as a script.
:param monkeypatch: pytest fixture.
:param bool colors: Enable, disable, or don't touch colors using CLI args or env variables.
:param bool light: Enable light, dark, or don't touch auto colors using CLI args or env variables.
"""
command = [sys.executable, '-m', 'colorclass' if sys.version_info >= (2, 7) else 'colorclass.__main__']
stdin = '{autored}Red{/autored} {red}Red{/red} {hired}Red{/hired}'.encode()
# Set options.
if colors is True:
monkeypatch.setenv('COLOR_ENABLE', 'true')
elif colors is False:
monkeypatch.setenv('COLOR_DISABLE', 'true')
if light is True:
monkeypatch.setenv('COLOR_LIGHT', 'true')
elif light is False:
monkeypatch.setenv('COLOR_DARK', 'true')
# Run.
proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
output = proc.communicate(stdin)[0].decode()
assert proc.poll() == 0
assert 'Red' in output
# Verify colors. Output is always stripped of all colors on Windows when piped to non-console (e.g. pytest).
if colors is False or IS_WINDOWS:
assert '\033[' not in output
assert 'Red Red Red' in output
return
assert '\033[' in output
# Verify light bg.
count_dark_fg = output.count('\033[31mRed')
count_light_fg = output.count('\033[91mRed')
if light:
assert count_dark_fg == 2
assert count_light_fg == 1
else:
assert count_dark_fg == 1
assert count_light_fg == 2

137
tests/test_codes.py Normal file
View file

@ -0,0 +1,137 @@
"""Test objects in module."""
import errno
import os
import subprocess
import sys
import time
import pytest
from colorclass.codes import ANSICodeMapping, BASE_CODES, list_tags
from colorclass.windows import IS_WINDOWS
def test_ansi_code_mapping_whitelist():
"""Test whitelist enforcement."""
auto_codes = ANSICodeMapping('{green}{bgred}Test{/all}')
# Test __getitem__.
with pytest.raises(KeyError):
assert not auto_codes['red']
assert auto_codes['green'] == 32
# Test iter and len.
assert sorted(auto_codes) == ['/all', 'bgred', 'green']
assert len(auto_codes) == 3
@pytest.mark.parametrize('toggle', ['light', 'dark', 'none'])
def test_auto_toggles(toggle):
"""Test auto colors and ANSICodeMapping class toggles.
:param str toggle: Toggle method to call.
"""
# Toggle.
if toggle == 'light':
ANSICodeMapping.enable_all_colors()
ANSICodeMapping.set_light_background()
assert ANSICodeMapping.DISABLE_COLORS is False
assert ANSICodeMapping.LIGHT_BACKGROUND is True
elif toggle == 'dark':
ANSICodeMapping.enable_all_colors()
ANSICodeMapping.set_dark_background()
assert ANSICodeMapping.DISABLE_COLORS is False
assert ANSICodeMapping.LIGHT_BACKGROUND is False
else:
ANSICodeMapping.disable_all_colors()
assert ANSICodeMapping.DISABLE_COLORS is True
assert ANSICodeMapping.LIGHT_BACKGROUND is False
# Test iter and len.
auto_codes = ANSICodeMapping('}{'.join([''] + list(BASE_CODES) + ['']))
count = 0
for k, v in auto_codes.items():
count += 1
assert str(k) == k
assert v is None or int(v) == v
assert len(auto_codes) == count
# Test foreground properties.
key_fg = '{autoblack}{autored}{autogreen}{autoyellow}{autoblue}{automagenta}{autocyan}{autowhite}'
actual = key_fg.format(**auto_codes)
if toggle == 'light':
assert actual == '3031323334353637'
elif toggle == 'dark':
assert actual == '9091929394959697'
else:
assert actual == 'NoneNoneNoneNoneNoneNoneNoneNone'
# Test background properties.
key_fg = '{autobgblack}{autobgred}{autobggreen}{autobgyellow}{autobgblue}{autobgmagenta}{autobgcyan}{autobgwhite}'
actual = key_fg.format(**auto_codes)
if toggle == 'light':
assert actual == '4041424344454647'
elif toggle == 'dark':
assert actual == '100101102103104105106107'
else:
assert actual == 'NoneNoneNoneNoneNoneNoneNoneNone'
def test_list_tags():
"""Test list_tags()."""
actual = list_tags()
assert ('red', '/red', 31, 39) in actual
assert sorted(t for i in actual for t in i[:2] if t is not None) == sorted(BASE_CODES)
@pytest.mark.parametrize('tty', [False, True])
def test_disable_colors_piped(tty):
"""Verify colors enabled by default when piped to TTY and disabled when not.
:param bool tty: Pipe to TTY/terminal?
"""
assert_statement = 'assert __import__("colorclass").codes.ANSICodeMapping.disable_if_no_tty() is {bool}'
command_colors_enabled = [sys.executable, '-c', assert_statement.format(bool='False')]
command_colors_disabled = [sys.executable, '-c', assert_statement.format(bool='True')]
# Run piped to this pytest process.
if not tty: # Outputs piped to non-terminal/non-tty. Colors disabled by default.
proc = subprocess.Popen(command_colors_disabled, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
output = proc.communicate()
assert not output[0]
assert not output[1]
assert proc.poll() == 0
return
# Run through a new console window (Windows).
if IS_WINDOWS:
c_flags = subprocess.CREATE_NEW_CONSOLE
proc = subprocess.Popen(command_colors_enabled, close_fds=True, creationflags=c_flags)
proc.communicate() # Pipes directed towards new console window. Not worth doing screenshot image processing.
assert proc.poll() == 0
return
# Run through pseudo tty (Linux/OSX).
master, slave = __import__('pty').openpty()
proc = subprocess.Popen(command_colors_enabled, stderr=subprocess.STDOUT, stdout=slave, close_fds=True)
os.close(slave)
# Read output.
output = ''
while True:
try:
data = os.read(master, 1024).decode()
except OSError as exc:
if exc.errno != errno.EIO: # EIO means EOF on some systems.
raise
data = None
if data:
output += data
elif proc.poll() is None:
time.sleep(0.01)
else:
break
os.close(master)
assert not output
assert proc.poll() == 0

185
tests/test_color.py Normal file
View file

@ -0,0 +1,185 @@
"""Test objects in module."""
import sys
from functools import partial
import pytest
from colorclass.color import Color
from tests.conftest import assert_both_values, get_instance
def test_colorize_methods():
"""Test colorize convenience methods."""
assert Color.black('TEST').value_colors == '\033[30mTEST\033[39m'
assert Color.bgblack('TEST').value_colors == '\033[40mTEST\033[49m'
assert Color.red('TEST').value_colors == '\033[31mTEST\033[39m'
assert Color.bgred('TEST').value_colors == '\033[41mTEST\033[49m'
assert Color.green('TEST').value_colors == '\033[32mTEST\033[39m'
assert Color.bggreen('TEST').value_colors == '\033[42mTEST\033[49m'
assert Color.yellow('TEST').value_colors == '\033[33mTEST\033[39m'
assert Color.bgyellow('TEST').value_colors == '\033[43mTEST\033[49m'
assert Color.blue('TEST').value_colors == '\033[34mTEST\033[39m'
assert Color.bgblue('TEST').value_colors == '\033[44mTEST\033[49m'
assert Color.magenta('TEST').value_colors == '\033[35mTEST\033[39m'
assert Color.bgmagenta('TEST').value_colors == '\033[45mTEST\033[49m'
assert Color.cyan('TEST').value_colors == '\033[36mTEST\033[39m'
assert Color.bgcyan('TEST').value_colors == '\033[46mTEST\033[49m'
assert Color.white('TEST').value_colors == '\033[37mTEST\033[39m'
assert Color.bgwhite('TEST').value_colors == '\033[47mTEST\033[49m'
assert Color.black('this is a test.', auto=True) == Color('{autoblack}this is a test.{/autoblack}')
assert Color.black('this is a test.') == Color('{black}this is a test.{/black}')
assert Color.bgblack('this is a test.', auto=True) == Color('{autobgblack}this is a test.{/autobgblack}')
assert Color.bgblack('this is a test.') == Color('{bgblack}this is a test.{/bgblack}')
assert Color.red('this is a test.', auto=True) == Color('{autored}this is a test.{/autored}')
assert Color.red('this is a test.') == Color('{red}this is a test.{/red}')
assert Color.bgred('this is a test.', auto=True) == Color('{autobgred}this is a test.{/autobgred}')
assert Color.bgred('this is a test.') == Color('{bgred}this is a test.{/bgred}')
assert Color.green('this is a test.', auto=True) == Color('{autogreen}this is a test.{/autogreen}')
assert Color.green('this is a test.') == Color('{green}this is a test.{/green}')
assert Color.bggreen('this is a test.', auto=True) == Color('{autobggreen}this is a test.{/autobggreen}')
assert Color.bggreen('this is a test.') == Color('{bggreen}this is a test.{/bggreen}')
assert Color.yellow('this is a test.', auto=True) == Color('{autoyellow}this is a test.{/autoyellow}')
assert Color.yellow('this is a test.') == Color('{yellow}this is a test.{/yellow}')
assert Color.bgyellow('this is a test.', auto=True) == Color('{autobgyellow}this is a test.{/autobgyellow}')
assert Color.bgyellow('this is a test.') == Color('{bgyellow}this is a test.{/bgyellow}')
assert Color.blue('this is a test.', auto=True) == Color('{autoblue}this is a test.{/autoblue}')
assert Color.blue('this is a test.') == Color('{blue}this is a test.{/blue}')
assert Color.bgblue('this is a test.', auto=True) == Color('{autobgblue}this is a test.{/autobgblue}')
assert Color.bgblue('this is a test.') == Color('{bgblue}this is a test.{/bgblue}')
assert Color.magenta('this is a test.', auto=True) == Color('{automagenta}this is a test.{/automagenta}')
assert Color.magenta('this is a test.') == Color('{magenta}this is a test.{/magenta}')
assert Color.bgmagenta('this is a test.', auto=True) == Color('{autobgmagenta}this is a test.{/autobgmagenta}')
assert Color.bgmagenta('this is a test.') == Color('{bgmagenta}this is a test.{/bgmagenta}')
assert Color.cyan('this is a test.', auto=True) == Color('{autocyan}this is a test.{/autocyan}')
assert Color.cyan('this is a test.') == Color('{cyan}this is a test.{/cyan}')
assert Color.bgcyan('this is a test.', auto=True) == Color('{autobgcyan}this is a test.{/autobgcyan}')
assert Color.bgcyan('this is a test.') == Color('{bgcyan}this is a test.{/bgcyan}')
assert Color.white('this is a test.', auto=True) == Color('{autowhite}this is a test.{/autowhite}')
assert Color.white('this is a test.') == Color('{white}this is a test.{/white}')
assert Color.bgwhite('this is a test.', auto=True) == Color('{autobgwhite}this is a test.{/autobgwhite}')
assert Color.bgwhite('this is a test.') == Color('{bgwhite}this is a test.{/bgwhite}')
@pytest.mark.parametrize('kind', ['str', 'Color plain', 'Color color'])
def test_chaining(kind):
"""Test chaining Color instances.
:param str kind: Type of string to test.
"""
assert_both = partial(assert_both_values, kind=kind)
# Test string.
instance = get_instance(kind, 'TEST')
for color in ('green', 'blue', 'yellow'):
instance = get_instance(kind, instance, color)
assert_both(instance, 'TEST', '\033[31mTEST\033[39m')
# Test empty.
instance = get_instance(kind)
for color in ('red', 'green', 'blue', 'yellow'):
instance = get_instance(kind, instance, color)
assert_both(instance, '', '\033[39m')
# Test complicated.
instance = 'TEST'
for color in ('black', 'bgred', 'green', 'bgyellow', 'blue', 'bgmagenta', 'cyan', 'bgwhite'):
instance = get_instance(kind, instance, color=color)
assert_both(instance, 'TEST', '\033[30;41mTEST\033[39;49m')
# Test format and length.
instance = get_instance(kind, '{0}').format(get_instance(kind, 'TEST'))
assert_both(instance, 'TEST', '\033[31mTEST\033[39m')
assert len(instance) == 4
instance = get_instance(kind, '{0}').format(instance)
assert_both(instance, 'TEST', '\033[31mTEST\033[39m')
assert len(instance) == 4
instance = get_instance(kind, '{0}').format(instance)
assert_both(instance, 'TEST', '\033[31mTEST\033[39m')
assert len(instance) == 4
@pytest.mark.parametrize('kind', ['str', 'Color plain', 'Color color'])
def test_empty(kind):
"""Test with empty string.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, u'')
assert_both = partial(assert_both_values, kind=kind)
assert len(instance) == 0
assert_both(instance * 2, '', '\033[39m')
assert_both(instance + instance, '', '\033[39m')
with pytest.raises(IndexError):
assert instance[0]
assert not [i for i in instance]
assert not list(instance)
assert instance.encode('utf-8') == instance.encode('utf-8')
assert instance.encode('utf-8').decode('utf-8') == instance
assert_both(instance.encode('utf-8').decode('utf-8'), '', '\033[39m')
assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), '', '\033[39m')
assert len(instance.encode('utf-8').decode('utf-8')) == 0
assert_both(instance.format(value=''), '', '\033[39m')
assert_both(instance.capitalize(), '', '\033[39m')
assert_both(instance.center(5), ' ', '\033[39m ')
assert instance.count('') == 1
assert instance.count('t') == 0
assert instance.endswith('') is True
assert instance.endswith('me') is False
assert instance.find('') == 0
assert instance.find('t') == -1
assert instance.index('') == 0
with pytest.raises(ValueError):
assert instance.index('t')
assert instance.isalnum() is False
assert instance.isalpha() is False
if sys.version_info[0] != 2:
assert instance.isdecimal() is False
assert instance.isdigit() is False
if sys.version_info[0] != 2:
assert instance.isnumeric() is False
assert instance.isspace() is False
assert instance.istitle() is False
assert instance.isupper() is False
assert_both(instance.join(['A', 'B']), 'AB', 'A\033[39mB')
assert_both(instance.ljust(5), ' ', '\033[39m ')
assert instance.rfind('') == 0
assert instance.rfind('t') == -1
assert instance.rindex('') == 0
with pytest.raises(ValueError):
assert instance.rindex('t')
assert_both(instance.rjust(5), ' ', '\033[39m ')
if kind in ('str', 'Color plain'):
assert instance.splitlines() == list()
else:
assert instance.splitlines() == ['\033[39m']
assert instance.startswith('') is True
assert instance.startswith('T') is False
assert_both(instance.swapcase(), '', '\033[39m')
assert_both(instance.title(), '', '\033[39m')
assert_both(instance.translate({ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'}), '', '\033[39m')
assert_both(instance.upper(), '', '\033[39m')
assert_both(instance.zfill(0), '', '')
assert_both(instance.zfill(1), '0', '0')
def test_keep_tags():
"""Test keep_tags keyword arg."""
assert_both = partial(assert_both_values, kind='Color color')
instance = Color('{red}Test{/red}', keep_tags=True)
assert_both(instance, '{red}Test{/red}', '{red}Test{/red}')
assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}TEST{/RED}')
assert len(instance) == 15
instance = Color('{red}\033[41mTest\033[49m{/red}', keep_tags=True)
assert_both(instance, '{red}Test{/red}', '{red}\033[41mTest\033[49m{/red}')
assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}\033[41mTEST\033[49m{/RED}')
assert len(instance) == 15

398
tests/test_core.py Normal file
View file

@ -0,0 +1,398 @@
"""Test objects in module."""
import sys
from functools import partial
import pytest
from colorclass.core import apply_text, ColorStr
from tests.conftest import assert_both_values, get_instance
def test_apply_text():
"""Test apply_text()."""
assert apply_text('', lambda _: 0 / 0) == ''
assert apply_text('TEST', lambda s: s.lower()) == 'test'
assert apply_text('!\033[31mRed\033[0m', lambda s: s.upper()) == '!\033[31mRED\033[0m'
assert apply_text('\033[1mA \033[31mB \033[32;41mC \033[0mD', lambda _: '') == '\033[1m\033[31m\033[32;41m\033[0m'
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_dunder(kind):
"""Test "dunder" methods (double-underscore).
:param str kind: Type of string to test.
"""
instance = get_instance(kind, 'test ME ')
assert_both = partial(assert_both_values, kind=kind)
assert len(instance) == 8
if kind == 'str':
assert repr(instance) == "'test ME '"
elif kind == 'ColorStr plain':
assert repr(instance) == "ColorStr('test ME ')"
else:
assert repr(instance) == "ColorStr('\\x1b[31mtest ME \\x1b[39m')"
assert_both(instance.__class__('1%s2' % instance), '1test ME 2', '1\033[31mtest ME \033[39m2')
assert_both(get_instance(kind, '1%s2') % 'test ME ', '1test ME 2', '\033[31m1test ME 2\033[39m')
assert_both(get_instance(kind, '1%s2') % instance, '1test ME 2', '\033[31m1test ME \033[39m2')
assert_both(instance * 2, 'test ME test ME ', '\033[31mtest ME test ME \033[39m')
assert_both(instance + instance, 'test ME test ME ', '\033[31mtest ME test ME \033[39m')
assert_both(instance + 'more', 'test ME more', '\033[31mtest ME \033[39mmore')
assert_both(instance.__class__('more' + instance), 'moretest ME ', 'more\033[31mtest ME \033[39m')
instance *= 2
assert_both(instance, 'test ME test ME ', '\033[31mtest ME test ME \033[39m')
instance += 'more'
assert_both(instance, 'test ME test ME more', '\033[31mtest ME test ME \033[39mmore')
assert_both(instance[0], 't', '\033[31mt\033[39m')
assert_both(instance[4], ' ', '\033[31m \033[39m')
assert_both(instance[-1], 'e', '\033[39me')
# assert_both(instance[1:-1], 'est ME test ME mor', '\033[31mest ME test ME \033[39mmor')
# assert_both(instance[1:9:2], 'etM ', '\033[31metM \033[39m')
# assert_both(instance[-1::-1], 'erom EM tset EM tset', 'erom\033[31m EM tset EM tset\033[39m')
with pytest.raises(IndexError):
assert instance[20]
actual = [i for i in instance]
assert len(actual) == 20
assert actual == list(instance)
assert_both(actual[0], 't', '\033[31mt\033[39m')
assert_both(actual[1], 'e', '\033[31me\033[39m')
assert_both(actual[2], 's', '\033[31ms\033[39m')
assert_both(actual[3], 't', '\033[31mt\033[39m')
assert_both(actual[4], ' ', '\033[31m \033[39m')
assert_both(actual[5], 'M', '\033[31mM\033[39m')
assert_both(actual[6], 'E', '\033[31mE\033[39m')
assert_both(actual[7], ' ', '\033[31m \033[39m')
assert_both(actual[8], 't', '\033[31mt\033[39m')
assert_both(actual[9], 'e', '\033[31me\033[39m')
assert_both(actual[10], 's', '\033[31ms\033[39m')
assert_both(actual[11], 't', '\033[31mt\033[39m')
assert_both(actual[12], ' ', '\033[31m \033[39m')
assert_both(actual[13], 'M', '\033[31mM\033[39m')
assert_both(actual[14], 'E', '\033[31mE\033[39m')
assert_both(actual[15], ' ', '\033[31m \033[39m')
assert_both(actual[16], 'm', '\033[39mm')
assert_both(actual[17], 'o', '\033[39mo')
assert_both(actual[18], 'r', '\033[39mr')
assert_both(actual[19], 'e', '\033[39me')
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_encode_decode(kind):
"""Test encode and decode methods.
:param str kind: Type of string to test.
"""
assert_both = partial(assert_both_values, kind=kind)
instance = get_instance(kind, 'test ME')
if sys.version_info[0] == 2:
assert instance.encode('utf-8') == instance
assert instance.decode('utf-8') == instance
assert_both(instance.decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m')
assert_both(instance.__class__.decode(instance, 'utf-8'), 'test ME', '\033[31mtest ME\033[39m')
assert len(instance.decode('utf-8')) == 7
else:
assert instance.encode('utf-8') != instance
assert instance.encode('utf-8') == instance.encode('utf-8')
assert instance.encode('utf-8').decode('utf-8') == instance
assert_both(instance.encode('utf-8').decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m')
assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), 'test ME', '\033[31mtest ME\033[39m')
assert len(instance.encode('utf-8').decode('utf-8')) == 7
@pytest.mark.parametrize('mode', ['fg within bg', 'bg within fg'])
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_format(kind, mode):
"""Test format method.
:param str kind: Type of string to test.
:param str mode: Which combination to test.
"""
assert_both = partial(assert_both_values, kind=kind)
# Test str.format(ColorStr()).
instance = get_instance(kind, 'test me')
assert_both(instance.__class__('1{0}2'.format(instance)), '1test me2', '1\033[31mtest me\033[39m2')
assert_both(instance.__class__(str.format('1{0}2', instance)), '1test me2', '1\033[31mtest me\033[39m2')
# Get actual.
template_pos = get_instance(kind, 'a{0}c{0}', 'bgred' if mode == 'fg within bg' else 'red')
template_kw = get_instance(kind, 'a{value}c{value}', 'bgred' if mode == 'fg within bg' else 'red')
instance = get_instance(kind, 'B', 'green' if mode == 'fg within bg' else 'bggreen')
# Get expected.
expected = ['aBcB', None]
if mode == 'fg within bg':
expected[1] = '\033[41ma\033[32mB\033[39mc\033[32mB\033[39;49m'
else:
expected[1] = '\033[31ma\033[42mB\033[49mc\033[42mB\033[39;49m'
# Test.
assert_both(template_pos.format(instance), expected[0], expected[1])
assert_both(template_kw.format(value=instance), expected[0], expected[1])
assert_both(instance.__class__.format(template_pos, instance), expected[0], expected[1])
assert_both(instance.__class__.format(template_kw, value=instance), expected[0], expected[1])
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_format_mixed(kind):
"""Test format method with https://github.com/Robpol86/colorclass/issues/16 in mind.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, 'XXX: ') + '{0}'
assert_both = partial(assert_both_values, kind=kind)
assert_both(instance, 'XXX: {0}', '\033[31mXXX: \033[39m{0}')
assert_both(instance.format('{blue}Moo{/blue}'), 'XXX: {blue}Moo{/blue}', '\033[31mXXX: \033[39m{blue}Moo{/blue}')
assert_both(instance.format(get_instance(kind, 'Moo', 'blue')), 'XXX: Moo', '\033[31mXXX: \033[34mMoo\033[39m')
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_c_f(kind):
"""Test C through F methods.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, 'test me')
assert_both = partial(assert_both_values, kind=kind)
assert_both(instance.capitalize(), 'Test me', '\033[31mTest me\033[39m')
assert_both(instance.center(11), ' test me ', ' \033[31mtest me\033[39m ')
assert_both(instance.center(11, '.'), '..test me..', '..\033[31mtest me\033[39m..')
assert_both(instance.center(12), ' test me ', ' \033[31mtest me\033[39m ')
assert instance.count('t') == 2
assert instance.endswith('me') is True
assert instance.endswith('ME') is False
assert instance.find('t') == 0
assert instance.find('t', 0) == 0
assert instance.find('t', 0, 1) == 0
assert instance.find('t', 1) == 3
assert instance.find('t', 1, 4) == 3
assert instance.find('t', 1, 3) == -1
assert instance.find('x') == -1
assert instance.find('m') == 5
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_i(kind):
"""Test I methods.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, 'tantamount')
assert instance.index('t') == 0
assert instance.index('t', 0) == 0
assert instance.index('t', 0, 1) == 0
assert instance.index('t', 1) == 3
assert instance.index('t', 1, 4) == 3
assert instance.index('m') == 5
with pytest.raises(ValueError):
assert instance.index('t', 1, 3)
with pytest.raises(ValueError):
assert instance.index('x')
assert instance.isalnum() is True
assert get_instance(kind, '123').isalnum() is True
assert get_instance(kind, '.').isalnum() is False
assert instance.isalpha() is True
assert get_instance(kind, '.').isalpha() is False
if sys.version_info[0] != 2:
assert instance.isdecimal() is False
assert get_instance(kind, '123').isdecimal() is True
assert get_instance(kind, '.').isdecimal() is False
assert instance.isdigit() is False
assert get_instance(kind, '123').isdigit() is True
assert get_instance(kind, '.').isdigit() is False
if sys.version_info[0] != 2:
assert instance.isnumeric() is False
assert get_instance(kind, '123').isnumeric() is True
assert get_instance(kind, '.').isnumeric() is False
assert instance.isspace() is False
assert get_instance(kind, ' ').isspace() is True
assert instance.istitle() is False
assert get_instance(kind, 'Test').istitle() is True
assert instance.isupper() is False
assert get_instance(kind, 'TEST').isupper() is True
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_j_s(kind):
"""Test J to S methods.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, 'test me')
assert_both = partial(assert_both_values, kind=kind)
assert_both(instance.join(['A', 'B']), 'Atest meB', 'A\033[31mtest me\033[39mB')
iterable = [get_instance(kind, 'A', 'green'), get_instance(kind, 'B', 'green')]
assert_both(instance.join(iterable), 'Atest meB', '\033[32mA\033[31mtest me\033[32mB\033[39m')
assert_both(instance.ljust(11), 'test me ', '\033[31mtest me\033[39m ')
assert_both(instance.ljust(11, '.'), 'test me....', '\033[31mtest me\033[39m....')
assert_both(instance.ljust(12), 'test me ', '\033[31mtest me\033[39m ')
assert instance.rfind('t') == 3
assert instance.rfind('t', 0) == 3
assert instance.rfind('t', 0, 4) == 3
assert instance.rfind('t', 0, 3) == 0
assert instance.rfind('t', 3, 3) == -1
assert instance.rfind('x') == -1
assert instance.rfind('m') == 5
tantamount = get_instance(kind, 'tantamount')
assert tantamount.rindex('t') == 9
assert tantamount.rindex('t', 0) == 9
assert tantamount.rindex('t', 0, 5) == 3
assert tantamount.rindex('m') == 5
with pytest.raises(ValueError):
assert tantamount.rindex('t', 1, 3)
with pytest.raises(ValueError):
assert tantamount.rindex('x')
assert_both(instance.rjust(11), ' test me', ' \033[31mtest me\033[39m')
assert_both(instance.rjust(11, '.'), '....test me', '....\033[31mtest me\033[39m')
assert_both(instance.rjust(12), ' test me', ' \033[31mtest me\033[39m')
actual = get_instance(kind, '1\n2\n3').splitlines()
assert len(actual) == 3
# assert_both(actual[0], '1', '\033[31m1\033[39m')
# assert_both(actual[1], '2', '\033[31m2\033[39m')
# assert_both(actual[2], '3', '\033[31m3\033[39m')
assert instance.startswith('t') is True
assert instance.startswith('T') is False
assert_both(get_instance(kind, 'AbC').swapcase(), 'aBc', '\033[31maBc\033[39m')
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_t_z(kind):
"""Test T to Z methods.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, u'test me')
assert_both = partial(assert_both_values, kind=kind)
assert_both(instance.title(), 'Test Me', '\033[31mTest Me\033[39m')
assert_both(get_instance(kind, 'TEST YOU').title(), 'Test You', '\033[31mTest You\033[39m')
table = {ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'}
assert_both(instance.translate(table), '1231 m2', '\033[31m1231 m2\033[39m')
assert_both(instance.upper(), 'TEST ME', '\033[31mTEST ME\033[39m')
number = get_instance(kind, '350')
assert_both(number.zfill(1), '350', '\033[31m350\033[39m')
assert_both(number.zfill(2), '350', '\033[31m350\033[39m')
assert_both(number.zfill(3), '350', '\033[31m350\033[39m')
assert_both(number.zfill(4), '0350', '\033[31m0350\033[39m')
assert_both(number.zfill(10), '0000000350', '\033[31m0000000350\033[39m')
assert_both(get_instance(kind, '-350').zfill(5), '-0350', '\033[31m-0350\033[39m')
assert_both(get_instance(kind, '-10.3').zfill(5), '-10.3', '\033[31m-10.3\033[39m')
assert_both(get_instance(kind, '-10.3').zfill(6), '-010.3', '\033[31m-010.3\033[39m')
@pytest.mark.parametrize('kind', ['str', 'ColorStr plain', 'ColorStr color'])
def test_empty(kind):
"""Test with empty string.
:param str kind: Type of string to test.
"""
instance = get_instance(kind, u'')
assert_both = partial(assert_both_values, kind=kind)
assert len(instance) == 0
assert_both(instance * 2, '', '\033[39m')
assert_both(instance + instance, '', '\033[39m')
with pytest.raises(IndexError):
assert instance[0]
assert not [i for i in instance]
assert not list(instance)
assert instance.encode('utf-8') == instance.encode('utf-8')
assert instance.encode('utf-8').decode('utf-8') == instance
assert_both(instance.encode('utf-8').decode('utf-8'), '', '\033[39m')
assert_both(instance.__class__.encode(instance, 'utf-8').decode('utf-8'), '', '\033[39m')
assert len(instance.encode('utf-8').decode('utf-8')) == 0
assert_both(instance.format(value=''), '', '\033[39m')
assert_both(instance.capitalize(), '', '\033[39m')
assert_both(instance.center(5), ' ', '\033[39m ')
assert instance.count('') == 1
assert instance.count('t') == 0
assert instance.endswith('') is True
assert instance.endswith('me') is False
assert instance.find('') == 0
assert instance.find('t') == -1
assert instance.index('') == 0
with pytest.raises(ValueError):
assert instance.index('t')
assert instance.isalnum() is False
assert instance.isalpha() is False
if sys.version_info[0] != 2:
assert instance.isdecimal() is False
assert instance.isdigit() is False
if sys.version_info[0] != 2:
assert instance.isnumeric() is False
assert instance.isspace() is False
assert instance.istitle() is False
assert instance.isupper() is False
assert_both(instance.join(['A', 'B']), 'AB', 'A\033[39mB')
assert_both(instance.ljust(5), ' ', '\033[39m ')
assert instance.rfind('') == 0
assert instance.rfind('t') == -1
assert instance.rindex('') == 0
with pytest.raises(ValueError):
assert instance.rindex('t')
assert_both(instance.rjust(5), ' ', '\033[39m ')
if kind in ('str', 'ColorStr plain'):
assert instance.splitlines() == list()
else:
assert instance.splitlines() == ['\033[39m']
assert instance.startswith('') is True
assert instance.startswith('T') is False
assert_both(instance.swapcase(), '', '\033[39m')
assert_both(instance.title(), '', '\033[39m')
assert_both(instance.translate({ord('t'): u'1', ord('e'): u'2', ord('s'): u'3'}), '', '\033[39m')
assert_both(instance.upper(), '', '\033[39m')
assert_both(instance.zfill(0), '', '')
assert_both(instance.zfill(1), '0', '0')
def test_keep_tags():
"""Test keep_tags keyword arg."""
assert_both = partial(assert_both_values, kind='ColorStr color')
instance = ColorStr('{red}Test{/red}', keep_tags=True)
assert_both(instance, '{red}Test{/red}', '{red}Test{/red}')
assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}TEST{/RED}')
assert len(instance) == 15
instance = ColorStr('{red}\033[41mTest\033[49m{/red}', keep_tags=True)
assert_both(instance, '{red}Test{/red}', '{red}\033[41mTest\033[49m{/red}')
assert_both(instance.upper(), '{RED}TEST{/RED}', '{RED}\033[41mTEST\033[49m{/RED}')
assert len(instance) == 15

96
tests/test_example.py Normal file
View file

@ -0,0 +1,96 @@
"""Test example script."""
import subprocess
import sys
import pytest
from colorclass.windows import IS_WINDOWS
from tests.conftest import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
@pytest.mark.parametrize('colors', [True, False, None])
@pytest.mark.parametrize('light_bg', [True, False, None])
def test_piped(colors, light_bg):
"""Test script with output piped to non-tty (this pytest process).
:param bool colors: Enable, disable, or omit color arguments (default is no colors due to no tty).
:param bool light_bg: Enable light, dark, or omit light/dark arguments.
"""
command = [sys.executable, str(PROJECT_ROOT.join('example.py')), 'print']
# Set options.
if colors is True:
command.append('--colors')
elif colors is False:
command.append('--no-colors')
if light_bg is True:
command.append('--light-bg')
elif light_bg is False:
command.append('--dark-bg')
# Run.
proc = subprocess.Popen(command, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
output = proc.communicate()[0].decode()
assert proc.poll() == 0
assert 'Autocolors for all backgrounds' in output
assert 'Red' in output
# Verify colors. Output is always stripped of all colors on Windows when piped to non-console (e.g. pytest).
if colors is False or IS_WINDOWS:
assert '\033[' not in output
assert 'Black Red Green Yellow Blue Magenta Cyan White' in output
return
assert '\033[' in output
# Verify light bg.
count_dark_fg = output.count('\033[31mRed')
count_light_fg = output.count('\033[91mRed')
if light_bg:
assert count_dark_fg == 2
assert count_light_fg == 1
else:
assert count_dark_fg == 1
assert count_light_fg == 2
@pytest.mark.skipif(str(not IS_WINDOWS))
@pytest.mark.parametrize('colors,light_bg', [
(True, False),
(True, True),
(False, False),
(None, False),
])
def test_windows_screenshot(colors, light_bg):
"""Test script on Windows in a new console window. Take a screenshot to verify colors work.
:param bool colors: Enable, disable, or omit color arguments (default has colors).
:param bool light_bg: Create console with white background color.
"""
screenshot = PROJECT_ROOT.join('test_example_test_windows_screenshot.png')
if screenshot.check():
screenshot.remove()
command = [sys.executable, str(PROJECT_ROOT.join('example.py')), 'print', '-w', str(screenshot)]
# Set options.
if colors is True:
command.append('--colors')
elif colors is False:
command.append('--no-colors')
# Setup expected.
if colors is False:
candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_sans_*.bmp')]
expected_count = 27
elif light_bg:
candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_dark_fg_*.bmp')]
expected_count = 2
else:
candidates = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_light_fg_*.bmp')]
expected_count = 2
assert candidates
# Run.
with RunNewConsole(command, maximized=True, white_bg=light_bg) as gen:
screenshot_until_match(str(screenshot), 15, candidates, expected_count, gen)

79
tests/test_parse.py Normal file
View file

@ -0,0 +1,79 @@
"""Test objects in module."""
import pytest
from colorclass.parse import parse_input, prune_overridden
@pytest.mark.parametrize('in_,expected', [
('', ''),
('test', 'test'),
('\033[31mTEST\033[0m', '\033[31mTEST\033[0m'),
('\033[32;31mTEST\033[39;0m', '\033[31mTEST\033[0m'),
('\033[1;2mTEST\033[22;22m', '\033[1;2mTEST\033[22m'),
('\033[1;1;1;1;1;1mTEST\033[22m', '\033[1mTEST\033[22m'),
('\033[31;32;41;42mTEST\033[39;49m', '\033[32;42mTEST\033[39;49m'),
])
def test_prune_overridden(in_, expected):
"""Test function.
:param str in_: Input string to pass to function.
:param str expected: Expected return value.
"""
actual = prune_overridden(in_)
assert actual == expected
@pytest.mark.parametrize('disable', [True, False])
@pytest.mark.parametrize('in_,expected_colors,expected_no_colors', [
('', '', ''),
('test', 'test', 'test'),
('{b}TEST{/b}', '\033[1mTEST\033[22m', 'TEST'),
('{red}{bgred}TEST{/all}', '\033[31;41mTEST\033[0m', 'TEST'),
('{b}A {red}B {green}{bgred}C {/all}', '\033[1mA \033[31mB \033[32;41mC \033[0m', 'A B C '),
('C {/all}{b}{blue}{hiblue}{bgcyan}D {/all}', 'C \033[0;1;46;94mD \033[0m', 'C D '),
('D {/all}{i}\033[31;103mE {/all}', 'D \033[0;3;31;103mE \033[0m', 'D E '),
('{b}{red}{bgblue}{/all}{i}TEST{/all}', '\033[0;3mTEST\033[0m', 'TEST'),
('{red}{green}{blue}{black}{yellow}TEST{/fg}{/all}', '\033[33mTEST\033[0m', 'TEST'),
('{bgred}{bggreen}{bgblue}{bgblack}{bgyellow}TEST{/bg}{/all}', '\033[43mTEST\033[0m', 'TEST'),
('{red}T{red}E{red}S{red}T{/all}', '\033[31mTEST\033[0m', 'TEST'),
('{red}T{/all}E{/all}S{/all}T{/all}', '\033[31mT\033[0mEST', 'TEST'),
('{red}{bgblue}TES{red}{bgblue}T{/all}', '\033[31;44mTEST\033[0m', 'TEST'),
])
def test_parse_input(disable, in_, expected_colors, expected_no_colors):
"""Test function.
:param bool disable: Disable colors?
:param str in_: Input string to pass to function.
:param str expected_colors: Expected first item of return value.
:param str expected_no_colors: Expected second item of return value.
"""
actual_colors, actual_no_colors = parse_input(in_, disable, False)
if disable:
assert actual_colors == expected_no_colors
else:
assert actual_colors == expected_colors
assert actual_no_colors == expected_no_colors
@pytest.mark.parametrize('disable', [True, False])
@pytest.mark.parametrize('in_,expected_colors,expected_no_colors', [
('', '', ''),
('test', 'test', 'test'),
('{b}TEST{/b}', '{b}TEST{/b}', '{b}TEST{/b}'),
('D {/all}{i}\033[31;103mE {/all}', 'D {/all}{i}\033[31;103mE {/all}', 'D {/all}{i}E {/all}'),
])
def test_parse_input_keep_tags(disable, in_, expected_colors, expected_no_colors):
"""Test function with keep_tags=True.
:param bool disable: Disable colors?
:param str in_: Input string to pass to function.
:param str expected_colors: Expected first item of return value.
:param str expected_no_colors: Expected second item of return value.
"""
actual_colors, actual_no_colors = parse_input(in_, disable, True)
if disable:
assert actual_colors == expected_no_colors
else:
assert actual_colors == expected_colors
assert actual_no_colors == expected_no_colors

51
tests/test_search.py Normal file
View file

@ -0,0 +1,51 @@
"""Test objects in module."""
import pytest
from colorclass.search import build_color_index, find_char_color
@pytest.mark.parametrize('in_,expected', [
['', ()],
['TEST', (0, 1, 2, 3)],
['!\033[31mRed\033[0m', (0, 6, 7, 8)],
['\033[1mA \033[31mB \033[32;41mC \033[0mD', (4, 5, 11, 12, 21, 22, 27)],
])
def test_build_color_index(in_, expected):
"""Test function.
:param str in_: Input string to pass to function.
:param str expected: Expected return value.
"""
actual = build_color_index(in_)
assert actual == expected
@pytest.mark.parametrize('in_,pos,expected', [
('TEST', 0, 'T'),
('\033[31mTEST', 0, '\033[31mT'),
('\033[31mTEST', 3, '\033[31mT'),
('\033[31mT\033[32mE\033[33mS\033[34mT', 0, '\033[31mT\033[32m\033[33m\033[34m'),
('\033[31mT\033[32mE\033[33mS\033[34mT', 2, '\033[31m\033[32m\033[33mS\033[34m'),
('\033[31mTEST\033[0m', 1, '\033[31mE\033[0m'),
('\033[31mTEST\033[0m', 3, '\033[31mT\033[0m'),
('T\033[31mES\033[0mT', 0, 'T\033[31m\033[0m'),
('T\033[31mES\033[0mT', 1, '\033[31mE\033[0m'),
('T\033[31mES\033[0mT', 2, '\033[31mS\033[0m'),
('T\033[31mES\033[0mT', 3, '\033[31m\033[0mT'),
])
def test_find_char_color(in_, pos, expected):
"""Test function.
:param str in_: Input string to pass to function.
:param int pos: Character position in non-color string to lookup.
:param str expected: Expected return value.
"""
index = build_color_index(in_)
color_pos = index[pos]
actual = find_char_color(in_, color_pos)
assert actual == expected

29
tests/test_toggles.py Normal file
View file

@ -0,0 +1,29 @@
"""Test objects in module."""
from colorclass import toggles
def test_disable():
"""Test functions."""
toggles.disable_all_colors()
assert not toggles.is_enabled()
toggles.enable_all_colors()
assert toggles.is_enabled()
toggles.disable_all_colors()
assert not toggles.is_enabled()
toggles.enable_all_colors()
assert toggles.is_enabled()
assert toggles.disable_if_no_tty() # pytest pipes stderr/stdout.
assert not toggles.is_enabled()
def test_light_bg():
"""Test functions."""
toggles.set_dark_background()
assert not toggles.is_light()
toggles.set_light_background()
assert toggles.is_enabled()
toggles.set_dark_background()
assert not toggles.is_light()
toggles.set_light_background()
assert toggles.is_enabled()

429
tests/test_windows.py Normal file
View file

@ -0,0 +1,429 @@
"""Test Windows methods."""
from __future__ import print_function
import ctypes
import sys
from textwrap import dedent
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import pytest
from colorclass import windows
from colorclass.codes import ANSICodeMapping
from colorclass.color import Color
from tests.conftest import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
class MockKernel32(object):
"""Mock kernel32."""
def __init__(self, stderr=windows.INVALID_HANDLE_VALUE, stdout=windows.INVALID_HANDLE_VALUE, set_mode=0x0):
"""Constructor."""
self.set_mode = set_mode
self.stderr = stderr
self.stdout = stdout
self.wAttributes = 7
def GetConsoleMode(self, _, dword_pointer): # noqa
"""Mock GetConsoleMode.
:param _: Unused handle.
:param dword_pointer: ctypes.byref(lpdword) return value.
"""
ulong_ptr = ctypes.POINTER(ctypes.c_ulong)
dword = ctypes.cast(dword_pointer, ulong_ptr).contents # Dereference pointer.
dword.value = self.set_mode
return 1
def GetConsoleScreenBufferInfo(self, _, csbi_pointer): # noqa
"""Mock GetConsoleScreenBufferInfo.
:param _: Unused handle.
:param csbi_pointer: ctypes.byref(csbi) return value.
"""
struct_ptr = ctypes.POINTER(windows.ConsoleScreenBufferInfo)
csbi = ctypes.cast(csbi_pointer, struct_ptr).contents # Dereference pointer.
csbi.wAttributes = self.wAttributes
return 1
def GetStdHandle(self, handle): # noqa
"""Mock GetStdHandle.
:param int handle: STD_ERROR_HANDLE or STD_OUTPUT_HANDLE.
"""
return self.stderr if handle == windows.STD_ERROR_HANDLE else self.stdout
def SetConsoleTextAttribute(self, _, color_code): # noqa
"""Mock SetConsoleTextAttribute.
:param _: Unused handle.
:param int color_code: Merged color code to set.
"""
self.wAttributes = color_code
return 1
class MockSys(object):
"""Mock sys standard library module."""
def __init__(self, stderr=None, stdout=None):
"""Constructor."""
self.stderr = stderr or type('', (), {})
self.stdout = stdout or type('', (), {})
@pytest.mark.skipif(str(not windows.IS_WINDOWS))
def test_init_kernel32_unique():
"""Make sure function doesn't override other LibraryLoaders."""
k32_a = ctypes.LibraryLoader(ctypes.WinDLL).kernel32
k32_a.GetStdHandle.argtypes = [ctypes.c_void_p]
k32_a.GetStdHandle.restype = ctypes.c_ulong
k32_b, stderr_b, stdout_b = windows.init_kernel32()
k32_c = ctypes.LibraryLoader(ctypes.WinDLL).kernel32
k32_c.GetStdHandle.argtypes = [ctypes.c_long]
k32_c.GetStdHandle.restype = ctypes.c_short
k32_d, stderr_d, stdout_d = windows.init_kernel32()
# Verify external.
assert k32_a.GetStdHandle.argtypes == [ctypes.c_void_p]
assert k32_a.GetStdHandle.restype == ctypes.c_ulong
assert k32_c.GetStdHandle.argtypes == [ctypes.c_long]
assert k32_c.GetStdHandle.restype == ctypes.c_short
# Verify ours.
assert k32_b.GetStdHandle.argtypes == [ctypes.c_ulong]
assert k32_b.GetStdHandle.restype == ctypes.c_void_p
assert k32_d.GetStdHandle.argtypes == [ctypes.c_ulong]
assert k32_d.GetStdHandle.restype == ctypes.c_void_p
assert stderr_b == stderr_d
assert stdout_b == stdout_d
@pytest.mark.parametrize('stderr_invalid', [False, True])
@pytest.mark.parametrize('stdout_invalid', [False, True])
def test_init_kernel32_valid_handle(monkeypatch, stderr_invalid, stdout_invalid):
"""Test valid/invalid handle handling.
:param monkeypatch: pytest fixture.
:param bool stderr_invalid: Mock stderr is valid.
:param bool stdout_invalid: Mock stdout is valid.
"""
mock_sys = MockSys()
monkeypatch.setattr(windows, 'sys', mock_sys)
if stderr_invalid:
setattr(mock_sys.stderr, '_original_stream', True)
if stdout_invalid:
setattr(mock_sys.stdout, '_original_stream', True)
stderr, stdout = windows.init_kernel32(MockKernel32(stderr=100, stdout=200))[1:]
if stderr_invalid and stdout_invalid:
assert stderr == windows.INVALID_HANDLE_VALUE
assert stdout == windows.INVALID_HANDLE_VALUE
elif stdout_invalid:
assert stderr == 100
assert stdout == windows.INVALID_HANDLE_VALUE
elif stderr_invalid:
assert stderr == windows.INVALID_HANDLE_VALUE
assert stdout == 200
else:
assert stderr == 100
assert stdout == 200
def test_get_console_info():
"""Test function."""
# Test error.
if windows.IS_WINDOWS:
with pytest.raises(OSError):
windows.get_console_info(windows.init_kernel32()[0], windows.INVALID_HANDLE_VALUE)
# Test no error with mock methods.
kernel32 = MockKernel32()
fg_color, bg_color, native_ansi = windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE)
assert fg_color == 7
assert bg_color == 0
assert native_ansi is False
# Test different console modes.
for not_native in (0x0, 0x1, 0x2, 0x1 | 0x2):
kernel32.set_mode = not_native
assert not windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE)[-1]
for native in (i | 0x4 for i in (0x0, 0x1, 0x2, 0x1 | 0x2)):
kernel32.set_mode = native
assert windows.get_console_info(kernel32, windows.INVALID_HANDLE_VALUE)[-1]
@pytest.mark.parametrize('stderr', [1, windows.INVALID_HANDLE_VALUE])
@pytest.mark.parametrize('stdout', [2, windows.INVALID_HANDLE_VALUE])
def test_bg_color_native_ansi(stderr, stdout):
"""Test function.
:param int stderr: Value of parameter.
:param int stdout: Value of parameter.
"""
kernel32 = MockKernel32(set_mode=0x4)
kernel32.wAttributes = 240
actual = windows.bg_color_native_ansi(kernel32, stderr, stdout)
if stderr == windows.INVALID_HANDLE_VALUE and stdout == windows.INVALID_HANDLE_VALUE:
expected = 0, False
else:
expected = 240, True
assert actual == expected
def test_windows_stream():
"""Test class."""
# Test error.
if windows.IS_WINDOWS:
stream = windows.WindowsStream(windows.init_kernel32()[0], windows.INVALID_HANDLE_VALUE, StringIO())
assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black'])
stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue'] # No exception, just ignore.
assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black'])
# Test __getattr__() and color resetting.
original_stream = StringIO()
stream = windows.WindowsStream(MockKernel32(), windows.INVALID_HANDLE_VALUE, original_stream)
assert stream.writelines == original_stream.writelines # Test __getattr__().
assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black'])
stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue']
assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue'])
stream.colors = None # Resets colors to original.
assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['black'])
# Test special negative codes.
stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue']
stream.colors = windows.WINDOWS_CODES['/fg']
assert stream.colors == (windows.WINDOWS_CODES['white'], windows.WINDOWS_CODES['bgblue'])
stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue']
stream.colors = windows.WINDOWS_CODES['/bg']
assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['black'])
stream.colors = windows.WINDOWS_CODES['red'] | windows.WINDOWS_CODES['bgblue']
stream.colors = windows.WINDOWS_CODES['bgblack']
assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['black'])
# Test write.
stream.write(Color('{/all}A{red}B{bgblue}C'))
original_stream.seek(0)
assert original_stream.read() == 'ABC'
assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue'])
# Test ignore invalid code.
original_stream.seek(0)
original_stream.truncate()
stream.write('\x1b[0mA\x1b[31mB\x1b[44;999mC')
original_stream.seek(0)
assert original_stream.read() == 'ABC'
assert stream.colors == (windows.WINDOWS_CODES['red'], windows.WINDOWS_CODES['bgblue'])
@pytest.mark.skipif(str(windows.IS_WINDOWS))
def test_windows_nix():
"""Test enable/disable on non-Windows platforms."""
with windows.Windows():
assert not windows.Windows.is_enabled()
assert not hasattr(sys.stderr, '_original_stream')
assert not hasattr(sys.stdout, '_original_stream')
assert not windows.Windows.is_enabled()
assert not hasattr(sys.stderr, '_original_stream')
assert not hasattr(sys.stdout, '_original_stream')
def test_windows_auto_colors(monkeypatch):
"""Test Windows class with/out valid_handle and with/out auto_colors. Don't replace streams.
:param monkeypatch: pytest fixture.
"""
mock_sys = MockSys()
monkeypatch.setattr(windows, 'atexit', type('', (), {'register': staticmethod(lambda _: 0 / 0)}))
monkeypatch.setattr(windows, 'IS_WINDOWS', True)
monkeypatch.setattr(windows, 'sys', mock_sys)
monkeypatch.setattr(ANSICodeMapping, 'LIGHT_BACKGROUND', None)
# Test no valid handles.
kernel32 = MockKernel32()
monkeypatch.setattr(windows, 'init_kernel32', lambda: (kernel32, -1, -1))
assert not windows.Windows.enable()
assert not windows.Windows.is_enabled()
assert not hasattr(mock_sys.stderr, '_original_stream')
assert not hasattr(mock_sys.stdout, '_original_stream')
assert ANSICodeMapping.LIGHT_BACKGROUND is None
# Test auto colors dark background.
kernel32.set_mode = 0x4 # Enable native ANSI to have Windows skip replacing streams.
monkeypatch.setattr(windows, 'init_kernel32', lambda: (kernel32, 1, 2))
assert not windows.Windows.enable(auto_colors=True)
assert not windows.Windows.is_enabled()
assert not hasattr(mock_sys.stderr, '_original_stream')
assert not hasattr(mock_sys.stdout, '_original_stream')
assert ANSICodeMapping.LIGHT_BACKGROUND is False
# Test auto colors light background.
kernel32.wAttributes = 240
assert not windows.Windows.enable(auto_colors=True)
assert not windows.Windows.is_enabled()
assert not hasattr(mock_sys.stderr, '_original_stream')
assert not hasattr(mock_sys.stdout, '_original_stream')
assert ANSICodeMapping.LIGHT_BACKGROUND is True
@pytest.mark.parametrize('valid', ['stderr', 'stdout', 'both'])
def test_windows_replace_streams(monkeypatch, tmpdir, valid):
"""Test Windows class stdout and stderr replacement.
:param monkeypatch: pytest fixture.
:param tmpdir: pytest fixture.
:param str valid: Which mock stream(s) should be valid.
"""
ac = list() # atexit called.
mock_sys = MockSys(stderr=tmpdir.join('stderr').open(mode='wb'), stdout=tmpdir.join('stdout').open(mode='wb'))
monkeypatch.setattr(windows, 'atexit', type('', (), {'register': staticmethod(lambda _: ac.append(1))}))
monkeypatch.setattr(windows, 'IS_WINDOWS', True)
monkeypatch.setattr(windows, 'sys', mock_sys)
# Mock init_kernel32.
stderr = 1 if valid in ('stderr', 'both') else windows.INVALID_HANDLE_VALUE
stdout = 2 if valid in ('stdout', 'both') else windows.INVALID_HANDLE_VALUE
monkeypatch.setattr(windows, 'init_kernel32', lambda: (MockKernel32(), stderr, stdout))
# Test.
assert windows.Windows.enable(reset_atexit=True)
assert windows.Windows.is_enabled()
assert len(ac) == 1
if stderr != windows.INVALID_HANDLE_VALUE:
assert hasattr(mock_sys.stderr, '_original_stream')
else:
assert not hasattr(mock_sys.stderr, '_original_stream')
if stdout != windows.INVALID_HANDLE_VALUE:
assert hasattr(mock_sys.stdout, '_original_stream')
else:
assert not hasattr(mock_sys.stdout, '_original_stream')
# Test multiple disable.
assert windows.Windows.disable()
assert not windows.Windows.is_enabled()
assert not windows.Windows.disable()
assert not windows.Windows.is_enabled()
# Test context manager.
with windows.Windows():
assert windows.Windows.is_enabled()
assert not windows.Windows.is_enabled()
@pytest.mark.skipif(str(not windows.IS_WINDOWS))
def test_enable_disable(tmpdir):
"""Test enabling, disabling, repeat. Make sure colors still work.
:param tmpdir: pytest fixture.
"""
screenshot = PROJECT_ROOT.join('test_windows_test_enable_disable.png')
if screenshot.check():
screenshot.remove()
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
script.write(dedent("""\
from __future__ import print_function
import os, time
from colorclass import Color, Windows
with Windows(auto_colors=True):
print(Color('{autored}Red{/autored}'))
print('Red')
with Windows(auto_colors=True):
print(Color('{autored}Red{/autored}'))
print('Red')
stop_after = time.time() + 20
while not os.path.exists(r'%s') and time.time() < stop_after:
time.sleep(0.5)
""") % str(screenshot))
# Setup expected.
with_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_light_fg_*.bmp')]
sans_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_red_sans_*.bmp')]
assert with_colors
assert sans_colors
# Run.
with RunNewConsole(command) as gen:
screenshot_until_match(str(screenshot), 15, with_colors, 2, gen)
screenshot_until_match(str(screenshot), 15, sans_colors, 2, gen)
@pytest.mark.skipif(str(not windows.IS_WINDOWS))
def test_box_characters(tmpdir):
"""Test for unicode errors with special characters.
:param tmpdir: pytest fixture.
"""
screenshot = PROJECT_ROOT.join('test_windows_test_box_characters.png')
if screenshot.check():
screenshot.remove()
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
script.write(dedent("""\
from __future__ import print_function
import os, time
from colorclass import Color, Windows
Windows.enable(auto_colors=True)
chars = [
'+', '-', '|',
b'\\xb3'.decode('ibm437'),
b'\\xb4'.decode('ibm437'),
b'\\xb9'.decode('ibm437'),
b'\\xba'.decode('ibm437'),
b'\\xbb'.decode('ibm437'),
b'\\xbc'.decode('ibm437'),
b'\\xbf'.decode('ibm437'),
b'\\xc0'.decode('ibm437'),
b'\\xc1'.decode('ibm437'),
b'\\xc2'.decode('ibm437'),
b'\\xc3'.decode('ibm437'),
b'\\xc4'.decode('ibm437'),
b'\\xc5'.decode('ibm437'),
b'\\xc8'.decode('ibm437'),
b'\\xc9'.decode('ibm437'),
b'\\xca'.decode('ibm437'),
b'\\xcb'.decode('ibm437'),
b'\\xcc'.decode('ibm437'),
b'\\xcd'.decode('ibm437'),
b'\\xce'.decode('ibm437'),
b'\\xd9'.decode('ibm437'),
b'\\xda'.decode('ibm437'),
]
for c in chars:
print(c, end='')
print()
for c in chars:
print(Color.green(c, auto=True), end='')
print()
stop_after = time.time() + 20
while not os.path.exists(r'%s') and time.time() < stop_after:
time.sleep(0.5)
""") % str(screenshot))
# Setup expected.
with_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_box_green_*.bmp')]
sans_colors = [str(p) for p in PROJECT_ROOT.join('tests').listdir('sub_box_sans_*.bmp')]
assert with_colors
assert sans_colors
# Run.
with RunNewConsole(command) as gen:
screenshot_until_match(str(screenshot), 15, with_colors, 1, gen)
screenshot_until_match(str(screenshot), 15, sans_colors, 1, gen)

78
tox.ini Normal file
View file

@ -0,0 +1,78 @@
[general]
author = @Robpol86
license = MIT
name = colorclass
version = 2.2.0
[tox]
envlist = lint,py{34,27,26}
[testenv]
commands =
python -c "import os, sys; sys.platform == 'win32' and os.system('easy_install pillow')"
py.test --cov-append --cov-report term-missing --cov-report xml --cov {[general]name} --cov-config tox.ini \
{posargs:tests}
deps =
docopt
pytest-cov
passenv =
WINDIR
setenv =
PYTHON_EGG_CACHE = {envtmpdir}
usedevelop = True
[testenv:py35x64]
basepython = C:\Python35-x64\python.exe
[testenv:py34x64]
basepython = C:\Python34-x64\python.exe
[testenv:py33x64]
basepython = C:\Python33-x64\python.exe
[testenv:py27x64]
basepython = C:\Python27-x64\python.exe
[testenv:py26x64]
basepython = C:\Python26-x64\python.exe
[testenv:lint]
commands =
coverage erase
python setup.py check --strict
python setup.py check --strict -m
python setup.py check --strict -s
flake8 --application-import-names={[general]name},tests
pylint --rcfile=tox.ini setup.py {[general]name}
python -c "assert '{[general]author}' == __import__('{[general]name}').__author__"
python -c "assert '{[general]license}' == __import__('{[general]name}').__license__"
python -c "assert '{[general]version}' == __import__('{[general]name}').__version__"
python -c "assert 'author=\'{[general]author}\'' in open('setup.py').read(102400)"
python -c "assert 'license=\'{[general]license}\'' in open('setup.py').read(102400)"
python -c "assert 'version=\'{[general]version}\'' in open('setup.py').read(102400)"
python -c "assert '\n{[general]version} - ' in open('README.rst').read(102400)"
deps =
coverage==4.0.3
flake8==2.5.4
flake8-import-order==0.5
flake8-pep257==1.0.5
pep8-naming==0.3.3
pylint==1.5.4
[flake8]
exclude = .tox/*,build/*,docs/*,env/*,get-pip.py
ignore = D203
import-order-style = google
max-line-length = 120
statistics = True
[pylint]
ignore = .tox/*,build/*,docs/*,env/*,get-pip.py
max-line-length = 120
reports = no
disable =
too-few-public-methods,
too-many-public-methods,
[run]
branch = True