├── VERSION ├── setup.cfg ├── tests ├── __init__.py ├── common.py ├── test_stream.py ├── test_mark.py ├── test_colorizer.py └── test_log.py ├── requirements.txt ├── chromalog ├── mark │ ├── __init__.py │ ├── objects.py │ └── helpers.py ├── stream.py ├── __init__.py ├── log.py └── colorizer.py ├── MANIFEST.in ├── doc ├── source │ ├── _static │ │ ├── fast-setup.png │ │ ├── home-sample.png │ │ ├── highlighting.png │ │ └── highlighting-fallback.png │ ├── api.rst │ ├── installation.rst │ ├── index.rst │ ├── quickstart.rst │ ├── conf.py │ └── advanced.rst ├── Makefile └── make.bat ├── dev_requirements.txt ├── samples ├── fast-setup.py ├── highlighting.py └── home-sample.py ├── tox.ini ├── .travis.yml ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── README.md └── setup.py /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests for chromalog. 3 | """ 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama==0.3.3 2 | future==0.14.3 3 | six==1.9.0 4 | -------------------------------------------------------------------------------- /chromalog/mark/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Marking classes and methods. 3 | """ 4 | 5 | from .objects import Mark 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include VERSION 3 | include requirements.txt 4 | include dev_requirements.txt 5 | -------------------------------------------------------------------------------- /doc/source/_static/fast-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freelan-developers/chromalog/HEAD/doc/source/_static/fast-setup.png -------------------------------------------------------------------------------- /doc/source/_static/home-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freelan-developers/chromalog/HEAD/doc/source/_static/home-sample.png -------------------------------------------------------------------------------- /doc/source/_static/highlighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freelan-developers/chromalog/HEAD/doc/source/_static/highlighting.png -------------------------------------------------------------------------------- /doc/source/_static/highlighting-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freelan-developers/chromalog/HEAD/doc/source/_static/highlighting-fallback.png -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | ipdb == 0.8 2 | nose >= 1.3, < 2 3 | coverage >= 3.7.1, < 4 4 | coveralls == 0.5 5 | mock >= 1.0.1, < 2 6 | pep8 >= 1.5.7, < 2 7 | nose-parameterized == 0.3.5 8 | wheel == 0.24.0 9 | Sphinx >= 1.2.3, < 2 10 | sphinx-rtd-theme == 0.1.6 11 | -------------------------------------------------------------------------------- /chromalog/stream.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream utilities. 3 | """ 4 | 5 | 6 | def stream_has_color_support(stream): 7 | """ 8 | Check if a stream has color support. 9 | 10 | :param stream: The stream to check. 11 | :returns: True if stream has color support. 12 | """ 13 | return getattr(stream, 'isatty', lambda: False)() 14 | -------------------------------------------------------------------------------- /samples/fast-setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import chromalog 3 | 4 | chromalog.basicConfig(level=logging.DEBUG) 5 | logger = logging.getLogger() 6 | 7 | logger.debug("This is a debug message") 8 | logger.info("This is an info message") 9 | logger.warning("This is a warning message") 10 | logger.error("This is an error message") 11 | logger.critical("This is a critical message") 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py33,py34,py35 3 | 4 | [testenv] 5 | deps = -rdev_requirements.txt 6 | commands = 7 | pep8 --count chromalog tests 8 | coverage run --include="chromalog/*" setup.py nosetests --with-doctest --doctest-extension=rst --tests tests,chromalog,doc/source 9 | sphinx-build -b doctest -W doc/source doc/build/html 10 | sphinx-build -b html -W doc/source doc/build/html 11 | coverage report -m --fail-under=100 12 | -------------------------------------------------------------------------------- /samples/highlighting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import chromalog 3 | 4 | from chromalog.mark.helpers.simple import success, error, important 5 | 6 | chromalog.basicConfig(format="%(message)s", level=logging.INFO) 7 | logger = logging.getLogger() 8 | 9 | filename = r'/var/lib/status' 10 | 11 | logger.info("Booting up system: %s", success("OK")) 12 | logger.info("Booting up network: %s", error("FAIL")) 13 | logger.info("Reading file at %s: %s", important(filename), success("OK")) 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | install: 9 | - pip install -r dev_requirements.txt 10 | - pip install --editable . 11 | script: 12 | - pep8 --count chromalog tests 13 | - coverage run --include="chromalog/*" setup.py nosetests --with-doctest --doctest-extension=rst --tests tests,chromalog,doc/source 14 | - sphinx-build -b doctest -W doc/source doc/build/html 15 | - sphinx-build -b html -W doc/source doc/build/html 16 | - coverage report -m --fail-under=100 17 | after_success: coveralls 18 | notifications: 19 | email: 20 | on_success: change 21 | on_failure: always 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions to `chromalog` are most welcome ! However, please note that the 4 | continuous integration process enforces the following things: 5 | 6 | * All unit-tests/doc-tests must pass. 7 | * No pep8 error are found, neither in the code or the tests. 8 | * Coverage is 100%. 9 | 10 | You obviously need to write tests whenever you add/modify a feature. Don't 11 | forget to update the relevant documentation entries as well. 12 | 13 | This may seem crazy but I firmly believe that those things help maintain a 14 | higher code quality and reduce the chances of bugs. 15 | 16 | Feel free to ask for help if you are stuck writing tests or are not sure what 17 | to test/how to document. 18 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ 4 | Common functions for tests. 5 | """ 6 | 7 | from nose_parameterized import parameterized 8 | 9 | 10 | def repeat_for_values(values=None): 11 | if not values: 12 | values = { 13 | "integers": 42, 14 | "floats": 3.14, 15 | "strings": "Hello you", 16 | "unicode_strings": u"éléphant is the french for elephant", 17 | "booleans": True, 18 | "none": None, 19 | } 20 | 21 | return parameterized.expand(list(values.items())) 22 | 23 | 24 | def repeat_for_integral_values(values=None): 25 | if not values: 26 | values = { 27 | "integers": 42, 28 | "floats": 3.14, 29 | "booleans": True, 30 | } 31 | 32 | return parameterized.expand(list(values.items())) 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Virtualenv 57 | .chromalog* 58 | -------------------------------------------------------------------------------- /tests/test_stream.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream tests. 3 | """ 4 | 5 | from unittest import TestCase 6 | 7 | from mock import MagicMock 8 | 9 | from chromalog.stream import stream_has_color_support 10 | 11 | 12 | class StreamTests(TestCase): 13 | def test_csh_color_support_with_color_stream(self): 14 | color_stream = MagicMock(spec=object) 15 | color_stream.isatty = lambda: True 16 | self.assertTrue(stream_has_color_support( 17 | color_stream 18 | )) 19 | 20 | def test_csh_color_support_with_no_color_stream(self): 21 | no_color_stream = MagicMock(spec=object) 22 | no_color_stream.isatty = lambda: False 23 | self.assertFalse( 24 | stream_has_color_support(no_color_stream), 25 | ) 26 | 27 | def test_csh_color_support_with_simple_stream(self): 28 | simple_stream = MagicMock(spec=object) 29 | self.assertFalse( 30 | stream_has_color_support(simple_stream), 31 | ) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 The freelan developers organization 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | Chromalog's API 4 | =============== 5 | 6 | Here is a comprehensive list of all modules, classes and function provided by 7 | **Chromalog**. 8 | 9 | ``chromalog`` 10 | ------------- 11 | 12 | .. automodule:: chromalog 13 | :members: 14 | 15 | ``chromalog.log`` 16 | ----------------- 17 | 18 | .. automodule:: chromalog.log 19 | :members: 20 | 21 | ``chromalog.colorizer`` 22 | ----------------------- 23 | 24 | .. automodule:: chromalog.colorizer 25 | :members: 26 | 27 | ``chromalog.mark`` 28 | ------------------ 29 | 30 | .. automodule:: chromalog.mark 31 | :members: 32 | 33 | ``chromalog.mark.objects`` 34 | -------------------------- 35 | 36 | .. automodule:: chromalog.mark.objects 37 | :members: 38 | 39 | ``chromalog.mark.helpers`` 40 | -------------------------- 41 | 42 | .. automodule:: chromalog.mark.helpers 43 | :members: 44 | 45 | ``chromalog.mark.helpers.simple`` 46 | --------------------------------- 47 | 48 | .. automodule:: chromalog.mark.helpers.simple 49 | :members: 50 | 51 | ``chromalog.mark.helpers.conditional`` 52 | -------------------------------------- 53 | 54 | .. automodule:: chromalog.mark.helpers.conditional 55 | :members: 56 | 57 | .. toctree:: 58 | :maxdepth: 3 59 | -------------------------------------------------------------------------------- /samples/home-sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | A sample using chromalog. 3 | """ 4 | 5 | import logging 6 | 7 | from chromalog.log import ( 8 | ColorizingStreamHandler, 9 | ColorizingFormatter, 10 | ) 11 | 12 | from chromalog.mark.helpers.simple import ( 13 | important, 14 | success, 15 | error, 16 | ) 17 | 18 | formatter = ColorizingFormatter('[%(levelname)s] %(message)s') 19 | 20 | handler = ColorizingStreamHandler() 21 | handler.setFormatter(formatter) 22 | 23 | logger = logging.getLogger() 24 | logger.setLevel(logging.DEBUG) 25 | logger.addHandler(handler) 26 | 27 | logger.info("This is a regular info log message.") 28 | logger.info( 29 | "Trying to read user information from %s using a json parser.", 30 | important(r'/usr/local/mylib/user-info.json'), 31 | ) 32 | logger.warning( 33 | "Unable to read the file at %s ! Something is wrong.", 34 | important(r'/usr/local/mylib/user-info.json'), 35 | ) 36 | logger.error("Something went really wrong !") 37 | logger.info( 38 | "This is a %s and this is an %s.", 39 | success("success"), 40 | error("error"), 41 | ) 42 | logger.info( 43 | "You can combine %s and %s to get an %s !", 44 | success("success"), 45 | important("important"), 46 | important(success("important-success")), 47 | ) 48 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ============ 5 | 6 | Using pip 7 | --------- 8 | 9 | The simplest way to install **Chromalog** is to use `pip 10 | `_. 11 | 12 | Just type the following command in your command prompt: 13 | 14 | .. code-block:: bash 15 | 16 | pip install chromalog 17 | 18 | That's it ! No configuration is needed. **Chromalog** is 19 | now installed on your system. 20 | 21 | From source 22 | ----------- 23 | 24 | If you feel in hacky mood, you can also install 25 | **Chromalog** from `source 26 | `_. 27 | 28 | Clone the Git repository: 29 | 30 | .. code-block:: bash 31 | 32 | git clone git@github.com:freelan-developers/chromalog.git 33 | 34 | Then, inside the cloned repository folder: 35 | 36 | .. code-block:: bash 37 | 38 | python setup.py install 39 | 40 | And that's it ! **Chromalog** should now be installed in 41 | your Python packages. 42 | 43 | You can easily test it by typing in a command prompt: 44 | 45 | .. code-block:: bash 46 | 47 | python -c "import chromalog" 48 | 49 | This should not raise any error (especially not an 50 | :py:exc:`ImportError`). 51 | 52 | .. toctree:: 53 | :maxdepth: 3 54 | 55 | What's next ? 56 | ------------- 57 | 58 | :ref:`Get started ` or explore :ref:`api`. 59 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | Chromalog's documentation 2 | ========================= 3 | 4 | Chromalog is a Python library that eases the use of 5 | colors in Python logging. 6 | 7 | It integrates seamlessly into any Python 2 or Python 3 project. Based on 8 | `colorama `_, it works on both Windows 9 | and \*NIX platforms and is highly configurable. 10 | 11 | Chromalog can detect whether the associated output stream is color-capable and 12 | even has a fallback mechanism: if color is not supported, your log will look no 13 | worse than it was before you colorized it. 14 | 15 | Using **Chromalog**, getting a logging-system that looks like this is a breeze: 16 | 17 | .. image:: _static/home-sample.png 18 | :align: center 19 | 20 | Its use is simple and straightforward: 21 | 22 | .. testsetup:: 23 | 24 | from logging import getLogger 25 | logger = getLogger() 26 | username = 'user' 27 | 28 | .. testcode:: 29 | 30 | from chromalog.mark.helpers.simple import important 31 | 32 | logger.info("Connected as %s for 2 hours.", important(username)) 33 | 34 | Ready to add some colors in your life ? :ref:`Get started ` or 35 | check out :ref:`api` ! 36 | 37 | Table of contents 38 | ================== 39 | 40 | .. toctree:: 41 | :maxdepth: 3 42 | 43 | installation 44 | quickstart 45 | advanced 46 | api 47 | 48 | 49 | Indices and tables 50 | ================== 51 | 52 | * :ref:`genindex` 53 | * :ref:`modindex` 54 | * :ref:`search` 55 | 56 | -------------------------------------------------------------------------------- /chromalog/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enhance Python logging with colors. 3 | """ 4 | 5 | import logging 6 | 7 | from .log import ( 8 | ColorizingFormatter, 9 | ColorizingStreamHandler, 10 | ) 11 | 12 | 13 | def basicConfig( 14 | format=None, 15 | datefmt=None, 16 | level=None, 17 | stream=None, 18 | colorizer=None, 19 | ): 20 | """ 21 | Does basic configuration for the logging system by creating a 22 | :class:`chromalog.log.ColorizingStreamHandler` with a default 23 | :class:`chromalog.log.ColorizingFormatter` and adding it to the root 24 | logger. 25 | 26 | This function does nothing if the root logger already has handlers 27 | configured for it. 28 | 29 | :param format: The format to be passed to the formatter. 30 | :param datefmt: The date format to be passed to the formatter. 31 | :param level: Set the root logger to the specified level. 32 | :param stream: Use the specified stream to initialize the stream handler. 33 | :param colorizer: Set the colorizer to be passed to the stream handler. 34 | """ 35 | logger = logging.getLogger() 36 | 37 | if not logger.handlers: 38 | if format is None: 39 | format = '%(levelname)s:%(name)s:%(message)s' 40 | 41 | formatter = ColorizingFormatter(fmt=format, datefmt=datefmt) 42 | handler = ColorizingStreamHandler(stream=stream, colorizer=colorizer) 43 | handler.setFormatter(formatter) 44 | logger.addHandler(handler) 45 | 46 | if level: 47 | logger.setLevel(level) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/freelan-developers/chromalog.svg)](https://travis-ci.org/freelan-developers/chromalog) 2 | [![Documentation Status](https://readthedocs.org/projects/chromalog/badge/?version=latest)](https://readthedocs.org/projects/chromalog/?badge=latest) 3 | [![Coverage Status](https://coveralls.io/repos/freelan-developers/chromalog/badge.svg?branch=master)](https://coveralls.io/r/freelan-developers/chromalog?branch=master) 4 | [![License](https://img.shields.io/pypi/l/chromalog.svg)](http://opensource.org/licenses/MIT) 5 | [![GitHub Tag](https://img.shields.io/github/tag/freelan-developers/chromalog.svg)](https://github.com/freelan-developers/chromalog) 6 | [![Latest Release](https://img.shields.io/pypi/v/chromalog.svg)](https://pypi.python.org/pypi/chromalog) 7 | 8 | # Chromalog 9 | 10 | **Chromalog** is a Python library that eases the use of colors in Python logging. 11 | 12 | It integrates seamlessly into any Python 2 or Python 3 project. Based on colorama, it works on both Windows and *NIX platforms. 13 | 14 | **Chromalog** can detect whether the associated output stream is color-capable and even has a fallback mechanism: if color is not supported, your log will look no worse than it was before you colorized it. 15 | 16 | Using **Chromalog**, getting a logging-system that looks like this is a breeze: 17 | 18 | ![home-sample](doc/source/_static/home-sample.png) 19 | 20 | Its use is simple and straightforward: 21 | 22 | from chromalog.mark.helpers.simple import important 23 | 24 | logger.info("Connected as %s for 2 hours.", important(username)) 25 | 26 | Ready to add some colors in your life ? Check out [Chromalog’s documentation](http://chromalog.readthedocs.org/en/latest/index.html) ! 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import ( 2 | setup, 3 | find_packages, 4 | ) 5 | 6 | setup( 7 | name='chromalog', 8 | url='http://chromalog.readthedocs.org/en/latest/index.html', 9 | author='Julien Kauffmann', 10 | author_email='julien.kauffmann@freelan.org', 11 | maintainer='Julien Kauffmann', 12 | maintainer_email='julien.kauffmann@freelan.org', 13 | license='MIT', 14 | version=open('VERSION').read().strip(), 15 | description=( 16 | "A non-intrusive way to use colors in your logs and generic output." 17 | ), 18 | long_description="""\ 19 | Chromalog integrates seemlessly in any Python project and allows the use of 20 | colors in log messages and generic output easily. It is based on colorama and 21 | so works on both Windows and *NIX platforms. Chromalog is able to detect 22 | without any configuration if the associated output stream has color 23 | capabilities and can fall back to the default monochromatic logging. 24 | 25 | Chromalog also offer helpers to easily highlight some important parts of a log 26 | message. 27 | """, 28 | packages=find_packages(exclude=[ 29 | 'tests', 30 | 'scripts', 31 | ]), 32 | install_requires=[ 33 | 'colorama>=0.3.7', 34 | 'future>=0.14.3', 35 | 'six>=1.9.0,<2', 36 | ], 37 | test_suite='tests', 38 | classifiers=[ 39 | 'Intended Audience :: Developers', 40 | 'Operating System :: OS Independent', 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Programming Language :: Python :: 3.5', 48 | 'Topic :: Software Development', 49 | 'Topic :: Software Development :: Libraries :: Python Modules', 50 | 'License :: OSI Approved :: MIT License', 51 | 'Development Status :: 5 - Production/Stable', 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /doc/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | If you haven't installed **Chromalog** yet, it is highly recommended that 7 | :ref:`you do so ` before reading any further. 8 | 9 | How it works 10 | ------------ 11 | 12 | **Chromalog** provides colored logging through the use of custom 13 | :py:class:`StreamHandler ` and 14 | :py:class:`Formatter `. 15 | 16 | The :py:class:`ColorizingStreamHandler ` 17 | is responsible for writing the log entries to the output stream. It can detect 18 | whether the associated stream has color capabilities and eventually fallback to 19 | a non-colored output mechanism. In this case it behaves exactly like a standard 20 | :py:class:`logging.StreamHandler`. It is associated to a :ref:`color map 21 | ` that is passed to every formatter that requests it. 22 | 23 | The :py:class:`ColorizingFormatter ` is 24 | responsible for adding the color-specific markup in the formatted string. If 25 | used with a non colorizing stream handler, the :py:class:`ColorizingFormatter 26 | ` will transparently fallback to a 27 | non-colorizing behavior. 28 | 29 | Fast setup 30 | ---------- 31 | 32 | **Chromalog** provides a :py:func:`basicConfig ` 33 | function, very similar to :py:func:`logging.basicConfig` that quickly sets up 34 | the root logger, but using a :py:class:`ColorizingStreamHandler 35 | ` and a :py:class:`ColorizingFormatter 36 | ` instead. 37 | 38 | It can be used like so to setup logging in a Python project: 39 | 40 | .. literalinclude:: ../../samples/fast-setup.py 41 | :language: python 42 | :linenos: 43 | 44 | Which produces the following output: 45 | 46 | .. image:: _static/fast-setup.png 47 | :align: center 48 | 49 | It's as simple as it gets ! 50 | 51 | Marking log objects 52 | ------------------- 53 | 54 | While **Chromalog** has the ability to color entire log lines, it can also mark 55 | some specific log elements to highlight them in the output. 56 | 57 | A good example of that could be: 58 | 59 | .. literalinclude:: ../../samples/highlighting.py 60 | :language: python 61 | :linenos: 62 | 63 | Which produces the following output: 64 | 65 | .. image:: _static/highlighting.png 66 | :align: center 67 | 68 | Note what happens when we redirect the output to a file: 69 | 70 | .. image:: _static/highlighting-fallback.png 71 | :align: center 72 | 73 | As you can see, **Chromalog** automatically detected that the output stream 74 | wasn't color-capable and disabled automatically the colorizing. Awesome ! 75 | 76 | Checkout :ref:`marking_functions` for the complete list of available marking 77 | functions. 78 | 79 | What's next ? 80 | ------------- 81 | 82 | Want to learn more about **Chromalog** ? Go read :ref:`advanced` ! 83 | 84 | .. toctree:: 85 | :maxdepth: 3 86 | -------------------------------------------------------------------------------- /chromalog/mark/objects.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mark log entries. 3 | """ 4 | from builtins import str 5 | from six import ( 6 | string_types, 7 | PY3, 8 | ) 9 | 10 | from ..colorizer import ColorizableMixin 11 | 12 | # Hack to define unicode in Python 3 and reach 100% coverage. 13 | unicode = str if PY3 else unicode 14 | 15 | 16 | class Mark(ColorizableMixin): 17 | """ 18 | Wraps any object and mark it for colored output. 19 | """ 20 | def __init__(self, obj, color_tag): 21 | """ 22 | Mark ``obj`` for coloration. 23 | 24 | :param obj: The object to mark for colored output. 25 | :param color_tag: The color tag to use for coloring. Can be either a 26 | list of a string. If ``color_tag`` is a string it will be converted 27 | into a single-element list automatically. 28 | 29 | .. note:: Nested :class:`chromalog.mark.Mark` objects are flattened 30 | automatically and their ``color_tag`` are appended. 31 | 32 | >>> from chromalog.mark.objects import Mark 33 | 34 | >>> Mark(42, 'a').color_tag 35 | ['a'] 36 | 37 | >>> Mark(42, ['a']).color_tag 38 | ['a'] 39 | 40 | >>> Mark(42, ['a', 'b']).color_tag 41 | ['a', 'b'] 42 | 43 | >>> Mark(Mark(42, 'c'), ['a', 'b']) == Mark(42, ['a', 'b', 'c']) 44 | True 45 | """ 46 | if isinstance(color_tag, string_types): 47 | color_tag = [color_tag] 48 | 49 | if isinstance(obj, Mark): 50 | color_tag.extend(obj.color_tag) 51 | obj = obj.obj 52 | 53 | super(Mark, self).__init__(color_tag=color_tag) 54 | self.obj = obj 55 | 56 | def __repr__(self): 57 | """ 58 | Gives a representation of the marked object. 59 | 60 | >>> repr(Mark('a', 'b')) 61 | "Mark('a', ['b'])" 62 | """ 63 | return '{klass}({obj!r}, {color_tag!r})'.format( 64 | klass=self.__class__.__name__, 65 | obj=self.obj, 66 | color_tag=self.color_tag, 67 | ) 68 | 69 | def __str__(self): 70 | """ 71 | Gives a string representation of the marked object. 72 | 73 | >>> str(Mark("hello", [])) 74 | 'hello' 75 | """ 76 | return str(self.obj) 77 | 78 | def __unicode__(self): 79 | """ 80 | Gives a string representation of the marked object. 81 | """ 82 | return unicode(self.obj) 83 | 84 | def __int__(self): 85 | """ 86 | Gives an integer representation of the marked object. 87 | 88 | >>> int(Mark(42, [])) 89 | 42 90 | """ 91 | return int(self.obj) 92 | 93 | def __float__(self): 94 | """ 95 | Gives a float representation of the marked object. 96 | 97 | >>> float(Mark(3.14, [])) 98 | %f 99 | """ % (float(self.obj)) # Account for Python 2.6 discrepancy 100 | return float(self.obj) 101 | 102 | def __bool__(self): 103 | """ 104 | Gives a boolean representation of the marked object. 105 | 106 | >>> bool(Mark(True, [])) 107 | True 108 | """ 109 | return bool(self.obj) 110 | 111 | def __eq__(self, other): 112 | """ 113 | Compares this marked object with another. 114 | 115 | :param other: The other instance to compare with. 116 | :returns: True if `other` is a :class:`chromalog.mark.Mark` instance 117 | with equal `obj` and `color_tag` members. 118 | 119 | >>> Mark(42, color_tag=[]) == Mark(42, color_tag=[]) 120 | True 121 | 122 | >>> Mark(42, color_tag=['a']) == Mark(42, color_tag=['b']) 123 | False 124 | """ 125 | if isinstance(other, self.__class__): 126 | return ( 127 | other.obj == self.obj and 128 | other.color_tag == self.color_tag 129 | ) 130 | -------------------------------------------------------------------------------- /tests/test_mark.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test object marking. 3 | """ 4 | 5 | from unittest import TestCase 6 | from six import ( 7 | PY2, 8 | PY3, 9 | ) 10 | 11 | from chromalog.mark import Mark 12 | 13 | from .common import ( 14 | repeat_for_values, 15 | repeat_for_integral_values, 16 | ) 17 | 18 | 19 | class MarkTests(TestCase): 20 | @repeat_for_values() 21 | def test_string_rendering_of_marked(self, _, value): 22 | # Python2 non-unicode string want to use the 'ascii' encoding for this 23 | # conversion, which cannot work. 24 | if PY2 and isinstance(value, unicode): 25 | return 26 | 27 | self.assertEqual('{0}'.format(value), '{0}'.format(Mark(value, 'a'))) 28 | 29 | @repeat_for_values() 30 | def test_unicode_rendering_of_marked(self, _, value): 31 | self.assertEqual(u'{0}'.format(value), u'{0}'.format(Mark(value, 'a'))) 32 | 33 | @repeat_for_integral_values() 34 | def test_int_rendering_of_marked(self, _, value): 35 | self.assertEqual('%d' % value, '%d' % Mark(value, 'a')) 36 | 37 | @repeat_for_integral_values() 38 | def test_hexadecimal_int_rendering_of_marked(self, _, value): 39 | # Apparently in Python 3, %x expects a real integer. If you know how to 40 | # make it work with a Marked integer, please let me know ! 41 | if PY3: 42 | return 43 | 44 | self.assertEqual('%x' % value, '%x' % Mark(value, 'a')) 45 | 46 | @repeat_for_integral_values() 47 | def test_float_rendering_of_marked(self, _, value): 48 | self.assertEqual('%f' % value, '%f' % Mark(value, 'a')) 49 | 50 | @repeat_for_values() 51 | def test_marked_objects_dont_compare_to_their_value_as(self, _, value): 52 | self.assertNotEqual(value, Mark(value, 'a')) 53 | 54 | @repeat_for_values() 55 | def test_marked_objects_have_a_color_tag_attribute_for(self, _, value): 56 | self.assertTrue(hasattr(Mark(value, 'a'), 'color_tag')) 57 | self.assertTrue( 58 | hasattr(Mark(value, color_tag='info'), 'color_tag'), 59 | ) 60 | 61 | @repeat_for_values() 62 | def test_marked_objects_can_be_nested_for(self, _, value): 63 | obj = Mark(Mark(value, 'b'), 'a') 64 | self.assertEqual(['a', 'b'], obj.color_tag) 65 | self.assertEqual(value, obj.obj) 66 | 67 | obj = Mark(Mark(value, ['b', 'c']), 'a') 68 | self.assertEqual(['a', 'b', 'c'], obj.color_tag) 69 | self.assertEqual(value, obj.obj) 70 | 71 | obj = Mark(Mark(value, 'c'), ['a', 'b']) 72 | self.assertEqual(['a', 'b', 'c'], obj.color_tag) 73 | self.assertEqual(value, obj.obj) 74 | 75 | obj = Mark(Mark(value, ['c', 'd']), ['a', 'b']) 76 | self.assertEqual(['a', 'b', 'c', 'd'], obj.color_tag) 77 | self.assertEqual(value, obj.obj) 78 | 79 | @repeat_for_values({ 80 | 'simple_name': 'alpha', 81 | 'underscore_name': 'alpha_beta', 82 | }) 83 | def test_simple_helpers_with(self, _, name): 84 | import chromalog.mark.helpers.simple as helpers 85 | helper = getattr(helpers, name) 86 | self.assertEqual([name], helper(42).color_tag) 87 | 88 | @repeat_for_values({ 89 | 'simple_name': 'alpha_or_beta', 90 | 'underscore_name': 'alpha_beta_or_gamma_delta', 91 | }) 92 | def test_conditional_helpers_with(self, _, name): 93 | import chromalog.mark.helpers.conditional as helpers 94 | helper = getattr(helpers, name) 95 | true_color_tag, false_color_tag = name.split('_or_') 96 | self.assertEqual([true_color_tag], helper(42, True).color_tag) 97 | self.assertEqual([false_color_tag], helper(42, False).color_tag) 98 | self.assertEqual([true_color_tag], helper(True).color_tag) 99 | self.assertEqual([false_color_tag], helper(False).color_tag) 100 | 101 | def test_explicit_unicode_in_python3(self): 102 | if PY3: 103 | self.assertEqual( 104 | u'test', 105 | Mark(u'test', 'foo').__unicode__(), 106 | ) 107 | -------------------------------------------------------------------------------- /chromalog/mark/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automatically generate marking helpers functions. 3 | """ 4 | 5 | import sys 6 | 7 | from .objects import Mark 8 | 9 | 10 | class SimpleHelpers(object): 11 | """ 12 | A class that is designed to act as a module and implement magic helper 13 | generation. 14 | """ 15 | 16 | def __init__(self): 17 | self.__helpers = {} 18 | 19 | def make_helper(self, color_tag): 20 | """ 21 | Make a simple helper. 22 | 23 | :param color_tag: The color tag to make a helper for. 24 | :returns: The helper function. 25 | """ 26 | helper = self.__helpers.get(color_tag) 27 | 28 | if not helper: 29 | def helper(obj): 30 | return Mark(obj=obj, color_tag=color_tag) 31 | 32 | helper.__name__ = color_tag 33 | helper.__doc__ = """ 34 | Mark an object for coloration. 35 | 36 | The color tag is set to {color_tag!r}. 37 | 38 | :param obj: The object to mark for coloration. 39 | :returns: A :class:`Mark` instance. 40 | 41 | >>> from chromalog.mark.helpers.simple import {color_tag} 42 | 43 | >>> {color_tag}(42).color_tag 44 | ['{color_tag}'] 45 | """.format(color_tag=color_tag) 46 | 47 | self.__helpers[color_tag] = helper 48 | 49 | return helper 50 | 51 | def __getattr__(self, name): 52 | """ 53 | Get a magic helper. 54 | 55 | :param name: The name of the helper to get. 56 | 57 | >>> SimpleHelpers().alpha(42).color_tag 58 | ['alpha'] 59 | 60 | >>> getattr(SimpleHelpers(), '_incorrect', None) 61 | """ 62 | if name.startswith('_'): 63 | raise AttributeError(name) 64 | 65 | return self.make_helper(color_tag=name) 66 | 67 | 68 | class ConditionalHelpers(object): 69 | """ 70 | A class that is designed to act as a module and implement magic helper 71 | generation. 72 | """ 73 | 74 | def __init__(self): 75 | self.__helpers = {} 76 | 77 | def make_helper(self, color_tag_true, color_tag_false): 78 | """ 79 | Make a conditional helper. 80 | 81 | :param color_tag_true: The color tag if the condition is met. 82 | :param color_tag_false: The color tag if the condition is not met. 83 | :returns: The helper function. 84 | """ 85 | helper = self.__helpers.get( 86 | (color_tag_true, color_tag_false), 87 | ) 88 | 89 | if not helper: 90 | def helper(obj, condition=None): 91 | if condition is None: 92 | condition = obj 93 | 94 | return Mark( 95 | obj=obj, 96 | color_tag=color_tag_true if condition else color_tag_false, 97 | ) 98 | 99 | helper.__name__ = '_or_'.join((color_tag_true, color_tag_false)) 100 | helper.__doc__ = """ 101 | Convenience helper method that marks an object with the 102 | {color_tag_true!r} color tag if `condition` is truthy, and with the 103 | {color_tag_false!r} color tag otherwise. 104 | 105 | :param obj: The object to mark for coloration. 106 | :param condition: The condition to verify. If `condition` is 107 | :const:`None`, the `obj` is evaluated instead. 108 | :returns: A :class:`Mark` instance. 109 | 110 | >>> from chromalog.mark.helpers.conditional import {name} 111 | 112 | >>> {name}(42, True).color_tag 113 | ['{color_tag_true}'] 114 | 115 | >>> {name}(42, False).color_tag 116 | ['{color_tag_false}'] 117 | 118 | >>> {name}(42).color_tag 119 | ['{color_tag_true}'] 120 | 121 | >>> {name}(0).color_tag 122 | ['{color_tag_false}'] 123 | """.format( 124 | name=helper.__name__, 125 | color_tag_true=color_tag_true, 126 | color_tag_false=color_tag_false, 127 | ) 128 | 129 | self.__helpers[ 130 | (color_tag_true, color_tag_false), 131 | ] = helper 132 | 133 | return helper 134 | 135 | def __getattr__(self, name): 136 | """ 137 | Get a magic helper. 138 | 139 | :param name: The name of the helper to get. Must be of the form 140 | 'a_or_b' where `a` and `b` are color tags. 141 | 142 | >>> ConditionalHelpers().alpha_or_beta(42, True).color_tag 143 | ['alpha'] 144 | 145 | >>> ConditionalHelpers().alpha_or_beta(42, False).color_tag 146 | ['beta'] 147 | 148 | >>> ConditionalHelpers().alpha_or_beta(42).color_tag 149 | ['alpha'] 150 | 151 | >>> ConditionalHelpers().alpha_or_beta(0).color_tag 152 | ['beta'] 153 | 154 | >>> getattr(ConditionalHelpers(), 'alpha_beta', None) 155 | >>> getattr(ConditionalHelpers(), '_incorrect', None) 156 | """ 157 | if name.startswith('_'): 158 | raise AttributeError(name) 159 | 160 | try: 161 | color_tag_true, color_tag_false = name.split('_or_') 162 | except ValueError: 163 | raise AttributeError(name) 164 | 165 | return self.make_helper( 166 | color_tag_true=color_tag_true, 167 | color_tag_false=color_tag_false, 168 | ) 169 | 170 | 171 | simple = SimpleHelpers() 172 | simple.__doc__ = """ 173 | Pseudo-module that generates simple helpers. 174 | 175 | See :class:`SimpleHelpers`. 176 | """ 177 | 178 | conditional = ConditionalHelpers() 179 | conditional.__doc__ = """ 180 | Pseudo-module that generates conditional helpers. 181 | 182 | See :class:`ConditionalHelpers`. 183 | """ 184 | 185 | sys.modules['.'.join([__name__, 'simple'])] = simple 186 | sys.modules['.'.join([__name__, 'conditional'])] = conditional 187 | -------------------------------------------------------------------------------- /chromalog/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Log-related functions and structures. 3 | """ 4 | from builtins import map 5 | 6 | import sys 7 | import logging 8 | 9 | from colorama import AnsiToWin32 10 | from functools import partial 11 | from contextlib import contextmanager 12 | 13 | from .colorizer import Colorizer 14 | from .mark.objects import Mark 15 | from .stream import stream_has_color_support 16 | 17 | 18 | class ColorizingFormatter(logging.Formatter, object): 19 | """ 20 | A formatter that colorize its output. 21 | """ 22 | 23 | @contextmanager 24 | def _patch_record(self, record, colorizer, message_color_tag): 25 | save_dict = record.__dict__.copy() 26 | 27 | if colorizer: 28 | if isinstance(record.args, dict): 29 | record.args = dict( 30 | ( 31 | k, colorizer.colorize( 32 | v, context_color_tag=message_color_tag 33 | ) 34 | ) for k, v in record.args.items() 35 | ) 36 | else: 37 | record.args = tuple(map( 38 | partial( 39 | colorizer.colorize, 40 | context_color_tag=message_color_tag, 41 | ), 42 | record.args, 43 | )) 44 | record.filename = colorizer.colorize(record.filename) 45 | record.funcName = colorizer.colorize(record.funcName) 46 | record.levelname = colorizer.colorize(record.levelname) 47 | record.module = colorizer.colorize(record.module) 48 | record.name = colorizer.colorize(record.name) 49 | record.pathname = colorizer.colorize(record.pathname) 50 | record.processName = colorizer.colorize(record.processName) 51 | record.threadName = colorizer.colorize(record.threadName) 52 | 53 | if message_color_tag: 54 | message = colorizer.colorize(Mark( 55 | record.getMessage(), 56 | color_tag=message_color_tag, 57 | )) 58 | record.getMessage = lambda: message 59 | 60 | try: 61 | yield 62 | finally: 63 | record.__dict__ = save_dict 64 | 65 | def format(self, record): 66 | """ 67 | Colorize the arguments of a record. 68 | 69 | :record: A `LogRecord` instance. 70 | :returns: The colorized formatted string. 71 | 72 | .. note:: The `record` object must have a `colorizer` attribute to be 73 | use for colorizing the formatted string. If no such attribute is 74 | found, the default non-colorized behaviour is used instead. 75 | """ 76 | colorizer = getattr(record, 'colorizer', None) 77 | message_color_tag = getattr(record, 'message_color_tag', None) 78 | 79 | with self._patch_record(record, colorizer, message_color_tag): 80 | return super(ColorizingFormatter, self).format(record) 81 | 82 | 83 | class ColorizingStreamHandler(logging.StreamHandler, object): 84 | """ 85 | A stream handler that colorize its output. 86 | """ 87 | 88 | _RECORD_ATTRIBUTE_NAME = 'colorizer' 89 | default_attributes_map = { 90 | 'name': 'important', 91 | 'levelname': lambda record: str(record.levelname).lower(), 92 | 'message': lambda record: str(record.levelname).lower(), 93 | } 94 | 95 | def __init__( 96 | self, 97 | stream=None, 98 | colorizer=None, 99 | highlighter=None, 100 | attributes_map=None, 101 | ): 102 | """ 103 | Initializes a colorizing stream handler. 104 | 105 | :param stream: The stream to use for output. 106 | :param colorizer: The colorizer to use for colorizing the output. If 107 | not specified, a :class:`chromalog.colorizer.Colorizer` is 108 | instantiated. 109 | :param highlighter: The colorizer to use for highlighting the output 110 | when color is not supported. 111 | :param attributes_map: A map of LogRecord attributes/color tags. 112 | """ 113 | if not stream: 114 | stream = sys.stderr 115 | 116 | self.has_color_support = stream_has_color_support(stream) 117 | self.color_disabled = False 118 | self.attributes_map = attributes_map or self.default_attributes_map 119 | 120 | if self.has_color_support: 121 | stream = AnsiToWin32(stream).stream 122 | 123 | super(ColorizingStreamHandler, self).__init__( 124 | stream 125 | ) 126 | self.colorizer = colorizer or Colorizer() 127 | self.highlighter = highlighter 128 | self.setFormatter(ColorizingFormatter()) 129 | 130 | @property 131 | def active_colorizer(self): 132 | """ 133 | The active colorizer or highlighter depending on whether color is 134 | supported. 135 | """ 136 | if ( 137 | self.has_color_support and 138 | not self.color_disabled and 139 | self.colorizer 140 | ): 141 | return self.colorizer 142 | 143 | return self.highlighter 144 | 145 | @contextmanager 146 | def __bind_to_record(self, record): 147 | setattr(record, self._RECORD_ATTRIBUTE_NAME, self.active_colorizer) 148 | 149 | try: 150 | yield 151 | finally: 152 | delattr(record, self._RECORD_ATTRIBUTE_NAME) 153 | 154 | def _color_tag_from_record(self, color_tag, record): 155 | if hasattr(color_tag, '__call__'): 156 | return color_tag(record) 157 | else: 158 | return color_tag.format(**record.__dict__) 159 | 160 | def format(self, record): 161 | """ 162 | Format a `LogRecord` and prints it to the associated stream. 163 | """ 164 | with self.__bind_to_record(record): 165 | for attribute, color_tag in self.attributes_map.items(): 166 | if attribute == 'message': 167 | record.message_color_tag = self._color_tag_from_record( 168 | color_tag, 169 | record, 170 | ) 171 | else: 172 | setattr(record, attribute, Mark( 173 | getattr(record, attribute), 174 | color_tag=self._color_tag_from_record( 175 | color_tag, 176 | record, 177 | ), 178 | )) 179 | 180 | return super(ColorizingStreamHandler, self).format(record) 181 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/chromalog.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/chromalog.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/chromalog" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/chromalog" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\chromalog.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\chromalog.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /chromalog/colorizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Colorizing functions and structures. 3 | """ 4 | from builtins import object 5 | from six import ( 6 | string_types, 7 | PY3, 8 | ) 9 | 10 | from colorama import ( 11 | Fore, 12 | Back, 13 | Style, 14 | ) 15 | 16 | # Hack to define unicode in Python 3 and reach 100% coverage. 17 | unicode = str if PY3 else unicode 18 | 19 | 20 | class ColorizableMixin(object): 21 | """ 22 | Make an object colorizable by a colorizer. 23 | """ 24 | 25 | def __init__(self, color_tag=None): 26 | """ 27 | Initialize a colorizable instance. 28 | 29 | :param color_tag: The color tag to associate to this instance. 30 | 31 | `color_tag` can be either a string or a list of strings. 32 | """ 33 | super(ColorizableMixin, self).__init__() 34 | self.color_tag = color_tag 35 | 36 | 37 | class ColorizedObject(object): 38 | """ 39 | Wraps any object to colorize it. 40 | """ 41 | 42 | def __init__(self, obj, color_pair=None): 43 | """ 44 | Initialize the colorized object. 45 | 46 | :param obj: The object to colorize. 47 | :param color_pair: The (start, stop) pair of color sequences to wrap 48 | that object in during string rendering. 49 | """ 50 | self.obj = obj 51 | self.color_pair = color_pair 52 | 53 | def __repr__(self): 54 | """ 55 | Gives a representation of the colorized object. 56 | """ 57 | if not self.color_pair: 58 | return repr(self.obj) 59 | else: 60 | return "{color_start}{obj!r}{color_stop}".format( 61 | color_start=self.color_pair[0], 62 | obj=self.obj, 63 | color_stop=self.color_pair[1], 64 | ) 65 | 66 | def __str__(self): 67 | """ 68 | Gives a string representation of the colorized object. 69 | """ 70 | if not self.color_pair: 71 | return str(self.obj) 72 | else: 73 | return "{color_start}{obj}{color_stop}".format( 74 | color_start=self.color_pair[0], 75 | obj=self.obj, 76 | color_stop=self.color_pair[1], 77 | ) 78 | 79 | def __unicode__(self): 80 | """ 81 | Gives a string representation of the colorized object. 82 | """ 83 | if not self.color_pair: 84 | return unicode(self.obj) 85 | else: 86 | return u"{color_start}{obj}{color_stop}".format( 87 | color_start=self.color_pair[0], 88 | obj=self.obj, 89 | color_stop=self.color_pair[1], 90 | ) 91 | 92 | def __int__(self): 93 | """ 94 | Gives an integer representation of the colorized object. 95 | """ 96 | return int(self.obj) 97 | 98 | def __float__(self): 99 | """ 100 | Gives a float representation of the colorized object. 101 | """ 102 | return float(self.obj) 103 | 104 | def __bool__(self): 105 | """ 106 | Gives a boolean representation of the colorized object. 107 | """ 108 | return bool(self.obj) 109 | 110 | def __eq__(self, other): 111 | """ 112 | Compares this colorized object with another. 113 | 114 | :param other: The other instance to compare with. 115 | :returns: True if `other` is a 116 | :class:`chromalog.colorizer.ColorizedObject` instance with equal `obj` 117 | and `color_pair` members. 118 | 119 | >>> ColorizedObject(42) == ColorizedObject(42) 120 | True 121 | 122 | >>> ColorizedObject(42) == ColorizedObject(24) 123 | False 124 | 125 | >>> ColorizedObject(42) == ColorizedObject(42, color_pair=('', '')) 126 | False 127 | 128 | >>> ColorizedObject(42, color_pair=('', '')) == \ 129 | ColorizedObject(42, color_pair=('', '')) 130 | True 131 | 132 | >>> ColorizedObject(42, color_pair=('a', 'a')) == \ 133 | ColorizedObject(42, color_pair=('b', 'b')) 134 | False 135 | """ 136 | if isinstance(other, self.__class__): 137 | return ( 138 | other.obj == self.obj and 139 | other.color_pair == self.color_pair 140 | ) 141 | 142 | 143 | class GenericColorizer(object): 144 | """ 145 | A class reponsible for colorizing log entries and 146 | :class:`chromalog.important.Important` objects. 147 | """ 148 | def __init__(self, color_map=None, default_color_tag=None): 149 | """ 150 | Initialize a new colorizer with a specified `color_map`. 151 | 152 | :param color_map: A dictionary where the keys are color tags and the 153 | value are couples of color sequences (start, stop). 154 | :param default_color_tag: The color tag to default to in case an 155 | unknown color tag is encountered. If set to a falsy value no 156 | default is used. 157 | """ 158 | self.color_map = color_map or self.default_color_map 159 | self.default_color_tag = default_color_tag 160 | 161 | def get_color_pair( 162 | self, 163 | color_tag, 164 | context_color_tag=None, 165 | use_default=True, 166 | ): 167 | """ 168 | Get the color pairs for the specified `color_tag` and 169 | `context_color_tag`. 170 | 171 | :param color_tag: A list of color tags. 172 | :param context_color_tag: A list of color tags to use as a context. 173 | :param use_default: If :const:`False` then the default value won't be 174 | used in case the ``color_tag`` is not found in the associated color 175 | map. 176 | :returns: A pair of color sequences. 177 | """ 178 | if isinstance(color_tag, string_types): 179 | color_tag = [color_tag] 180 | 181 | pairs = list( 182 | filter(None, (self.color_map.get(tag) for tag in color_tag)) 183 | ) 184 | 185 | if not pairs and use_default: 186 | pair = self.color_map.get(self.default_color_tag) 187 | 188 | if pair: 189 | pairs = [pair] 190 | 191 | if context_color_tag: 192 | ctx_pair = self.get_color_pair( 193 | color_tag=context_color_tag, 194 | use_default=False, 195 | ) 196 | 197 | if ctx_pair: 198 | pairs = [ctx_pair[::-1], ctx_pair] + pairs 199 | 200 | return ( 201 | ''.join(x[0] for x in pairs), 202 | ''.join(x[1] for x in reversed(pairs)), 203 | ) 204 | 205 | def colorize(self, obj, color_tag=None, context_color_tag=None): 206 | """ 207 | Colorize an object. 208 | 209 | :param obj: The object to colorize. 210 | :param color_tag: The color tag to use as a default if ``obj`` is not 211 | marked. 212 | :param context_color_tag: The color tag to use as context. 213 | :returns: ``obj`` if ``obj`` is not a colorizable object. A colorized 214 | string otherwise. 215 | 216 | .. note: A colorizable object must have a truthy-``color_tag`` 217 | attribute. 218 | """ 219 | color_tag = getattr(obj, 'color_tag', color_tag) 220 | 221 | if color_tag: 222 | color_pair = self.get_color_pair( 223 | color_tag=color_tag, 224 | context_color_tag=context_color_tag, 225 | ) 226 | else: 227 | color_pair = None 228 | 229 | return ColorizedObject(obj=obj, color_pair=color_pair) 230 | 231 | def colorize_message(self, message, *args, **kwargs): 232 | """ 233 | Colorize a message. 234 | 235 | :param message: The message to colorize. If message is a marked object, 236 | its color tag will be used as a ``context_color_tag``. ``message`` 237 | may contain formatting placeholders as described in 238 | :func:`str.format`. 239 | :returns: The colorized message. 240 | 241 | .. warning:: 242 | This function has no way of check the color-capability of any 243 | stream that the resulting string might be printed to. 244 | """ 245 | context_color_tag = getattr(message, 'color_tag', None) 246 | args = [ 247 | self.colorize(arg, context_color_tag=context_color_tag) 248 | for arg in args 249 | ] 250 | kwargs = dict( 251 | ( 252 | key, self.colorize(value, context_color_tag=context_color_tag) 253 | ) for key, value in kwargs.items() 254 | ) 255 | if context_color_tag: 256 | return str(self.colorize( 257 | str(message).format(*args, **kwargs), 258 | color_tag=context_color_tag, 259 | )) 260 | else: 261 | return message.format(*args, **kwargs) 262 | 263 | 264 | class Colorizer(GenericColorizer): 265 | """ 266 | Colorize log entries. 267 | """ 268 | default_color_map = { 269 | 'debug': (Style.DIM + Fore.CYAN, Style.RESET_ALL), 270 | 'info': (Style.RESET_ALL, Style.RESET_ALL), 271 | 'important': (Style.BRIGHT, Style.RESET_ALL), 272 | 'success': (Fore.GREEN, Style.RESET_ALL), 273 | 'warning': (Fore.YELLOW, Style.RESET_ALL), 274 | 'error': (Fore.RED, Style.RESET_ALL), 275 | 'critical': (Back.RED, Style.RESET_ALL), 276 | } 277 | 278 | 279 | class MonochromaticColorizer(Colorizer): 280 | """ 281 | Monochromatic colorizer for non-color-capable streams that only highlights 282 | :class:`chromalog.mark.Mark` objects with an ``important`` color tag. 283 | """ 284 | default_color_map = { 285 | 'important': ('**', '**'), 286 | } 287 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # chromalog documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Feb 10 18:40:21 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sphinx_rtd_theme 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.doctest', 34 | 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.viewcode', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'chromalog' 54 | copyright = u'2015, Julien Kauffmann' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = open( 62 | os.path.join(os.path.dirname(__file__), '..', '..', 'VERSION'), 63 | ).read().strip() 64 | 65 | # The full version, including alpha/beta/rc tags. 66 | release = version 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | #language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = [] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all 83 | # documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | # If true, keep warnings as "system message" paragraphs in the built documents. 104 | #keep_warnings = False 105 | 106 | 107 | # -- Options for HTML output ---------------------------------------------- 108 | 109 | # The theme to use for HTML and HTML Help pages. See the documentation for 110 | # a list of builtin themes. 111 | html_theme = 'sphinx_rtd_theme' 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | #html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | #html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | #html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ['_static'] 141 | 142 | # Add any extra paths that contain custom files (such as robots.txt or 143 | # .htaccess) here, relative to this directory. These files are copied 144 | # directly to the root of the documentation. 145 | #html_extra_path = [] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 148 | # using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names to 159 | # template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 175 | #html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 178 | #html_show_copyright = True 179 | 180 | # If true, an OpenSearch description file will be output, and all pages will 181 | # contain a tag referring to it. The value of this option must be the 182 | # base URL from which the finished HTML is served. 183 | #html_use_opensearch = '' 184 | 185 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 186 | #html_file_suffix = None 187 | 188 | # Output file base name for HTML help builder. 189 | htmlhelp_basename = 'chromalogdoc' 190 | 191 | 192 | # -- Options for LaTeX output --------------------------------------------- 193 | 194 | latex_elements = { 195 | # The paper size ('letterpaper' or 'a4paper'). 196 | #'papersize': 'letterpaper', 197 | 198 | # The font size ('10pt', '11pt' or '12pt'). 199 | #'pointsize': '10pt', 200 | 201 | # Additional stuff for the LaTeX preamble. 202 | #'preamble': '', 203 | } 204 | 205 | # Grouping the document tree into LaTeX files. List of tuples 206 | # (source start file, target name, title, 207 | # author, documentclass [howto, manual, or own class]). 208 | latex_documents = [ 209 | ( 210 | 'index', 211 | 'chromalog.tex', 212 | u'chromalog Documentation', 213 | u'Julien Kauffmann', 214 | 'manual' 215 | ), 216 | ] 217 | 218 | # The name of an image file (relative to this directory) to place at the top of 219 | # the title page. 220 | #latex_logo = None 221 | 222 | # For "manual" documents, if this is true, then toplevel headings are parts, 223 | # not chapters. 224 | #latex_use_parts = False 225 | 226 | # If true, show page references after internal links. 227 | #latex_show_pagerefs = False 228 | 229 | # If true, show URL addresses after external links. 230 | #latex_show_urls = False 231 | 232 | # Documents to append as an appendix to all manuals. 233 | #latex_appendices = [] 234 | 235 | # If false, no module index is generated. 236 | #latex_domain_indices = True 237 | 238 | 239 | # -- Options for manual page output --------------------------------------- 240 | 241 | # One entry per manual page. List of tuples 242 | # (source start file, name, description, authors, manual section). 243 | man_pages = [ 244 | ('index', 'chromalog', u'chromalog Documentation', 245 | [u'Julien Kauffmann'], 1) 246 | ] 247 | 248 | # If true, show URL addresses after external links. 249 | #man_show_urls = False 250 | 251 | 252 | # -- Options for Texinfo output ------------------------------------------- 253 | 254 | # Grouping the document tree into Texinfo files. List of tuples 255 | # (source start file, target name, title, author, 256 | # dir menu entry, description, category) 257 | texinfo_documents = [ 258 | ( 259 | 'index', 260 | 'chromalog', 261 | u'chromalog Documentation', 262 | u'Julien Kauffmann', 263 | 'chromalog', 264 | 'Provide color capabilities to Python logging.', 265 | 'Miscellaneous', 266 | ), 267 | ] 268 | 269 | # Documents to append as an appendix to all manuals. 270 | #texinfo_appendices = [] 271 | 272 | # If false, no module index is generated. 273 | #texinfo_domain_indices = True 274 | 275 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 276 | #texinfo_show_urls = 'footnote' 277 | 278 | # If true, do not generate a @detailmenu in the "Top" node's menu. 279 | #texinfo_no_detailmenu = False 280 | 281 | 282 | # Example configuration for intersphinx: refer to the Python standard library. 283 | intersphinx_mapping = {'http://docs.python.org/': None} 284 | 285 | # Autodocument classes methods. 286 | autoclass_content = 'both' 287 | -------------------------------------------------------------------------------- /tests/test_colorizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test colorizers. 3 | """ 4 | from builtins import str # noqa 5 | 6 | from unittest import TestCase 7 | from six import PY3 8 | 9 | from chromalog.colorizer import ( 10 | ColorizedObject, 11 | Colorizer, 12 | ColorizableMixin, 13 | ) 14 | from chromalog.mark import Mark 15 | 16 | from .common import repeat_for_values 17 | 18 | 19 | class ColorizerTests(TestCase): 20 | def test_colorizer_get_color_pair_not_found(self): 21 | colorizer = Colorizer({}) 22 | self.assertEqual(('', ''), colorizer.get_color_pair(color_tag=['a'])) 23 | 24 | def test_colorizer_get_color_pair_found(self): 25 | colorizer = Colorizer({ 26 | 'a': ('[', ']'), 27 | }) 28 | self.assertEqual(('[', ']'), colorizer.get_color_pair(color_tag=['a'])) 29 | 30 | def test_colorizer_get_color_pair_found_double(self): 31 | colorizer = Colorizer({ 32 | 'a': ('[', ']'), 33 | 'b': ('<', '>'), 34 | }) 35 | self.assertEqual( 36 | ('[<', '>]'), 37 | colorizer.get_color_pair(color_tag=['a', 'b']), 38 | ) 39 | 40 | def test_colorizer_get_color_pair_not_found_with_default(self): 41 | colorizer = Colorizer( 42 | { 43 | 'a': ('[', ']'), 44 | 'b': ('<', '>'), 45 | }, 46 | default_color_tag='b', 47 | ) 48 | self.assertEqual(('<', '>'), colorizer.get_color_pair(color_tag=['c'])) 49 | 50 | def test_colorizer_get_color_pair_not_found_with_disabled_default(self): 51 | colorizer = Colorizer( 52 | { 53 | 'a': ('[', ']'), 54 | 'b': ('<', '>'), 55 | }, 56 | default_color_tag='b', 57 | ) 58 | self.assertEqual( 59 | ('', ''), 60 | colorizer.get_color_pair(color_tag=['c'], use_default=False), 61 | ) 62 | 63 | def test_colorizer_get_color_pair_found_with_context(self): 64 | colorizer = Colorizer( 65 | { 66 | 'a': ('[', ']'), 67 | 'b': ('<', '>'), 68 | }, 69 | ) 70 | self.assertEqual( 71 | ('><[', ']><'), 72 | colorizer.get_color_pair(color_tag=['a'], context_color_tag='b'), 73 | ) 74 | 75 | def test_colorizer_get_color_pair_found_with_list_context(self): 76 | colorizer = Colorizer( 77 | { 78 | 'a': ('[', ']'), 79 | 'b': ('<', '>'), 80 | 'c': ('(', ')'), 81 | }, 82 | ) 83 | self.assertEqual( 84 | (')><([', '])><('), 85 | colorizer.get_color_pair( 86 | color_tag=['a'], 87 | context_color_tag=['b', 'c'], 88 | ), 89 | ) 90 | 91 | @repeat_for_values() 92 | def test_colorizer_converts_unknown_types(self, _, value): 93 | colorizer = Colorizer(color_map={ 94 | 'a': ('[', ']'), 95 | 'b': ('<', '>'), 96 | }) 97 | self.assertEqual(ColorizedObject(value), colorizer.colorize(value)) 98 | 99 | @repeat_for_values() 100 | def test_colorizer_changes_colorizable_types(self, _, value): 101 | colorizer = Colorizer(color_map={ 102 | 'a': ('[', ']'), 103 | }) 104 | self.assertEqual( 105 | ColorizedObject(Mark(value, 'a'), ('[', ']')), 106 | colorizer.colorize(Mark(value, 'a')), 107 | ) 108 | 109 | @repeat_for_values() 110 | def test_colorizer_changes_colorizable_types_with_tags(self, _, value): 111 | colorizer = Colorizer(color_map={ 112 | 'a': ('[', ']'), 113 | 'b': ('<', '>'), 114 | }) 115 | self.assertEqual( 116 | ColorizedObject(Mark(value, ['a', 'b']), ('[<', '>]')), 117 | colorizer.colorize(Mark(value, ['a', 'b'])), 118 | ) 119 | 120 | @repeat_for_values() 121 | def test_colorizer_changes_colorizable_types_with_context(self, _, value): 122 | colorizer = Colorizer(color_map={ 123 | 'a': ('[', ']'), 124 | 'b': ('<', '>'), 125 | }) 126 | self.assertEqual( 127 | ColorizedObject(Mark(value, 'a'), ('><[', ']><')), 128 | colorizer.colorize(Mark(value, 'a'), context_color_tag='b'), 129 | ) 130 | 131 | @repeat_for_values() 132 | def test_colorizer_changes_colorizable_types_with_tags_and_context( 133 | self, 134 | _, 135 | value, 136 | ): 137 | colorizer = Colorizer(color_map={ 138 | 'a': ('[', ']'), 139 | 'b': ('(', ')'), 140 | 'c': ('<', '>'), 141 | }) 142 | self.assertEqual( 143 | ColorizedObject(Mark(value, ['a', 'b']), ('><[(', ')]><')), 144 | colorizer.colorize(Mark(value, ['a', 'b']), context_color_tag='c'), 145 | ) 146 | 147 | @repeat_for_values({ 148 | "default_colorizable": ColorizableMixin(), 149 | "specific_colorizable": ColorizableMixin(color_tag='info'), 150 | }) 151 | def test_colorizable_mixin_has_a_color_tag_attribute_for(self, _, value): 152 | self.assertTrue(hasattr(value, 'color_tag')) 153 | 154 | def test_colorizer_colorizes_with_known_color_tag(self): 155 | colorizer = Colorizer( 156 | color_map={ 157 | 'my_tag': ('START_MARK', 'STOP_MARK'), 158 | }, 159 | ) 160 | result = colorizer.colorize(Mark('hello', color_tag='my_tag')) 161 | self.assertEqual( 162 | ColorizedObject( 163 | Mark( 164 | 'hello', 165 | 'my_tag', 166 | ), 167 | ( 168 | 'START_MARK', 169 | 'STOP_MARK', 170 | ), 171 | ), 172 | result, 173 | ) 174 | 175 | def test_colorizer_colorizes_with_known_color_tag_and_default(self): 176 | colorizer = Colorizer( 177 | color_map={ 178 | 'my_tag': ('START_MARK', 'STOP_MARK'), 179 | 'default': ('START_DEFAULT_MARK', 'STOP_DEFAULT_MARK') 180 | }, 181 | default_color_tag='default', 182 | ) 183 | result = colorizer.colorize(Mark('hello', color_tag='my_tag')) 184 | self.assertEqual( 185 | ColorizedObject( 186 | Mark( 187 | 'hello', 188 | 'my_tag', 189 | ), 190 | ( 191 | 'START_MARK', 192 | 'STOP_MARK', 193 | ), 194 | ), 195 | result, 196 | ) 197 | 198 | def test_colorizer_doesnt_colorize_with_unknown_color_tag(self): 199 | colorizer = Colorizer( 200 | color_map={ 201 | 'my_tag': ('START_MARK', 'STOP_MARK'), 202 | }, 203 | ) 204 | result = colorizer.colorize(Mark('hello', color_tag='my_unknown_tag')) 205 | self.assertEqual( 206 | ColorizedObject(Mark('hello', 'my_unknown_tag'), ('', '')), 207 | result, 208 | ) 209 | 210 | def test_colorizer_colorizes_with_unknown_color_tag_and_default(self): 211 | colorizer = Colorizer( 212 | color_map={ 213 | 'my_tag': ('START_MARK', 'STOP_MARK'), 214 | 'default': ('START_DEFAULT_MARK', 'STOP_DEFAULT_MARK') 215 | }, 216 | default_color_tag='default', 217 | ) 218 | result = colorizer.colorize(Mark('hello', color_tag='my_unknown_tag')) 219 | self.assertEqual( 220 | ColorizedObject( 221 | Mark( 222 | 'hello', 223 | 'my_unknown_tag', 224 | ), 225 | ( 226 | 'START_DEFAULT_MARK', 227 | 'STOP_DEFAULT_MARK', 228 | ), 229 | ), 230 | result, 231 | ) 232 | 233 | def test_colorize_message(self): 234 | colorizer = Colorizer(color_map={ 235 | 'a': ('[', ']'), 236 | 'b': ('(', ')'), 237 | }) 238 | message = '{0}-{1}_{a}~{b}' 239 | args = [42, Mark(42, ['a', 'b'])] 240 | kwargs = { 241 | 'a': 0, 242 | 'b': Mark(0, ['b', 'a']), 243 | } 244 | self.assertEqual( 245 | '42-[(42)]_0~([0])', 246 | colorizer.colorize_message(message, *args, **kwargs), 247 | ) 248 | 249 | def test_colorize_message_with_context(self): 250 | colorizer = Colorizer(color_map={ 251 | 'a': ('[', ']'), 252 | 'b': ('(', ')'), 253 | 'c': ('<', '>'), 254 | }) 255 | message = Mark('{0}-{1}_{a}~{b}', 'c') 256 | args = [42, Mark(42, ['a', 'b'])] 257 | kwargs = { 258 | 'a': 0, 259 | 'b': Mark(0, ['b', 'a']), 260 | } 261 | self.assertEqual( 262 | '<42-><[(42)]><_0~><([0])><>', 263 | colorizer.colorize_message(message, *args, **kwargs), 264 | ) 265 | 266 | @repeat_for_values() 267 | def test_colorized_object_conversion(self, _, value): 268 | self.assertEqual( 269 | u'{0}'.format(value), 270 | u'{0}'.format(ColorizedObject(value)), 271 | ) 272 | 273 | @repeat_for_values() 274 | def test_colorized_object_conversion_with_color_pair(self, _, value): 275 | self.assertEqual( 276 | u'<{0}>'.format(value), 277 | u'{0}'.format(ColorizedObject(value, color_pair=('<', '>'))), 278 | ) 279 | 280 | @repeat_for_values() 281 | def test_colorized_object_representation(self, _, value): 282 | self.assertEqual( 283 | repr(value), 284 | repr(ColorizedObject(value)), 285 | ) 286 | 287 | @repeat_for_values() 288 | def test_colorized_object_representation_with_color_pair(self, _, value): 289 | self.assertEqual( 290 | u'<{0!r}>'.format(value), 291 | repr(ColorizedObject(value, color_pair=('<', '>'))), 292 | ) 293 | 294 | @repeat_for_values({ 295 | "integer": int, 296 | "float": float, 297 | "boolean": bool, 298 | }) 299 | def test_colorized_object_cast(self, _, type_): 300 | self.assertEqual( 301 | type_(), 302 | type_(ColorizedObject(type_())), 303 | ) 304 | 305 | @repeat_for_values({ 306 | "integer": int, 307 | "float": float, 308 | "boolean": bool, 309 | }) 310 | def test_colorized_object_cast_with_color_pair(self, _, type_): 311 | self.assertEqual( 312 | type_(), 313 | type_(ColorizedObject(type_(), color_pair=('<', '>'))), 314 | ) 315 | 316 | def test_explicit_unicode_in_python3(self): 317 | if PY3: 318 | self.assertEqual( 319 | u'test', 320 | ColorizedObject(u'test').__unicode__(), 321 | ) 322 | self.assertEqual( 323 | u'', 324 | ColorizedObject(u'test', color_pair=('<', '>')).__unicode__(), 325 | ) 326 | -------------------------------------------------------------------------------- /doc/source/advanced.rst: -------------------------------------------------------------------------------- 1 | .. _advanced: 2 | 3 | Advanced usage 4 | ============== 5 | 6 | .. testsetup:: 7 | 8 | from __future__ import print_function 9 | 10 | We've seen in :ref:`quickstart` how to quickly colorize your logging output. 11 | But **Chromalog** has much more to offer than just that ! 12 | 13 | .. _marking_functions: 14 | 15 | Marking functions 16 | ----------------- 17 | 18 | The :mod:`chromalog.mark` module contains all **Chromalog**'s marking logic. 19 | 20 | Its main component is the :class:`Mark ` class which wraps 21 | any Python object and associates it with one or several *color tags*. 22 | 23 | Those color tags are evaluated during the formatting phase by the 24 | :class:`ColorizingFormatter` and transformed 25 | into color sequences, as defined in the 26 | :class:`ColorizingStreamHandler`'s 27 | :ref:`color map`. 28 | 29 | To decorate a Python object, one can just do: 30 | 31 | .. testcode:: 32 | 33 | from chromalog.mark import Mark 34 | 35 | marked_value = Mark('value', 'my_color_tag') 36 | 37 | You may define several color tags at once, by specifying a list: 38 | 39 | .. testcode:: 40 | 41 | from chromalog.mark import Mark 42 | 43 | marked_value = Mark('value', ['my_color_tag', 'some_other_tag']) 44 | 45 | Nested :class:`Mark ` instances are actually flattened 46 | automatically and their color tags appended. 47 | 48 | .. testcode:: 49 | 50 | from chromalog.mark import Mark 51 | 52 | marked_value = Mark(Mark('value', 'some_other_tag'), 'my_color_tag') 53 | 54 | .. warning:: 55 | 56 | Be careful when specifying several color tags: their order **matters** ! 57 | 58 | Depending on the color sequences of your color map, the formatted result 59 | might differ. 60 | 61 | See :ref:`color_maps` for an example. 62 | 63 | Helpers 64 | +++++++ 65 | 66 | **Chromalog** also comes with several built-in helpers which make marking 67 | objects even more readable. Those helpers are generated automatically by several 68 | *magic* modules. 69 | 70 | Simple helpers 71 | ############## 72 | 73 | Simple helpers are a quick way of marking an object and an explicit way of 74 | highlighting a value. 75 | 76 | You can generate simple helpers by importing them from the 77 | :mod:`chromalog.mark.helpers.simple` magic module, like so: 78 | 79 | .. testcode:: 80 | 81 | from chromalog.mark.helpers.simple import important 82 | 83 | print(important(42).color_tag) 84 | 85 | Which gives the following output: 86 | 87 | .. testoutput:: 88 | 89 | ['important'] 90 | 91 | An helper function with a color tag similar to its name will be generated and 92 | made accessible transparently. 93 | 94 | Like :class:`Mark` instances, you can obviously combine 95 | several helpers to cumulate the effects. 96 | 97 | For instance: 98 | 99 | .. testcode:: 100 | 101 | from chromalog.mark.helpers.simple import important, success 102 | 103 | print(important(success(42)).color_tag) 104 | 105 | Gives: 106 | 107 | .. testoutput:: 108 | 109 | ['important', 'success'] 110 | 111 | If the name of the helper you want to generate is not a suitable python 112 | identifier, you can use the :func:`chromalog.mark.helpers.simple.make_helper` 113 | function instead. 114 | 115 | Note that, should you need it, documentation is generated for each helper. For 116 | instance, here is the generated documentation for the 117 | :func:`chromalog.mark.helpers.simple.success` function: 118 | 119 | .. autofunction:: chromalog.mark.helpers.simple.success 120 | 121 | Conditional helpers 122 | ################### 123 | 124 | Conditional helpers are a quick way of associating a color tag to an object 125 | depending on a boolean condition. 126 | 127 | You can generate conditional helpers by importing them from the 128 | :mod:`chromalog.mark.helpers.conditional` magic module: 129 | 130 | .. testcode:: 131 | 132 | from chromalog.mark.helpers.conditional import success_or_error 133 | 134 | print(success_or_error(42, True).color_tag) 135 | print(success_or_error(42, False).color_tag) 136 | print(success_or_error(42).color_tag) 137 | print(success_or_error(0).color_tag) 138 | 139 | Which gives: 140 | 141 | .. testoutput:: 142 | 143 | ['success'] 144 | ['error'] 145 | ['success'] 146 | ['error'] 147 | 148 | .. warning:: 149 | 150 | Automatically generated conditional helpers must have a name of the form 151 | ``a_or_b`` where ``a`` and ``b`` are color tags. 152 | 153 | If the name of the helper you want to generate is not a suitable python 154 | identifier, you can use the 155 | :func:`chromalog.mark.helpers.conditional.make_helper` function instead. 156 | 157 | .. note:: 158 | 159 | If no ``condition`` is specified, then the value itself is evaluated as a 160 | boolean value. 161 | 162 | This is useful for outputing exit codes for instance. 163 | 164 | Colorizers 165 | ---------- 166 | 167 | The :class:`GenericColorizer` class is 168 | responsible for turning color tags into colors (or decoration sequences). 169 | 170 | .. _color_maps: 171 | 172 | Color maps 173 | ++++++++++ 174 | 175 | To do so, each :class:`GenericColorizer` 176 | instance has a ``color_map`` :class:`dictionary` which has the following 177 | structure: 178 | 179 | .. code-block:: python 180 | 181 | color_map = { 182 | 'alpha': ('[', ']'), 183 | 'beta': ('{', '}'), 184 | } 185 | 186 | That is, each *key* is the color tag, and each *value* is a pair 187 | ``(start_sequence, stop_sequence)`` of start and stop sequences that will 188 | surround the decorated value when it is output. 189 | 190 | Values are decorated in order with the seqauences that match their associated 191 | color tags. For instance: 192 | 193 | .. testcode:: 194 | 195 | from chromalog.mark.helpers.simple import alpha, beta 196 | from chromalog.colorizer import GenericColorizer 197 | 198 | colorizer = GenericColorizer(color_map={ 199 | 'alpha': ('[', ']'), 200 | 'beta': ('{', '}'), 201 | }) 202 | 203 | print(colorizer.colorize(alpha(beta(42)))) 204 | print(colorizer.colorize(beta(alpha(42)))) 205 | 206 | Which gives: 207 | 208 | .. testoutput:: 209 | 210 | [{42}] 211 | {[42]} 212 | 213 | Context colorizing 214 | ++++++++++++++++++ 215 | 216 | Note that the :func:`colorize` 217 | method takes an optional parameter ``context_color_tag`` which is mainly used 218 | by the :class:`ColorizingFormatter` 219 | to colorize subparts of a colorized message. 220 | 221 | ``context_color_tag`` should match the color tag used to colorize the 222 | contextual message as a whole. Unless you write your own formatter, you 223 | shouldn't have to care much about it. 224 | 225 | Here is an example on how ``context_color_tag`` modifies the output: 226 | 227 | .. testcode:: 228 | 229 | from chromalog.mark.helpers.simple import alpha 230 | from chromalog.colorizer import GenericColorizer 231 | 232 | colorizer = GenericColorizer(color_map={ 233 | 'alpha': ('[', ']'), 234 | 'beta': ('{', '}'), 235 | }) 236 | 237 | print(colorizer.colorize(alpha(42), context_color_tag='beta')) 238 | 239 | Which gives: 240 | 241 | .. testoutput:: 242 | 243 | }{[42]}{ 244 | 245 | As you can see, the context color tag is first closed then reopened, then the 246 | usual color tags are used. This behavior is required as it prevents some color 247 | escaping sequences to persist after the tags get closed on some terminals. 248 | 249 | Built-in colorizers 250 | +++++++++++++++++++ 251 | 252 | **Chromalog** ships with two default colorizers: 253 | 254 | - :class:`Colorizer` which is associated to a 255 | color map constitued of color escaping sequences. 256 | - :class:`MonochromaticColorizer` 257 | which may be used on non color-capable output streams and that only decorates 258 | objects marked with the ``'important'`` color tag. 259 | 260 | See :ref:`default_color_maps` for a comprehensive list of default color tags 261 | and their resulting sequences. 262 | 263 | Custom colorizers 264 | ################# 265 | 266 | One can create its own colorizer by simply deriving from the 267 | :class:`GenericColorizer` class and 268 | defining the ``default_color_map`` class attribute, like so: 269 | 270 | .. testcode:: 271 | 272 | from chromalog.colorizer import GenericColorizer 273 | 274 | from colorama import ( 275 | Fore, 276 | Back, 277 | Style, 278 | ) 279 | 280 | class MyColorizer(GenericColorizer): 281 | default_color_map = { 282 | 'success': (Fore.GREEN, Style.RESET_ALL), 283 | } 284 | 285 | Decorating messages 286 | ################### 287 | 288 | Colorizers also provide a method to directly colorize a message, regardless of any output stream and its color capabilities: 289 | 290 | .. automethod:: chromalog.colorizer.GenericColorizer.colorize_message 291 | :noindex: 292 | 293 | Here is an example of usage: 294 | 295 | .. testcode:: 296 | 297 | from chromalog.colorizer import GenericColorizer 298 | from chromalog.mark.helpers.simple import alpha 299 | 300 | colorizer = GenericColorizer(color_map={ 301 | 'alpha': ('[', ']'), 302 | }) 303 | 304 | print(colorizer.colorize_message( 305 | 'hello {0} ! How {are} you ?', 306 | alpha('world'), 307 | are=alpha('are'), 308 | )) 309 | 310 | This gives the following output: 311 | 312 | .. testoutput:: 313 | 314 | hello [world] ! How [are] you ? 315 | 316 | .. _default_color_maps: 317 | 318 | Default color maps and sequences 319 | ################################ 320 | 321 | Here is a list of the default color tags and their associated sequences: 322 | 323 | +-----------------------------------------------------------------------------+-------------+-----------------------------+ 324 | | Colorizer | Color tag | Effect | 325 | +-----------------------------------------------------------------------------+-------------+-----------------------------+ 326 | | :class:`Colorizer` | `debug` | Light blue color. | 327 | | +-------------+-----------------------------+ 328 | | | `info` | Default terminal style. | 329 | | +-------------+-----------------------------+ 330 | | | `important` | Brighter output. | 331 | | +-------------+-----------------------------+ 332 | | | `success` | Green color. | 333 | | +-------------+-----------------------------+ 334 | | | `warning` | Yellow color. | 335 | | +-------------+-----------------------------+ 336 | | | `error` | Red color. | 337 | | +-------------+-----------------------------+ 338 | | | `critical` | Red background. | 339 | +-----------------------------------------------------------------------------+-------------+-----------------------------+ 340 | | :class:`MonochromaticColorizer` | `important` | Value surrounded by ``**``. | 341 | +-----------------------------------------------------------------------------+-------------+-----------------------------+ 342 | 343 | .. toctree:: 344 | :maxdepth: 3 345 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test colorized logging structures. 3 | """ 4 | import sys 5 | import logging 6 | 7 | from unittest import TestCase 8 | from logging import ( 9 | LogRecord, 10 | DEBUG, 11 | ) 12 | from mock import ( 13 | MagicMock, 14 | patch, 15 | ) 16 | from six import StringIO 17 | 18 | from chromalog import basicConfig 19 | from chromalog.colorizer import GenericColorizer 20 | from chromalog.mark import Mark 21 | from chromalog.log import ( 22 | ColorizingFormatter, 23 | ColorizingStreamHandler, 24 | ) 25 | 26 | 27 | class LogTests(TestCase): 28 | def create_colorizer(self, format): 29 | def colorize(obj, context_color_tag=None): 30 | return format % obj 31 | 32 | result = MagicMock(spec=GenericColorizer) 33 | result.colorize = MagicMock(side_effect=colorize) 34 | 35 | return result 36 | 37 | def test_colorizing_formatter_without_a_colorizer(self): 38 | formatter = ColorizingFormatter(fmt='%(message)s') 39 | record = LogRecord( 40 | name='my_record', 41 | level=DEBUG, 42 | pathname='my_path', 43 | lineno=42, 44 | msg='%d + %d gives %d', 45 | args=(4, 5, 4 + 5,), 46 | exc_info=None, 47 | ) 48 | self.assertEqual('4 + 5 gives 9', formatter.format(record)) 49 | 50 | def test_colorizing_formatter_without_a_colorizer_mapping(self): 51 | formatter = ColorizingFormatter(fmt='%(message)s') 52 | record = LogRecord( 53 | name='my_record', 54 | level=DEBUG, 55 | pathname='my_path', 56 | lineno=42, 57 | msg='%(summand1)d + %(summand2)d gives %(sum)d', 58 | args=({'summand1': 4, 'summand2': 5, 'sum': 4 + 5},), 59 | exc_info=None, 60 | ) 61 | self.assertEqual('4 + 5 gives 9', formatter.format(record)) 62 | 63 | def test_colorizing_formatter_with_a_colorizer(self): 64 | formatter = ColorizingFormatter(fmt='%(message)s') 65 | record = LogRecord( 66 | name='my_record', 67 | level=DEBUG, 68 | pathname='my_path', 69 | lineno=42, 70 | msg='%s + %s gives %s', 71 | args=(4, 5, 4 + 5,), 72 | exc_info=None, 73 | ) 74 | setattr( 75 | record, 76 | ColorizingStreamHandler._RECORD_ATTRIBUTE_NAME, 77 | self.create_colorizer(format='[%s]'), 78 | ) 79 | 80 | self.assertEqual('[4] + [5] gives [9]', formatter.format(record)) 81 | 82 | colorizer = getattr( 83 | record, 84 | ColorizingStreamHandler._RECORD_ATTRIBUTE_NAME, 85 | ) 86 | colorizer.colorize.assert_any_call(4, context_color_tag=None) 87 | colorizer.colorize.assert_any_call(5, context_color_tag=None) 88 | colorizer.colorize.assert_any_call(9, context_color_tag=None) 89 | 90 | def test_colorizing_formatter_with_a_colorizer_mapping(self): 91 | formatter = ColorizingFormatter(fmt='%(message)s') 92 | record = LogRecord( 93 | name='my_record', 94 | level=DEBUG, 95 | pathname='my_path', 96 | lineno=42, 97 | msg='%(summand1)s + %(summand2)s gives %(sum)s', 98 | args=({'summand1': 4, 'summand2': 5, 'sum': 4 + 5},), 99 | exc_info=None, 100 | ) 101 | setattr( 102 | record, 103 | ColorizingStreamHandler._RECORD_ATTRIBUTE_NAME, 104 | self.create_colorizer(format='[%s]'), 105 | ) 106 | 107 | self.assertEqual('[4] + [5] gives [9]', formatter.format(record)) 108 | 109 | colorizer = getattr( 110 | record, 111 | ColorizingStreamHandler._RECORD_ATTRIBUTE_NAME, 112 | ) 113 | colorizer.colorize.assert_any_call(4, context_color_tag=None) 114 | colorizer.colorize.assert_any_call(5, context_color_tag=None) 115 | colorizer.colorize.assert_any_call(9, context_color_tag=None) 116 | 117 | @patch('sys.stderr', spec=sys.stderr) 118 | def test_csh_uses_stderr_as_default(self, stream): 119 | stream.isatty = lambda: False 120 | handler = ColorizingStreamHandler() 121 | self.assertEqual(stream, handler.stream) 122 | 123 | def test_csh_uses_streamwrapper(self): 124 | stream = StringIO() 125 | 126 | with patch( 127 | 'chromalog.log.stream_has_color_support', 128 | return_value=True, 129 | ): 130 | handler = ColorizingStreamHandler(stream=stream) 131 | 132 | self.assertFalse(handler.stream is stream) 133 | 134 | def test_csh_dont_uses_streamwrapper_if_no_color(self): 135 | stream = StringIO() 136 | handler = ColorizingStreamHandler(stream=stream) 137 | self.assertTrue(handler.stream is stream) 138 | 139 | def test_csh_format(self): 140 | colorizer = GenericColorizer(color_map={ 141 | 'bracket': ('[', ']'), 142 | }) 143 | highlighter = GenericColorizer(color_map={ 144 | 'bracket': ('<', '>'), 145 | }) 146 | formatter = ColorizingFormatter(fmt='%(message)s') 147 | color_stream = MagicMock() 148 | color_stream.isatty = lambda: True 149 | handler = ColorizingStreamHandler( 150 | stream=color_stream, 151 | colorizer=colorizer, 152 | highlighter=highlighter, 153 | ) 154 | handler.setFormatter(formatter) 155 | 156 | record = LogRecord( 157 | name='my_record', 158 | level=DEBUG, 159 | pathname='my_path', 160 | lineno=42, 161 | msg='%s + %s gives %s', 162 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 163 | exc_info=None, 164 | ) 165 | 166 | self.assertEqual('4 + 5 gives [9]', handler.format(record)) 167 | 168 | # Make sure that the colorizer attribute was removed after processing. 169 | self.assertFalse(hasattr(record, 'colorizer')) 170 | 171 | def test_csh_format_with_context(self): 172 | colorizer = GenericColorizer(color_map={ 173 | 'bracket': ('[', ']'), 174 | 'context': ('{', '}'), 175 | }) 176 | highlighter = GenericColorizer(color_map={ 177 | 'bracket': ('<', '>'), 178 | 'context': ('(', ')'), 179 | }) 180 | formatter = ColorizingFormatter(fmt='%(levelname)s %(message)s') 181 | color_stream = MagicMock() 182 | color_stream.isatty = lambda: True 183 | handler = ColorizingStreamHandler( 184 | stream=color_stream, 185 | colorizer=colorizer, 186 | highlighter=highlighter, 187 | attributes_map={ 188 | 'message': 'context', 189 | 'levelname': 'bracket', 190 | }, 191 | ) 192 | handler.setFormatter(formatter) 193 | 194 | record = LogRecord( 195 | name='my_record', 196 | level=DEBUG, 197 | pathname='my_path', 198 | lineno=42, 199 | msg='%s + %s gives %s', 200 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 201 | exc_info=None, 202 | ) 203 | 204 | self.assertEqual( 205 | '[DEBUG] {4 + 5 gives }{[9]}{}', 206 | handler.format(record), 207 | ) 208 | 209 | # Make sure that the colorizer attribute was removed after processing. 210 | self.assertFalse(hasattr(record, 'colorizer')) 211 | 212 | def test_csh_format_no_color_support(self): 213 | colorizer = GenericColorizer(color_map={ 214 | 'bracket': ('[', ']'), 215 | }) 216 | highlighter = GenericColorizer(color_map={ 217 | 'bracket': ('<', '>'), 218 | }) 219 | formatter = ColorizingFormatter(fmt='%(message)s') 220 | no_color_stream = MagicMock() 221 | no_color_stream.isatty = lambda: False 222 | handler = ColorizingStreamHandler( 223 | stream=no_color_stream, 224 | colorizer=colorizer, 225 | highlighter=highlighter, 226 | ) 227 | handler.setFormatter(formatter) 228 | 229 | record = LogRecord( 230 | name='my_record', 231 | level=DEBUG, 232 | pathname='my_path', 233 | lineno=42, 234 | msg='%s + %s gives %s', 235 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 236 | exc_info=None, 237 | ) 238 | 239 | self.assertEqual('4 + 5 gives <9>', handler.format(record)) 240 | 241 | # Make sure that the colorizer attribute was removed after processing. 242 | self.assertFalse(hasattr(record, 'colorizer')) 243 | 244 | def test_csh_format_no_highlighter(self): 245 | colorizer = GenericColorizer(color_map={ 246 | 'bracket': ('[', ']'), 247 | }) 248 | formatter = ColorizingFormatter(fmt='%(message)s') 249 | color_stream = MagicMock() 250 | color_stream.isatty = lambda: True 251 | handler = ColorizingStreamHandler( 252 | stream=color_stream, 253 | colorizer=colorizer, 254 | ) 255 | handler.setFormatter(formatter) 256 | 257 | record = LogRecord( 258 | name='my_record', 259 | level=DEBUG, 260 | pathname='my_path', 261 | lineno=42, 262 | msg='%s + %s gives %s', 263 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 264 | exc_info=None, 265 | ) 266 | 267 | self.assertEqual('4 + 5 gives [9]', handler.format(record)) 268 | 269 | # Make sure that the colorizer attribute was removed after processing. 270 | self.assertFalse(hasattr(record, 'colorizer')) 271 | 272 | def test_csh_format_no_highlighter_no_color_support(self): 273 | colorizer = GenericColorizer(color_map={ 274 | 'bracket': ('[', ']'), 275 | }) 276 | formatter = ColorizingFormatter(fmt='%(message)s') 277 | color_stream = MagicMock() 278 | color_stream.isatty = lambda: False 279 | handler = ColorizingStreamHandler( 280 | stream=color_stream, 281 | colorizer=colorizer, 282 | ) 283 | handler.setFormatter(formatter) 284 | 285 | record = LogRecord( 286 | name='my_record', 287 | level=DEBUG, 288 | pathname='my_path', 289 | lineno=42, 290 | msg='%s + %s gives %s', 291 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 292 | exc_info=None, 293 | ) 294 | 295 | self.assertEqual('4 + 5 gives 9', handler.format(record)) 296 | 297 | # Make sure that the colorizer attribute was removed after processing. 298 | self.assertFalse(hasattr(record, 'colorizer')) 299 | 300 | def test_csh_format_disabled_color_support(self): 301 | colorizer = GenericColorizer(color_map={ 302 | 'bracket': ('[', ']'), 303 | }) 304 | highlighter = GenericColorizer(color_map={ 305 | 'bracket': ('<', '>'), 306 | }) 307 | formatter = ColorizingFormatter(fmt='%(message)s') 308 | color_stream = MagicMock() 309 | color_stream.isatty = lambda: True 310 | handler = ColorizingStreamHandler( 311 | stream=color_stream, 312 | colorizer=colorizer, 313 | highlighter=highlighter, 314 | ) 315 | handler.color_disabled = True 316 | handler.setFormatter(formatter) 317 | 318 | record = LogRecord( 319 | name='my_record', 320 | level=DEBUG, 321 | pathname='my_path', 322 | lineno=42, 323 | msg='%s + %s gives %s', 324 | args=(4, 5, Mark(4 + 5, color_tag='bracket'),), 325 | exc_info=None, 326 | ) 327 | 328 | self.assertEqual('4 + 5 gives <9>', handler.format(record)) 329 | 330 | # Make sure that the colorizer attribute was removed after processing. 331 | self.assertFalse(hasattr( 332 | record, 333 | ColorizingStreamHandler._RECORD_ATTRIBUTE_NAME, 334 | )) 335 | 336 | def test_basic_config_add_a_stream_handler(self): 337 | logger = logging.Logger('test') 338 | 339 | self.assertEqual([], logger.handlers) 340 | 341 | with patch('logging.getLogger', new=lambda: logger): 342 | basicConfig() 343 | self.assertEqual(1, len(logger.handlers)) 344 | 345 | def test_basic_config_sets_level(self): 346 | logger = logging.Logger('test') 347 | 348 | with patch('logging.getLogger', new=lambda: logger): 349 | basicConfig(level=logging.DEBUG) 350 | self.assertEqual(logging.DEBUG, logger.level) 351 | 352 | def test_basic_config_sets_format(self): 353 | logger = logging.Logger('test') 354 | 355 | with patch('logging.getLogger', new=lambda: logger): 356 | basicConfig(format='my format') 357 | self.assertEqual('my format', logger.handlers[0].formatter._fmt) 358 | --------------------------------------------------------------------------------