├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── check.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── pyproject.toml ├── src └── pytest_print │ ├── __init__.py │ └── py.typed ├── tests ├── __init__.py ├── conftest.py ├── example_create_pretty_print.py ├── example_pretty_print.py ├── example_print.py ├── test_create_pretty_print.py ├── test_pretty_print.py └── test_print.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/pytest-print" 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | - pre-commit-ci 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: ["**"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | concurrency: 12 | group: check-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | env: 22 | - "3.13" 23 | - "3.12" 24 | - "3.11" 25 | - "3.10" 26 | - "3.9" 27 | - "3.8" 28 | - type 29 | - dev 30 | - pkg_meta 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 35 | - name: Install the latest version of uv 36 | uses: astral-sh/setup-uv@v6 37 | with: 38 | enable-cache: true 39 | cache-dependency-glob: "pyproject.toml" 40 | github-token: ${{ secrets.GITHUB_TOKEN }} 41 | - name: Install tox 42 | run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv 43 | - name: Install Python 44 | if: startsWith(matrix.env, '3.') && matrix.env != '3.13' 45 | run: uv python install --python-preference only-managed ${{ matrix.env }} 46 | - name: Setup test suite 47 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} 48 | - name: Run test suite 49 | run: tox run --skip-pkg-install -e ${{ matrix.env }} 50 | env: 51 | PYTEST_ADDOPTS: "-vv --durations=20" 52 | DIFF_AGAINST: HEAD 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v6 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.13 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/pytest-print/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v4 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.12.4 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.tox 2 | .coverage* 3 | coverage.xml 4 | .*_cache 5 | __pycache__ 6 | **.pyc 7 | src/pytest_print/_version.py 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.33.0 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | additional_dependencies: ["tomli>=2.0.1"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.5.0" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.6.0" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.11.11" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: https://github.com/rbubley/mirrors-prettier 33 | rev: "v3.5.3" 34 | hooks: 35 | - id: prettier 36 | args: ["--print-width=120", "--prose-wrap=always"] 37 | - repo: meta 38 | hooks: 39 | - id: check-hooks-apply 40 | - id: check-useless-excludes 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.3.0](https://github.com/pytest-dev/pytest-print/tree/0.3.0) (2021-06-17) 4 | 5 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.2.1...0.3.0) 6 | 7 | **Merged pull requests:** 8 | 9 | - Drop python 2 support [\#17](https://github.com/pytest-dev/pytest-print/pull/17) 10 | ([gaborbernat](https://github.com/gaborbernat)) 11 | - \[pre-commit.ci\] pre-commit autoupdate [\#14](https://github.com/pytest-dev/pytest-print/pull/14) 12 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 13 | - \[pre-commit.ci\] pre-commit autoupdate [\#13](https://github.com/pytest-dev/pytest-print/pull/13) 14 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 15 | - \[pre-commit.ci\] pre-commit autoupdate [\#12](https://github.com/pytest-dev/pytest-print/pull/12) 16 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 17 | - \[pre-commit.ci\] pre-commit autoupdate [\#11](https://github.com/pytest-dev/pytest-print/pull/11) 18 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 19 | - \[pre-commit.ci\] pre-commit autoupdate [\#10](https://github.com/pytest-dev/pytest-print/pull/10) 20 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 21 | - \[pre-commit.ci\] pre-commit autoupdate [\#9](https://github.com/pytest-dev/pytest-print/pull/9) 22 | ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) 23 | 24 | ## [0.2.1](https://github.com/pytest-dev/pytest-print/tree/0.2.1) (2020-10-23) 25 | 26 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.2.0...0.2.1) 27 | 28 | **Closed issues:** 29 | 30 | - add session level support [\#6](https://github.com/pytest-dev/pytest-print/issues/6) 31 | 32 | ## [0.2.0](https://github.com/pytest-dev/pytest-print/tree/0.2.0) (2020-08-04) 33 | 34 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.1.3...0.2.0) 35 | 36 | ## [0.1.3](https://github.com/pytest-dev/pytest-print/tree/0.1.3) (2019-09-03) 37 | 38 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.1.2...0.1.3) 39 | 40 | **Implemented enhancements:** 41 | 42 | - allow force on, document flags [\#3](https://github.com/pytest-dev/pytest-print/pull/3) 43 | ([gaborbernat](https://github.com/gaborbernat)) 44 | 45 | **Merged pull requests:** 46 | 47 | - Remove PyPy special cases [\#4](https://github.com/pytest-dev/pytest-print/pull/4) 48 | ([vtbassmatt](https://github.com/vtbassmatt)) 49 | 50 | ## [0.1.2](https://github.com/pytest-dev/pytest-print/tree/0.1.2) (2018-11-29) 51 | 52 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.1.1...0.1.2) 53 | 54 | ## [0.1.1](https://github.com/pytest-dev/pytest-print/tree/0.1.1) (2018-11-15) 55 | 56 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/0.1.0...0.1.1) 57 | 58 | **Closed issues:** 59 | 60 | - How to use “pytest_print” [\#1](https://github.com/pytest-dev/pytest-print/issues/1) 61 | 62 | **Merged pull requests:** 63 | 64 | - Update setup.py [\#2](https://github.com/pytest-dev/pytest-print/pull/2) 65 | ([shashanksingh28](https://github.com/shashanksingh28)) 66 | 67 | ## [0.1.0](https://github.com/pytest-dev/pytest-print/tree/0.1.0) (2018-04-14) 68 | 69 | [Full Changelog](https://github.com/pytest-dev/pytest-print/compare/727896d18cab117ad84010086cbc4c9a16d9e8f7...0.1.0) 70 | 71 | \* _This Changelog was automatically generated by 72 | [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_ 73 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | tox-dev@python.org. The project team will review and investigate all complaints, and will respond in a way that it deems 48 | appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter 49 | of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 58 | 59 | [homepage]: https://www.contributor-covenant.org/ 60 | [version]: https://www.contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Permission is hereby granted, free of charge, to any person obtaining a 3 | copy of this software and associated documentation files (the 4 | "Software"), to deal in the Software without restriction, including 5 | without limitation the rights to use, copy, modify, merge, publish, 6 | distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to 8 | the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 17 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-print 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/pytest-print?style=flat-square)](https://pypi.org/project/pytest-print) 4 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/pytest-print?style=flat-square)](https://pypi.org/project/pytest-print) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-print?style=flat-square)](https://pypi.org/project/pytest-print) 6 | [![Downloads](https://static.pepy.tech/badge/pytest-print/month)](https://pepy.tech/project/pytest-print) 7 | [![PyPI - License](https://img.shields.io/pypi/l/pytest-print?style=flat-square)](https://opensource.org/licenses/MIT) 8 | [![check](https://github.com/pytest-dev/pytest-print/actions/workflows/check.yaml/badge.svg)](https://github.com/pytest-dev/pytest-print/actions/workflows/check.yaml) 9 | 10 | Allows to print extra content onto the PyTest reporting. This can be used for example to report sub-steps for long 11 | running tests, or to print debug information in your tests when you cannot debug the code (so that the end user does not 12 | need to wonder if the test froze/dead locked or not). 13 | 14 | 15 | 16 | - [Install](#install) 17 | - [CLI flags](#cli-flags) 18 | - [API](#api) 19 | - [Example: printer_session](#example-printer_session) 20 | - [Example: pretty_printer](#example-pretty_printer) 21 | - [Example: create_pretty_printer](#example-create_pretty_printer) 22 | 23 | 24 | 25 | ## Install 26 | 27 | ```sh 28 | pip install pytest-print 29 | ``` 30 | 31 | ## CLI flags 32 | 33 | The following flags are registered for the pytest command: 34 | 35 | - `--print` by default the module activates print when pytest verbosity is greater than zero, this allows to bypass this 36 | and force print irrespective of the verbosity 37 | - `--print-relative-time` will print the relative time since the start of the test (display how long it takes to reach 38 | prints) 39 | 40 | ## API 41 | 42 | This library provides the following fixtures that help you print messages within a pytest run (bypasses the pytest 43 | output capture, so it will show up in the standard output, even if the test passes): 44 | 45 | - `printer: Printter` - function level fixture, when called prints a message line (with very simple formatting), 46 | - [`printer_session: Printter`](#example-printer_session) - session scoped fixture same as above but using (this exists 47 | as a backwards compatibility layer, as we didn't want to switch the originally function scope variant to session one), 48 | - [`pretty_printer: PrettyPrintter`](#example-pretty_printer) - session scoped fixture, when called prints a message 49 | line (with fancy formatting of space for indentation, `⏩` icon for every message, and elapsed time format in form of 50 | `[{elapsed:.20f}]`) and also allows creating a printer that will be indented one level deeper (and optionally use a 51 | different icon). 52 | - [`create_pretty_printer: PrettyPrinterFactory`](#example-create_pretty_printer) - allows the caller to customize the 53 | fancy formatter as they wish. Takes one `formatter` argument, whose arguments should be interpreted as: 54 | 55 | ```shell 56 | ┌──────┐ ┌──────────┐┌─────────┐┌────────┐ 57 | │ pre │ ==│ head ││ icon ││ space │ 58 | └──────┘ └──────────┘└─────────┘└────────┘ 59 | 60 | ┌─────────────┐┌───────┐┌──────┐┌────────────┐ 61 | │ indentation ││ timer ││ pre ││ msg │ 62 | └─────────────┘└───────┘└──────┘└────────────┘ 63 | ┌───────┐┌────────────────────┐┌──────┐┌────────────┐ 64 | │ timer ││ spacer ││ pre ││ msg │ 65 | └───────┘└────────────────────┘└──────┘└────────────┘ 66 | ┌───────┐┌────────────────────┐┌────────────────────┐┌──────┐┌────────────┐ 67 | │ timer ││ spacer ││ spacer ││ pre ││ msg │ 68 | └───────┘└────────────────────┘└────────────────────┘└──────┘└────────────┘ 69 | ``` 70 | 71 | ### Example: `printer_session` 72 | 73 | ```python 74 | from __future__ import annotations 75 | 76 | from typing import TYPE_CHECKING, Iterator 77 | 78 | import pytest 79 | 80 | if TYPE_CHECKING: 81 | from pytest_print import Printer 82 | 83 | 84 | @pytest.fixture(scope="session") 85 | def _expensive_setup(printer_session: Printer) -> Iterator[None]: 86 | printer_session("setup") 87 | yield 88 | printer_session("teardown") 89 | 90 | 91 | @pytest.mark.usefixtures("_expensive_setup") 92 | def test_run(printer_session: Printer) -> None: 93 | printer_session("running") 94 | ``` 95 | 96 | ```shell 97 | pytest magic.py -vvvv 98 | ... 99 | 100 | magic.py::test_run 101 | setup expensive operation 102 | running test 103 | 104 | magic.py::test_run PASSED 105 | teardown expensive operation 106 | ``` 107 | 108 | ### Example: `pretty_printer` 109 | 110 | ```python 111 | from __future__ import annotations 112 | 113 | from typing import TYPE_CHECKING 114 | 115 | import pytest 116 | 117 | from pytest_print import Formatter 118 | 119 | if TYPE_CHECKING: 120 | from pytest_print import PrettyPrinter, PrettyPrinterFactory 121 | 122 | 123 | @pytest.fixture(scope="session") 124 | def pretty(create_pretty_printer: PrettyPrinterFactory) -> PrettyPrinter: 125 | formatter = Formatter(indentation=" ", head=" ", space=" ", icon="⏩", timer_fmt="[{elapsed:.20f}]") 126 | return create_pretty_printer(formatter=formatter) 127 | 128 | 129 | def test_long_running(pretty: PrettyPrinter) -> None: 130 | pretty("Starting test") 131 | 132 | pretty_printer_1 = pretty.indent(icon="🚀") 133 | pretty_printer_1("Drill down to 1st level details") 134 | 135 | pretty_printer_2 = pretty_printer_1.indent(icon="🚀") 136 | pretty_printer_2("Drill down to 2nd level details") 137 | 138 | pretty("Finished test") 139 | ``` 140 | 141 | ```shell 142 | magic.py::test_long_running 143 | ⏩ Starting test 144 | 🚀 Drill down to 1st level details 145 | 🚀 Drill down to 2nd level details 146 | ⏩ Finished test 147 | 148 | magic.py::test_long_running PASSED 149 | ``` 150 | 151 | ### Example: `create_pretty_printer` 152 | 153 | If you need nested messages you can use the `printer_factory` fixture or the `pprinter`. 154 | 155 | ```python 156 | from __future__ import annotations 157 | 158 | from typing import TYPE_CHECKING 159 | 160 | import pytest 161 | 162 | from pytest_print import Formatter 163 | 164 | if TYPE_CHECKING: 165 | from pytest_print import PrettyPrinter, PrettyPrinterFactory 166 | 167 | 168 | @pytest.fixture(scope="session") 169 | def pretty(create_pretty_printer: PrettyPrinterFactory) -> PrettyPrinter: 170 | formatter = Formatter( 171 | indentation=" I ", 172 | head=" H ", 173 | space=" S ", 174 | icon="🧹", 175 | timer_fmt="[{elapsed:.5f}]", 176 | ) 177 | return create_pretty_printer(formatter=formatter) 178 | 179 | 180 | def test_long_running(pretty: PrettyPrinter) -> None: 181 | pretty("Starting test") 182 | 183 | pretty_printer_1 = pretty.indent(icon="🚀") 184 | pretty_printer_1("Drill down to 1st level details") 185 | 186 | pretty_printer_2 = pretty_printer_1.indent(icon="🚀") 187 | pretty_printer_2("Drill down to 2nd level details") 188 | 189 | pretty("Finished test") 190 | ``` 191 | 192 | ```bash 193 | pytest magic.py --print --print-relative-time 194 | ... 195 | 196 | magic.py 197 | I [0.00022] H 🧹 S Starting test 198 | [0.00029] H 🚀 S Drill down to 1st level details 199 | [0.00034] H 🚀 S Drill down to 2nd level details 200 | I [0.00038] H 🧹 S Finished test 201 | ``` 202 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-vcs>=0.4", 5 | "hatchling>=1.25", 6 | ] 7 | 8 | [project] 9 | name = "pytest-print" 10 | description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" 11 | readme = "README.md" 12 | keywords = [ 13 | "env", 14 | "pytest", 15 | ] 16 | license.file = "LICENSE" 17 | maintainers = [ 18 | { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, 19 | ] 20 | requires-python = ">=3.8" 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Programming Language :: Python :: Implementation :: CPython", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | ] 37 | dynamic = [ 38 | "version", 39 | ] 40 | dependencies = [ 41 | "pytest>=8.3.2", 42 | ] 43 | optional-dependencies.test = [ 44 | "covdefaults>=2.3", 45 | "coverage>=7.6.1", 46 | "pytest-mock>=3.14", 47 | ] 48 | urls.Homepage = "https://github.com/pytest-dev/pytest-print" 49 | urls.Source = "https://github.com/pytest-dev/pytest-print" 50 | urls.Tracker = "https://github.com/pytest-dev/pytest-print/issues" 51 | entry-points.pytest11.print = "pytest_print" 52 | 53 | [tool.hatch] 54 | version.source = "vcs" 55 | 56 | [tool.ruff] 57 | target-version = "py38" 58 | line-length = 120 59 | format.preview = true 60 | format.docstring-code-line-length = 100 61 | format.docstring-code-format = true 62 | lint.select = [ 63 | "ALL", 64 | ] 65 | lint.ignore = [ 66 | "ANN401", # allow Any as type annotation 67 | "COM812", # Conflict with formatter 68 | "CPY", # No copyright statements 69 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 70 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 71 | "DOC", # no support 72 | "ISC001", # Conflict with formatter 73 | "S104", # Possible binding to all interface 74 | ] 75 | lint.per-file-ignores."tests/**/*.py" = [ 76 | "D", # don't care about documentation in tests 77 | "FBT", # don't care about booleans as positional arguments in tests 78 | "INP001", # no implicit namespace 79 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 80 | "S101", # asserts allowed in tests 81 | "S603", # `subprocess` call: check for execution of untrusted input 82 | ] 83 | lint.isort = { known-first-party = [ 84 | "pytest_print", 85 | ], required-imports = [ 86 | "from __future__ import annotations", 87 | ] } 88 | lint.preview = true 89 | 90 | [tool.codespell] 91 | builtin = "clear,usage,en-GB_to_en-US" 92 | write-changes = true 93 | count = true 94 | 95 | [tool.pyproject-fmt] 96 | max_supported_python = "3.13" 97 | 98 | [tool.coverage] 99 | run.source = [ 100 | "pytest_print", 101 | "tests", 102 | ] 103 | run.dynamic_context = "test_function" 104 | run.branch = true 105 | run.parallel = true 106 | report.omit = [ 107 | "tests/example_*.py", 108 | ] 109 | report.fail_under = 100 110 | report.show_missing = true 111 | html.show_contexts = true 112 | html.skip_covered = false 113 | paths.source = [ 114 | "src", 115 | ".tox*/*/lib/python*/site-packages", 116 | ".tox*/pypy*/site-packages", 117 | ".tox*\\*\\Lib\\site-packages", 118 | "*/src", 119 | "*\\src", 120 | ] 121 | run.plugins = [ 122 | "covdefaults", 123 | ] 124 | 125 | [tool.mypy] 126 | python_version = "3.13" 127 | show_error_codes = true 128 | strict = true 129 | -------------------------------------------------------------------------------- /src/pytest_print/__init__.py: -------------------------------------------------------------------------------- 1 | """Pytest print functionality.""" 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | from dataclasses import dataclass, replace 7 | from timeit import default_timer 8 | from typing import TYPE_CHECKING, Optional, Protocol, TypeVar, cast 9 | 10 | import pytest 11 | 12 | if TYPE_CHECKING: 13 | from _pytest.capture import CaptureManager 14 | from _pytest.fixtures import SubRequest 15 | from _pytest.terminal import TerminalReporter 16 | 17 | 18 | def pytest_addoption(parser: pytest.Parser) -> None: 19 | group = parser.getgroup("general") 20 | group.addoption( 21 | "--print", 22 | action="store_true", 23 | dest="pytest_print_on", 24 | default=False, 25 | help="By default the plugins if verbosity is greater than zero (-v flag), this forces on", 26 | ) 27 | group.addoption( 28 | "--print-relative-time", 29 | action="store_true", 30 | dest="pytest_print_relative_time", 31 | default=False, 32 | help="Time in milliseconds when the print was invoked, relative to the time the fixture was created.", 33 | ) 34 | 35 | 36 | class Printer(Protocol): 37 | """Printer within a pytest session.""" 38 | 39 | def __call__(self, msg: str) -> None: 40 | """ 41 | Print the given message. 42 | 43 | :param msg: message to print 44 | """ 45 | 46 | 47 | @pytest.fixture(scope="session") 48 | def printer_session(request: SubRequest) -> Printer: 49 | """Pytest plugin to print test progress steps in verbose mode (session scoped).""" 50 | return _create(request, _Printer, Formatter()) 51 | 52 | 53 | @pytest.fixture(name="printer") 54 | def printer(printer_session: Printer) -> Printer: 55 | """Pytest plugin to print test progress steps in verbose mode.""" 56 | return printer_session 57 | 58 | 59 | class PrettyPrinter(Protocol): 60 | """Printer within a pytest session.""" 61 | 62 | def __call__(self, msg: str, *, icon: str | None = None) -> None: 63 | """ 64 | Print the given message in pretty mode. 65 | 66 | :param msg: message to print 67 | :param icon: icon to use, will use the one configured at printer creation 68 | """ 69 | 70 | def indent(self, *, icon: str) -> PrettyPrinter: 71 | """ 72 | Create an indented pretty printer. 73 | 74 | :param icon: change the icon from the parents printer 75 | """ 76 | 77 | 78 | @pytest.fixture(scope="session") 79 | def pretty_printer(request: SubRequest) -> PrettyPrinter: 80 | """Pytest plugin to print test progress steps in verbose mode.""" 81 | formatter = Formatter(head=" ", icon="⏩", space=" ", indentation=" ", timer_fmt="[{elapsed:.20f}]") 82 | return _create(request, _PrettyPrinter, formatter) 83 | 84 | 85 | class PrettyPrinterFactory(Protocol): 86 | """Create a new pretty printer.""" 87 | 88 | def __call__(self, *, formatter: Formatter) -> PrettyPrinter: 89 | """ 90 | Create a new pretty printer. 91 | 92 | :param formatter: the formatter to use with this printer 93 | """ 94 | 95 | 96 | @pytest.fixture(scope="session") 97 | def create_pretty_printer(request: SubRequest) -> PrettyPrinterFactory: 98 | """Pytest plugin to print test progress steps in verbose mode.""" 99 | Formatter(head=" ", icon="⏩", space=" ", indentation=" ", timer_fmt="[{elapsed:.20f}]") 100 | 101 | def meth(*, formatter: Formatter) -> PrettyPrinter: 102 | return _create(request, _PrettyPrinter, formatter) 103 | 104 | return meth 105 | 106 | 107 | @dataclass(frozen=True, **{"slots": True, "kw_only": True} if sys.version_info >= (3, 10) else {}) 108 | class Formatter: 109 | """Configures how to format messages to be printed.""" 110 | 111 | head: str = "" #: start every line with this prefix 112 | icon: str = "" #: an icon text printed immediately after the head 113 | space: str = "" #: character to print right after the prefix 114 | indentation: str = "\t" #: use this character to indent the message, only useful for indented printers 115 | timer_fmt: str = "{elapsed}\t" #: how to print out elapsed time since the creation of the printer - float (seconds) 116 | 117 | @property 118 | def _pre(self) -> str: 119 | return f"{self.head}{self.icon}{self.space}" 120 | 121 | def __call__(self, msg: str, level: int, elapsed: float | None) -> str: 122 | """ 123 | Format the given message. 124 | 125 | :param msg: the message to format 126 | :param level: indentation level 127 | :param elapsed: time elapsed 128 | :return: the formatted message 129 | """ 130 | indentation = " " * (len(self.indentation)) if level else self.indentation 131 | spacer = " " * len(self._pre) * level 132 | timer = self.timer_fmt.format(elapsed=elapsed) if elapsed else "" 133 | return f"{indentation}{timer}{spacer}{self._pre}{msg}" 134 | 135 | 136 | class _Printer: 137 | def __init__( 138 | self, 139 | *, 140 | reporter: TerminalReporter | None, 141 | capture_manager: CaptureManager | None, 142 | formatter: Formatter, 143 | level: int, 144 | start: float | None, 145 | ) -> None: 146 | self._reporter = reporter 147 | self._capture_manager = capture_manager 148 | self._formatter = formatter 149 | self._level = level 150 | self._start = start 151 | 152 | def __call__(self, msg: str) -> None: 153 | self._print(msg, self._formatter) 154 | 155 | def _print(self, msg: str, formatter: Formatter) -> None: 156 | if self._reporter is None or self._capture_manager is None: # disabled 157 | return 158 | msg = formatter(msg, self._level, None if self._start is None else default_timer() - self._start) 159 | with self._capture_manager.global_and_fixture_disabled(): 160 | self._reporter.write_line(msg) 161 | 162 | 163 | class _PrettyPrinter(_Printer): 164 | def __call__(self, msg: str, *, icon: str | None = None) -> None: 165 | self._print(msg, self._formatter if icon is None else replace(self._formatter, icon=icon)) 166 | 167 | def indent(self, *, icon: str) -> PrettyPrinter: 168 | return _PrettyPrinter( 169 | reporter=self._reporter, 170 | capture_manager=self._capture_manager, 171 | formatter=replace(self._formatter, icon=icon), 172 | level=self._level + 1, 173 | start=self._start, 174 | ) 175 | 176 | 177 | _OfType = TypeVar("_OfType", bound=_Printer) 178 | 179 | 180 | def _create(request: SubRequest, of_type: type[_OfType], formatter: Formatter) -> _OfType: 181 | return of_type( 182 | reporter=cast("Optional[TerminalReporter]", request.config.pluginmanager.getplugin("terminalreporter")) 183 | if request.config.getoption("pytest_print_on") or cast("int", request.config.getoption("verbose")) > 0 184 | else None, 185 | capture_manager=cast("Optional[CaptureManager]", request.config.pluginmanager.getplugin("capturemanager")), 186 | formatter=formatter, 187 | start=default_timer() if request.config.getoption("pytest_print_relative_time") else None, 188 | level=0, 189 | ) 190 | 191 | 192 | __all__ = [ 193 | "Formatter", 194 | "PrettyPrinter", 195 | "PrettyPrinterFactory", 196 | "Printer", 197 | ] 198 | -------------------------------------------------------------------------------- /src/pytest_print/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-print/89467ebc65eb3967786beec488f1d49dcfab834c/src/pytest_print/py.typed -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from pathlib import Path 5 | from shutil import copy2 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | import pytest 10 | 11 | 12 | def extract_printer_text(lines: list[str]) -> str: 13 | output = "\n".join(line for line in lines if not line.startswith("=" * 20)) 14 | output = output[output.find("test_a.py::") :] 15 | output = re.sub(r"\s*\[\s*\d+%\]", "", output) 16 | output = output.replace("test_a::", "a::") 17 | return re.sub(r"[ \t]+\n", "\n", output) 18 | 19 | 20 | def seed_test(filename: str, testdir: pytest.Testdir) -> pytest.Testdir: 21 | src = Path(__file__).parent / filename 22 | assert src.exists() 23 | dest = Path(str(testdir.tmpdir)) / "test_a.py" 24 | copy2(src, dest) 25 | return testdir 26 | 27 | 28 | __all__ = [ 29 | "extract_printer_text", 30 | "seed_test", 31 | ] 32 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | pytest_plugins = ["pytester"] 4 | -------------------------------------------------------------------------------- /tests/example_create_pretty_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from time import sleep 4 | from typing import TYPE_CHECKING, Iterator 5 | 6 | import pytest 7 | 8 | from pytest_print import Formatter 9 | 10 | if TYPE_CHECKING: 11 | from pytest_print import PrettyPrinter, PrettyPrinterFactory 12 | 13 | 14 | def create_virtual_environment() -> None: 15 | sleep(0.001) 16 | 17 | 18 | def start_server() -> None: 19 | sleep(0.001) 20 | 21 | 22 | def parallel_requests() -> None: 23 | sleep(0.001) 24 | 25 | 26 | @pytest.fixture(scope="session") 27 | def pretty(create_pretty_printer: PrettyPrinterFactory) -> PrettyPrinter: 28 | formatter = Formatter(indentation=" ", head=" ", space=" ", icon="⏩", timer_fmt="[{elapsed:.20f}]") 29 | return create_pretty_printer(formatter=formatter) 30 | 31 | 32 | @pytest.fixture(name="pprinter", scope="session") 33 | def pprinter(pretty: PrettyPrinter) -> PrettyPrinter: 34 | return pretty.indent(icon="🚀") 35 | 36 | 37 | @pytest.fixture(scope="session") 38 | def _expensive_setup(pretty: PrettyPrinter) -> Iterator[None]: 39 | pretty("attempt global peace") 40 | yield 41 | pretty("teardown global peace") 42 | 43 | 44 | @pytest.mark.usefixtures("_expensive_setup") 45 | def test_global_peace(pretty: PrettyPrinter) -> None: 46 | pretty("here we have global peace") 47 | 48 | 49 | @pytest.mark.usefixtures("_expensive_setup") 50 | def test_server_parallel_requests(pprinter: PrettyPrinter) -> None: 51 | pprinter("create virtual environment") 52 | create_virtual_environment() 53 | 54 | pprinter("start server from virtual env") 55 | start_server() 56 | 57 | pprinter("do the parallel request test") 58 | parallel_requests() 59 | 60 | 61 | def test_create_pretty_printer_usage(create_pretty_printer: PrettyPrinterFactory) -> None: 62 | formatter = Formatter(icon="⏩", head=" ", space=" ", indentation="..", timer_fmt="[{elapsed:.20f}]") 63 | printer = create_pretty_printer(formatter=formatter) 64 | printer("start here the test start") 65 | 66 | printer1 = printer.indent(icon="🚀") 67 | printer1("start an indented printer") 68 | 69 | printer2 = printer1.indent(icon="🧹") 70 | printer2("start an indented indented printer") 71 | printer2("a message with a twist", icon="🔄") 72 | printer2("end an indented indented printer") 73 | 74 | printer1("end an indented printer") 75 | 76 | printer("end here the test end") 77 | -------------------------------------------------------------------------------- /tests/example_pretty_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from pytest_print import PrettyPrinter 7 | 8 | 9 | def test_pprinter_usage(pretty_printer: PrettyPrinter) -> None: 10 | pretty_printer("start here the test start") 11 | 12 | printer1 = pretty_printer.indent(icon="🚀") 13 | printer1("start an indented printer") 14 | 15 | printer2 = printer1.indent(icon="🧹") 16 | printer2("start an indented indented printer") 17 | printer2("a message with a twist", icon="🔄") 18 | printer2("end an indented indented printer") 19 | 20 | printer1("end an indented printer") 21 | 22 | pretty_printer("end here the test end") 23 | -------------------------------------------------------------------------------- /tests/example_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from time import sleep 4 | from typing import TYPE_CHECKING, Iterator 5 | 6 | import pytest 7 | 8 | if TYPE_CHECKING: 9 | from pytest_print import Printer 10 | 11 | 12 | def create_virtual_environment() -> None: 13 | sleep(0.001) 14 | 15 | 16 | def start_server() -> None: 17 | sleep(0.001) 18 | 19 | 20 | def parallel_requests() -> None: 21 | sleep(0.001) 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def _expensive_setup(printer_session: Printer) -> Iterator[None]: 26 | printer_session("attempt global peace") 27 | yield 28 | printer_session("teardown global peace") 29 | 30 | 31 | @pytest.mark.usefixtures("_expensive_setup") 32 | def test_global_peace(printer_session: Printer) -> None: 33 | printer_session("here we have global peace") 34 | 35 | 36 | @pytest.mark.usefixtures("_expensive_setup") 37 | def test_server_parallel_requests(printer: Printer) -> None: 38 | printer("create virtual environment") 39 | create_virtual_environment() 40 | 41 | printer("start server from virtual env") 42 | start_server() 43 | 44 | printer("do the parallel request test") 45 | parallel_requests() 46 | -------------------------------------------------------------------------------- /tests/test_create_pretty_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | from tests import extract_printer_text, seed_test 8 | 9 | 10 | @pytest.fixture 11 | def example(testdir: pytest.Testdir) -> pytest.Testdir: 12 | return seed_test("example_create_pretty_print.py", testdir) 13 | 14 | 15 | def fix_floats_in_relative_time(txt: str) -> str: 16 | float_pattern = r"[-+]?\d*\.\d+([eE][-+]?\d+)?" 17 | return re.sub(float_pattern, "0.1", txt) 18 | 19 | 20 | def test_progress_no_v(example: pytest.Testdir) -> None: 21 | result = example.runpytest() 22 | result.assert_outcomes(passed=3) 23 | assert " start server from virtual env" not in result.outlines 24 | assert "global peace" not in result.outlines 25 | 26 | 27 | def test_progress_v_no_relative(example: pytest.Testdir) -> None: 28 | result_verbose = example.runpytest("-v", "--print") 29 | result_verbose.assert_outcomes(passed=3) 30 | 31 | output = extract_printer_text(result_verbose.outlines) 32 | 33 | expected = """\ 34 | test_a.py::test_global_peace 35 | ⏩ attempt global peace 36 | ⏩ here we have global peace 37 | 38 | test_a.py::test_global_peace PASSED 39 | test_a.py::test_server_parallel_requests 40 | 🚀 create virtual environment 41 | 🚀 start server from virtual env 42 | 🚀 do the parallel request test 43 | 44 | test_a.py::test_server_parallel_requests PASSED 45 | test_a.py::test_create_pretty_printer_usage 46 | .. ⏩ start here the test start 47 | 🚀 start an indented printer 48 | 🧹 start an indented indented printer 49 | 🔄 a message with a twist 50 | 🧹 end an indented indented printer 51 | 🚀 end an indented printer 52 | .. ⏩ end here the test end 53 | 54 | test_a.py::test_create_pretty_printer_usage PASSED 55 | ⏩ teardown global peace 56 | """ 57 | assert output == expected 58 | 59 | 60 | def test_progress_v_relative(example: pytest.Testdir) -> None: 61 | result_verbose_relative = example.runpytest( 62 | "--print", 63 | "-v", 64 | "--print-relative-time", 65 | "-k", 66 | "test_server_parallel_requests", 67 | ) 68 | result_verbose_relative.assert_outcomes(passed=1) 69 | 70 | output = extract_printer_text(result_verbose_relative.outlines) 71 | output = fix_floats_in_relative_time(output) 72 | 73 | expected = """\ 74 | test_a.py::test_server_parallel_requests 75 | [0.1] ⏩ attempt global peace 76 | [0.1] 🚀 create virtual environment 77 | [0.1] 🚀 start server from virtual env 78 | [0.1] 🚀 do the parallel request test 79 | 80 | test_a.py::test_server_parallel_requests PASSED 81 | [0.1] ⏩ teardown global peace 82 | """ 83 | assert output == expected 84 | 85 | 86 | def test_progress_no_v_but_with_print_request(example: pytest.Testdir) -> None: 87 | result = example.runpytest("--print") 88 | result.assert_outcomes(passed=3) 89 | assert " 🚀 start server from virtual env" in result.outlines 90 | assert " ⏩ attempt global peace" in result.outlines 91 | -------------------------------------------------------------------------------- /tests/test_pretty_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | 5 | import pytest 6 | 7 | from tests import extract_printer_text, seed_test 8 | 9 | 10 | @pytest.fixture 11 | def example(testdir: pytest.Testdir) -> pytest.Testdir: 12 | return seed_test("example_pretty_print.py", testdir) 13 | 14 | 15 | def fix_floats_in_relative_time(txt: str) -> str: 16 | float_pattern = r"[-+]?\d*\.\d+([eE][-+]?\d+)?" 17 | return re.sub(float_pattern, "0.1", txt) 18 | 19 | 20 | def test_progress_v_no_relative(example: pytest.Testdir) -> None: 21 | result = example.runpytest("-v", "--print") 22 | result.assert_outcomes(passed=1) 23 | 24 | output = extract_printer_text(result.outlines) 25 | expected = """\ 26 | test_a.py::test_pprinter_usage 27 | ⏩ start here the test start 28 | 🚀 start an indented printer 29 | 🧹 start an indented indented printer 30 | 🔄 a message with a twist 31 | 🧹 end an indented indented printer 32 | 🚀 end an indented printer 33 | ⏩ end here the test end 34 | 35 | test_a.py::test_pprinter_usage PASSED 36 | """ 37 | assert output == expected 38 | 39 | 40 | def test_progress_v_relative(example: pytest.Testdir) -> None: 41 | result = example.runpytest( 42 | "--print", 43 | "-v", 44 | "--print-relative-time", 45 | ) 46 | result.assert_outcomes(passed=1) 47 | 48 | output = extract_printer_text(result.outlines) 49 | output = fix_floats_in_relative_time(output) 50 | 51 | expected = """\ 52 | test_a.py::test_pprinter_usage 53 | [0.1] ⏩ start here the test start 54 | [0.1] 🚀 start an indented printer 55 | [0.1] 🧹 start an indented indented printer 56 | [0.1] 🔄 a message with a twist 57 | [0.1] 🧹 end an indented indented printer 58 | [0.1] 🚀 end an indented printer 59 | [0.1] ⏩ end here the test end 60 | 61 | test_a.py::test_pprinter_usage PASSED 62 | """ 63 | assert output == expected 64 | -------------------------------------------------------------------------------- /tests/test_print.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from tests import extract_printer_text, seed_test 6 | 7 | 8 | @pytest.fixture 9 | def example(testdir: pytest.Testdir) -> pytest.Testdir: 10 | return seed_test("example_print.py", testdir) 11 | 12 | 13 | def test_progress_no_v(example: pytest.Testdir) -> None: 14 | result = example.runpytest() 15 | result.assert_outcomes(passed=2) 16 | assert " start server from virtual env" not in result.outlines 17 | assert "global peace" not in result.outlines 18 | 19 | 20 | def test_progress_v_no_relative(example: pytest.Testdir, monkeypatch: pytest.MonkeyPatch) -> None: 21 | monkeypatch.setattr("_pytest._io.terminalwriter.get_terminal_width", lambda: 80) 22 | monkeypatch.setenv("COLUMNS", str(80)) 23 | result_verbose = example.runpytest("-v", "--print") 24 | result_verbose.assert_outcomes(passed=2) 25 | 26 | found = extract_printer_text(result_verbose.outlines) 27 | expected = """test_a.py::test_global_peace 28 | attempt global peace 29 | here we have global peace 30 | 31 | test_a.py::test_global_peace PASSED 32 | test_a.py::test_server_parallel_requests 33 | create virtual environment 34 | start server from virtual env 35 | do the parallel request test 36 | 37 | test_a.py::test_server_parallel_requests PASSED 38 | teardown global peace 39 | """ 40 | assert found == expected 41 | 42 | 43 | def test_progress_v_relative(example: pytest.Testdir) -> None: 44 | result_verbose_relative = example.runpytest( 45 | "--print", 46 | "-v", 47 | "--print-relative-time", 48 | "-k", 49 | "test_server_parallel_requests", 50 | ) 51 | out = "\n".join(result_verbose_relative.outlines) 52 | result_verbose_relative.assert_outcomes(passed=1) 53 | 54 | assert "a.py::test_server_parallel_requests " in out, out 55 | output = (i.split("\t") for i in result_verbose_relative.outlines if i.startswith("\t")) 56 | found = [(float(relative), msg) for _, relative, msg in output] 57 | 58 | test = [m for _, m in sorted(i for i in found if "peace" not in i[1])] 59 | assert test == [ 60 | "create virtual environment", 61 | "start server from virtual env", 62 | "do the parallel request test", 63 | ], test 64 | 65 | session = [m for _, m in sorted(i for i in found if "peace" in i[1])] 66 | assert session == [ 67 | "attempt global peace", 68 | "teardown global peace", 69 | ], session 70 | 71 | 72 | def test_progress_no_v_but_with_print_request(example: pytest.Testdir) -> None: 73 | result = example.runpytest("--print") 74 | result.assert_outcomes(passed=2) 75 | assert " start server from virtual env" in result.outlines 76 | assert " attempt global peace" in result.outlines 77 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | fix 6 | 3.13 7 | 3.12 8 | 3.11 9 | 3.10 10 | 3.9 11 | 3.8 12 | type 13 | pkg_meta 14 | skip_missing_interpreters = true 15 | 16 | [testenv] 17 | description = run the tests with pytest 18 | package = wheel 19 | wheel_build_env = .pkg 20 | extras = 21 | test 22 | set_env = 23 | COVERAGE_FILE = {env:COVERAGE_FILE:{work_dir}{/}.coverage.{env_name}} 24 | commands = 25 | coverage erase 26 | coverage run -m pytest {tty:--color=yes} \ 27 | --junitxml {work_dir}{/}junit.{env_name}.xml \ 28 | {posargs:tests} 29 | coverage combine 30 | coverage report 31 | coverage html -d {env_tmp_dir}{/}htmlcov 32 | 33 | [testenv:fix] 34 | description = run static analysis and style check using flake8 35 | skip_install = true 36 | deps = 37 | pre-commit-uv>=4.1 38 | pass_env = 39 | HOMEPATH 40 | PROGRAMDATA 41 | commands = 42 | pre-commit run --all-files --show-diff-on-failure 43 | 44 | [testenv:type] 45 | description = run type check on code base 46 | deps = 47 | mypy==1.11.2 48 | commands = 49 | mypy --strict src 50 | mypy --strict tests 51 | 52 | [testenv:pkg_meta] 53 | description = check that the long description is valid 54 | skip_install = true 55 | deps = 56 | check-wheel-contents>=0.6 57 | twine>=5.1.1 58 | uv>=0.4.10 59 | commands = 60 | uv build --sdist --wheel --out-dir {env_tmp_dir} . 61 | twine check {env_tmp_dir}{/}* 62 | check-wheel-contents --no-config {env_tmp_dir} 63 | 64 | [testenv:dev] 65 | description = generate a DEV environment 66 | package = editable 67 | extras = 68 | test 69 | commands = 70 | uv pip tree 71 | python -c 'import sys; print(sys.executable)' 72 | --------------------------------------------------------------------------------