summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore69
-rw-r--r--.travis.yml25
-rw-r--r--CONTRIBUTING.md35
-rw-r--r--LICENSE21
-rw-r--r--README.rst229
-rw-r--r--appveyor.yml17
-rw-r--r--colorclass/__init__.py38
-rw-r--r--colorclass/__main__.py33
-rw-r--r--colorclass/codes.py229
-rw-r--r--colorclass/color.py220
-rw-r--r--colorclass/core.py342
-rw-r--r--colorclass/parse.py96
-rw-r--r--colorclass/search.py49
-rw-r--r--colorclass/toggles.py42
-rw-r--r--colorclass/windows.py388
-rw-r--r--example.pngbin0 -> 295535 bytes
-rwxr-xr-xexample.py229
-rw-r--r--example_windows.pngbin0 -> 155348 bytes
-rwxr-xr-xsetup.py63
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/conftest.py78
-rw-r--r--tests/screenshot.py299
-rw-r--r--tests/sub_box_green_win10.bmpbin0 -> 5026 bytes
-rw-r--r--tests/sub_box_green_winxp.bmpbin0 -> 7254 bytes
-rw-r--r--tests/sub_box_sans_win10.bmpbin0 -> 5026 bytes
-rw-r--r--tests/sub_box_sans_winxp.bmpbin0 -> 7254 bytes
-rw-r--r--tests/sub_red_dark_fg_win10.bmpbin0 -> 446 bytes
-rw-r--r--tests/sub_red_dark_fg_winxp.bmpbin0 -> 702 bytes
-rw-r--r--tests/sub_red_light_fg_win10.bmpbin0 -> 418 bytes
-rw-r--r--tests/sub_red_light_fg_winxp.bmpbin0 -> 702 bytes
-rw-r--r--tests/sub_red_sans_win10.bmpbin0 -> 446 bytes
-rw-r--r--tests/sub_red_sans_winxp.bmpbin0 -> 882 bytes
-rw-r--r--tests/test___main__.py64
-rw-r--r--tests/test_codes.py137
-rw-r--r--tests/test_color.py185
-rw-r--r--tests/test_core.py398
-rw-r--r--tests/test_example.py96
-rw-r--r--tests/test_parse.py79
-rw-r--r--tests/test_search.py51
-rw-r--r--tests/test_toggles.py29
-rw-r--r--tests/test_windows.py429
-rw-r--r--tox.ini78
42 files changed, 4049 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..11757d4
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e919857
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2ac3098
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d314c3c
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b236580
--- /dev/null
+++ b/README.rst
@@ -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.
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..5b14160
--- /dev/null
+++ b/appveyor.yml
@@ -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
diff --git a/colorclass/__init__.py b/colorclass/__init__.py
new file mode 100644
index 0000000..cae4016
--- /dev/null
+++ b/colorclass/__init__.py
@@ -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'
diff --git a/colorclass/__main__.py b/colorclass/__main__.py
new file mode 100644
index 0000000..d8f3f00
--- /dev/null
+++ b/colorclass/__main__.py
@@ -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))
diff --git a/colorclass/codes.py b/colorclass/codes.py
new file mode 100644
index 0000000..b0ecb03
--- /dev/null
+++ b/colorclass/codes.py
@@ -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
diff --git a/colorclass/color.py b/colorclass/color.py
new file mode 100644
index 0000000..2849d06
--- /dev/null
+++ b/colorclass/color.py
@@ -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)
diff --git a/colorclass/core.py b/colorclass/core.py
new file mode 100644
index 0000000..481bb40
--- /dev/null
+++ b/colorclass/core.py
@@ -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)
diff --git a/colorclass/parse.py b/colorclass/parse.py
new file mode 100644
index 0000000..46dc28e
--- /dev/null
+++ b/colorclass/parse.py
@@ -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
diff --git a/colorclass/search.py b/colorclass/search.py
new file mode 100644
index 0000000..555402d
--- /dev/null
+++ b/colorclass/search.py
@@ -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)
diff --git a/colorclass/toggles.py b/colorclass/toggles.py
new file mode 100644
index 0000000..1ba6bce
--- /dev/null
+++ b/colorclass/toggles.py
@@ -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
diff --git a/colorclass/windows.py b/colorclass/windows.py
new file mode 100644
index 0000000..8f69478
--- /dev/null
+++ b/colorclass/windows.py
@@ -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()
diff --git a/example.png b/example.png
new file mode 100644
index 0000000..352aa54
--- /dev/null
+++ b/example.png
Binary files differ
diff --git a/example.py b/example.py
new file mode 100755
index 0000000..c8a8f79
--- /dev/null
+++ b/example.py
@@ -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()
diff --git a/example_windows.png b/example_windows.png
new file mode 100644
index 0000000..a171bb1
--- /dev/null
+++ b/example_windows.png
Binary files differ
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..3f22821
--- /dev/null
+++ b/setup.py
@@ -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,
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..bf16d1e
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Allows importing from conftest."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..5f404db
--- /dev/null
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/screenshot.py b/tests/screenshot.py
new file mode 100644
index 0000000..cc391eb
--- /dev/null
+++ b/tests/screenshot.py
@@ -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)
diff --git a/tests/sub_box_green_win10.bmp b/tests/sub_box_green_win10.bmp
new file mode 100644
index 0000000..485788e
--- /dev/null
+++ b/tests/sub_box_green_win10.bmp
Binary files differ
diff --git a/tests/sub_box_green_winxp.bmp b/tests/sub_box_green_winxp.bmp
new file mode 100644
index 0000000..0823dd0
--- /dev/null
+++ b/tests/sub_box_green_winxp.bmp
Binary files differ
diff --git a/tests/sub_box_sans_win10.bmp b/tests/sub_box_sans_win10.bmp
new file mode 100644
index 0000000..48344b9
--- /dev/null
+++ b/tests/sub_box_sans_win10.bmp
Binary files differ
diff --git a/tests/sub_box_sans_winxp.bmp b/tests/sub_box_sans_winxp.bmp
new file mode 100644
index 0000000..8277d1e
--- /dev/null
+++ b/tests/sub_box_sans_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_dark_fg_win10.bmp b/tests/sub_red_dark_fg_win10.bmp
new file mode 100644
index 0000000..4d66fa9
--- /dev/null
+++ b/tests/sub_red_dark_fg_win10.bmp
Binary files differ
diff --git a/tests/sub_red_dark_fg_winxp.bmp b/tests/sub_red_dark_fg_winxp.bmp
new file mode 100644
index 0000000..2846c5a
--- /dev/null
+++ b/tests/sub_red_dark_fg_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_light_fg_win10.bmp b/tests/sub_red_light_fg_win10.bmp
new file mode 100644
index 0000000..43229a3
--- /dev/null
+++ b/tests/sub_red_light_fg_win10.bmp
Binary files differ
diff --git a/tests/sub_red_light_fg_winxp.bmp b/tests/sub_red_light_fg_winxp.bmp
new file mode 100644
index 0000000..e930459
--- /dev/null
+++ b/tests/sub_red_light_fg_winxp.bmp
Binary files differ
diff --git a/tests/sub_red_sans_win10.bmp b/tests/sub_red_sans_win10.bmp
new file mode 100644
index 0000000..738c172
--- /dev/null
+++ b/tests/sub_red_sans_win10.bmp
Binary files differ
diff --git a/tests/sub_red_sans_winxp.bmp b/tests/sub_red_sans_winxp.bmp
new file mode 100644
index 0000000..a9f0f2e
--- /dev/null
+++ b/tests/sub_red_sans_winxp.bmp
Binary files differ
diff --git a/tests/test___main__.py b/tests/test___main__.py
new file mode 100644
index 0000000..17a4b76
--- /dev/null
+++ b/tests/test___main__.py
@@ -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
diff --git a/tests/test_codes.py b/tests/test_codes.py
new file mode 100644
index 0000000..dd146c8
--- /dev/null
+++ b/tests/test_codes.py
@@ -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
diff --git a/tests/test_color.py b/tests/test_color.py
new file mode 100644
index 0000000..d5c7170
--- /dev/null
+++ b/tests/test_color.py
@@ -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
diff --git a/tests/test_core.py b/tests/test_core.py
new file mode 100644
index 0000000..2d398cd
--- /dev/null
+++ b/tests/test_core.py
@@ -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
diff --git a/tests/test_example.py b/tests/test_example.py
new file mode 100644
index 0000000..7ee8c05
--- /dev/null
+++ b/tests/test_example.py
@@ -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)
diff --git a/tests/test_parse.py b/tests/test_parse.py
new file mode 100644
index 0000000..c93d77f
--- /dev/null
+++ b/tests/test_parse.py
@@ -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
diff --git a/tests/test_search.py b/tests/test_search.py
new file mode 100644
index 0000000..0d5000d
--- /dev/null
+++ b/tests/test_search.py
@@ -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
diff --git a/tests/test_toggles.py b/tests/test_toggles.py
new file mode 100644
index 0000000..a2a6cda
--- /dev/null
+++ b/tests/test_toggles.py
@@ -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()
diff --git a/tests/test_windows.py b/tests/test_windows.py
new file mode 100644
index 0000000..e96e4f9
--- /dev/null
+++ b/tests/test_windows.py
@@ -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)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..c214557
--- /dev/null
+++ b/tox.ini
@@ -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