├── .github └── workflows │ ├── lint.yml │ ├── publish_compiled.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── DESCRIPTION.rst ├── LICENSE ├── Makefile ├── README.md ├── dependencies └── pip-22.2.2-py3-none-any.whl ├── docs ├── Makefile ├── requirements.txt └── source │ ├── changelog.rst │ ├── conf.py │ ├── images │ └── example_logs.png │ ├── index.rst │ └── topics │ ├── introduction.rst │ └── usage.rst ├── pyproject.toml ├── src └── log_color │ ├── __init__.py │ ├── colors.py │ ├── formatters.py │ ├── lib.py │ ├── py.typed │ └── regex.py └── tests ├── __init__.py ├── test_colors.py ├── test_formatters.py ├── test_imports.py └── test_regexes.py /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_call: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: ["3.7","3.8","3.9","3.10"] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install build wheel 29 | python -m pip install .[dev] 30 | - name: Typecheck with Mypy 31 | run: | 32 | python -m mypy src/ 33 | - name: Check code formatting 34 | run: | 35 | python -m black --check -l 120 src/ tests/ 36 | - name: Check import sorting 37 | run: | 38 | python -m isort --check-only 120 src/ tests/ 39 | -------------------------------------------------------------------------------- /.github/workflows/publish_compiled.yml: -------------------------------------------------------------------------------- 1 | name: publish-compiled-wheels 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | workflow_dispatch: 8 | branches: [ main, master, support/1.0, support/1.1, support/2.0 ] 9 | 10 | jobs: 11 | mac-and-windows-and-linux-publish: 12 | runs-on: ${{ matrix.builds.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | builds: [ 17 | {os: "macOS-latest", python_requires: ">=3.7.0"}, 18 | {os: "windows-latest", python_requires: ">=3.7.0"}, 19 | {os: "ubuntu-latest", python_requires: ">=3.7.0,<3.11.0"} 20 | ] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: 3.8 27 | - name: Install deps 28 | run: python -m pip install cibuildwheel==2.9.0 twine==4.0.1 29 | - name: Build compiled wheels 30 | env: 31 | CIBW_PROJECT_REQUIRES_PYTHON: ${{ matrix.builds.python_requires }} 32 | CIBW_BUILD: "cp3*" 33 | PYTHON_MYPY_COMPILE: "true" 34 | run: python -m cibuildwheel --output-dir wheelhouse 35 | - name: Publish compiled wheels 36 | env: 37 | TWINE_USERNAME: __token__ 38 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 39 | run: | 40 | twine upload --skip-existing wheelhouse/* 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_call: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | python-version: ["3.7","3.8","3.9","3.10","3.11"] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v3 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install build wheel 29 | python -m pip install .[dev,types,build] 30 | - name: Run the unittests 31 | run: | 32 | python -m unittest discover --start-directory tests --verbose --locals 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pyre/* 2 | .pyre_configuration 3 | .watchmanconfig 4 | .idea/* 5 | \#*\# 6 | *~ 7 | .coverage 8 | coverage.xml 9 | docs/build/ 10 | pep8.out 11 | test_results.xml 12 | __pycache__/ 13 | *.py[cod] 14 | build/* 15 | dist/* 16 | *egg-info* 17 | RPMS 18 | SOURCES 19 | BUILDROOT 20 | .vagrant 21 | .env 22 | .env_* 23 | .tox 24 | .env* 25 | wheelhouse 26 | wheelhouse/* 27 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: docs/requirements.txt 30 | -------------------------------------------------------------------------------- /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | ``log_color`` provides Logging formatters for colorized outputs. A very simple domain specific language (DSL) is used 5 | to colorize all or part of a particular log message. For example:: 6 | 7 | LOG.debug('Found file at #g<%s>', path) 8 | 9 | The above would colorize the contents of the 'path' variable green when output to a command line terminal.\n 10 | 11 | A formatter which strips color sequences from the output is also included for situations like logging to files where 12 | having ANSI color sequences embedded in the output would not make sense. 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | Copyright © 2016 Brant Watson 4 | This work is free. You can redistribute it and/or modify it under the 5 | terms of the Do What The Fuck You Want To Public License, Version 2, 6 | as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. 7 | 8 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 9 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 10 | 0. You just DO WHAT THE FUCK YOU WANT TO. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Select Python 3.9 or 3.10 (or error out) 2 | ifneq (, $(shell command -v python3.9)) 3 | PYTHON=python3.9 4 | else ifneq (, $(shell command -v python3.10)) 5 | PYTHON=python3.10 6 | else ifneq (, $(shell command -v python3.11)) 7 | PYTHON=python3.11 8 | else ifneq (, $(shell command -v python3.12)) 9 | PYTHON=python3.12 10 | else ifneq (, $(shell command -v python3.13)) 11 | PYTHON=python3.13 12 | else 13 | $(error "No Python version 3.9, 3.10, 3.11, 3.12, or 3.13 found on: $(PATH)") 14 | endif 15 | 16 | ENV_DIR=.env_$(PYTHON) 17 | 18 | define linebreak 19 | 20 | 21 | endef 22 | 23 | ifeq ($(OS),Windows_NT) 24 | IN_ENV=. $(ENV_DIR)/Scripts/activate && 25 | else 26 | IN_ENV=. $(ENV_DIR)/bin/activate && 27 | endif 28 | 29 | MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) 30 | MAKEFILE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH))) 31 | 32 | PIP_WHL=dependencies/pip-22.2.2-py3-none-any.whl 33 | PIP_CMD=$(PYTHON) $(PIP_WHL)/pip 34 | 35 | .PHONY: all 36 | all: test format-code docs artifacts 37 | 38 | env: $(ENV_DIR) 39 | 40 | .PHONY: qt 41 | qt: 42 | $(IN_ENV) python -m unittest discover --start-directory tests --verbose -b 43 | 44 | .PHONY: test 45 | test: build check-code mypy qt 46 | 47 | .PHONY: artifacts 48 | artifacts: build-reqs sdist wheel 49 | 50 | $(ENV_DIR): 51 | $(PYTHON) -m venv $(ENV_DIR) 52 | 53 | .PHONY: build-reqs 54 | build-reqs: env 55 | $(IN_ENV) $(PIP_CMD) install build 56 | 57 | .PHONY: build 58 | build: env 59 | $(IN_ENV) $(PIP_CMD) install -e .[dev,docs] 60 | 61 | .PHONY: sdist 62 | sdist: build-reqs 63 | $(IN_ENV) $(PYTHON) -m build --sdist 64 | 65 | .PHONY: wheel 66 | wheel: build-reqs 67 | $(IN_ENV) $(PYTHON) -m build --wheel 68 | 69 | .PHONY: format-code 70 | format-code: 71 | $(IN_ENV) black src/ tests/ docs/source/conf.py 72 | $(IN_ENV) isort src/ tests/ docs/source/conf.py 73 | 74 | .PHONY: check-code 75 | check-code: 76 | $(IN_ENV) black --check src/ tests/ docs/source/conf.py 77 | $(IN_ENV) isort --check-only src/ tests/ docs/source/conf.py 78 | 79 | .PHONY: docs 80 | docs: build-reqs 81 | $(IN_ENV) $(MAKE) -C docs html 82 | 83 | .PHONY: publish 84 | publish: 85 | $(IN_ENV) twine upload dist/* 86 | 87 | # Static Analysis 88 | 89 | .PHONY: mypy 90 | mypy: 91 | $(IN_ENV) mypy src/ 92 | 93 | .PHONY: freeze 94 | freeze: env 95 | - $(IN_ENV) pip freeze 96 | 97 | .PHONY: shell 98 | shell: env 99 | - $(IN_ENV) $(PYTHON) 100 | 101 | .PHONY: clean 102 | clean: 103 | - @rm -rf BUILD 104 | - @rm -rf BUILDROOT 105 | - @rm -rf RPMS 106 | - @rm -rf SRPMS 107 | - @rm -rf SOURCES 108 | - @rm -rf docs/build 109 | - @rm -rf src/*.egg-info 110 | - @rm -rf build 111 | - @rm -rf dist 112 | - @rm -f .coverage 113 | - @rm -f test_results.xml 114 | - @rm -f coverage.xml 115 | - @rm -f tests/coverage.xml 116 | - @rm -f pep8.out 117 | - @find . -name '*.orig' -delete 118 | - @find . -name '*.DS_Store' -delete 119 | - @find . -name '*.pyc' -delete 120 | - @find . -name '*.pyd' -delete 121 | - @find . -name '*.pyo' -delete 122 | - @find . -name '*__pycache__*' -delete 123 | 124 | .PHONY: env-clean 125 | env-clean: clean 126 | - @rm -rf .env* 127 | - @git clean -dfX 128 | - @rm -rf $(ENV_DIR) 129 | - @rm -rf .tox 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogColor 2 | [![Stable Version](https://img.shields.io/pypi/v/log-color?color=blue)](https://pypi.org/project/log-color/) 3 | [![Documentation Status](https://readthedocs.org/projects/log-color/badge/?version=latest)](https://log-color.readthedocs.io/en/latest/?badge=latest) 4 | [![Downloads](https://img.shields.io/pypi/dm/log-color)](https://pypistats.org/packages/log-color) 5 | ![Test](https://github.com/induane/logcolor/actions/workflows/test.yml/badge.svg) ![Lint](https://github.com/induane/logcolor/actions/workflows/lint.yml/badge.svg) 6 | [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) 7 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 8 | 9 | When making command line interfaces, it's often useful to colorize the output to emphasize salient pieces of 10 | information or otherwise enhance the user experience. Unfortunately it's quite cumbersome to add colorized outputs to 11 | Python log messages. 12 | 13 | ## ColorFormatter 14 | 15 | The ColorFormatter is a logging formatter that parses your log messages and adds color codes to the log messages. 16 | 17 | ![example](https://raw.githubusercontent.com/induane/logcolor/master/docs/source/images/example_logs.png) 18 | 19 | ## ColorStripper 20 | 21 | The ColorStripper formatter is the inverse of the ColorFormatter. It strips the color information from your messages so 22 | that you can log to a file without it being full of color codes. 23 | 24 | ## Installation 25 | I'm on pypi! 26 | 27 | ``` 28 | $ pip install log_color 29 | ``` 30 | 31 | ## Features 32 | 33 | - Simple to use 34 | - No external dependencies 35 | - Compatibility with Python 3.7+, PyPy 36 | - Fast! Compiled binaries are available for some systems! 37 | 38 | ## http://no-color.org/ 39 | LogColor honors the ``NO_COLOR`` environment variable. 40 | -------------------------------------------------------------------------------- /dependencies/pip-22.2.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/induane/logcolor/d95ccbf80d9bb4faed4eb1c6af5eed1fb03c7b14/dependencies/pip-22.2.2-py3-none-any.whl -------------------------------------------------------------------------------- /docs/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/logcolor.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/logcolor.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/logcolor" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/logcolor" 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 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_rtd_theme 2 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 2.0.0 5 | ----- 6 | - Dropped support for Python 3.6 (due to setuptools incompatibility) 7 | - Added the ability to precompile some binaries using ``mypyc`` 8 | - Added ``logcolor.lib`` module and moved some functionality there (hence major version number change) 9 | 10 | 1.1.0 11 | ----- 12 | - Dropped setup.cfg entirely 13 | - Added ``pyre`` type checking 14 | - Added ``py.typed`` to package data 15 | - Fixed conf.py location in ``.readthedocs.yaml`` to fix documentation build issues 16 | 17 | 1.0.8 18 | ----- 19 | - Modernized build system and avoid invoking ``setup.py`` directly 20 | - Added a ``qt`` Makefile target for quickly invoking the unittest suite 21 | - Added ``mypy`` type checking 22 | - Dropped support for Python2.x 23 | - Started using support branch pattern 24 | - Removed compat module (was for Python 2.x compatibility) 25 | - Removed old-style super calls 26 | - Use more f-strings 27 | - Removed custom assertion TestCase class (was for Python 2.x compatibility) 28 | - Some minor documentation updates 29 | - Added ``pyproject.toml`` 30 | - Added ``setup.cfg`` 31 | - Added unittest that verifies standard logs can be formatted by passing args 32 | 33 | 1.0.7 34 | ----- 35 | - Added automatic code formatting with ``black`` 36 | - Dropped support for Python 2.6 37 | - Use vanilla python -m unittest instead of third party testrunner ``nosetests`` 38 | because it is no longer maintained and it's simpler to not have a third 39 | party test runner in use 40 | - Simplified code formatting logic 41 | - Some minor documentation updates 42 | - WARNING: This is the last release that will support Python 2.x 43 | - Updated ``setup.py`` metadata to indicate which versions of Python are 44 | supported by ``log_color`` 45 | 46 | 1.0.6 47 | ----- 48 | - Added support for darker colors 49 | 50 | 1.0.5 51 | ----- 52 | - Honor the NO_COLOR environment variable 53 | 54 | 1.0.4 55 | ----- 56 | - Fixed a bug with logging non text objects 57 | 58 | 1.0.3 59 | ----- 60 | - Improved cross platform testing with tox 61 | - Added base test assertions for use in multiple 62 | versions of Python 63 | 64 | 1.0.2 65 | ----- 66 | - Updated README.md with a nice picture 67 | - Added publish option to Makefile 68 | - Fixed an import and coverage package name 69 | 70 | 1.0.1 71 | ----- 72 | - Added licence information 73 | 74 | 1.0.0 75 | ----- 76 | - Base loggers implemented 77 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | import datetime 3 | 4 | import sphinx_rtd_theme 5 | 6 | # ---------------------------------------------------------------------------- 7 | # General Settings 8 | # ---------------------------------------------------------------------------- 9 | extensions = ["sphinx.ext.autodoc"] 10 | templates_path = ["_templates"] 11 | source_suffix = ".rst" 12 | master_doc = "index" 13 | project = "log_color" 14 | copyright = f"2017 - {datetime.datetime.now().year}, Brant Watson" 15 | version = "1.0.7" 16 | release = "0" 17 | exclude_patterns = [] 18 | pygments_style = "sphinx" 19 | 20 | # ---------------------------------------------------------------------------- 21 | # HTML Settings 22 | # ---------------------------------------------------------------------------- 23 | html_theme = "sphinx_rtd_theme" 24 | html_show_copyright = True 25 | htmlhelp_basename = "logcolordoc" 26 | -------------------------------------------------------------------------------- /docs/source/images/example_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/induane/logcolor/d95ccbf80d9bb4faed4eb1c6af5eed1fb03c7b14/docs/source/images/example_logs.png -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. logcolor documentation master file, created by 2 | sphinx-quickstart on Tue Jan 27 12:04:31 2015. 3 | 4 | Welcome to Log Color's documentation! 5 | ===================================== 6 | The ``log_color`` module provides Python logging formatters with a basic markup syntax for nicely colored log outputs. 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | topics/introduction 14 | topics/usage 15 | changelog 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/source/topics/introduction.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | From the project root, you can run the following commands: 4 | 5 | * ``make test``: Run unit tests 6 | * ``make artifacts``: Build all artifacts (sdist, wheel, etc... ) 7 | * ``make sdist``: Build a Python sdist 8 | * ``make wheel``: Build a Python wheel 9 | * ``make docs``: Build the HTML documentation 10 | * ``make format-code``: Format the code with ``black`` 11 | * ``make publish``: Publish artifacts in dist folder to Pypi 12 | * ``make freeze``: List packages installed in base virtual environment 13 | * ``make clean``: Remove all build files 14 | * ``make env-clean``: Remove all build files and virtual environments 15 | -------------------------------------------------------------------------------- /docs/source/topics/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | .. _color_language: 4 | 5 | Color Language 6 | -------------- 7 | In order to colorize a word or words, the ``ColorFormatter`` implements a *DSL* or **D**\ omain **S**\ pecific **L**\ anguage. 8 | 9 | Colorizing a string of text automatically can be done by using the following as a schemata: ``#x`` where ``x`` is 10 | the lowercase first letter of the color you want the text to appear in the terminal and ``text`` is the string of 11 | characters to be colored. For example, to color a string blue:: 12 | 13 | "#b" 14 | 15 | This can be done on a substring leaving the rest of the words untouched. In the following example only the word 16 | ``green`` would be colored, the rest of the text would appear in the terminals default color:: 17 | 18 | "This text has some #g words. Okay, just one actually." 19 | 20 | .. tip:: Automatic coloring of text occurs only within the context of a log message. No other string values are handled. 21 | 22 | Here is a more complete example of a log message with colored text; in this 23 | example, the *path* substring would be colored green:: 24 | 25 | LOG.debug("Processing asset #g<%s>", path) 26 | 27 | The same example but with Python3-only f-strings:: 28 | 29 | LOG.debug(f"Processing asset #g<{path}>") 30 | 31 | In the above examples the value of the variable *path* would be printed as green text to the console. 32 | 33 | **Logging Tip**\ : 34 | 35 | Do not use f-strings in log messages. The format-pointer style with arguments passed directly to the log handler is the 36 | preferred method for logging because it will avoid formatting the strings if the log message will not be omitted. 37 | 38 | Bad: 39 | 40 | ``LOG.debug("Foo %s" % bar)`` 41 | 42 | ``LOG.debug("Foo {}".format(bar))`` 43 | 44 | ``LOG.debug(f"Foo {bar}")`` 45 | 46 | Good: 47 | 48 | ``LOG.debug("Foo %s", bar)`` 49 | 50 | .. tip:: The difference between the *incorrect* ``LOG.debug("Foo %s" % bar)`` and the correct ``LOG.debug("Foo %s", bar)`` 51 | is that the good one doesn't use the ``%`` operator, but instead passes the format variables to the log handler 52 | as arguments. 53 | 54 | Supported Colors 55 | ^^^^^^^^^^^^^^^^ 56 | Only a few colors are supported by the ``ColoredFormatter``, but they should be 57 | sufficient for most purposes. 58 | 59 | - ``b``\ —``#b`` 60 | - ``c``\ —``#b`` 61 | - ``g``\ —``#b`` 62 | - ``m``\ —``#b`` 63 | - ``r``\ —``#b`` 64 | - ``w``\ —``#b`` 65 | - ``y``\ —``#b`` 66 | - ``db``\ —``#b`` 67 | - ``dc``\ —``#b`` 68 | - ``dg``\ —``#b`` 69 | - ``dm``\ —``#b`` 70 | - ``dr``\ —``#b`` 71 | - ``dy``\ —``#b`` 72 | 73 | 74 | Color Stripping 75 | ^^^^^^^^^^^^^^^ 76 | If you're using the ``#b`` notation, you probably realize that this means that if you log to a file, you'll end 77 | up with text that looks more like ``\033[94mblue\033[0m`` since it contains the color escape codes. Or, alternatively, 78 | if you use a different formatter, you'll have the literal ``#b`` string in the text. Neither of these are ideal. 79 | To that end, another formatter is provided called: ``ColorStripper``. The purpose of this is to remove any escape codes 80 | and/or :ref:`color_language` references from the text passing through the formatter. 81 | 82 | This allows you to easily write plain text to files or other log handlers, while still using colored output for a 83 | console logger. 84 | 85 | Configuration 86 | ------------- 87 | Configuration is very simple. Simply import the ``ColorFormatter`` and use it like you would any other log formatter. 88 | 89 | Simple Example:: 90 | 91 | import logging 92 | from log_color import ColorFormatter 93 | 94 | handler = logging.StreamHandler() 95 | formatter = ColorFormatter("%(levelname)s: %(message)s") 96 | handler.setFormatter(formatter) 97 | 98 | log = logging.getLogger('color_log') 99 | log.setLevel(logging.INFO) 100 | log.addHandler(handler) 101 | log.info('Look, #b text!') 102 | 103 | 104 | Here is a DictConfig (that also uses the ColorStripper formatter for a file 105 | handler):: 106 | 107 | from logging.config import dictConfig 108 | from log_color import ColorFormatter, ColorStripper 109 | 110 | BASE_CONFIG = { 111 | 'version': 1, 112 | 'disable_existing_loggers': False, 113 | 'formatters': { 114 | "ConsoleFormatter": { 115 | '()': ColorFormatter, 116 | "format": "%(levelname)s: %(message)s", 117 | }, 118 | "FileFormatter": { 119 | '()': ColorStripper, 120 | "format": ("%(levelname)-8s: %(asctime)s '%(message)s' " 121 | "%(name)s:%(lineno)s"), 122 | "datefmt": "%Y-%m-%d %H:%M:%S", 123 | }, 124 | }, 125 | 'handlers': { 126 | "console": { 127 | "level": "DEBUG", 128 | "class": "logging.StreamHandler", 129 | "formatter": "ConsoleFormatter", 130 | }, 131 | "filehandler": { 132 | 'level': "DEBUG", 133 | 'class': 'logging.handlers.RotatingFileHandler', 134 | 'filename': "/tmp/logfile", 135 | 'formatter': 'FileFormatter', 136 | }, 137 | }, 138 | 'loggers': { 139 | 'my_script': { 140 | 'handlers': ["console", "filehandler"], 141 | 'level': 'INFO', 142 | }, 143 | } 144 | } 145 | dictConfig(BASE_CONFIG) 146 | 147 | 148 | Troubleshooting 149 | --------------- 150 | 151 | Output Not Colorized 152 | ^^^^^^^^^^^^^^^^^^^^ 153 | 154 | There are a couple of things to check for: 155 | 156 | 1. If you're running on Windows, colorized output is *not* supported. 157 | 2. If you're in a posix terminal and ANSI color codes are supported but you're not 158 | seeing colorized output, check for the ``NO_COLOR`` environment variable. See 159 | `no-color.org `_ for more information on this standard. If the ``NO_COLOR`` environment 160 | variable is set, colorized output is automatically suppressed. 161 | 3. Something else: It could be the case that the detection scheme that ``log_color`` ships is failing to detect that 162 | your particular terminal supports ANSI color codes. Detection of color support could be offloaded to a third party 163 | library, but one of the goals of ``log_color`` is to have no dependencies. Color support detection isn't 164 | standardized and most libraries that do color support detection employ reaching out to ncurses or including a huge 165 | array of information about very specific (and often obscure) terminals. I'm willing to include support for specific 166 | terminals if someone wishes to add them, but I'm not going to default to doing all of that work if no one is using 167 | them anyway. Color detection support can be found in ``src/log_color/colors.py`` in the ColorStr class. The 168 | ``color_supported`` method handles detection so feel free to add it there. 169 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "build", 4 | "mypy", 5 | "setuptools >= 65.3.0", 6 | "setuptools_scm[toml]", 7 | "wheel", 8 | ] 9 | build-backend = "setuptools.build_meta" 10 | 11 | [project] 12 | name = "log_color" 13 | description = "Simple log formatters for colored output" 14 | dynamic = ['version'] 15 | authors = [ 16 | { "name" = "Brant Watson", "email" = "oldspiceap@gmail.com" }, 17 | ] 18 | license = {file = "LICENSE"} 19 | keywords = ["logging", "color", "formatter", ] 20 | readme = "README.md" 21 | classifiers = [ 22 | "Intended Audience :: Developers", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3.6", 26 | "Programming Language :: Python :: 3.7", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3.12", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | "Typing :: Typed", 34 | ] 35 | requires-python = ">=3.6" 36 | dependencies = [] 37 | 38 | [project.urls] 39 | homepage = "https://github.com/induane/logcolor" 40 | documentation = "http://log-color.readthedocs.io/en/latest/" 41 | repository = "https://github.com/induane/logcolor" 42 | changelog = "https://github.com/induane/logcolor/blob/master/docs/source/changelog.rst" 43 | 44 | [tool.setuptools.package-data] 45 | "log_color" = [ "py.typed",] 46 | 47 | [project.optional-dependencies] 48 | dev = [ 49 | "black", 50 | "isort", 51 | "mypy", 52 | "pyre-check", 53 | "twine", 54 | ] 55 | docs = [ 56 | "sphinx", 57 | "sphinx_rtd_theme", 58 | ] 59 | 60 | [tool.setuptools_scm] 61 | root = "." 62 | 63 | [tool.vermin] 64 | setuptools_scm = true 65 | 66 | [tool.isort] 67 | profile = "black" 68 | line_length = 120 69 | 70 | [tool.black] 71 | line-length = 120 72 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] 73 | include = '\.pyi?$' 74 | exclude = ''' 75 | /( 76 | \.eggs 77 | | \.git 78 | | \.hg 79 | | \.mypy_cache 80 | | \.venv 81 | | _build 82 | | buck-out 83 | | build 84 | | dist 85 | )/ 86 | ''' 87 | 88 | [tool.mypy] 89 | check_untyped_defs = true 90 | disallow_untyped_decorators = true 91 | disallow_untyped_defs = true 92 | warn_return_any = true 93 | show_error_codes = true 94 | warn_unused_ignores = true 95 | namespace_packages = true 96 | warn_redundant_casts = true 97 | warn_no_return = true 98 | warn_unreachable = true 99 | pretty = true 100 | show_error_context = true 101 | # Mypy ignores hidden directories but it needs scan __pycache__ for .pyc and pyi files, so it cannot honor gitignore. 102 | exclude = [ 103 | '''^(?:.*\/)+[tT]ests?''', 104 | 'venv/', 105 | 'build/', 106 | '.env_python3/', 107 | '.env_python3.6/', 108 | '.env_python3.7/', 109 | '.env_python3.8/', 110 | '.env_python3.9/', 111 | '.env_python3.10/', 112 | '.env_python3.11/', 113 | '.env_python3.12/', 114 | ] 115 | -------------------------------------------------------------------------------- /src/log_color/__init__.py: -------------------------------------------------------------------------------- 1 | from log_color.formatters import ColorFormatter, ColorStripper 2 | 3 | __all__ = ("ColorFormatter", "ColorStripper") 4 | -------------------------------------------------------------------------------- /src/log_color/colors.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sys 4 | from functools import lru_cache 5 | from typing import Optional 6 | 7 | from .lib import COLOR_END 8 | 9 | 10 | class ColorStr(str): 11 | """Subclasses string to optionally include ascii color""" 12 | 13 | def __new__(cls, value: str, color: str, force_seq: Optional[bool] = None) -> "ColorStr": 14 | if cls.color_supported(force_seq=force_seq): 15 | return str.__new__(cls, f"{color}{value}{COLOR_END}") 16 | return str.__new__(cls, value) 17 | 18 | @staticmethod 19 | @lru_cache(maxsize=None) 20 | def color_supported(force_seq: Optional[bool] = None) -> bool: 21 | """Shoddy detection of color support.""" 22 | # If True or False, override autodetection and return. 23 | if force_seq is False or force_seq is True: 24 | return force_seq 25 | 26 | environ_get = os.environ.get # Stash this set of lookups 27 | 28 | # Honor NO_COLOR environment variable: 29 | if environ_get("NO_COLOR", None): 30 | return False 31 | 32 | # Check CLICOLOR environment variable: 33 | if environ_get("CLICOLOR", None) == "0": 34 | return False 35 | 36 | # Check CLICOLOR_FORCE environment variable: 37 | CLICOLOR_FORCE = environ_get("CLICOLOR_FORCE", None) 38 | if CLICOLOR_FORCE and CLICOLOR_FORCE != "0": 39 | return True 40 | 41 | # Attempt simple autodetection 42 | if (hasattr(sys.stderr, "isatty") and sys.stderr.isatty()) or ( 43 | "TERM" in os.environ and os.environ["TERM"] == "ANSI" 44 | ): 45 | if "windows" in platform.system().lower() and not ("TERM" in os.environ and os.environ["TERM"] == "ANSI"): 46 | return False # Windows console, no ANSI support 47 | else: 48 | return True # ANSI output allowed 49 | else: 50 | return False # ANSI output not allowed 51 | -------------------------------------------------------------------------------- /src/log_color/formatters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging import LogRecord 3 | 4 | from .colors import ColorStr 5 | from .lib import COLOR_MAP, multi_replace, strip_color 6 | from .regex import COLOR_EXP 7 | 8 | 9 | class ColorStripper(logging.Formatter): 10 | """ 11 | Strip references to colored output from message. 12 | 13 | This formatter removes all color code text from a message. For example 14 | it would convert ``this #g text`` to ``this is text``. 15 | """ 16 | 17 | def format(self, record: LogRecord) -> str: 18 | """Format the message.""" 19 | msg = super().format(record) 20 | msg = strip_color(msg) 21 | for val in COLOR_EXP.findall(msg): 22 | if val.startswith("#d"): 23 | msg = msg.replace(val, val[4:-1]) 24 | else: 25 | msg = msg.replace(val, val[3:-1]) 26 | return msg 27 | 28 | 29 | class ColorFormatter(logging.Formatter): 30 | """ 31 | Insert color sequences for logging messages. 32 | 33 | Convert all portions of a message that contain color codes into a ColorStr instance that matches the color map data 34 | then re-insert it into the message. On supported platforms this will insert a color codes around the specified 35 | text. 36 | 37 | Color sequences should be in the following format: #y which will be formatted as yellow. The colors 38 | allowed are: 39 | 40 | - m (magenta) 41 | - b (blue) 42 | - c (cyan) 43 | - g (green) 44 | - y (yellow) 45 | - r (red) 46 | - w (white) 47 | 48 | - dm (dark magenta) 49 | - db (dark blue) 50 | - dc (dark cyan) 51 | - dg (dark green) 52 | - dy (dark yellow) 53 | - dr (dark red) 54 | """ 55 | 56 | def format(self, record: LogRecord) -> str: 57 | """Format the message.""" 58 | msg = super().format(record) 59 | replace_map = [] 60 | for val in COLOR_EXP.findall(msg): 61 | if val.startswith("#d"): 62 | replace_map.append((val, ColorStr(val[4:-1], COLOR_MAP[val[1:3]]))) 63 | else: 64 | replace_map.append((val, ColorStr(val[3:-1], COLOR_MAP[val[1]]))) 65 | msg = multi_replace(msg, replace_map) 66 | return msg 67 | -------------------------------------------------------------------------------- /src/log_color/lib.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import lru_cache 3 | from typing import Dict, FrozenSet, List, Match, Sequence, Tuple 4 | 5 | SUBMAP_TYPE = Sequence[Tuple[str, str]] 6 | """Substitution mapping type.""" 7 | 8 | COLOR_MAP: Dict[str, str] = { 9 | "b": "\033[94m", 10 | "c": "\033[96m", 11 | "g": "\033[92m", 12 | "m": "\033[95m", 13 | "r": "\033[91m", 14 | "y": "\033[93m", 15 | "w": "\033[97m", 16 | "db": "\033[34m", 17 | "dc": "\033[36m", 18 | "dg": "\033[32m", 19 | "dm": "\033[35m", 20 | "dr": "\033[31m", 21 | "dy": "\033[33m", 22 | } 23 | 24 | COLOR_END: str = "\033[0m" 25 | # Assemble list of all color sequences 26 | ALL_SEQ: FrozenSet[str] = frozenset([x for x in COLOR_MAP.values()] + [COLOR_END]) 27 | 28 | 29 | class MultiReplace: 30 | """ 31 | MultiReplace is a tool for doing multiple find/replace actions in one pass. 32 | 33 | Given a mapping of values to be replaced it allows for all of the matching values to be replaced in a single pass 34 | which can save a lot of performance on very large strings. In addition to simple replace, it also allows for 35 | replacing based on regular expressions. 36 | 37 | Keyword Arguments: 38 | 39 | :type regex: bool 40 | :param regex: Treat search keys as regular expressions [Default: False] 41 | :type flags: int 42 | :param flags: flags to pass to the regex engine during compile 43 | 44 | Usage:: 45 | 46 | from boltons import stringutils 47 | s = stringutils.MultiReplace(( 48 | ('foo', 'zoo'), 49 | ('cat', 'hat'), 50 | ('bat', 'kraken)' 51 | )) 52 | new = s.sub('The foo bar cat ate a bat') 53 | new == 'The zoo bar hat ate a kraken' 54 | """ 55 | 56 | def __init__(self, sub_map: SUBMAP_TYPE) -> None: 57 | """Compile any regular expressions that have been passed.""" 58 | self.group_map: Dict[str, str] = {} 59 | regex_values: List[str] = [] 60 | 61 | for idx, vals in enumerate(sub_map): 62 | group_name = f"group{idx}" 63 | exp = re.escape(vals[0]) 64 | regex_values.append(f"(?P<{group_name}>{exp})") 65 | self.group_map[group_name] = vals[1] 66 | 67 | self.combined_pattern = re.compile("|".join(regex_values)) 68 | 69 | def _get_value(self, match: Match) -> str: 70 | """Given a match object find replacement value.""" 71 | group_dict = match.groupdict() 72 | key = [x for x in group_dict if group_dict[x]][0] 73 | return self.group_map[key] 74 | 75 | def sub(self, text: str) -> str: 76 | """Run substitutions on the input text.""" 77 | try: 78 | return self.combined_pattern.sub(self._get_value, text) 79 | except IndexError: 80 | # There are no matches so just return the original text 81 | return text 82 | 83 | 84 | def multi_replace(text: str, sub_map: SUBMAP_TYPE) -> str: 85 | """ 86 | Shortcut function to invoke MultiReplace in a single call. 87 | 88 | Example Usage:: 89 | 90 | from boltons.stringutils import multi_replace 91 | new = multi_replace( 92 | 'The foo bar cat ate a bat', 93 | {'foo': 'zoo', 'cat': 'hat', 'bat': 'kraken'} 94 | ) 95 | new == 'The zoo bar hat ate a kraken' 96 | """ 97 | m = MultiReplace(sub_map) 98 | return m.sub(text) 99 | 100 | 101 | @lru_cache(maxsize=None) 102 | def get_strip_map() -> Tuple[Tuple[str, str], ...]: 103 | return tuple((x, "") for x in ALL_SEQ) 104 | 105 | 106 | def strip_color(value: str) -> str: 107 | """Strip all color values from a given string""" 108 | return multi_replace(value, get_strip_map()) 109 | -------------------------------------------------------------------------------- /src/log_color/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/induane/logcolor/d95ccbf80d9bb4faed4eb1c6af5eed1fb03c7b14/src/log_color/py.typed -------------------------------------------------------------------------------- /src/log_color/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | COLOR_EXP = re.compile(r"#d?[mbcgyrw]<.+?>", re.DOTALL) 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/induane/logcolor/d95ccbf80d9bb4faed4eb1c6af5eed1fb03c7b14/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_colors.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from log_color.colors import ColorStr 4 | from log_color.lib import COLOR_MAP, strip_color 5 | 6 | 7 | class TestColorModule(TestCase): 8 | def test_make_str_magenta(self): 9 | """Verify can color strings magenta""" 10 | self.assertEqual(ColorStr("colored", COLOR_MAP["m"], force_seq=True), "\033[95mcolored\033[0m") 11 | 12 | def test_make_str_blue(self): 13 | """Verify can color strings blue""" 14 | self.assertEqual(ColorStr("colored", COLOR_MAP["b"], force_seq=True), "\033[94mcolored\033[0m") 15 | 16 | def test_make_str_green(self): 17 | """Verify can color strings green""" 18 | self.assertEqual(ColorStr("colored", COLOR_MAP["g"], force_seq=True), "\033[92mcolored\033[0m") 19 | 20 | def test_make_str_yellow(self): 21 | """Verify can color strings yellow""" 22 | self.assertEqual(ColorStr("colored", COLOR_MAP["y"], force_seq=True), "\033[93mcolored\033[0m") 23 | 24 | def test_make_str_red(self): 25 | """Verify can color strings red""" 26 | self.assertEqual(ColorStr("colored", COLOR_MAP["r"], force_seq=True), "\033[91mcolored\033[0m") 27 | 28 | def test_make_str_cyan(self): 29 | """Verify can color strings cyan""" 30 | self.assertEqual(ColorStr("colored", COLOR_MAP["c"], force_seq=True), "\033[96mcolored\033[0m") 31 | 32 | def test_make_str_white(self): 33 | """Verify can color strings white""" 34 | self.assertEqual(ColorStr("colored", COLOR_MAP["w"], force_seq=True), "\033[97mcolored\033[0m") 35 | 36 | def test_make_str_dark_magenta(self): 37 | """Verify can color strings dark magenta""" 38 | self.assertEqual(ColorStr("colored", COLOR_MAP["dm"], force_seq=True), "\033[35mcolored\033[0m") 39 | 40 | def test_make_str_dark_blue(self): 41 | """Verify can color strings dark blue""" 42 | self.assertEqual(ColorStr("colored", COLOR_MAP["db"], force_seq=True), "\033[34mcolored\033[0m") 43 | 44 | def test_make_str_dark_green(self): 45 | """Verify can color strings dark green""" 46 | self.assertEqual(ColorStr("colored", COLOR_MAP["dg"], force_seq=True), "\033[32mcolored\033[0m") 47 | 48 | def test_make_str_dark_yellow(self): 49 | """Verify can color strings dark yellow""" 50 | self.assertEqual(ColorStr("colored", COLOR_MAP["dy"], force_seq=True), "\033[33mcolored\033[0m") 51 | 52 | def test_make_str_dark_red(self): 53 | """Verify can color strings dark red""" 54 | self.assertEqual(ColorStr("colored", COLOR_MAP["dr"], force_seq=True), "\033[31mcolored\033[0m") 55 | 56 | def test_make_str_dark_cyan(self): 57 | """Verify can color strings dark cyan""" 58 | self.assertEqual(ColorStr("colored", COLOR_MAP["dc"], force_seq=True), "\033[36mcolored\033[0m") 59 | 60 | def test_make_str_magenta_unsupported(self): 61 | """Verify no color sequences on magenta""" 62 | self.assertEqual(ColorStr("colored", COLOR_MAP["m"], force_seq=False), "colored") 63 | 64 | def test_make_str_blue_unsupported(self): 65 | """Verify no color sequences on blue""" 66 | self.assertEqual(ColorStr("colored", COLOR_MAP["b"], force_seq=False), "colored") 67 | 68 | def test_make_str_green_unsupported(self): 69 | """Verify no color sequences on green""" 70 | self.assertEqual(ColorStr("colored", COLOR_MAP["g"], force_seq=False), "colored") 71 | 72 | def test_make_str_yellow_unsupported(self): 73 | """Verify no color sequences on yellow""" 74 | self.assertEqual(ColorStr("colored", COLOR_MAP["y"], force_seq=False), "colored") 75 | 76 | def test_make_str_red_unsupported(self): 77 | """Verify no color sequences on red""" 78 | self.assertEqual(ColorStr("colored", COLOR_MAP["r"], force_seq=False), "colored") 79 | 80 | def test_make_str_cyan_unsupported(self): 81 | """Verify no color sequences on cyan""" 82 | self.assertEqual(ColorStr("colored", COLOR_MAP["c"], force_seq=False), "colored") 83 | 84 | def test_make_str_white_unsupported(self): 85 | """Verify no color sequences on white""" 86 | self.assertEqual(ColorStr("colored", COLOR_MAP["w"], force_seq=False), "colored") 87 | 88 | def test_make_str_dark_magenta_unsupported(self): 89 | """Verify no color sequences on dark magenta""" 90 | self.assertEqual(ColorStr("colored", COLOR_MAP["dm"], force_seq=False), "colored") 91 | 92 | def test_make_str_dark_blue_unsupported(self): 93 | """Verify no color sequences on dark blue""" 94 | self.assertEqual(ColorStr("colored", COLOR_MAP["db"], force_seq=False), "colored") 95 | 96 | def test_make_str_dark_green_unsupported(self): 97 | """Verify no color sequences on dark green""" 98 | self.assertEqual(ColorStr("colored", COLOR_MAP["dg"], force_seq=False), "colored") 99 | 100 | def test_make_str_dark_yellow_unsupported(self): 101 | """Verify no color sequences on dark yellow""" 102 | self.assertEqual(ColorStr("colored", COLOR_MAP["dy"], force_seq=False), "colored") 103 | 104 | def test_make_str_dark_red_unsupported(self): 105 | """Verify no color sequences on dark red""" 106 | self.assertEqual(ColorStr("colored", COLOR_MAP["dr"], force_seq=False), "colored") 107 | 108 | def test_make_str_dark_cyan_unsupported(self): 109 | """Verify no color sequences on dark cyan""" 110 | self.assertEqual(ColorStr("colored", COLOR_MAP["dc"], force_seq=False), "colored") 111 | 112 | def test_plat_det(self): 113 | """Attempt to run color sequence support detection""" 114 | # This test doesn't really DO much, but it at least attempts to run the 115 | # code which is very slightly better than no test at all. 116 | self.assertIn( 117 | ColorStr.color_supported(), 118 | (True, False), 119 | ) 120 | 121 | def test_strip_colors(self): 122 | """Make sure colors can be stripped from a string""" 123 | self.assertEqual( 124 | strip_color("\033[97mwhite\033[0m \033[96mcyan\033[0m " "\033[92mgreen\033[0m"), "white cyan green" 125 | ) 126 | -------------------------------------------------------------------------------- /tests/test_formatters.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from io import StringIO 3 | from unittest import TestCase 4 | 5 | from log_color.colors import ColorStr 6 | from log_color.formatters import ColorFormatter, ColorStripper 7 | from log_color.lib import COLOR_MAP 8 | 9 | 10 | class TestColorFormatter(TestCase): 11 | def setUp(self): 12 | self.stream = StringIO() 13 | self.handler = logging.StreamHandler(self.stream) 14 | formatter = ColorFormatter("%(levelname)s: %(message)s") 15 | self.handler.setFormatter(formatter) 16 | 17 | self.log = logging.getLogger("test_logger") 18 | self.log.setLevel(logging.INFO) 19 | for handler in self.log.handlers: 20 | self.log.removeHandler(handler) 21 | self.log.addHandler(self.handler) 22 | 23 | def test_format_matches(self): 24 | """Test format includes expected output on preformatted strings.""" 25 | base_str = "blue" 26 | blue_str = ColorStr(base_str, COLOR_MAP["b"]) 27 | expected = f"INFO: this is {blue_str}\n" 28 | self.log.info(f"this is #b<{base_str}>") 29 | self.assertEqual(self.stream.getvalue(), expected) 30 | 31 | def test_format_matches_arg_manage(self): 32 | """Test format includes expected output when formatted with logger args.""" 33 | base_str = "blue" 34 | blue_str = ColorStr(base_str, COLOR_MAP["b"]) 35 | expected = f"INFO: this is {blue_str}\n" 36 | self.log.info("this is #b<%s>", base_str) 37 | self.assertEqual(self.stream.getvalue(), expected) 38 | 39 | def test_format_object(self): 40 | """Test formatting a non-string object.""" 41 | expected = "INFO: {}\n" 42 | self.log.info({}) 43 | self.assertEqual(self.stream.getvalue(), expected) 44 | 45 | 46 | class TestColorStripper(TestCase): 47 | def setUp(self): 48 | self.stream = StringIO() 49 | self.handler = logging.StreamHandler(self.stream) 50 | formatter = ColorStripper("%(levelname)s: %(message)s") 51 | self.handler.setFormatter(formatter) 52 | 53 | self.log = logging.getLogger("test_logger") 54 | self.log.setLevel(logging.INFO) 55 | for handler in self.log.handlers: 56 | self.log.removeHandler(handler) 57 | self.log.addHandler(self.handler) 58 | 59 | def test_format_matches(self): 60 | """Test format includes expected output.""" 61 | base_str = "blue" 62 | expected = f"INFO: this is {base_str}\n" 63 | self.log.info("\033[94mthis\033[0m is #b<%s>", base_str) 64 | self.assertEqual(self.stream.getvalue(), expected) 65 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | # Standard 2 | from unittest import TestCase 3 | 4 | 5 | class TestImports(TestCase): 6 | def test_import_color_stripper(self): 7 | """Test importing ColorStripper.""" 8 | try: 9 | from log_color import ColorStripper # noqa 10 | except ImportError: 11 | raise AssertionError("Unable to import ColorStripper from module") 12 | 13 | def test_import_color_formatter(self): 14 | """Test importing ColorFormatter.""" 15 | try: 16 | from log_color import ColorFormatter # noqa 17 | except ImportError: 18 | raise AssertionError("Unable to import ColorFormatter from module") 19 | -------------------------------------------------------------------------------- /tests/test_regexes.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from log_color import regex 4 | 5 | 6 | class TestRegexes(TestCase): 7 | def test_color_re(self): 8 | """Test matching basic values""" 9 | good_vals = ( 10 | "#m", 11 | "#b", 12 | "#g", 13 | "#y", 14 | "#c", 15 | "#w", 16 | "#db", 17 | "#dc", 18 | "#dg", 19 | "#dm", 20 | "#dr", 21 | "#dy", 22 | ) 23 | bad_vals = ("Some text", "#f", "#m[magenta]") 24 | for good_val in good_vals: 25 | self.assertIsNotNone(regex.COLOR_EXP.match(good_val)) 26 | 27 | for bad_val in bad_vals: 28 | self.assertIsNone(regex.COLOR_EXP.match(bad_val)) 29 | 30 | def test_color_extract(self): 31 | """Make sure regular expression catches all instances on findall""" 32 | text = ( 33 | "This text has some very colourful words in it like: #m, " 34 | " #b, #g, #y, #c, and #w" 35 | ) 36 | vals = regex.COLOR_EXP.findall(text) 37 | 38 | for item in ("#m", "#b", "#g", "#y", "#c", "#w"): 39 | if item not in vals: 40 | raise AssertionError(f"{item} was not detected") 41 | --------------------------------------------------------------------------------