ansimarkup-1.4.0/ 0000755 0001750 0001750 00000000000 13542427016 014054 5 ustar alastair alastair ansimarkup-1.4.0/PKG-INFO 0000644 0001750 0001750 00000027004 13542427012 015150 0 ustar alastair alastair Metadata-Version: 1.1
Name: ansimarkup
Version: 1.4.0
Summary: Produce colored terminal text with an xml-like markup
Home-page: https://github.com/gvalkov/python-ansimarkup
Author: Georgi Valkov
Author-email: georgi.t.valkov@gmail.com
License: Revised BSD License
Description-Content-Type: UNKNOWN
Description: Ansimarkup
==========
.. class:: no-web no-pdf
|pypi| |build| |license|
Ansimarkup is an XML-like markup for producing colored terminal text.
.. code-block:: python
from ansimarkup import ansiprint as print
print("bold text"))
print("red text", "red text on a green background")
print("orange text")
Installation
------------
The latest stable version of ansimarkup can be installed from pypi:
.. code-block:: bash
$ pip install ansimarkup
Usage
-----
Basic
~~~~~
.. code-block:: python
from ansimarkup import parse, ansiprint
# parse() converts the tags to the corresponding ansi escape sequence.
parse("bold dim")
# ansiprint() works exactly like print(), but first runs parse() on all arguments.
ansiprint("bold", "dim")
ansiprint("bold", "dim", sep=":", file=sys.stderr)
Colors and styles
~~~~~~~~~~~~~~~~~
.. code-block:: python
# Colors may be specified in one of several ways.
parse("red foreground")
parse("red background")
parse("red foreground")
parse("red background")
# Xterm, hex and rgb colors are accepted by the and tags.
parse("aquamarine foreground")
parse("dark blue background")
parse("dark green foreground")
# Tags may be nested.
parse("red text on a yellow foreground")
# The above may be more concisely written as:
parse("red text on a yellow background")
# This shorthand also supports style tags.
parse("bold red text on a yellow background")
parse("bold red text")
parse("bold regular text on a yellow background")
# Unrecognized tags are left as-is.
parse("")
For a list of markup tags, please refer to `tags.py`_.
User-defined tags
~~~~~~~~~~~~~~~~~
Custom tags or overrides for existing tags may be defined by creating a new
``AnsiMarkup`` instance:
.. code-block:: python
from ansimarkup import AnsiMarkup, parse
user_tags = {
# Add a new tag (e.g. we want to expand to "").
"info": parse("")
# The ansi escape sequence can be used directly.
"info": "e\x1b[32m\x1b[1m",
# Tag names may also be callables.
"err": lambda: parse("")
# Colors may also be given convenient tag names.
"orange": parse(""),
# User-defined tags always take precedence over existing tags.
"bold": parse("")
}
am = AnsiMarkup(tags=user_tags)
am.parse("bold green")
am.ansiprint("red")
# Calling the instance is equivalent to calling its parse method.
am("bold") == am.parse("bold")
Alignment and length
~~~~~~~~~~~~~~~~~~~~
Aligning formatted strings can be challenging because the length of the rendered
string is different that the number of printable characters. Consider this example:
.. code-block:: python
>>> a = '| {:30} |'.format('abc')
>>> b = '| {:30} |'.format(parse('abc'))
>>> print(a, b, sep='\n')
| abc |
| abc |
This can be addressed by using the ``ansistring`` function or the
``AnsiMarkup.string(markup)`` method, which has the following useful properties:
.. code-block:: python
>>> s = ansistring('abc')
>>> print(repr(s), '->', s)
abc -> abc # abc is printed in bold
>>> len(s), len(am.parse('abc'), s.delta
3, 11, 8
With the help of the ``delta`` property, it is easy to align the strings in the
above example:
.. code-block:: python
>>> s = ansistring('abc')
>>> a = '| {:{width}} |'.format('abc', width=30)
>>> b = '| {:{width}} |'.format(s, width=(30 + s.delta))
>>> print(a, b, sep='\n')
| abc |
| abc |
Other features
~~~~~~~~~~~~~~
The default tag separators can be changed by passing the ``tag_sep`` argument to
``AnsiMarkup``:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(tag_sep="{}")
am.parse("{b}{r}bold red{/b}{/r}")
Markup tags can be removed using the ``strip()`` method:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup()
am.strip("bold red")
The ``strict`` option instructs the parser to raise ``MismatchedTag`` if opening
tags don't have corresponding closing tags:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(strict=True)
am.parse("bold red")
# ansimarkup.MismatchedTag: opening tag "" has no corresponding closing tag
Command-line
~~~~~~~~~~~~
Ansimarkup may also be used on the command-line. This works as if all
arguments were passed to ``ansiprint()``::
$ python -m ansimarkup "bold" "red"
Logging formatter
~~~~~~~~~~~~~~~~~
Ansimarkup also comes with a formatter for the standard library `logging`
module. It can be used as:
.. code-block:: python
import logging
from ansimarkup.logformatter import AnsiMarkupFormatter
log = logging.getLogger()
hdl = logging.StreamHandler()
fmt = AnsiMarkupFormatter()
hdl.setFormatter(fmt)
log.addHandler(hdl)
log.info("bold text")
Windows
~~~~~~~
Ansimarkup uses the colorama_ library internally, which means that Windows
support for ansi escape sequences is available by first running:
.. code-block:: python
import colorama
colorama.init()
For more information on Windows support, consult the "Usage" section of the
colorama_ documentation.
Performance
-----------
While the focus of ansimarkup is convenience, it does try to keep processing to
a minimum. The `benchmark.py`_ script attempts to benchmark different ansi
escape code libraries::
Benchmark 1: red bold
colorama 0.2998 μs
termcolor 3.2339 μs
colr 3.6483 μs
ansimarkup 6.8679 μs
pastel 28.8538 μs
plumbum 53.5004 μs
Benchmark 2: red boldredbold
colorama 0.8269 μs
termcolor 8.9296 μs
ansimarkup 9.3099 μs
colr 9.6244 μs
pastel 62.2018 μs
plumbum 120.8048 μs
Limitations
-----------
Ansimarkup is a simple wrapper around colorama. It does very little in the way
of validating that markup strings are well-formed. This is a conscious decision
with the goal of keeping things simple and fast.
Unbalanced nesting, such as in the following example, will produce incorrect
output::
12
Todo
----
- Many corner cases remain to be fixed.
- More elaborate testing. The current test suite mostly covers the
"happy paths".
- Replace ``tag_list.index`` in ``sub_end`` with something more
efficient (i.e. something like an ordered MultiDict).
Similar libraries
-----------------
- pastel_: bring colors to your terminal
- `plumbum.colors`_: small yet feature-rich library for shell script-like programs in Python
- colr_: easy terminal colors, with chainable methods
License
-------
Ansimarkup is released under the terms of the `Revised BSD License`_.
.. |pypi| image:: https://img.shields.io/pypi/v/ansimarkup.svg?style=flat-square&label=latest%20stable%20version
:target: https://pypi.python.org/pypi/ansimarkup
:alt: Latest version released on PyPi
.. |license| image:: https://img.shields.io/pypi/l/ansimarkup.svg?style=flat-square&label=license
:target: https://pypi.python.org/pypi/ansimarkup
:alt: BSD 3-Clause
.. |build| image:: https://img.shields.io/travis/gvalkov/python-ansimarkup/master.svg?style=flat-square&label=build
:target: http://travis-ci.org/gvalkov/python-ansimarkup
:alt: Build status
.. _tags.py: https://github.com/gvalkov/python-ansimarkup/blob/master/ansimarkup/tags.py
.. _benchmark.py: https://github.com/gvalkov/python-ansimarkup/blob/master/tests/benchmark.py
.. _colorama: https://pypi.python.org/pypi/colorama
.. _pastel: https://github.com/sdispater/pastel
.. _plumbum.colors: https://plumbum.readthedocs.io/en/latest/cli.html#colors
.. _colr: https://pypi.python.org/pypi/Colr/
.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-ansimarkup/master/LICENSE
Keywords: ansi terminal markup
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Classifier: License :: OSI Approved :: BSD License
ansimarkup-1.4.0/MANIFEST.in 0000644 0001750 0001750 00000000017 13542427012 015604 0 ustar alastair alastair include LICENSE ansimarkup-1.4.0/ansimarkup.egg-info/ 0000755 0001750 0001750 00000000000 13542427012 017714 5 ustar alastair alastair ansimarkup-1.4.0/ansimarkup.egg-info/PKG-INFO 0000644 0001750 0001750 00000027004 13542427012 021014 0 ustar alastair alastair Metadata-Version: 1.1
Name: ansimarkup
Version: 1.4.0
Summary: Produce colored terminal text with an xml-like markup
Home-page: https://github.com/gvalkov/python-ansimarkup
Author: Georgi Valkov
Author-email: georgi.t.valkov@gmail.com
License: Revised BSD License
Description-Content-Type: UNKNOWN
Description: Ansimarkup
==========
.. class:: no-web no-pdf
|pypi| |build| |license|
Ansimarkup is an XML-like markup for producing colored terminal text.
.. code-block:: python
from ansimarkup import ansiprint as print
print("bold text"))
print("red text", "red text on a green background")
print("orange text")
Installation
------------
The latest stable version of ansimarkup can be installed from pypi:
.. code-block:: bash
$ pip install ansimarkup
Usage
-----
Basic
~~~~~
.. code-block:: python
from ansimarkup import parse, ansiprint
# parse() converts the tags to the corresponding ansi escape sequence.
parse("bold dim")
# ansiprint() works exactly like print(), but first runs parse() on all arguments.
ansiprint("bold", "dim")
ansiprint("bold", "dim", sep=":", file=sys.stderr)
Colors and styles
~~~~~~~~~~~~~~~~~
.. code-block:: python
# Colors may be specified in one of several ways.
parse("red foreground")
parse("red background")
parse("red foreground")
parse("red background")
# Xterm, hex and rgb colors are accepted by the and tags.
parse("aquamarine foreground")
parse("dark blue background")
parse("dark green foreground")
# Tags may be nested.
parse("red text on a yellow foreground")
# The above may be more concisely written as:
parse("red text on a yellow background")
# This shorthand also supports style tags.
parse("bold red text on a yellow background")
parse("bold red text")
parse("bold regular text on a yellow background")
# Unrecognized tags are left as-is.
parse("")
For a list of markup tags, please refer to `tags.py`_.
User-defined tags
~~~~~~~~~~~~~~~~~
Custom tags or overrides for existing tags may be defined by creating a new
``AnsiMarkup`` instance:
.. code-block:: python
from ansimarkup import AnsiMarkup, parse
user_tags = {
# Add a new tag (e.g. we want to expand to "").
"info": parse("")
# The ansi escape sequence can be used directly.
"info": "e\x1b[32m\x1b[1m",
# Tag names may also be callables.
"err": lambda: parse("")
# Colors may also be given convenient tag names.
"orange": parse(""),
# User-defined tags always take precedence over existing tags.
"bold": parse("")
}
am = AnsiMarkup(tags=user_tags)
am.parse("bold green")
am.ansiprint("red")
# Calling the instance is equivalent to calling its parse method.
am("bold") == am.parse("bold")
Alignment and length
~~~~~~~~~~~~~~~~~~~~
Aligning formatted strings can be challenging because the length of the rendered
string is different that the number of printable characters. Consider this example:
.. code-block:: python
>>> a = '| {:30} |'.format('abc')
>>> b = '| {:30} |'.format(parse('abc'))
>>> print(a, b, sep='\n')
| abc |
| abc |
This can be addressed by using the ``ansistring`` function or the
``AnsiMarkup.string(markup)`` method, which has the following useful properties:
.. code-block:: python
>>> s = ansistring('abc')
>>> print(repr(s), '->', s)
abc -> abc # abc is printed in bold
>>> len(s), len(am.parse('abc'), s.delta
3, 11, 8
With the help of the ``delta`` property, it is easy to align the strings in the
above example:
.. code-block:: python
>>> s = ansistring('abc')
>>> a = '| {:{width}} |'.format('abc', width=30)
>>> b = '| {:{width}} |'.format(s, width=(30 + s.delta))
>>> print(a, b, sep='\n')
| abc |
| abc |
Other features
~~~~~~~~~~~~~~
The default tag separators can be changed by passing the ``tag_sep`` argument to
``AnsiMarkup``:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(tag_sep="{}")
am.parse("{b}{r}bold red{/b}{/r}")
Markup tags can be removed using the ``strip()`` method:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup()
am.strip("bold red")
The ``strict`` option instructs the parser to raise ``MismatchedTag`` if opening
tags don't have corresponding closing tags:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(strict=True)
am.parse("bold red")
# ansimarkup.MismatchedTag: opening tag "" has no corresponding closing tag
Command-line
~~~~~~~~~~~~
Ansimarkup may also be used on the command-line. This works as if all
arguments were passed to ``ansiprint()``::
$ python -m ansimarkup "bold" "red"
Logging formatter
~~~~~~~~~~~~~~~~~
Ansimarkup also comes with a formatter for the standard library `logging`
module. It can be used as:
.. code-block:: python
import logging
from ansimarkup.logformatter import AnsiMarkupFormatter
log = logging.getLogger()
hdl = logging.StreamHandler()
fmt = AnsiMarkupFormatter()
hdl.setFormatter(fmt)
log.addHandler(hdl)
log.info("bold text")
Windows
~~~~~~~
Ansimarkup uses the colorama_ library internally, which means that Windows
support for ansi escape sequences is available by first running:
.. code-block:: python
import colorama
colorama.init()
For more information on Windows support, consult the "Usage" section of the
colorama_ documentation.
Performance
-----------
While the focus of ansimarkup is convenience, it does try to keep processing to
a minimum. The `benchmark.py`_ script attempts to benchmark different ansi
escape code libraries::
Benchmark 1: red bold
colorama 0.2998 μs
termcolor 3.2339 μs
colr 3.6483 μs
ansimarkup 6.8679 μs
pastel 28.8538 μs
plumbum 53.5004 μs
Benchmark 2: red boldredbold
colorama 0.8269 μs
termcolor 8.9296 μs
ansimarkup 9.3099 μs
colr 9.6244 μs
pastel 62.2018 μs
plumbum 120.8048 μs
Limitations
-----------
Ansimarkup is a simple wrapper around colorama. It does very little in the way
of validating that markup strings are well-formed. This is a conscious decision
with the goal of keeping things simple and fast.
Unbalanced nesting, such as in the following example, will produce incorrect
output::
12
Todo
----
- Many corner cases remain to be fixed.
- More elaborate testing. The current test suite mostly covers the
"happy paths".
- Replace ``tag_list.index`` in ``sub_end`` with something more
efficient (i.e. something like an ordered MultiDict).
Similar libraries
-----------------
- pastel_: bring colors to your terminal
- `plumbum.colors`_: small yet feature-rich library for shell script-like programs in Python
- colr_: easy terminal colors, with chainable methods
License
-------
Ansimarkup is released under the terms of the `Revised BSD License`_.
.. |pypi| image:: https://img.shields.io/pypi/v/ansimarkup.svg?style=flat-square&label=latest%20stable%20version
:target: https://pypi.python.org/pypi/ansimarkup
:alt: Latest version released on PyPi
.. |license| image:: https://img.shields.io/pypi/l/ansimarkup.svg?style=flat-square&label=license
:target: https://pypi.python.org/pypi/ansimarkup
:alt: BSD 3-Clause
.. |build| image:: https://img.shields.io/travis/gvalkov/python-ansimarkup/master.svg?style=flat-square&label=build
:target: http://travis-ci.org/gvalkov/python-ansimarkup
:alt: Build status
.. _tags.py: https://github.com/gvalkov/python-ansimarkup/blob/master/ansimarkup/tags.py
.. _benchmark.py: https://github.com/gvalkov/python-ansimarkup/blob/master/tests/benchmark.py
.. _colorama: https://pypi.python.org/pypi/colorama
.. _pastel: https://github.com/sdispater/pastel
.. _plumbum.colors: https://plumbum.readthedocs.io/en/latest/cli.html#colors
.. _colr: https://pypi.python.org/pypi/Colr/
.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-ansimarkup/master/LICENSE
Keywords: ansi terminal markup
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Classifier: License :: OSI Approved :: BSD License
ansimarkup-1.4.0/ansimarkup.egg-info/zip-safe 0000644 0001750 0001750 00000000001 13542427012 021344 0 ustar alastair alastair
ansimarkup-1.4.0/ansimarkup.egg-info/top_level.txt 0000644 0001750 0001750 00000000013 13542427012 022440 0 ustar alastair alastair ansimarkup
ansimarkup-1.4.0/ansimarkup.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 13542427012 023762 0 ustar alastair alastair
ansimarkup-1.4.0/ansimarkup.egg-info/SOURCES.txt 0000644 0001750 0001750 00000000620 13542427012 021576 0 ustar alastair alastair LICENSE
MANIFEST.in
README.rst
setup.cfg
setup.py
ansimarkup/__init__.py
ansimarkup/__main__.py
ansimarkup/ansi.py
ansimarkup/compat.py
ansimarkup/logformatter.py
ansimarkup/markup.py
ansimarkup/tags.py
ansimarkup.egg-info/PKG-INFO
ansimarkup.egg-info/SOURCES.txt
ansimarkup.egg-info/dependency_links.txt
ansimarkup.egg-info/requires.txt
ansimarkup.egg-info/top_level.txt
ansimarkup.egg-info/zip-safe ansimarkup-1.4.0/ansimarkup.egg-info/requires.txt 0000644 0001750 0001750 00000000227 13542427012 022315 0 ustar alastair alastair colorama
[devel]
bumpversion>=0.5.2
check-manifest>=0.35
readme-renderer>=16.0
flake8
pep8-naming
[tests]
tox>=2.6.0
pytest>=3.0.3
pytest-cov>=2.3.1
ansimarkup-1.4.0/setup.cfg 0000644 0001750 0001750 00000000441 13542427012 015670 0 ustar alastair alastair [bumpversion]
current_version = 1.4.0
message = Bump version: {current_version} -> {new_version}
commit = True
tag = True
[wheel]
universal = 1
[flake8]
ignore = W191,E302,E265,E241,F403,E401,E221
max-line-length = 120
[bumpversion:file:setup.py]
[egg_info]
tag_build =
tag_date = 0
ansimarkup-1.4.0/setup.py 0000644 0001750 0001750 00000003116 13542427012 015563 0 ustar alastair alastair from setuptools import setup, find_packages
classifiers = [
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries',
'License :: OSI Approved :: BSD License',
]
install_requires = [
'colorama',
]
extras_require = {
'tests': [
'tox >= 2.6.0',
'pytest >= 3.0.3',
'pytest-cov >= 2.3.1',
],
'devel': [
'bumpversion >= 0.5.2',
'check-manifest >= 0.35',
'readme-renderer >= 16.0',
'flake8',
'pep8-naming',
]
}
kw = {
'name': 'ansimarkup',
'version': '1.4.0',
'description': 'Produce colored terminal text with an xml-like markup',
'long_description': open('README.rst').read(),
'author': 'Georgi Valkov',
'author_email': 'georgi.t.valkov@gmail.com',
'license': 'Revised BSD License',
'keywords': 'ansi terminal markup',
'url': 'https://github.com/gvalkov/python-ansimarkup',
'classifiers': classifiers,
'install_requires': install_requires,
'extras_require': extras_require,
'packages': find_packages(),
'zip_safe': True,
}
if __name__ == '__main__':
setup(**kw)
ansimarkup-1.4.0/LICENSE 0000644 0001750 0001750 00000002752 13542427012 015063 0 ustar alastair alastair BSD 3-Clause License
Copyright (c) 2017, Georgi Valkov. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ansimarkup-1.4.0/ansimarkup/ 0000755 0001750 0001750 00000000000 13542427012 016222 5 ustar alastair alastair ansimarkup-1.4.0/ansimarkup/logformatter.py 0000644 0001750 0001750 00000000753 13542427012 021306 0 ustar alastair alastair import sys
import logging
from . import markup
class AnsiMarkupFormatter(logging.Formatter):
def __init__(self, *args, **kwargs):
self.ansimarkup = markup.AnsiMarkup()
super(AnsiMarkupFormatter, self).__init__(*args)
def format(self, record):
message = super(AnsiMarkupFormatter, self).format(record)
message = self.ansimarkup.parse(message)
return message
# TODO: Add stream handler that strips tags when the output stream is not a tty.
ansimarkup-1.4.0/ansimarkup/compat.py 0000644 0001750 0001750 00000000157 13542427012 020062 0 ustar alastair alastair try:
import builtins
except ImportError:
import __builtin__ as builtins # noqa
__all__ = 'builtins'
ansimarkup-1.4.0/ansimarkup/ansi.py 0000644 0001750 0001750 00000000403 13542427012 017523 0 ustar alastair alastair from colorama.ansi import AnsiCodes
from colorama import Fore, Back, Style
class AnsiExtendedStyle(AnsiCodes):
ITALIC = 3
UNDERLINE = 4
BLINK = 5
REVERSE = 7
STRIKE = 8
HIDE = 9
ExtendedStyle = AnsiExtendedStyle()
ansimarkup-1.4.0/ansimarkup/markup.py 0000644 0001750 0001750 00000016706 13542427012 020105 0 ustar alastair alastair from __future__ import print_function
import re
from colorama import Style
from .tags import style, background, foreground, all_tags
from .compat import builtins
class AnsiMarkupError(Exception):
pass
class MismatchedTag(AnsiMarkupError):
pass
class UnbalancedTag(AnsiMarkupError):
pass
class AnsiMarkup:
'''
Produce colored terminal text with a tag-based markup.
'''
def __init__(self, tags=None, always_reset=False, strict=False, tag_sep='<>', ansistring_cls=None):
'''
Arguments
---------
tags: dict
User-supplied tags, which are a mapping of tag names to the strings
they will be substituted with.
always_reset: bool
Whether or not ``parse()`` should always end strings with a reset code.
strict: bool
Whether or not ``parse()`` should raise an error for missing closing tags.
tag_sep: str
The opening and closing characters of each tag (e.g. ``<>``, ``{}``).
ansistring_cls: type
Class to use in ``ansistring()`` method.
'''
self.user_tags = tags if tags else {}
self.always_reset = always_reset
self.strict = strict
self.tag_sep = tag_sep
self.ansistring_cls = ansistring_cls if ansistring_cls else AnsiMarkupString
self.re_tag = self.compile_tag_regex(tag_sep)
def parse(self, text):
'''Return a string with markup tags converted to ansi-escape sequences.'''
tags, results = [], []
text = self.re_tag.sub(lambda m: self.sub_tag(m, tags, results), text)
if self.strict and tags:
markup = "%s%s%s" % (self.tag_sep[0], tags.pop(0), self.tag_sep[1])
raise MismatchedTag('opening tag "%s" has no corresponding closing tag' % markup)
if self.always_reset:
if not text.endswith(Style.RESET_ALL):
text += Style.RESET_ALL
return text
def ansiprint(self, *args, **kwargs):
'''Wrapper around builtins.print() that runs parse() on all arguments first.'''
args = (self.parse(str(i)) for i in args)
builtins.print(*args, **kwargs)
def strip(self, text):
'''Return string with markup tags removed.'''
tags, results = [], []
return self.re_tag.sub(lambda m: self.clear_tag(m, tags, results), text)
def ansistring(self, markup):
return self.ansistring_cls(self, markup)
def __call__(self, text):
return self.parse(text)
def sub_tag(self, match, tag_list, res_list):
markup, tag = match.group(0), match.group(1)
closing = markup[1] == '/'
res = None
# Early exit if the closing tag matches the last known opening tag.
if closing and tag_list and tag_list[-1] == tag:
tag_list.pop()
res_list.pop()
return Style.RESET_ALL + ''.join(res_list)
# User-defined tags take preference over all other.
if tag in self.user_tags:
utag = self.user_tags[tag]
res = utag() if callable(utag) else utag
# Substitute on a direct match.
elif tag in all_tags:
res = all_tags[tag]
# An alternative syntax for setting the color (e.g. , ).
elif tag.startswith('fg ') or tag.startswith('bg '):
st, color = tag[:2], tag[3:]
code = '38' if st == 'fg' else '48'
if st == 'fg' and color in foreground:
res = foreground[color]
elif st == 'bg' and color.islower() and color.upper() in background:
res = background[color.upper()]
elif color.isdigit() and int(color) <= 255:
res = '\033[%s;5;%sm' % (code, color)
elif re.match(r'#(?:[a-fA-F0-9]{3}){1,2}$', color):
hex_color = color[1:]
if len(hex_color) == 3:
hex_color *= 2
res = '\033[%s;2;%s;%s;%sm' % ((code,) + hex_to_rgb(hex_color))
elif color.count(',') == 2:
colors = tuple(color.split(','))
if all(x.isdigit() and int(x) <= 255 for x in colors):
res = '\033[%s;2;%s;%s;%sm' % ((code,) + colors)
# Shorthand formats (e.g. , ).
elif ',' in tag:
el_count = tag.count(',')
if el_count == 1:
fg, bg = tag.split(',')
if fg in foreground and bg.islower() and bg.upper() in background:
res = foreground[fg] + background[bg.upper()]
elif el_count == 2:
st, fg, bg = tag.split(',')
if st in style and (fg != '' or bg != ''):
if fg == '' or fg in foreground:
if bg == '' or (bg.islower() and bg.upper() in background):
res = style[st] + foreground.get(fg, '') + background.get(bg.upper(), '')
# If nothing matches, return the full tag (i.e. text).
if res is None:
return markup
# If closing tag is known, but did not early exit, something is wrong.
if closing:
if tag in tag_list:
raise UnbalancedTag('closing tag "%s" violates nesting rules.' % markup)
else:
raise MismatchedTag('closing tag "%s" has no corresponding opening tag' % markup)
tag_list.append(tag)
res_list.append(res)
return res
def clear_tag(self, match, tag_list, res_list):
pre_length = len(tag_list)
try:
self.sub_tag(match, tag_list, res_list)
# If list did not change, the tag is unknown
if len(tag_list) == pre_length:
return match.group(0)
# Otherwise, tag matched so remove it
else:
return ''
# Tag matched but is invalid, remove it anyway
except (UnbalancedTag, MismatchedTag):
return ''
def compile_tag_regex(self, tag_sep):
# Optimize the default:
if tag_sep == '<>':
tag_regex = re.compile(r'?([^<>]+)>')
return tag_regex
if len(tag_sep) != 2:
raise ValueError('tag_sep needs to have exactly two elements (e.g. "<>")')
if tag_sep[0] == tag_sep[1]:
raise ValueError('opening and closing characters cannot be the same')
tag_regex = r'{0}/?([^{0}{1}]+){1}'.format(tag_sep[0], tag_sep[1])
return re.compile(tag_regex)
class AnsiMarkupString(str):
'''
A string containing the original markup, the formatted string and the
string with tags stripped off. Example usage::
>>> t = AnsiMarkup().string('abc')
>>> repr(t)
abc
>>> print(t)
abc # in bold
>>> len(t)
3
>>> len(AnsiMarkup().parse('abc>> t.delta
8
'''
def __new__(cls, am, markup):
parsed = am.parse(markup)
new_str = str.__new__(cls, parsed)
new_str.markup = markup
new_str.stripped = am.strip(markup)
# The difference in length between the formatted string and the string
# with markup tags stripped off.
new_str.delta = len(parsed) - len(new_str.stripped)
return new_str
def __len__(self):
return len(self.stripped)
def __repr__(self):
return self.markup
def hex_to_rgb(value):
return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4))
#return tuple(bytes.fromhex(value))
ansimarkup-1.4.0/ansimarkup/tags.py 0000644 0001750 0001750 00000005364 13542427012 017542 0 ustar alastair alastair from .ansi import Fore, Back, Style, ExtendedStyle
style = {
'b': Style.BRIGHT,
'd': Style.DIM,
'n': Style.NORMAL,
'0': Style.RESET_ALL,
'h': ExtendedStyle.HIDE,
'i': ExtendedStyle.ITALIC,
'l': ExtendedStyle.BLINK,
's': ExtendedStyle.STRIKE,
'u': ExtendedStyle.UNDERLINE,
'v': ExtendedStyle.REVERSE,
'bold': Style.BRIGHT,
'dim': Style.DIM,
'normal': Style.NORMAL,
'reset': Style.RESET_ALL,
'hide': ExtendedStyle.HIDE,
'italic': ExtendedStyle.ITALIC,
'blink': ExtendedStyle.BLINK,
'strike': ExtendedStyle.STRIKE,
'underline': ExtendedStyle.UNDERLINE,
'reverse': ExtendedStyle.REVERSE,
}
foreground = {
'k': Fore.BLACK,
'r': Fore.RED,
'g': Fore.GREEN,
'y': Fore.YELLOW,
'e': Fore.BLUE,
'm': Fore.MAGENTA,
'c': Fore.CYAN,
'w': Fore.WHITE,
'lk': Fore.LIGHTBLACK_EX,
'lr': Fore.LIGHTRED_EX,
'lg': Fore.LIGHTGREEN_EX,
'ly': Fore.LIGHTYELLOW_EX,
'le': Fore.LIGHTBLUE_EX,
'lm': Fore.LIGHTMAGENTA_EX,
'lc': Fore.LIGHTCYAN_EX,
'lw': Fore.LIGHTWHITE_EX,
'black': Fore.BLACK,
'red': Fore.RED,
'green': Fore.GREEN,
'yellow': Fore.YELLOW,
'blue': Fore.BLUE,
'magenta': Fore.MAGENTA,
'cyan': Fore.CYAN,
'white': Fore.WHITE,
'light-black': Fore.LIGHTBLACK_EX,
'light-red': Fore.LIGHTRED_EX,
'light-green': Fore.LIGHTGREEN_EX,
'light-yellow': Fore.LIGHTYELLOW_EX,
'light-blue': Fore.LIGHTBLUE_EX,
'light-magenta': Fore.LIGHTMAGENTA_EX,
'light-cyan': Fore.LIGHTCYAN_EX,
'light-white': Fore.LIGHTWHITE_EX,
}
background = {
'K': Back.BLACK,
'R': Back.RED,
'G': Back.GREEN,
'Y': Back.YELLOW,
'E': Back.BLUE,
'M': Back.MAGENTA,
'C': Back.CYAN,
'W': Back.WHITE,
'LK': Back.LIGHTBLACK_EX,
'LR': Back.LIGHTRED_EX,
'LG': Back.LIGHTGREEN_EX,
'LY': Back.LIGHTYELLOW_EX,
'LE': Back.LIGHTBLUE_EX,
'LM': Back.LIGHTMAGENTA_EX,
'LC': Back.LIGHTCYAN_EX,
'LW': Back.LIGHTWHITE_EX,
'BLACK': Back.BLACK,
'RED': Back.RED,
'GREEN': Back.GREEN,
'YELLOW': Back.YELLOW,
'BLUE': Back.BLUE,
'MAGENTA': Back.MAGENTA,
'CYAN': Back.CYAN,
'WHITE': Back.WHITE,
'LIGHT-BLACK': Back.LIGHTBLACK_EX,
'LIGHT-RED': Back.LIGHTRED_EX,
'LIGHT-GREEN': Back.LIGHTGREEN_EX,
'LIGHT-YELLOW': Back.LIGHTYELLOW_EX,
'LIGHT-BLUE': Back.LIGHTBLUE_EX,
'LIGHT-MAGENTA': Back.LIGHTMAGENTA_EX,
'LIGHT-CYAN': Back.LIGHTCYAN_EX,
'LIGHT-WHITE': Back.LIGHTWHITE_EX,
}
all_tags = {}
for i in style, foreground, background:
all_tags.update(i)
ansimarkup-1.4.0/ansimarkup/__init__.py 0000644 0001750 0001750 00000000541 13542427012 020333 0 ustar alastair alastair from . markup import AnsiMarkup, AnsiMarkupError, MismatchedTag, UnbalancedTag
_ansimarkup = AnsiMarkup()
parse = _ansimarkup.parse
strip = _ansimarkup.strip
ansiprint = _ansimarkup.ansiprint
ansistring = _ansimarkup.ansistring
__all__ = 'AnsiMarkup', 'AnsiMarkupError', 'MismatchedTag', 'UnbalancedTag', 'parse', 'strip', 'ansiprint', 'ansistring'
ansimarkup-1.4.0/ansimarkup/__main__.py 0000644 0001750 0001750 00000000607 13542427012 020317 0 ustar alastair alastair from sys import argv, exit
from . import print_function
if len(argv) == 1:
from textwrap import dedent
usage = '''
Usage: python -m ansimarkup [ ...]
Example usage:
python -m ansimarkup 'Bold' 'Red'
python -m ansimarkup 'Bold Red'
'''
print(dedent(usage).strip())
exit(0)
else:
print_function(*argv[1:])
ansimarkup-1.4.0/README.rst 0000644 0001750 0001750 00000020273 13542427012 015543 0 ustar alastair alastair Ansimarkup
==========
.. class:: no-web no-pdf
|pypi| |build| |license|
Ansimarkup is an XML-like markup for producing colored terminal text.
.. code-block:: python
from ansimarkup import ansiprint as print
print("bold text"))
print("red text", "red text on a green background")
print("orange text")
Installation
------------
The latest stable version of ansimarkup can be installed from pypi:
.. code-block:: bash
$ pip install ansimarkup
Usage
-----
Basic
~~~~~
.. code-block:: python
from ansimarkup import parse, ansiprint
# parse() converts the tags to the corresponding ansi escape sequence.
parse("bold dim")
# ansiprint() works exactly like print(), but first runs parse() on all arguments.
ansiprint("bold", "dim")
ansiprint("bold", "dim", sep=":", file=sys.stderr)
Colors and styles
~~~~~~~~~~~~~~~~~
.. code-block:: python
# Colors may be specified in one of several ways.
parse("red foreground")
parse("red background")
parse("red foreground")
parse("red background")
# Xterm, hex and rgb colors are accepted by the and tags.
parse("aquamarine foreground")
parse("dark blue background")
parse("dark green foreground")
# Tags may be nested.
parse("red text on a yellow foreground")
# The above may be more concisely written as:
parse("red text on a yellow background")
# This shorthand also supports style tags.
parse("bold red text on a yellow background")
parse("bold red text")
parse("bold regular text on a yellow background")
# Unrecognized tags are left as-is.
parse("")
For a list of markup tags, please refer to `tags.py`_.
User-defined tags
~~~~~~~~~~~~~~~~~
Custom tags or overrides for existing tags may be defined by creating a new
``AnsiMarkup`` instance:
.. code-block:: python
from ansimarkup import AnsiMarkup, parse
user_tags = {
# Add a new tag (e.g. we want to expand to "").
"info": parse("")
# The ansi escape sequence can be used directly.
"info": "e\x1b[32m\x1b[1m",
# Tag names may also be callables.
"err": lambda: parse("")
# Colors may also be given convenient tag names.
"orange": parse(""),
# User-defined tags always take precedence over existing tags.
"bold": parse("")
}
am = AnsiMarkup(tags=user_tags)
am.parse("bold green")
am.ansiprint("red")
# Calling the instance is equivalent to calling its parse method.
am("bold") == am.parse("bold")
Alignment and length
~~~~~~~~~~~~~~~~~~~~
Aligning formatted strings can be challenging because the length of the rendered
string is different that the number of printable characters. Consider this example:
.. code-block:: python
>>> a = '| {:30} |'.format('abc')
>>> b = '| {:30} |'.format(parse('abc'))
>>> print(a, b, sep='\n')
| abc |
| abc |
This can be addressed by using the ``ansistring`` function or the
``AnsiMarkup.string(markup)`` method, which has the following useful properties:
.. code-block:: python
>>> s = ansistring('abc')
>>> print(repr(s), '->', s)
abc -> abc # abc is printed in bold
>>> len(s), len(am.parse('abc'), s.delta
3, 11, 8
With the help of the ``delta`` property, it is easy to align the strings in the
above example:
.. code-block:: python
>>> s = ansistring('abc')
>>> a = '| {:{width}} |'.format('abc', width=30)
>>> b = '| {:{width}} |'.format(s, width=(30 + s.delta))
>>> print(a, b, sep='\n')
| abc |
| abc |
Other features
~~~~~~~~~~~~~~
The default tag separators can be changed by passing the ``tag_sep`` argument to
``AnsiMarkup``:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(tag_sep="{}")
am.parse("{b}{r}bold red{/b}{/r}")
Markup tags can be removed using the ``strip()`` method:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup()
am.strip("bold red")
The ``strict`` option instructs the parser to raise ``MismatchedTag`` if opening
tags don't have corresponding closing tags:
.. code-block:: python
from ansimarkup import AnsiMarkup
am = AnsiMarkup(strict=True)
am.parse("bold red")
# ansimarkup.MismatchedTag: opening tag "" has no corresponding closing tag
Command-line
~~~~~~~~~~~~
Ansimarkup may also be used on the command-line. This works as if all
arguments were passed to ``ansiprint()``::
$ python -m ansimarkup "bold" "red"
Logging formatter
~~~~~~~~~~~~~~~~~
Ansimarkup also comes with a formatter for the standard library `logging`
module. It can be used as:
.. code-block:: python
import logging
from ansimarkup.logformatter import AnsiMarkupFormatter
log = logging.getLogger()
hdl = logging.StreamHandler()
fmt = AnsiMarkupFormatter()
hdl.setFormatter(fmt)
log.addHandler(hdl)
log.info("bold text")
Windows
~~~~~~~
Ansimarkup uses the colorama_ library internally, which means that Windows
support for ansi escape sequences is available by first running:
.. code-block:: python
import colorama
colorama.init()
For more information on Windows support, consult the "Usage" section of the
colorama_ documentation.
Performance
-----------
While the focus of ansimarkup is convenience, it does try to keep processing to
a minimum. The `benchmark.py`_ script attempts to benchmark different ansi
escape code libraries::
Benchmark 1: red bold
colorama 0.2998 μs
termcolor 3.2339 μs
colr 3.6483 μs
ansimarkup 6.8679 μs
pastel 28.8538 μs
plumbum 53.5004 μs
Benchmark 2: red boldredbold
colorama 0.8269 μs
termcolor 8.9296 μs
ansimarkup 9.3099 μs
colr 9.6244 μs
pastel 62.2018 μs
plumbum 120.8048 μs
Limitations
-----------
Ansimarkup is a simple wrapper around colorama. It does very little in the way
of validating that markup strings are well-formed. This is a conscious decision
with the goal of keeping things simple and fast.
Unbalanced nesting, such as in the following example, will produce incorrect
output::
12
Todo
----
- Many corner cases remain to be fixed.
- More elaborate testing. The current test suite mostly covers the
"happy paths".
- Replace ``tag_list.index`` in ``sub_end`` with something more
efficient (i.e. something like an ordered MultiDict).
Similar libraries
-----------------
- pastel_: bring colors to your terminal
- `plumbum.colors`_: small yet feature-rich library for shell script-like programs in Python
- colr_: easy terminal colors, with chainable methods
License
-------
Ansimarkup is released under the terms of the `Revised BSD License`_.
.. |pypi| image:: https://img.shields.io/pypi/v/ansimarkup.svg?style=flat-square&label=latest%20stable%20version
:target: https://pypi.python.org/pypi/ansimarkup
:alt: Latest version released on PyPi
.. |license| image:: https://img.shields.io/pypi/l/ansimarkup.svg?style=flat-square&label=license
:target: https://pypi.python.org/pypi/ansimarkup
:alt: BSD 3-Clause
.. |build| image:: https://img.shields.io/travis/gvalkov/python-ansimarkup/master.svg?style=flat-square&label=build
:target: http://travis-ci.org/gvalkov/python-ansimarkup
:alt: Build status
.. _tags.py: https://github.com/gvalkov/python-ansimarkup/blob/master/ansimarkup/tags.py
.. _benchmark.py: https://github.com/gvalkov/python-ansimarkup/blob/master/tests/benchmark.py
.. _colorama: https://pypi.python.org/pypi/colorama
.. _pastel: https://github.com/sdispater/pastel
.. _plumbum.colors: https://plumbum.readthedocs.io/en/latest/cli.html#colors
.. _colr: https://pypi.python.org/pypi/Colr/
.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-ansimarkup/master/LICENSE