Adding upstream version 2.2.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
69
.gitignore
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
After Width: | Height: | Size: 289 KiB |
229
example.py
Executable 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
After Width: | Height: | Size: 152 KiB |
63
setup.py
Executable 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
|
@ -0,0 +1 @@
|
|||
"""Allows importing from conftest."""
|
78
tests/conftest.py
Normal 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
|
@ -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)
|
BIN
tests/sub_box_green_win10.bmp
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
tests/sub_box_green_winxp.bmp
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/sub_box_sans_win10.bmp
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
tests/sub_box_sans_winxp.bmp
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/sub_red_dark_fg_win10.bmp
Normal file
After Width: | Height: | Size: 446 B |
BIN
tests/sub_red_dark_fg_winxp.bmp
Normal file
After Width: | Height: | Size: 702 B |
BIN
tests/sub_red_light_fg_win10.bmp
Normal file
After Width: | Height: | Size: 418 B |
BIN
tests/sub_red_light_fg_winxp.bmp
Normal file
After Width: | Height: | Size: 702 B |
BIN
tests/sub_red_sans_win10.bmp
Normal file
After Width: | Height: | Size: 446 B |
BIN
tests/sub_red_sans_winxp.bmp
Normal file
After Width: | Height: | Size: 882 B |
64
tests/test___main__.py
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|