├── .github └── workflows │ ├── deploy.yml │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── demo └── .gitkeep ├── pygments_pytest.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── testing └── make-index ├── tests ├── __init__.py ├── conftest.py └── pygments_pytest_test.py └── tox.ini /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | pr: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.x 14 | - run: pip install tox 15 | - run: tox -e push 16 | env: 17 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [main, test-me-*] 6 | tags: '*' 7 | pull_request: 8 | 9 | jobs: 10 | main: 11 | uses: asottile/workflows/.github/workflows/tox.yml@v1.5.0 12 | with: 13 | env: '["py38", "py38-main", "py39-main"]' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | /.coverage 4 | /.tox 5 | /demo/*.html 6 | /index.html 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: name-tests-test 11 | - id: requirements-txt-fixer 12 | - repo: https://github.com/asottile/setup-cfg-fmt 13 | rev: v2.5.0 14 | hooks: 15 | - id: setup-cfg-fmt 16 | - repo: https://github.com/asottile/reorder-python-imports 17 | rev: v3.12.0 18 | hooks: 19 | - id: reorder-python-imports 20 | args: [--py38-plus, --add-import, 'from __future__ import annotations'] 21 | - repo: https://github.com/asottile/add-trailing-comma 22 | rev: v3.1.0 23 | hooks: 24 | - id: add-trailing-comma 25 | - repo: https://github.com/asottile/pyupgrade 26 | rev: v3.15.2 27 | hooks: 28 | - id: pyupgrade 29 | args: [--py38-plus] 30 | - repo: https://github.com/hhatto/autopep8 31 | rev: v2.1.0 32 | hooks: 33 | - id: autopep8 34 | - repo: https://github.com/PyCQA/flake8 35 | rev: 7.0.0 36 | hooks: 37 | - id: flake8 38 | - repo: https://github.com/pre-commit/mirrors-mypy 39 | rev: v1.9.0 40 | hooks: 41 | - id: mypy 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Anthony Sottile 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://github.com/asottile/pygments-pytest/actions/workflows/main.yml/badge.svg)](https://github.com/asottile/pygments-pytest/actions/workflows/main.yml) 2 | [![Build Status](https://github.com/asottile/pygments-pytest/actions/workflows/deploy.yml/badge.svg)](https://github.com/asottile/pygments-pytest/actions) 3 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/asottile/pygments-pytest/main.svg)](https://results.pre-commit.ci/latest/github/asottile/pygments-pytest/main) 4 | 5 | pygments-pytest 6 | =============== 7 | 8 | A pygments lexer for pytest output 9 | 10 | See [some demos](https://asottile.github.io/pygments-pytest)! Or 11 | [see it in action on pytest.org](https://pytest.org). 12 | 13 | ## Installation 14 | 15 | ```bash 16 | pip install pygments-pytest 17 | ``` 18 | 19 | ## Usage 20 | 21 | This library provides a pygments lexer called `pytest`. 22 | 23 | ```rst 24 | .. code-block:: pytest 25 | 26 | $ pytest test.py 27 | ========================== test session starts =========================== 28 | platform linux -- Python 3.6.6, pytest-4.0.1.dev43+g0d529847.d20181123, py-1.7.0, pluggy-0.8.0 29 | rootdir: /home/asottile/workspace/pytest, inifile: tox.ini 30 | collected 1 item 31 | 32 | test.py . [100%] 33 | 34 | ======================== 1 passed in 0.01 seconds ======================== 35 | ``` 36 | 37 | This library also provides a sphinx extension. It can be enabled by adding 38 | `'pygments_pytest'` to the `extensions` setting in your `conf.py`. 39 | 40 | The colors can be tweaked using the sphinx setting (in `conf.py`) 41 | `pygments_pytest_ansi_colors`: 42 | 43 | ```python 44 | pygments_pytest_ansi_colors = { 45 | 'Cyan': '#06989a', 46 | 'Green': '#4e9a06', 47 | 'Red': '#c00', 48 | 'Yellow': '#c4A000', 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /demo/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pygments-pytest/5d07ca184090217df39016173d2e35e87cdd6875/demo/.gitkeep -------------------------------------------------------------------------------- /pygments_pytest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os.path 4 | import re 5 | from typing import Any 6 | from typing import Generator 7 | from typing import Match 8 | from typing import Tuple 9 | 10 | import pygments.lexer 11 | import pygments.token 12 | 13 | Tok = Tuple[int, Any, str] 14 | 15 | Color = pygments.token.Token.Color 16 | STATUSES = ( 17 | 'failed', 'passed', 'skipped', 'deselected', 'error', 'no tests ran', 18 | ) 19 | BOLDIFY = { 20 | Color.Red: Color.Bold.Red, 21 | Color.Green: Color.Bold.Green, 22 | Color.Yellow: Color.Bold.Yellow, 23 | } 24 | 25 | 26 | class PytestLexer(pygments.lexer.RegexLexer): 27 | name = 'pytest' 28 | aliases = ('pytest',) 29 | flags = re.MULTILINE 30 | 31 | def filename_line(self, match: Match[str]) -> Generator[Tok, None, None]: 32 | yield match.start(1), Color.Bold.Red, match[1] 33 | yield match.start(2), pygments.token.Text, match[2] 34 | 35 | def status_line(self, match: Match[str]) -> Generator[Tok, None, None]: 36 | if match['failed'] or match['errors']: 37 | start_end_color = Color.Red 38 | elif ( 39 | match['skipped'] or 40 | match['xfailed'] or 41 | match['xpassed'] or 42 | match['warnings'] 43 | ): 44 | start_end_color = Color.Yellow 45 | else: 46 | start_end_color = Color.Green 47 | 48 | if match['before']: 49 | yield match.start('before'), start_end_color, match['before'] 50 | for k, color in ( 51 | ('failed', Color.Red), 52 | ('passed', Color.Green), 53 | ('skipped', Color.Yellow), 54 | ('deselected', Color.Yellow), 55 | ('xfailed', Color.Yellow), 56 | ('xpassed', Color.Yellow), 57 | ('warnings', Color.Yellow), 58 | ('errors', Color.Red), 59 | ): 60 | if color == start_end_color: 61 | color = BOLDIFY[color] 62 | kcomma = f'{k}comma' 63 | if match[k]: 64 | yield match.start(k), color, match[k] 65 | if match[kcomma]: 66 | yield match.start(kcomma), pygments.token.Text, match[kcomma] 67 | yield match.start('time'), start_end_color, match['time'] 68 | if match['after']: 69 | yield match.start('after'), start_end_color, match['after'] 70 | 71 | _next_section = ( 72 | (r'(?=^=+ )', pygments.token.Text, '#pop'), 73 | ( 74 | r'(?=^[1-9]\d* ({}))'.format('|'.join(STATUSES)), 75 | pygments.token.Text, 76 | '#pop', 77 | ), 78 | ) 79 | 80 | tokens = { 81 | 'root': [ 82 | (r'^=+ test session starts =+$', Color.Bold), 83 | (r'^collecting \.\.\.', Color.Bold), 84 | (r'^(?=.+\[ *\d+%\]$)', pygments.token.Text, 'progress_line'), 85 | (r'^=+ (ERRORS|FAILURES) =+$', pygments.token.Text, 'failures'), 86 | (r'^=+ warnings summary( \(final\))? =+$', Color.Yellow), 87 | (r'^=+ short test summary info =+$\n', Color.Bold.Cyan, 'summary'), 88 | ( 89 | r'^(?P=+ )?' 90 | r'(?P\d+ failed)?(?P, )?' 91 | r'(?P\d+ passed)?(?P, )?' 92 | r'(?P\d+ skipped)?(?P, )?' 93 | r'(?P\d+ deselected)?(?P, )?' 94 | r'(?P\d+ xfailed)?(?P, )?' 95 | r'(?P\d+ xpassed)?(?P, )?' 96 | r'(?P\d+ warnings?)?(?P, )?' 97 | r'(?P\d+ errors?)?(?P)?' 98 | r'(?P