├── 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 | [](https://travis-ci.org/freelan-developers/chromalog)
2 | [](https://readthedocs.org/projects/chromalog/?badge=latest)
3 | [](https://coveralls.io/r/freelan-developers/chromalog?branch=master)
4 | [](http://opensource.org/licenses/MIT)
5 | [](https://github.com/freelan-developers/chromalog)
6 | [](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 | 
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 |
--------------------------------------------------------------------------------