├── .codecov.yml ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── demo.py.png ├── devtools ├── __init__.py ├── __main__.py ├── ansi.py ├── debug.py ├── prettier.py ├── py.typed ├── pytest_plugin.py ├── timer.py ├── utils.py └── version.py ├── docs ├── examples │ ├── ansi_colours.py │ ├── complex.py │ ├── example.py │ ├── other.py │ ├── prettier.py │ └── return_args.py ├── history.md ├── index.md ├── install.md ├── plugins.py ├── requirements.txt ├── theme │ └── customization.css └── usage.md ├── mkdocs.yml ├── pyproject.toml ├── requirements ├── all.txt ├── docs.in ├── docs.txt ├── linting.in ├── linting.txt ├── pyproject.txt ├── testing.in └── testing.txt └── tests ├── __init__.py ├── conftest.py ├── test_ansi.py ├── test_custom_pretty.py ├── test_expr_render.py ├── test_insert_assert.py ├── test_main.py ├── test_prettier.py ├── test_timer.py ├── test_utils.py └── utils.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | range: [95, 100] 4 | status: 5 | patch: false 6 | project: false 7 | 8 | comment: 9 | layout: 'header, diff, flags, files, footer' 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: samuelcolvin 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '**' 9 | pull_request: {} 10 | 11 | env: 12 | COLUMNS: 150 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.10' 24 | 25 | - run: pip install -r requirements/linting.txt -r requirements/pyproject.txt 26 | 27 | - run: mypy devtools 28 | 29 | - uses: pre-commit/action@v3.0.0 30 | with: 31 | extra_args: --all-files --verbose 32 | 33 | docs-build: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - uses: actions/setup-python@v4 39 | with: 40 | python-version: '3.10' 41 | 42 | - run: pip install -r requirements/docs.txt -r requirements/pyproject.txt 43 | - run: pip install . 44 | 45 | - run: make docs 46 | 47 | - name: Store docs site 48 | uses: actions/upload-artifact@v3 49 | with: 50 | name: docs 51 | path: site 52 | 53 | test: 54 | name: test py${{ matrix.python-version }} on ${{ matrix.os }} 55 | strategy: 56 | fail-fast: false 57 | matrix: 58 | os: [ubuntu, macos, windows] 59 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] 60 | 61 | env: 62 | PYTHON: ${{ matrix.python-version }} 63 | OS: ${{ matrix.os }} 64 | EXTRAS: yes 65 | 66 | runs-on: ${{ matrix.os }}-latest 67 | 68 | steps: 69 | - uses: actions/checkout@v3 70 | 71 | - name: set up python 72 | uses: actions/setup-python@v4 73 | with: 74 | python-version: ${{ matrix.python-version }} 75 | 76 | - run: pip install -r requirements/testing.txt -r requirements/pyproject.txt 77 | 78 | - run: pip freeze 79 | 80 | - name: test with extras 81 | run: make test 82 | 83 | - run: coverage xml 84 | 85 | - uses: codecov/codecov-action@v3 86 | with: 87 | file: ./coverage.xml 88 | env_vars: EXTRAS,PYTHON,OS 89 | 90 | - name: uninstall extras 91 | run: pip uninstall -y multidict numpy pydantic asyncpg sqlalchemy 92 | 93 | - name: test without extras 94 | run: make test 95 | 96 | - run: coverage xml 97 | 98 | - uses: codecov/codecov-action@v3 99 | with: 100 | file: ./coverage.xml 101 | env_vars: EXTRAS,PYTHON,OS 102 | env: 103 | EXTRAS: no 104 | 105 | # https://github.com/marketplace/actions/alls-green#why used for branch protection checks 106 | check: 107 | if: always() 108 | needs: [test, lint, docs-build] 109 | runs-on: ubuntu-latest 110 | steps: 111 | - name: Decide whether the needed jobs succeeded or failed 112 | uses: re-actors/alls-green@release/v1 113 | with: 114 | jobs: ${{ toJSON(needs) }} 115 | 116 | deploy: 117 | needs: 118 | - check 119 | if: "success() && startsWith(github.ref, 'refs/tags/')" 120 | runs-on: ubuntu-latest 121 | 122 | steps: 123 | - uses: actions/checkout@v3 124 | 125 | - name: set up python 126 | uses: actions/setup-python@v4 127 | with: 128 | python-version: '3.10' 129 | 130 | - name: get docs 131 | uses: actions/download-artifact@v3 132 | with: 133 | name: docs 134 | path: site 135 | 136 | - name: check GITHUB_REF matches package version 137 | id: check-tag 138 | uses: samuelcolvin/check-python-version@v4.1 139 | with: 140 | version_file_path: devtools/version.py 141 | 142 | - name: install 143 | run: pip install build twine 144 | 145 | - name: build 146 | run: python -m build 147 | 148 | - run: twine check dist/* 149 | 150 | - name: upload to pypi 151 | run: twine upload dist/* 152 | env: 153 | TWINE_USERNAME: __token__ 154 | TWINE_PASSWORD: ${{ secrets.pypi_token }} 155 | 156 | - name: publish docs 157 | if: '!fromJSON(steps.check-tag.outputs.IS_PRERELEASE)' 158 | uses: cloudflare/wrangler-action@2.0.0 159 | with: 160 | apiToken: ${{ secrets.cloudflare_api_token }} 161 | command: pages publish --project-name=python-devtools --branch=main site 162 | env: 163 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.cloudflare_account_id }} 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | env/ 3 | env*/ 4 | *.py[cod] 5 | *.egg-info/ 6 | dist/ 7 | .cache/ 8 | .mypy_cache/ 9 | test.py 10 | .coverage 11 | htmlcov/ 12 | benchmarks/*.json 13 | docs/_build/ 14 | /docs/.*.md 15 | docs/examples/*.html 16 | old-version/ 17 | *.swp 18 | /site/ 19 | /site.zip 20 | /build/ 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | 10 | - repo: local 11 | hooks: 12 | - id: ruff 13 | name: Ruff 14 | entry: ruff 15 | args: [--fix, --exit-non-zero-on-fix] 16 | types: [python] 17 | language: system 18 | files: ^devtools/|^tests/ 19 | - id: black 20 | name: Black 21 | entry: black 22 | types: [python] 23 | language: system 24 | files: ^devtools/|^tests/ 25 | exclude: test_expr_render.py 26 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## v0.12.1 (2023-08-17) 2 | 3 | fix docs release 4 | 5 | ## v0.12.0 (2023-08-17) 6 | 7 | * build docs on CI by @samuelcolvin in #127 8 | * Update usage to reflect the recent addition of the pytest plugin by @tomhamiltonstubber in #128 9 | * support dataclasses with slots by @samuelcolvin in #136 10 | * Make `Pygments` required #137 11 | 12 | ## v0.11.0 (2023-04-05) 13 | 14 | * added support for sqlalchemy2 by @the-vty in #120 15 | * switch to ruff by @samuelcolvin in #124 16 | * support displaying ast types by @samuelcolvin in #125 17 | * Insert assert by @samuelcolvin in #126 18 | 19 | ## v0.10.0 (2022-11-28) 20 | 21 | * Use secure builtins standard module, instead of the `__builtins__` by @0xsirsaif in #109 22 | * upgrade executing to fix 3.10 by @samuelcolvin in #110 23 | * Fix windows build by @samuelcolvin in #111 24 | * Allow executing dependency to be >1.0.0 by @staticf0x in #115 25 | * more precise timer summary by @banteg in #113 26 | * Python 3.11 by @samuelcolvin in #118 27 | 28 | ## v0.9.0 (2022-07-26) 29 | 30 | * fix format of nested dataclasses, #99 thanks @aliereno 31 | * Moving to `pyproject.toml`, complete type hints and test with mypy, #107 32 | * add `install` command to add `debug` to `__builtins__`, #108 33 | 34 | ## v0.8.0 (2021-09-29) 35 | 36 | * test with python 3.10 #91 37 | * display `SQLAlchemy` objects nicely #94 38 | * fix tests on windows #93 39 | * show function `qualname` #95 40 | * cache pygments loading (significant speedup) #96 41 | 42 | ## v0.7.0 (2021-09-03) 43 | 44 | * switch to [`executing`](https://pypi.org/project/executing/) and [`asttokens`](https://pypi.org/project/asttokens/) 45 | for finding and printing debug arguments, #82, thanks @alexmojaki 46 | * correct changelog links, #76, thanks @Cielquan 47 | * return `debug()` arguments, #87 48 | * display more generators like `map` and `filter`, #88 49 | * display `Counter` and similar dict-like objects properly, #88 50 | * display `dataclasses` properly, #88 51 | * uprev test dependencies, #81, #83, #90 52 | 53 | ## v0.6.1 (2020-10-22) 54 | 55 | compatibility with python 3.8.6 56 | 57 | ## v0.6.0 (2020-07-29) 58 | 59 | * improve `__pretty__` to work better with pydantic classes, #52 60 | * improve the way statement ranges are calculated, #58 61 | * drastically improve import time, #50 62 | * pretty printing for non-standard dicts, #60 63 | * better statement finding for multi-line statements, #61 64 | * colors in windows, #57 65 | * fix `debug(type(dict(...)))`, #62 66 | 67 | ## v0.5.1 (2019-10-09) 68 | 69 | * fix python tag in `setup.cfg`, #46 70 | 71 | ## v0.5.0 (2019-01-03) 72 | 73 | * support `MultiDict`, #34 74 | * support `__pretty__` method, #36 75 | 76 | ## v0.4.0 (2018-12-29) 77 | 78 | * remove use of `warnings`, include in output, #30 79 | * fix rendering errors #31 80 | * better str and bytes wrapping #32 81 | * add `len` everywhere possible, part of #16 82 | 83 | ## v0.3.0 (2017-10-11) 84 | 85 | * allow `async/await` arguments 86 | * fix subscript 87 | * fix weird named tuples eg. `mock > call_args` 88 | * add `timer` 89 | 90 | ## v0.2.0 (2017-09-14) 91 | 92 | * improve output 93 | * numerous bug fixes 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 to present Samuel Colvin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | sources = devtools tests docs/plugins.py 3 | 4 | .PHONY: install 5 | install: 6 | python -m pip install -U pip pre-commit 7 | pip install -U -r requirements/all.txt 8 | pip install -e . 9 | pre-commit install 10 | 11 | .PHONY: refresh-lockfiles 12 | refresh-lockfiles: 13 | find requirements/ -name '*.txt' ! -name 'all.txt' -type f -delete 14 | make update-lockfiles 15 | 16 | .PHONY: update-lockfiles 17 | update-lockfiles: 18 | @echo "Updating requirements/*.txt files using pip-compile" 19 | pip-compile -q --resolver backtracking -o requirements/linting.txt requirements/linting.in 20 | pip-compile -q --resolver backtracking -o requirements/testing.txt requirements/testing.in 21 | pip-compile -q --resolver backtracking -o requirements/docs.txt requirements/docs.in 22 | pip-compile -q --resolver backtracking -o requirements/pyproject.txt pyproject.toml 23 | pip install --dry-run -r requirements/all.txt 24 | 25 | .PHONY: format 26 | format: 27 | black $(sources) 28 | ruff $(sources) --fix-only 29 | 30 | .PHONY: lint 31 | lint: 32 | black $(sources) --check --diff 33 | ruff $(sources) 34 | mypy devtools 35 | 36 | .PHONY: test 37 | test: 38 | coverage run -m pytest 39 | 40 | .PHONY: testcov 41 | testcov: 42 | coverage run -m pytest 43 | @echo "building coverage html" 44 | @coverage html 45 | 46 | .PHONY: all 47 | all: lint testcov 48 | 49 | .PHONY: clean 50 | clean: 51 | rm -rf `find . -name __pycache__` 52 | rm -f `find . -type f -name '*.py[co]' ` 53 | rm -f `find . -type f -name '*~' ` 54 | rm -f `find . -type f -name '.*~' ` 55 | rm -rf .cache 56 | rm -rf htmlcov 57 | rm -rf *.egg-info 58 | rm -f .coverage 59 | rm -f .coverage.* 60 | rm -rf build 61 | python setup.py clean 62 | make -C docs clean 63 | 64 | .PHONY: docs 65 | docs: 66 | ruff --line-length=80 docs/examples/ 67 | mkdocs build 68 | 69 | .PHONY: docs-serve 70 | docs-serve: 71 | mkdocs serve 72 | 73 | .PHONY: publish-docs 74 | publish-docs: docs 75 | zip -r site.zip site 76 | @curl -H "Content-Type: application/zip" -H "Authorization: Bearer ${NETLIFY}" \ 77 | --data-binary "@site.zip" https://api.netlify.com/api/v1/sites/python-devtools.netlify.com/deploys 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python devtools 2 | 3 | [![CI](https://github.com/samuelcolvin/python-devtools/workflows/CI/badge.svg?event=push)](https://github.com/samuelcolvin/python-devtools/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) 4 | [![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/main/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) 5 | [![pypi](https://img.shields.io/pypi/v/devtools.svg)](https://pypi.python.org/pypi/devtools) 6 | [![versions](https://img.shields.io/pypi/pyversions/devtools.svg)](https://github.com/samuelcolvin/python-devtools) 7 | [![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/main/LICENSE) 8 | 9 | **Python's missing debug print command and other development tools.** 10 | 11 | For more information, see [documentation](https://python-devtools.helpmanual.io/). 12 | 13 | ## Install 14 | 15 | Just 16 | 17 | ```bash 18 | pip install devtools 19 | ``` 20 | 21 | If you've got python 3.7+ and `pip` installed, you're good to go. 22 | 23 | ## Usage 24 | 25 | ```py 26 | from devtools import debug 27 | 28 | whatever = [1, 2, 3] 29 | debug(whatever) 30 | ``` 31 | 32 | Outputs: 33 | 34 | ```py 35 | test.py:4 : 36 | whatever: [1, 2, 3] (list) 37 | ``` 38 | 39 | 40 | That's only the tip of the iceberg, for example: 41 | 42 | ```py 43 | import numpy as np 44 | 45 | data = { 46 | 'foo': np.array(range(20)), 47 | 'bar': {'apple', 'banana', 'carrot', 'grapefruit'}, 48 | 'spam': [{'a': i, 'b': (i for i in range(3))} for i in range(3)], 49 | 'sentence': 'this is just a boring sentence.\n' * 4 50 | } 51 | 52 | debug(data) 53 | ``` 54 | 55 | outputs: 56 | 57 | ![python-devtools demo](https://raw.githubusercontent.com/samuelcolvin/python-devtools/main/demo.py.png) 58 | 59 | ## Usage without Import 60 | 61 | devtools can be used without `from devtools import debug` if you add `debug` into `__builtins__` 62 | in `sitecustomize.py`. 63 | 64 | For instructions on adding `debug` to `__builtins__`, 65 | see the [installation docs](https://python-devtools.helpmanual.io/usage/#usage-without-import). 66 | -------------------------------------------------------------------------------- /demo.py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiangolo/python-devtools/ec406ffdd841f65b132e81f3d715321d3cfb5efa/demo.py.png -------------------------------------------------------------------------------- /devtools/__init__.py: -------------------------------------------------------------------------------- 1 | from .ansi import sformat, sprint 2 | from .debug import Debug, debug 3 | from .prettier import PrettyFormat, pformat, pprint 4 | from .timer import Timer 5 | from .version import VERSION 6 | 7 | __version__ = VERSION 8 | 9 | __all__ = 'sformat', 'sprint', 'Debug', 'debug', 'PrettyFormat', 'pformat', 'pprint', 'Timer', 'VERSION' 10 | -------------------------------------------------------------------------------- /devtools/__main__.py: -------------------------------------------------------------------------------- 1 | import builtins 2 | import sys 3 | from pathlib import Path 4 | 5 | from .version import VERSION 6 | 7 | # language=python 8 | install_code = """ 9 | # add devtools `debug` function to builtins 10 | # we don't want to import devtools until it's required since it breaks pytest, hence this proxy 11 | class DebugProxy: 12 | def __init__(self): 13 | self._debug = None 14 | 15 | def _import_debug(self): 16 | if self._debug is None: 17 | from devtools import debug 18 | self._debug = debug 19 | 20 | def __call__(self, *args, **kwargs): 21 | self._import_debug() 22 | kwargs['frame_depth_'] = 3 23 | return self._debug(*args, **kwargs) 24 | 25 | def format(self, *args, **kwargs): 26 | self._import_debug() 27 | kwargs['frame_depth_'] = 3 28 | return self._debug.format(*args, **kwargs) 29 | 30 | def __getattr__(self, item): 31 | self._import_debug() 32 | return getattr(self._debug, item) 33 | 34 | import builtins 35 | setattr(builtins, 'debug', DebugProxy()) 36 | """ 37 | 38 | 39 | def print_code() -> int: 40 | print(install_code) 41 | return 0 42 | 43 | 44 | def install() -> int: 45 | try: 46 | import sitecustomize # type: ignore 47 | except ImportError: 48 | paths = [Path(p) for p in sys.path] 49 | try: 50 | path = next(p for p in paths if p.is_dir() and p.name == 'site-packages') 51 | except StopIteration: 52 | # what else makes sense to try? 53 | print(f'unable to file a suitable path to save `sitecustomize.py` to from sys.path: {paths}') 54 | return 1 55 | else: 56 | install_path = path / 'sitecustomize.py' 57 | else: 58 | install_path = Path(sitecustomize.__file__) 59 | 60 | if hasattr(builtins, 'debug'): 61 | print(f'Looks like devtools is already installed, probably in `{install_path}`.') 62 | return 0 63 | 64 | print(f'Found path `{install_path}` to install devtools into `builtins`') 65 | print('To install devtools, run the following command:\n') 66 | print(f' python -m devtools print-code >> {install_path}\n') 67 | if not install_path.is_relative_to(Path.home()): 68 | print('or maybe\n') 69 | print(f' python -m devtools print-code | sudo tee -a {install_path} > /dev/null\n') 70 | print('Note: "sudo" might be required because the path is in your home directory.') 71 | 72 | return 0 73 | 74 | 75 | if __name__ == '__main__': 76 | if 'install' in sys.argv: 77 | sys.exit(install()) 78 | elif 'print-code' in sys.argv: 79 | sys.exit(print_code()) 80 | else: 81 | print(f'python-devtools v{VERSION}, CLI usage: `python -m devtools install|print-code`') 82 | sys.exit(1) 83 | -------------------------------------------------------------------------------- /devtools/ansi.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from .utils import isatty 4 | 5 | __all__ = 'sformat', 'sprint' 6 | 7 | MYPY = False 8 | if MYPY: 9 | from typing import Any, Mapping, Union 10 | 11 | 12 | def strip_ansi(value: str) -> str: 13 | import re 14 | 15 | return re.sub('\033\\[((?:\\d|;)*)([a-zA-Z])', '', value) 16 | 17 | 18 | class Style(IntEnum): 19 | reset = 0 20 | 21 | bold = 1 22 | not_bold = 22 23 | 24 | dim = 2 25 | not_dim = 22 26 | 27 | italic = 3 28 | not_italic = 23 29 | 30 | underline = 4 31 | not_underline = 24 32 | 33 | blink = 5 34 | not_blink = 25 35 | 36 | reverse = 7 37 | not_reverse = 27 38 | 39 | strike_through = 9 40 | not_strike_through = 29 41 | 42 | # foreground colours 43 | black = 30 44 | red = 31 45 | green = 32 46 | yellow = 33 47 | blue = 34 48 | magenta = 35 49 | cyan = 36 50 | white = 37 51 | 52 | # background colours 53 | bg_black = 40 54 | bg_red = 41 55 | bg_green = 42 56 | bg_yellow = 43 57 | bg_blue = 44 58 | bg_magenta = 45 59 | bg_cyan = 46 60 | bg_white = 47 61 | 62 | # this is a meta value used for the "Style" instance which is the "style" function 63 | function = -1 64 | 65 | def __call__(self, input: 'Any', *styles: 'Union[Style, int, str]', reset: bool = True, apply: bool = True) -> str: 66 | """ 67 | Styles text with ANSI styles and returns the new string. 68 | 69 | By default the styling is cleared at the end of the string, this can be prevented with``reset=False``. 70 | 71 | Examples:: 72 | 73 | print(sformat('Hello World!', sformat.green)) 74 | print(sformat('ATTENTION!', sformat.bg_magenta)) 75 | print(sformat('Some things', sformat.reverse, sformat.bold)) 76 | 77 | :param input: the object to style with ansi codes. 78 | :param *styles: zero or more styles to apply to the text, should be either style instances or strings 79 | matching style names. 80 | :param reset: if False the ansi reset code is not appended to the end of the string 81 | :param: apply: if False no ansi codes are applied 82 | """ 83 | text = str(input) 84 | if not apply: 85 | return text 86 | codes = [] 87 | for s in styles: 88 | # raw ints are allowed 89 | if not isinstance(s, self.__class__) and not isinstance(s, int): 90 | try: 91 | s = self.styles[s] 92 | except KeyError: 93 | raise ValueError(f'invalid style "{s}"') 94 | codes.append(_style_as_int(s.value)) # type: ignore 95 | 96 | if codes: 97 | r = _as_ansi(';'.join(codes)) + text 98 | else: 99 | r = text 100 | 101 | if reset: 102 | r += _as_ansi(_style_as_int(self.reset)) 103 | return r 104 | 105 | @property 106 | def styles(self) -> 'Mapping[str, Style]': 107 | return self.__class__.__members__ 108 | 109 | def __repr__(self) -> str: 110 | if self == self.function: 111 | return '' 112 | else: 113 | return super().__repr__() 114 | 115 | def __str__(self) -> str: 116 | if self == self.function: 117 | return repr(self) 118 | else: 119 | # this matches `super().__str__()` in python 3.7 - 3.10 120 | # required since IntEnum.__str__ was changed in 3.11, 121 | # see https://docs.python.org/3/library/enum.html#enum.IntEnum 122 | return f'{self.__class__.__name__}.{self._name_}' 123 | 124 | 125 | def _style_as_int(v: 'Union[Style, int]') -> str: 126 | if isinstance(v, Style): 127 | return str(v.value) 128 | else: 129 | return str(v) 130 | 131 | 132 | def _as_ansi(s: str) -> str: 133 | return f'\033[{s}m' 134 | 135 | 136 | sformat = Style(-1) 137 | 138 | 139 | class StylePrint: 140 | """ 141 | Annoyingly enums do not allow inheritance, a lazy design mistake, this is an ugly work around 142 | for that mistake. 143 | """ 144 | 145 | def __call__( 146 | self, 147 | input: str, 148 | *styles: 'Union[Style, int, str]', 149 | reset: bool = True, 150 | flush: bool = True, 151 | file: 'Any' = None, 152 | **print_kwargs: 'Any', 153 | ) -> None: 154 | text = sformat(input, *styles, reset=reset, apply=isatty(file)) 155 | print(text, flush=flush, file=file, **print_kwargs) 156 | 157 | def __getattr__(self, item: str) -> str: 158 | return getattr(sformat, item) 159 | 160 | def __repr__(self) -> str: 161 | return '' 162 | 163 | 164 | sprint = StylePrint() 165 | -------------------------------------------------------------------------------- /devtools/debug.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from .ansi import sformat 5 | from .prettier import PrettyFormat 6 | from .timer import Timer 7 | from .utils import env_bool, env_true, is_literal, use_highlight 8 | 9 | __all__ = 'Debug', 'debug' 10 | MYPY = False 11 | if MYPY: 12 | from types import FrameType 13 | from typing import Any, Generator, List, Optional, Union 14 | 15 | pformat = PrettyFormat( 16 | indent_step=int(os.getenv('PY_DEVTOOLS_INDENT', 4)), 17 | simple_cutoff=int(os.getenv('PY_DEVTOOLS_SIMPLE_CUTOFF', 10)), 18 | width=int(os.getenv('PY_DEVTOOLS_WIDTH', 120)), 19 | yield_from_generators=env_true('PY_DEVTOOLS_YIELD_FROM_GEN', True), 20 | ) 21 | # required for type hinting because I (stupidly) added methods called `str` 22 | StrType = str 23 | 24 | 25 | class DebugArgument: 26 | __slots__ = 'value', 'name', 'extra' 27 | 28 | def __init__(self, value: 'Any', *, name: 'Optional[str]' = None, **extra: 'Any') -> None: 29 | self.value = value 30 | self.name = name 31 | self.extra = [] 32 | try: 33 | length = len(value) 34 | except TypeError: 35 | pass 36 | else: 37 | self.extra.append(('len', length)) 38 | self.extra += [(k, v) for k, v in extra.items() if v is not None] 39 | 40 | def str(self, highlight: bool = False) -> StrType: 41 | s = '' 42 | if self.name and not is_literal(self.name): 43 | s = f'{sformat(self.name, sformat.blue, apply=highlight)}: ' 44 | 45 | suffix = sformat( 46 | f" ({self.value.__class__.__name__}){''.join(f' {k}={v}' for k, v in self.extra)}", 47 | sformat.dim, 48 | apply=highlight, 49 | ) 50 | try: 51 | s += pformat(self.value, indent=4, highlight=highlight) 52 | except Exception as exc: 53 | v = sformat(f'!!! error pretty printing value: {exc!r}', sformat.yellow, apply=highlight) 54 | s += f'{self.value!r}{suffix}\n {v}' 55 | else: 56 | s += suffix 57 | return s 58 | 59 | def __str__(self) -> StrType: 60 | return self.str() 61 | 62 | 63 | class DebugOutput: 64 | """ 65 | Represents the output of a debug command. 66 | """ 67 | 68 | arg_class = DebugArgument 69 | __slots__ = 'filename', 'lineno', 'frame', 'arguments', 'warning' 70 | 71 | def __init__( 72 | self, 73 | *, 74 | filename: str, 75 | lineno: int, 76 | frame: str, 77 | arguments: 'List[DebugArgument]', 78 | warning: 'Union[None, str, bool]' = None, 79 | ) -> None: 80 | self.filename = filename 81 | self.lineno = lineno 82 | self.frame = frame 83 | self.arguments = arguments 84 | self.warning = warning 85 | 86 | def str(self, highlight: bool = False) -> StrType: 87 | if highlight: 88 | prefix = ( 89 | f'{sformat(self.filename, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' 90 | f'{sformat(self.frame, sformat.green, sformat.italic)}' 91 | ) 92 | if self.warning: 93 | prefix += sformat(f' ({self.warning})', sformat.dim) 94 | else: 95 | prefix = f'{self.filename}:{self.lineno} {self.frame}' 96 | if self.warning: 97 | prefix += f' ({self.warning})' 98 | return f'{prefix}\n ' + '\n '.join(a.str(highlight) for a in self.arguments) 99 | 100 | def __str__(self) -> StrType: 101 | return self.str() 102 | 103 | def __repr__(self) -> StrType: 104 | arguments = ' '.join(str(a) for a in self.arguments) 105 | return f'' 106 | 107 | 108 | class Debug: 109 | output_class = DebugOutput 110 | 111 | def __init__(self, *, warnings: 'Optional[bool]' = None, highlight: 'Optional[bool]' = None): 112 | self._show_warnings = env_bool(warnings, 'PY_DEVTOOLS_WARNINGS', True) 113 | self._highlight = highlight 114 | 115 | def __call__( 116 | self, 117 | *args: 'Any', 118 | file_: 'Any' = None, 119 | flush_: bool = True, 120 | frame_depth_: int = 2, 121 | **kwargs: 'Any', 122 | ) -> 'Any': 123 | d_out = self._process(args, kwargs, frame_depth_) 124 | s = d_out.str(use_highlight(self._highlight, file_)) 125 | print(s, file=file_, flush=flush_) 126 | if kwargs: 127 | return (*args, kwargs) 128 | elif len(args) == 1: 129 | return args[0] 130 | else: 131 | return args 132 | 133 | def format(self, *args: 'Any', frame_depth_: int = 2, **kwargs: 'Any') -> DebugOutput: 134 | return self._process(args, kwargs, frame_depth_) 135 | 136 | def breakpoint(self) -> None: 137 | import pdb 138 | 139 | pdb.Pdb(skip=['devtools.*']).set_trace() 140 | 141 | def timer(self, name: 'Optional[str]' = None, *, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> Timer: 142 | return Timer(name=name, verbose=verbose, file=file, dp=dp) 143 | 144 | def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: 145 | """ 146 | BEWARE: this must be called from a function exactly `frame_depth` levels below the top of the stack. 147 | """ 148 | # HELP: any errors other than ValueError from _getframe? If so please submit an issue 149 | try: 150 | call_frame: 'FrameType' = sys._getframe(frame_depth) 151 | except ValueError: 152 | # "If [ValueError] is deeper than the call stack, ValueError is raised" 153 | return self.output_class( 154 | filename='', 155 | lineno=0, 156 | frame='', 157 | arguments=list(self._args_inspection_failed(args, kwargs)), 158 | warning=self._show_warnings and 'error parsing code, call stack too shallow', 159 | ) 160 | 161 | function = call_frame.f_code.co_name 162 | 163 | from pathlib import Path 164 | 165 | path = Path(call_frame.f_code.co_filename) 166 | if path.is_absolute(): 167 | # make the path relative 168 | cwd = Path('.').resolve() 169 | try: 170 | path = path.relative_to(cwd) 171 | except ValueError: 172 | # happens if filename path is not within CWD 173 | pass 174 | 175 | lineno = call_frame.f_lineno 176 | warning = None 177 | 178 | import executing 179 | 180 | source = executing.Source.for_frame(call_frame) 181 | if not source.text: 182 | warning = 'no code context for debug call, code inspection impossible' 183 | arguments = list(self._args_inspection_failed(args, kwargs)) 184 | else: 185 | ex = source.executing(call_frame) 186 | function = ex.code_qualname() 187 | if not ex.node: 188 | warning = 'executing failed to find the calling node' 189 | arguments = list(self._args_inspection_failed(args, kwargs)) 190 | else: 191 | arguments = list(self._process_args(ex, args, kwargs)) 192 | 193 | return self.output_class( 194 | filename=str(path), 195 | lineno=lineno, 196 | frame=function, 197 | arguments=arguments, 198 | warning=self._show_warnings and warning, 199 | ) 200 | 201 | def _args_inspection_failed(self, args: 'Any', kwargs: 'Any') -> 'Generator[DebugArgument, None, None]': 202 | for arg in args: 203 | yield self.output_class.arg_class(arg) 204 | for name, value in kwargs.items(): 205 | yield self.output_class.arg_class(value, name=name) 206 | 207 | def _process_args(self, ex: 'Any', args: 'Any', kwargs: 'Any') -> 'Generator[DebugArgument, None, None]': 208 | import ast 209 | 210 | func_ast = ex.node 211 | atok = ex.source.asttokens() 212 | for arg, ast_arg in zip(args, func_ast.args): 213 | if isinstance(ast_arg, ast.Name): 214 | yield self.output_class.arg_class(arg, name=ast_arg.id) 215 | else: 216 | name = ' '.join(map(str.strip, atok.get_text(ast_arg).splitlines())) 217 | yield self.output_class.arg_class(arg, name=name) 218 | 219 | kw_arg_names = {} 220 | for kw in func_ast.keywords: 221 | if isinstance(kw.value, ast.Name): 222 | kw_arg_names[kw.arg] = kw.value.id 223 | 224 | for name, value in kwargs.items(): 225 | yield self.output_class.arg_class(value, name=name, variable=kw_arg_names.get(name)) 226 | 227 | 228 | debug = Debug() 229 | -------------------------------------------------------------------------------- /devtools/prettier.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import io 3 | import os 4 | from collections import OrderedDict 5 | from collections.abc import Generator 6 | 7 | from .utils import DataClassType, LaxMapping, SQLAlchemyClassType, env_true, isatty 8 | 9 | try: 10 | from functools import cache 11 | except ImportError: 12 | from functools import lru_cache 13 | 14 | cache = lru_cache() 15 | 16 | try: 17 | from sqlalchemy import inspect as sa_inspect 18 | except ImportError: 19 | sa_inspect = None # type: ignore[assignment] 20 | 21 | __all__ = 'PrettyFormat', 'pformat', 'pprint' 22 | MYPY = False 23 | if MYPY: 24 | from typing import Any, Callable, Iterable, List, Set, Tuple, Union 25 | 26 | PARENTHESES_LOOKUP = [ 27 | (list, '[', ']'), 28 | (set, '{', '}'), 29 | (frozenset, 'frozenset({', '})'), 30 | ] 31 | DEFAULT_WIDTH = int(os.getenv('PY_DEVTOOLS_WIDTH', 120)) 32 | MISSING = object() 33 | PRETTY_KEY = '__prettier_formatted_value__' 34 | 35 | 36 | def fmt(v: 'Any') -> 'Any': 37 | return {PRETTY_KEY: v} 38 | 39 | 40 | class SkipPretty(Exception): 41 | pass 42 | 43 | 44 | @cache 45 | def get_pygments() -> 'Tuple[Any, Any, Any]': 46 | try: 47 | import pygments 48 | from pygments.formatters import Terminal256Formatter 49 | from pygments.lexers import PythonLexer 50 | except ImportError: # pragma: no cover 51 | return None, None, None 52 | else: 53 | return pygments, PythonLexer(), Terminal256Formatter(style='vim') 54 | 55 | 56 | # common generator types (this is not exhaustive: things like chain are not include to avoid the import) 57 | generator_types = Generator, map, filter, zip, enumerate 58 | 59 | 60 | class PrettyFormat: 61 | def __init__( 62 | self, 63 | indent_step: int = 4, 64 | indent_char: str = ' ', 65 | repr_strings: bool = False, 66 | simple_cutoff: int = 10, 67 | width: int = 120, 68 | yield_from_generators: bool = True, 69 | ): 70 | self._indent_step = indent_step 71 | self._c = indent_char 72 | self._repr_strings = repr_strings 73 | self._repr_generators = not yield_from_generators 74 | self._simple_cutoff = simple_cutoff 75 | self._width = width 76 | self._type_lookup: 'List[Tuple[Any, Callable[[Any, str, int, int], None]]]' = [ 77 | (dict, self._format_dict), 78 | ((str, bytes), self._format_str_bytes), 79 | (tuple, self._format_tuples), 80 | ((list, set, frozenset), self._format_list_like), 81 | (bytearray, self._format_bytearray), 82 | (generator_types, self._format_generator), 83 | # put these last as the check can be slow 84 | (ast.AST, self._format_ast_expression), 85 | (LaxMapping, self._format_dict), 86 | (DataClassType, self._format_dataclass), 87 | (SQLAlchemyClassType, self._format_sqlalchemy_class), 88 | ] 89 | 90 | def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, highlight: bool = False) -> str: 91 | self._stream = io.StringIO() 92 | self._format(value, indent_current=indent, indent_first=indent_first) 93 | s = self._stream.getvalue() 94 | pygments, pyg_lexer, pyg_formatter = get_pygments() 95 | if highlight and pygments: 96 | # apparently highlight adds a trailing new line we don't want 97 | s = pygments.highlight(s, lexer=pyg_lexer, formatter=pyg_formatter).rstrip('\n') 98 | return s 99 | 100 | def _format(self, value: 'Any', indent_current: int, indent_first: bool) -> None: 101 | if indent_first: 102 | self._stream.write(indent_current * self._c) 103 | 104 | try: 105 | pretty_func = getattr(value, '__pretty__') 106 | except AttributeError: 107 | pass 108 | else: 109 | # `pretty_func.__class__.__name__ == 'method'` should only be true for bound methods, 110 | # `hasattr(pretty_func, '__self__')` is more canonical but weirdly is true for unbound cython functions 111 | from unittest.mock import _Call as MockCall 112 | 113 | if pretty_func.__class__.__name__ == 'method' and not isinstance(value, MockCall): 114 | try: 115 | gen = pretty_func(fmt=fmt, skip_exc=SkipPretty) 116 | self._render_pretty(gen, indent_current) 117 | except SkipPretty: 118 | pass 119 | else: 120 | return None 121 | 122 | value_repr = repr(value) 123 | if len(value_repr) <= self._simple_cutoff and not isinstance(value, generator_types): 124 | self._stream.write(value_repr) 125 | else: 126 | indent_new = indent_current + self._indent_step 127 | for t, func in self._type_lookup: 128 | if isinstance(value, t): 129 | func(value, value_repr, indent_current, indent_new) 130 | return None 131 | 132 | self._format_raw(value, value_repr, indent_current, indent_new) 133 | 134 | def _render_pretty(self, gen: 'Iterable[Any]', indent: int) -> None: 135 | prefix = False 136 | for v in gen: 137 | if isinstance(v, int) and v in {-1, 0, 1}: 138 | indent += v * self._indent_step 139 | prefix = True 140 | else: 141 | if prefix: 142 | self._stream.write('\n' + self._c * indent) 143 | prefix = False 144 | 145 | pretty_value = v.get(PRETTY_KEY, MISSING) if (isinstance(v, dict) and len(v) == 1) else MISSING 146 | if pretty_value is not MISSING: 147 | self._format(pretty_value, indent, False) 148 | elif isinstance(v, str): 149 | self._stream.write(v) 150 | else: 151 | # shouldn't happen but will 152 | self._stream.write(repr(v)) 153 | 154 | def _format_dict(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: 155 | open_, before_, split_, after_, close_ = '{\n', indent_new * self._c, ': ', ',\n', '}' 156 | if isinstance(value, OrderedDict): 157 | open_, split_, after_, close_ = 'OrderedDict([\n', ', ', '),\n', '])' 158 | before_ += '(' 159 | elif type(value) != dict: 160 | open_, close_ = f'<{value.__class__.__name__}({{\n', '})>' 161 | 162 | self._stream.write(open_) 163 | for k, v in value.items(): 164 | self._stream.write(before_) 165 | self._format(k, indent_new, False) 166 | self._stream.write(split_) 167 | self._format(v, indent_new, False) 168 | self._stream.write(after_) 169 | self._stream.write(indent_current * self._c + close_) 170 | 171 | def _format_list_like( 172 | self, value: 'Union[List[Any], Tuple[Any, ...], Set[Any]]', _: str, indent_current: int, indent_new: int 173 | ) -> None: 174 | open_, close_ = '(', ')' 175 | for t, *oc in PARENTHESES_LOOKUP: 176 | if isinstance(value, t): 177 | open_, close_ = oc 178 | break 179 | 180 | self._stream.write(open_ + '\n') 181 | for v in value: 182 | self._format(v, indent_new, True) 183 | self._stream.write(',\n') 184 | self._stream.write(indent_current * self._c + close_) 185 | 186 | def _format_tuples(self, value: 'Tuple[Any, ...]', value_repr: str, indent_current: int, indent_new: int) -> None: 187 | fields = getattr(value, '_fields', None) 188 | if fields: 189 | # named tuple 190 | self._format_fields(value, zip(fields, value), indent_current, indent_new) 191 | else: 192 | # normal tuples are just like other similar iterables 193 | self._format_list_like(value, value_repr, indent_current, indent_new) 194 | 195 | def _format_str_bytes( 196 | self, value: 'Union[str, bytes]', value_repr: str, indent_current: int, indent_new: int 197 | ) -> None: 198 | if self._repr_strings: 199 | self._stream.write(value_repr) 200 | else: 201 | lines = list(self._wrap_lines(value, indent_new)) 202 | if len(lines) > 1: 203 | self._str_lines(lines, indent_current, indent_new) 204 | else: 205 | self._stream.write(value_repr) 206 | 207 | def _str_lines(self, lines: 'Iterable[Union[str, bytes]]', indent_current: int, indent_new: int) -> None: 208 | self._stream.write('(\n') 209 | prefix = indent_new * self._c 210 | for line in lines: 211 | self._stream.write(prefix + repr(line) + '\n') 212 | self._stream.write(indent_current * self._c + ')') 213 | 214 | def _wrap_lines(self, s: 'Union[str, bytes]', indent_new: int) -> 'Generator[Union[str, bytes], None, None]': 215 | width = self._width - indent_new - 3 216 | for line in s.splitlines(True): 217 | start = 0 218 | for pos in range(width, len(line), width): 219 | yield line[start:pos] 220 | start = pos 221 | yield line[start:] 222 | 223 | def _format_generator( 224 | self, value: 'Generator[Any, None, None]', value_repr: str, indent_current: int, indent_new: int 225 | ) -> None: 226 | if self._repr_generators: 227 | self._stream.write(value_repr) 228 | else: 229 | name = value.__class__.__name__ 230 | if name == 'generator': 231 | # no name if the name is just "generator" 232 | self._stream.write('(\n') 233 | else: 234 | self._stream.write(f'{name}(\n') 235 | for v in value: 236 | self._format(v, indent_new, True) 237 | self._stream.write(',\n') 238 | self._stream.write(indent_current * self._c + ')') 239 | 240 | def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: 241 | self._stream.write('bytearray') 242 | lines = self._wrap_lines(bytes(value), indent_new) 243 | self._str_lines(lines, indent_current, indent_new) 244 | 245 | def _format_ast_expression(self, value: ast.AST, _: str, indent_current: int, indent_new: int) -> None: 246 | try: 247 | s = ast.dump(value, indent=self._indent_step) 248 | except TypeError: 249 | # no indent before 3.9 250 | s = ast.dump(value) 251 | lines = s.splitlines(True) 252 | self._stream.write(lines[0]) 253 | for line in lines[1:]: 254 | self._stream.write(indent_current * self._c + line) 255 | 256 | def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: 257 | try: 258 | field_items = value.__dict__.items() 259 | except AttributeError: 260 | # slots 261 | field_items = ((f, getattr(value, f)) for f in value.__slots__) 262 | self._format_fields(value, field_items, indent_current, indent_new) 263 | 264 | def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: 265 | if sa_inspect is not None: 266 | state = sa_inspect(value) 267 | deferred = state.unloaded 268 | else: 269 | deferred = set() 270 | 271 | fields = [ 272 | (field, getattr(value, field) if field not in deferred else '') 273 | for field in dir(value) 274 | if not (field.startswith('_') or field in ['metadata', 'registry']) 275 | ] 276 | self._format_fields(value, fields, indent_current, indent_new) 277 | 278 | def _format_raw(self, _: 'Any', value_repr: str, indent_current: int, indent_new: int) -> None: 279 | lines = value_repr.splitlines(True) 280 | if len(lines) > 1 or (len(value_repr) + indent_current) >= self._width: 281 | self._stream.write('(\n') 282 | wrap_at = self._width - indent_new 283 | prefix = indent_new * self._c 284 | 285 | from textwrap import wrap 286 | 287 | for line in lines: 288 | sub_lines = wrap(line, wrap_at) 289 | for sline in sub_lines: 290 | self._stream.write(prefix + sline + '\n') 291 | self._stream.write(indent_current * self._c + ')') 292 | else: 293 | self._stream.write(value_repr) 294 | 295 | def _format_fields( 296 | self, value: 'Any', fields: 'Iterable[Tuple[str, Any]]', indent_current: int, indent_new: int 297 | ) -> None: 298 | self._stream.write(f'{value.__class__.__name__}(\n') 299 | for field, v in fields: 300 | self._stream.write(indent_new * self._c) 301 | if field: # field is falsy sometimes for odd things like call_args 302 | self._stream.write(f'{field}=') 303 | self._format(v, indent_new, False) 304 | self._stream.write(',\n') 305 | self._stream.write(indent_current * self._c + ')') 306 | 307 | 308 | pformat = PrettyFormat() 309 | force_highlight = env_true('PY_DEVTOOLS_HIGHLIGHT', None) 310 | 311 | 312 | def pprint(s: 'Any', file: 'Any' = None) -> None: 313 | highlight = isatty(file) if force_highlight is None else force_highlight 314 | print(pformat(s, highlight=highlight), file=file, flush=True) 315 | -------------------------------------------------------------------------------- /devtools/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiangolo/python-devtools/ec406ffdd841f65b132e81f3d715321d3cfb5efa/devtools/py.typed -------------------------------------------------------------------------------- /devtools/pytest_plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations as _annotations 2 | 3 | import ast 4 | import builtins 5 | import sys 6 | import textwrap 7 | from contextvars import ContextVar 8 | from dataclasses import dataclass 9 | from enum import Enum 10 | from functools import lru_cache 11 | from itertools import groupby 12 | from pathlib import Path 13 | from types import FrameType 14 | from typing import TYPE_CHECKING, Any, Callable, Generator, Sized 15 | 16 | import pytest 17 | from executing import Source 18 | 19 | from . import debug 20 | 21 | if TYPE_CHECKING: 22 | pass 23 | 24 | __all__ = ('insert_assert',) 25 | 26 | 27 | @dataclass 28 | class ToReplace: 29 | file: Path 30 | start_line: int 31 | end_line: int | None 32 | code: str 33 | 34 | 35 | to_replace: list[ToReplace] = [] 36 | insert_assert_calls: ContextVar[int] = ContextVar('insert_assert_calls', default=0) 37 | insert_assert_summary: ContextVar[list[str]] = ContextVar('insert_assert_summary') 38 | 39 | 40 | def insert_assert(value: Any) -> int: 41 | call_frame: FrameType = sys._getframe(1) 42 | if sys.version_info < (3, 8): # pragma: no cover 43 | raise RuntimeError('insert_assert() requires Python 3.8+') 44 | 45 | format_code = load_black() 46 | ex = Source.for_frame(call_frame).executing(call_frame) 47 | if ex.node is None: # pragma: no cover 48 | python_code = format_code(str(custom_repr(value))) 49 | raise RuntimeError( 50 | f'insert_assert() was unable to find the frame from which it was called, called with:\n{python_code}' 51 | ) 52 | ast_arg = ex.node.args[0] # type: ignore[attr-defined] 53 | if isinstance(ast_arg, ast.Name): 54 | arg = ast_arg.id 55 | else: 56 | arg = ' '.join(map(str.strip, ex.source.asttokens().get_text(ast_arg).splitlines())) 57 | 58 | python_code = format_code(f'# insert_assert({arg})\nassert {arg} == {custom_repr(value)}') 59 | 60 | python_code = textwrap.indent(python_code, ex.node.col_offset * ' ') 61 | to_replace.append(ToReplace(Path(call_frame.f_code.co_filename), ex.node.lineno, ex.node.end_lineno, python_code)) 62 | calls = insert_assert_calls.get() + 1 63 | insert_assert_calls.set(calls) 64 | return calls 65 | 66 | 67 | def pytest_addoption(parser: Any) -> None: 68 | parser.addoption( 69 | '--insert-assert-print', 70 | action='store_true', 71 | default=False, 72 | help='Print statements that would be substituted for insert_assert(), instead of writing to files', 73 | ) 74 | parser.addoption( 75 | '--insert-assert-fail', 76 | action='store_true', 77 | default=False, 78 | help='Fail tests which include one or more insert_assert() calls', 79 | ) 80 | 81 | 82 | @pytest.fixture(scope='session', autouse=True) 83 | def insert_assert_add_to_builtins() -> None: 84 | try: 85 | setattr(builtins, 'insert_assert', insert_assert) 86 | # we also install debug here since the default script doesn't install it 87 | setattr(builtins, 'debug', debug) 88 | except TypeError: 89 | # happens on pypy 90 | pass 91 | 92 | 93 | @pytest.fixture(autouse=True) 94 | def insert_assert_maybe_fail(pytestconfig: pytest.Config) -> Generator[None, None, None]: 95 | insert_assert_calls.set(0) 96 | yield 97 | print_instead = pytestconfig.getoption('insert_assert_print') 98 | if not print_instead: 99 | count = insert_assert_calls.get() 100 | if count: 101 | pytest.fail(f'devtools-insert-assert: {count} assert{plural(count)} will be inserted', pytrace=False) 102 | 103 | 104 | @pytest.fixture(name='insert_assert') 105 | def insert_assert_fixture() -> Callable[[Any], int]: 106 | return insert_assert 107 | 108 | 109 | def pytest_report_teststatus(report: pytest.TestReport, config: pytest.Config) -> Any: 110 | if report.when == 'teardown' and report.failed and 'devtools-insert-assert:' in repr(report.longrepr): 111 | return 'insert assert', 'i', ('INSERT ASSERT', {'cyan': True}) 112 | 113 | 114 | @pytest.fixture(scope='session', autouse=True) 115 | def insert_assert_session(pytestconfig: pytest.Config) -> Generator[None, None, None]: 116 | """ 117 | Actual logic for updating code examples. 118 | """ 119 | try: 120 | __builtins__['insert_assert'] = insert_assert 121 | except TypeError: 122 | # happens on pypy 123 | pass 124 | 125 | yield 126 | 127 | if not to_replace: 128 | return None 129 | 130 | print_instead = pytestconfig.getoption('insert_assert_print') 131 | 132 | highlight = None 133 | if print_instead: 134 | highlight = get_pygments() 135 | 136 | files = 0 137 | dup_count = 0 138 | summary = [] 139 | for file, group in groupby(to_replace, key=lambda tr: tr.file): 140 | # we have to substitute lines in reverse order to avoid messing up line numbers 141 | lines = file.read_text().splitlines() 142 | duplicates: set[int] = set() 143 | for tr in sorted(group, key=lambda x: x.start_line, reverse=True): 144 | if print_instead: 145 | hr = '-' * 80 146 | code = highlight(tr.code) if highlight else tr.code 147 | line_no = f'{tr.start_line}' if tr.start_line == tr.end_line else f'{tr.start_line}-{tr.end_line}' 148 | summary.append(f'{file} - {line_no}:\n{hr}\n{code}{hr}\n') 149 | else: 150 | if tr.start_line in duplicates: 151 | dup_count += 1 152 | else: 153 | duplicates.add(tr.start_line) 154 | lines[tr.start_line - 1 : tr.end_line] = tr.code.splitlines() 155 | if not print_instead: 156 | file.write_text('\n'.join(lines)) 157 | files += 1 158 | prefix = 'Printed' if print_instead else 'Replaced' 159 | summary.append( 160 | f'{prefix} {len(to_replace)} insert_assert() call{plural(to_replace)} in {files} file{plural(files)}' 161 | ) 162 | if dup_count: 163 | summary.append( 164 | f'\n{dup_count} insert skipped because an assert statement on that line had already be inserted!' 165 | ) 166 | 167 | insert_assert_summary.set(summary) 168 | to_replace.clear() 169 | 170 | 171 | def pytest_terminal_summary() -> None: 172 | summary = insert_assert_summary.get(None) 173 | if summary: 174 | print('\n'.join(summary)) 175 | 176 | 177 | def custom_repr(value: Any) -> Any: 178 | if isinstance(value, (list, tuple, set, frozenset)): 179 | return value.__class__(map(custom_repr, value)) 180 | elif isinstance(value, dict): 181 | return value.__class__((custom_repr(k), custom_repr(v)) for k, v in value.items()) 182 | if isinstance(value, Enum): 183 | return PlainRepr(f'{value.__class__.__name__}.{value.name}') 184 | else: 185 | return PlainRepr(repr(value)) 186 | 187 | 188 | class PlainRepr(str): 189 | """ 190 | String class where repr doesn't include quotes. 191 | """ 192 | 193 | def __repr__(self) -> str: 194 | return str(self) 195 | 196 | 197 | def plural(v: int | Sized) -> str: 198 | if isinstance(v, (int, float)): 199 | n = v 200 | else: 201 | n = len(v) 202 | return '' if n == 1 else 's' 203 | 204 | 205 | @lru_cache(maxsize=None) 206 | def load_black() -> Callable[[str], str]: 207 | """ 208 | Build black configuration from "pyproject.toml". 209 | 210 | Black doesn't have a nice self-contained API for reading pyproject.toml, hence all this. 211 | """ 212 | try: 213 | from black import format_file_contents 214 | from black.files import find_pyproject_toml, parse_pyproject_toml 215 | from black.mode import Mode, TargetVersion 216 | from black.parsing import InvalidInput 217 | except ImportError: 218 | return lambda x: x 219 | 220 | def convert_target_version(target_version_config: Any) -> set[Any] | None: 221 | if target_version_config is not None: 222 | return None 223 | elif not isinstance(target_version_config, list): 224 | raise ValueError('Config key "target_version" must be a list') 225 | else: 226 | return {TargetVersion[tv.upper()] for tv in target_version_config} 227 | 228 | @dataclass 229 | class ConfigArg: 230 | config_name: str 231 | keyword_name: str 232 | converter: Callable[[Any], Any] 233 | 234 | config_mapping: list[ConfigArg] = [ 235 | ConfigArg('target_version', 'target_versions', convert_target_version), 236 | ConfigArg('line_length', 'line_length', int), 237 | ConfigArg('skip_string_normalization', 'string_normalization', lambda x: not x), 238 | ConfigArg('skip_magic_trailing_commas', 'magic_trailing_comma', lambda x: not x), 239 | ] 240 | 241 | config_str = find_pyproject_toml((str(Path.cwd()),)) 242 | mode_ = None 243 | fast = False 244 | if config_str: 245 | try: 246 | config = parse_pyproject_toml(config_str) 247 | except (OSError, ValueError) as e: 248 | raise ValueError(f'Error reading configuration file: {e}') 249 | 250 | if config: 251 | kwargs = dict() 252 | for config_arg in config_mapping: 253 | try: 254 | value = config[config_arg.config_name] 255 | except KeyError: 256 | pass 257 | else: 258 | value = config_arg.converter(value) 259 | if value is not None: 260 | kwargs[config_arg.keyword_name] = value 261 | 262 | mode_ = Mode(**kwargs) 263 | fast = bool(config.get('fast')) 264 | 265 | mode = mode_ or Mode() 266 | 267 | def format_code(code: str) -> str: 268 | try: 269 | return format_file_contents(code, fast=fast, mode=mode) 270 | except InvalidInput as e: 271 | print('black error, you will need to format the code manually,', e) 272 | return code 273 | 274 | return format_code 275 | 276 | 277 | # isatty() is false inside pytest, hence calling this now 278 | try: 279 | std_out_istty = sys.stdout.isatty() 280 | except Exception: 281 | std_out_istty = False 282 | 283 | 284 | @lru_cache(maxsize=None) 285 | def get_pygments() -> Callable[[str], str] | None: # pragma: no cover 286 | if not std_out_istty: 287 | return None 288 | try: 289 | import pygments 290 | from pygments.formatters import Terminal256Formatter 291 | from pygments.lexers import PythonLexer 292 | except ImportError as e: # pragma: no cover 293 | print(e) 294 | return None 295 | else: 296 | pyg_lexer, terminal_formatter = PythonLexer(), Terminal256Formatter() 297 | 298 | def highlight(code: str) -> str: 299 | return pygments.highlight(code, lexer=pyg_lexer, formatter=terminal_formatter) 300 | 301 | return highlight 302 | -------------------------------------------------------------------------------- /devtools/timer.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | __all__ = ('Timer',) 4 | 5 | MYPY = False 6 | if MYPY: 7 | from typing import Any, List, Optional 8 | 9 | # required for type hinting because I (stupidly) added methods called `str` 10 | StrType = str 11 | 12 | 13 | class TimerResult: 14 | def __init__(self, name: 'Optional[str]' = None, verbose: bool = True) -> None: 15 | self._name = name 16 | self.verbose = verbose 17 | self.finish: 'Optional[float]' = None 18 | self.start = perf_counter() 19 | 20 | def capture(self) -> None: 21 | self.finish = perf_counter() 22 | 23 | def elapsed(self) -> float: 24 | if self.finish: 25 | return self.finish - self.start 26 | else: 27 | return -1 28 | 29 | def str(self, dp: int = 3) -> StrType: 30 | if self._name: 31 | return f'{self._name}: {self.elapsed():0.{dp}f}s elapsed' 32 | else: 33 | return f'{self.elapsed():0.{dp}f}s elapsed' 34 | 35 | def __str__(self) -> StrType: 36 | return self.str() 37 | 38 | 39 | _SUMMARY_TEMPLATE = '{count} times: mean={mean:0.{dp}f}s stdev={stddev:0.{dp}f}s min={min:0.{dp}f}s max={max:0.{dp}f}s' 40 | 41 | 42 | class Timer: 43 | def __init__(self, name: 'Optional[str]' = None, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> None: 44 | self.file = file 45 | self.dp = dp 46 | self._name = name 47 | self._verbose = verbose 48 | self.results: 'List[TimerResult]' = [] 49 | 50 | def __call__(self, name: 'Optional[str]' = None, verbose: 'Optional[bool]' = None) -> 'Timer': 51 | if name: 52 | self._name = name 53 | if verbose is not None: 54 | self._verbose = verbose 55 | return self 56 | 57 | def start(self, name: 'Optional[str]' = None, verbose: 'Optional[bool]' = None) -> 'Timer': 58 | self.results.append(TimerResult(name or self._name, self._verbose if verbose is None else verbose)) 59 | return self 60 | 61 | def capture(self, verbose: 'Optional[bool]' = None) -> 'TimerResult': 62 | r = self.results[-1] 63 | r.capture() 64 | print_ = r.verbose if verbose is None else verbose 65 | if print_: 66 | print(r.str(self.dp), file=self.file, flush=True) 67 | return r 68 | 69 | def summary(self, verbose: bool = False) -> 'List[float]': 70 | times = [] 71 | for r in self.results: 72 | if not r.finish: 73 | r.capture() 74 | if verbose: 75 | print(f' {r.str(self.dp)}', file=self.file) 76 | times.append(r.elapsed()) 77 | 78 | if times: 79 | from statistics import mean, stdev 80 | 81 | print( 82 | f'{len(times)} times: ' 83 | f'mean={mean(times):0.{self.dp}f}s ' 84 | f'stdev={stdev(times) if len(times) > 1 else 0:0.{self.dp}f}s ' 85 | f'min={min(times):0.{self.dp}f}s ' 86 | f'max={max(times):0.{self.dp}f}s', 87 | file=self.file, 88 | flush=True, 89 | ) 90 | else: 91 | raise RuntimeError('timer not started') 92 | return times 93 | 94 | def __enter__(self) -> 'Timer': 95 | self.start() 96 | return self 97 | 98 | def __exit__(self, *args: 'Any') -> None: 99 | self.capture() 100 | -------------------------------------------------------------------------------- /devtools/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | __all__ = ( 5 | 'isatty', 6 | 'env_true', 7 | 'env_bool', 8 | 'use_highlight', 9 | 'is_literal', 10 | 'LaxMapping', 11 | 'DataClassType', 12 | 'SQLAlchemyClassType', 13 | ) 14 | 15 | MYPY = False 16 | if MYPY: 17 | from typing import Any, Optional, no_type_check 18 | else: 19 | 20 | def no_type_check(x: 'Any') -> 'Any': 21 | return x 22 | 23 | 24 | def isatty(stream: 'Any' = None) -> bool: 25 | stream = stream or sys.stdout 26 | try: 27 | return stream.isatty() 28 | except Exception: 29 | return False 30 | 31 | 32 | def env_true(var_name: str, alt: 'Optional[bool]' = None) -> 'Any': 33 | env = os.getenv(var_name, None) 34 | if env: 35 | return env.upper() in {'1', 'TRUE'} 36 | else: 37 | return alt 38 | 39 | 40 | def env_bool(value: 'Optional[bool]', env_name: str, env_default: 'Optional[bool]') -> 'Optional[bool]': 41 | if value is None: 42 | return env_true(env_name, env_default) 43 | else: 44 | return value 45 | 46 | 47 | @no_type_check 48 | def activate_win_color() -> bool: # pragma: no cover 49 | """ 50 | Activate ANSI support on windows consoles. 51 | 52 | As of Windows 10, the windows conolse got some support for ANSI escape 53 | sequences. Unfortunately it has to be enabled first using `SetConsoleMode`. 54 | See: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences 55 | 56 | Code snippet source: https://bugs.python.org/msg291732 57 | """ 58 | import ctypes 59 | import msvcrt 60 | import os 61 | from ctypes import wintypes 62 | 63 | def _check_bool(result, func, args): 64 | if not result: 65 | raise ctypes.WinError(ctypes.get_last_error()) 66 | return args 67 | 68 | ERROR_INVALID_PARAMETER = 0x0057 69 | ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 70 | 71 | LPDWORD = ctypes.POINTER(wintypes.DWORD) 72 | 73 | kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) 74 | kernel32.GetConsoleMode.errcheck = _check_bool 75 | kernel32.GetConsoleMode.argtypes = (wintypes.HANDLE, LPDWORD) 76 | kernel32.SetConsoleMode.errcheck = _check_bool 77 | kernel32.SetConsoleMode.argtypes = (wintypes.HANDLE, wintypes.DWORD) 78 | 79 | def _set_conout_mode(new_mode, mask=0xFFFFFFFF): 80 | # don't assume StandardOutput is a console. 81 | # open CONOUT$ instead 82 | fdout = os.open('CONOUT$', os.O_RDWR) 83 | try: 84 | hout = msvcrt.get_osfhandle(fdout) 85 | old_mode = wintypes.DWORD() 86 | kernel32.GetConsoleMode(hout, ctypes.byref(old_mode)) 87 | mode = (new_mode & mask) | (old_mode.value & ~mask) 88 | kernel32.SetConsoleMode(hout, mode) 89 | return old_mode.value 90 | finally: 91 | os.close(fdout) 92 | 93 | mode = mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING 94 | try: 95 | _set_conout_mode(mode, mask) 96 | except OSError as e: 97 | if e.winerror == ERROR_INVALID_PARAMETER: 98 | return False 99 | raise 100 | return True 101 | 102 | 103 | def use_highlight(highlight: 'Optional[bool]' = None, file_: 'Any' = None) -> bool: 104 | highlight = env_bool(highlight, 'PY_DEVTOOLS_HIGHLIGHT', None) 105 | 106 | if highlight is not None: 107 | return highlight 108 | 109 | if sys.platform == 'win32': # pragma: no cover 110 | return isatty(file_) and activate_win_color() 111 | return isatty(file_) 112 | 113 | 114 | def is_literal(s: 'Any') -> bool: 115 | import ast 116 | 117 | try: 118 | ast.literal_eval(s) 119 | except (TypeError, MemoryError, SyntaxError, ValueError): 120 | return False 121 | else: 122 | return True 123 | 124 | 125 | class MetaLaxMapping(type): 126 | def __instancecheck__(self, instance: 'Any') -> bool: 127 | return ( 128 | hasattr(instance, '__getitem__') 129 | and hasattr(instance, 'items') 130 | and callable(instance.items) 131 | and type(instance) != type 132 | ) 133 | 134 | 135 | class LaxMapping(metaclass=MetaLaxMapping): 136 | pass 137 | 138 | 139 | class MetaDataClassType(type): 140 | def __instancecheck__(self, instance: 'Any') -> bool: 141 | from dataclasses import is_dataclass 142 | 143 | return is_dataclass(instance) 144 | 145 | 146 | class DataClassType(metaclass=MetaDataClassType): 147 | pass 148 | 149 | 150 | class MetaSQLAlchemyClassType(type): 151 | def __instancecheck__(self, instance: 'Any') -> bool: 152 | try: 153 | from sqlalchemy.orm import DeclarativeBase 154 | except ImportError: 155 | pass 156 | else: 157 | if isinstance(instance, DeclarativeBase): 158 | return True 159 | 160 | try: 161 | from sqlalchemy.ext.declarative import DeclarativeMeta 162 | except ImportError: 163 | pass 164 | else: 165 | return isinstance(instance.__class__, DeclarativeMeta) 166 | 167 | return False 168 | 169 | 170 | class SQLAlchemyClassType(metaclass=MetaSQLAlchemyClassType): 171 | pass 172 | -------------------------------------------------------------------------------- /devtools/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.12.2' 2 | -------------------------------------------------------------------------------- /docs/examples/ansi_colours.py: -------------------------------------------------------------------------------- 1 | from devtools import sformat, sprint 2 | 3 | sprint('this is red', sprint.red) 4 | 5 | sprint('this is bold underlined blue on a green background. yuck', 6 | sprint.blue, sprint.bg_yellow, sprint.bold, sprint.underline) 7 | 8 | v = sformat('i am dim', sprint.dim) 9 | print(repr(v)) 10 | # > '\x1b[2mi am dim\x1b[0m' 11 | -------------------------------------------------------------------------------- /docs/examples/complex.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from devtools import debug 4 | 5 | foo = { 6 | 'foo': np.array(range(20)), 7 | 'bar': [{'a': i, 'b': {j for j in range(1 + i * 2)}} for i in range(3)], 8 | 'spam': (i for i in ['i', 'am', 'a', 'generator']), 9 | } 10 | 11 | debug(foo) 12 | 13 | # kwargs can be used as keys for what you are printing 14 | debug( 15 | long_string='long strings get wrapped ' * 10, 16 | new_line_string='wraps also on newline\n' * 3, 17 | ) 18 | 19 | bar = {1: 2, 11: 12} 20 | # debug can also show the output of expressions 21 | debug( 22 | len(foo), 23 | bar[1], 24 | foo == bar 25 | ) 26 | -------------------------------------------------------------------------------- /docs/examples/example.py: -------------------------------------------------------------------------------- 1 | from devtools import debug 2 | 3 | v1 = { 4 | 'foo': {1: 'nested', 2: 'dict'}, 5 | 'bar': ['apple', 'banana', 'carrot', 'grapefruit'], 6 | } 7 | 8 | debug(v1, sum(range(5))) 9 | -------------------------------------------------------------------------------- /docs/examples/other.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from devtools import debug 4 | 5 | # debug.format() behaves the same as debug() except it 6 | # returns an object to inspect or print 7 | r = debug.format(x=123, y=321) 8 | print(r) 9 | print(repr(r)) 10 | 11 | values = list(range(int(1e5))) 12 | # timer can be used as a context manager or directly 13 | with debug.timer('shuffle values'): 14 | random.shuffle(values) 15 | 16 | t2 = debug.timer('sort values').start() 17 | sorted(values) 18 | t2.capture() 19 | 20 | # if used repeatedly a summary is available 21 | t3 = debug.timer() 22 | for i in [1e4, 1e6, 1e7]: 23 | with t3(f'sum {i}', verbose=False): 24 | sum(range(int(i))) 25 | 26 | t3.summary(verbose=True) 27 | 28 | # debug.breakpoint() 29 | # would drop to a prompt: 30 | # > /python-devtools/docs/examples/more_debug.py(28)()->None 31 | # -> debug.breakpoint() 32 | # (Pdb) 33 | -------------------------------------------------------------------------------- /docs/examples/prettier.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from devtools import PrettyFormat, pformat, pprint 4 | 5 | v = { 6 | 'foo': {'whatever': [3, 2, 1]}, 7 | 'sentence': 'hello\nworld', 8 | 'generator': (i * 2 for i in [1, 2, 3]), 9 | 'matrix': np.matrix([[1, 2, 3, 4], 10 | [50, 60, 70, 80], 11 | [900, 1000, 1100, 1200], 12 | [13000, 14000, 15000, 16000]]) 13 | } 14 | 15 | # pretty print of v 16 | pprint(v) 17 | 18 | # as above without colours, the generator will also be empty as 19 | # it's already been evaluated 20 | s = pformat(v, highlight=False) 21 | print(s) 22 | 23 | pp = PrettyFormat( 24 | indent_step=2, # default: 4 25 | indent_char='.', # default: space 26 | repr_strings=True, # default: False 27 | # default: 10 (if line is below this length it'll be shown on one line) 28 | simple_cutoff=2, 29 | width=80, # default: 120 30 | # default: True (whether to evaluate generators 31 | yield_from_generators=False, 32 | ) 33 | 34 | print(pp(v, highlight=True)) 35 | -------------------------------------------------------------------------------- /docs/examples/return_args.py: -------------------------------------------------------------------------------- 1 | from devtools import debug 2 | 3 | assert debug('foo') == 'foo' 4 | assert debug('foo', 'bar') == ('foo', 'bar') 5 | assert debug('foo', 'bar', spam=123) == ('foo', 'bar', {'spam': 123}) 6 | assert debug(spam=123) == ({'spam': 123},) 7 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | {!.history.md!} 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Python devtools 2 | 3 | [![CI](https://github.com/samuelcolvin/python-devtools/workflows/CI/badge.svg?event=push)](https://github.com/samuelcolvin/python-devtools/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) 4 | [![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/main/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) 5 | [![pypi](https://img.shields.io/pypi/v/devtools.svg)](https://pypi.python.org/pypi/devtools) 6 | [![versions](https://img.shields.io/pypi/pyversions/devtools.svg)](https://github.com/samuelcolvin/python-devtools) 7 | [![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/main/LICENSE) 8 | 9 | {{ version }} 10 | 11 | **Python's missing debug print command and other development tools.** 12 | 13 | ```py 14 | {!examples/example.py!} 15 | ``` 16 | 17 | {{ example_html(examples/example.py) }} 18 | 19 | Python devtools can do much more, see [Usage](./usage.md) for examples. 20 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | Installation is as simple as: 2 | 3 | ```bash 4 | pip install devtools 5 | ``` 6 | 7 | `devtools` has [very few dependencies](https://github.com/samuelcolvin/python-devtools/blob/main/pyproject.toml#L37). 8 | If you've got python 3.7+ and `pip` installed, you're good to go. 9 | -------------------------------------------------------------------------------- /docs/plugins.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import os 4 | import re 5 | import subprocess 6 | import sys 7 | from importlib.machinery import SourceFileLoader 8 | from pathlib import Path 9 | 10 | from ansi2html import Ansi2HTMLConverter 11 | from mkdocs.config import Config 12 | from mkdocs.structure.files import Files 13 | from mkdocs.structure.pages import Page 14 | 15 | THIS_DIR = Path(__file__).parent 16 | PROJECT_ROOT = THIS_DIR / '..' 17 | 18 | logger = logging.getLogger('mkdocs.test_examples') 19 | 20 | # see mkdocs.yml for how these methods ar used 21 | __all__ = 'on_pre_build', 'on_page_markdown', 'on_files' 22 | 23 | 24 | def on_pre_build(config: Config): 25 | build_history() 26 | 27 | 28 | def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str: 29 | markdown = set_version(markdown, page) 30 | return gen_example_html(markdown) 31 | 32 | 33 | def on_files(files: Files, config: Config) -> Files: 34 | return remove_files(files) 35 | 36 | 37 | def build_history(): 38 | history = (PROJECT_ROOT / 'HISTORY.md').read_text() 39 | history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/python-devtools/issues/\1)', history) 40 | history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) 41 | history = re.sub('@@', '@', history) 42 | output_file = THIS_DIR / '.history.md' 43 | if not output_file.exists() or history != output_file.read_text(): 44 | (THIS_DIR / '.history.md').write_text(history) 45 | 46 | 47 | def gen_example_html(markdown: str): 48 | return re.sub(r'{{ *example_html\((.*?)\) *}}', gen_examples_html, markdown) 49 | 50 | 51 | def gen_examples_html(m: re.Match) -> str: 52 | sys.path.append(str(THIS_DIR.resolve())) 53 | 54 | os.environ.update(PY_DEVTOOLS_HIGHLIGHT='true', PY_DEVTOOLS_WIDTH='80') 55 | conv = Ansi2HTMLConverter() 56 | name = THIS_DIR / Path(m.group(1)) 57 | 58 | logger.info('running %s to generate HTML...', name) 59 | p = subprocess.run((sys.executable, str(name)), stdout=subprocess.PIPE, check=True) 60 | html = conv.convert(p.stdout.decode(), full=False).strip('\r\n') 61 | html = html.replace('docs/build/../examples/', '') 62 | return f'
\n{html}\n
\n' 63 | 64 | 65 | def set_version(markdown: str, page: Page) -> str: 66 | if page.abs_url == '/': 67 | version = SourceFileLoader('version', str(PROJECT_ROOT / 'devtools/version.py')).load_module() 68 | version_str = f'Documentation for version: **v{version.VERSION}**' 69 | markdown = re.sub(r'{{ *version *}}', version_str, markdown) 70 | return markdown 71 | 72 | 73 | def remove_files(files: Files) -> Files: 74 | to_remove = [] 75 | for file in files: 76 | if file.src_path == 'requirements.txt': 77 | to_remove.append(file) 78 | elif file.src_path.startswith('__pycache__/'): 79 | to_remove.append(file) 80 | elif file.src_path.endswith('.py'): 81 | to_remove.append(file) 82 | 83 | logger.debug('removing files: %s', [f.src_path for f in to_remove]) 84 | for f in to_remove: 85 | files.remove(f) 86 | 87 | return files 88 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | ansi2html==1.8.0 2 | mkdocs==1.3.1 3 | mkdocs-exclude==1.0.2 4 | mkdocs-material==8.3.9 5 | mkdocs-simple-hooks==0.1.5 6 | markdown-include==0.7.0 7 | pygments==2.15.0 8 | -------------------------------------------------------------------------------- /docs/theme/customization.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu+Mono&display=swap'); 2 | 3 | .divider { 4 | text-align: center; 5 | font-weight: bold; 6 | margin: -12px; 7 | } 8 | .ansi2html-content { 9 | font-family: 'Ubuntu Mono', monospace; 10 | font-size: 0.95em; 11 | white-space: pre-wrap; 12 | word-wrap: break-word; 13 | line-height: normal; 14 | background-color: #300a24; 15 | padding: 10px 15px; 16 | color: white !important; 17 | border-radius: 5px; 18 | } 19 | .body_foreground { color: #AAAAAA; } 20 | .body_background { background-color: #000000; } 21 | .body_foreground > .bold,.bold > .body_foreground, body.body_foreground > pre > .bold { color: #FFFFFF; font-weight: normal; } 22 | .inv_foreground { color: #000000; } 23 | .inv_background { background-color: #AAAAAA; } 24 | .ansi1 { font-weight: bold; } 25 | .ansi2 { color: #666; } 26 | .ansi3 { font-style: italic; } 27 | .ansi4 { text-decoration: underline; } 28 | .ansi5 { text-decoration: blink; } 29 | .ansi6 { text-decoration: blink; } 30 | .ansi8 { visibility: hidden; } 31 | .ansi9 { text-decoration: line-through; } 32 | .ansi30 { color: #000316; } 33 | .inv30 { background-color: #000316; } 34 | .ansi31 { color: #aa0000; } 35 | .inv31 { background-color: #aa0000; } 36 | .ansi32 { color: #00aa00; } 37 | .inv32 { background-color: #00aa00; } 38 | .ansi33 { color: #aa5500; } 39 | .inv33 { background-color: #aa5500; } 40 | .ansi34 { color: #3363A0; } 41 | .inv34 { background-color: #3363A0; } 42 | .ansi35 { color: #E850A8; } 43 | .inv35 { background-color: #E850A8; } 44 | .ansi36 { color: #00aaaa; } 45 | .inv36 { background-color: #00aaaa; } 46 | .ansi37 { color: #F5F1DE; } 47 | .inv37 { background-color: #F5F1DE; } 48 | .ansi40 { background-color: #000316; } 49 | .inv40 { color: #000316; } 50 | .ansi41 { background-color: #aa0000; } 51 | .inv41 { color: #aa0000; } 52 | .ansi42 { background-color: #00aa00; } 53 | .inv42 { color: #00aa00; } 54 | .ansi43 { background-color: #aa5500; } 55 | .inv43 { color: #aa5500; } 56 | .ansi44 { background-color: #3363A0; } 57 | .inv44 { color: #3363A0; } 58 | .ansi45 { background-color: #E850A8; } 59 | .inv45 { color: #E850A8; } 60 | .ansi46 { background-color: #00aaaa; } 61 | .inv46 { color: #00aaaa; } 62 | .ansi47 { background-color: #F5F1DE; } 63 | .inv47 { color: #F5F1DE; } 64 | .ansi38-0 { color: #000316; } 65 | .inv38-0 { background-color: #000316; } 66 | .ansi38-1 { color: #aa0000; } 67 | .inv38-1 { background-color: #aa0000; } 68 | .ansi38-2 { color: #00aa00; } 69 | .inv38-2 { background-color: #00aa00; } 70 | .ansi38-3 { color: #aa5500; } 71 | .inv38-3 { background-color: #aa5500; } 72 | .ansi38-4 { color: #3363A0; } 73 | .inv38-4 { background-color: #3363A0; } 74 | .ansi38-5 { color: #E850A8; } 75 | .inv38-5 { background-color: #E850A8; } 76 | .ansi38-6 { color: #00aaaa; } 77 | .inv38-6 { background-color: #00aaaa; } 78 | .ansi38-7 { color: #F5F1DE; } 79 | .inv38-7 { background-color: #F5F1DE; } 80 | .ansi38-8 { color: #7f7f7f; } 81 | .inv38-8 { background-color: #7f7f7f; } 82 | .ansi38-9 { color: #ff0000; } 83 | .inv38-9 { background-color: #ff0000; } 84 | .ansi38-10 { color: #00ff00; } 85 | .inv38-10 { background-color: #00ff00; } 86 | .ansi38-11 { color: #ffff00; } 87 | .inv38-11 { background-color: #ffff00; } 88 | .ansi38-12 { color: #5c5cff; } 89 | .inv38-12 { background-color: #5c5cff; } 90 | .ansi38-13 { color: #ff00ff; } 91 | .inv38-13 { background-color: #ff00ff; } 92 | .ansi38-14 { color: #00ffff; } 93 | .inv38-14 { background-color: #00ffff; } 94 | .ansi38-15 { color: #ffffff; } 95 | .inv38-15 { background-color: #ffffff; } 96 | .ansi48-0 { background-color: #000316; } 97 | .inv48-0 { color: #000316; } 98 | .ansi48-1 { background-color: #aa0000; } 99 | .inv48-1 { color: #aa0000; } 100 | .ansi48-2 { background-color: #00aa00; } 101 | .inv48-2 { color: #00aa00; } 102 | .ansi48-3 { background-color: #aa5500; } 103 | .inv48-3 { color: #aa5500; } 104 | .ansi48-4 { background-color: #3363A0; } 105 | .inv48-4 { color: #3363A0; } 106 | .ansi48-5 { background-color: #E850A8; } 107 | .inv48-5 { color: #E850A8; } 108 | .ansi48-6 { background-color: #00aaaa; } 109 | .inv48-6 { color: #00aaaa; } 110 | .ansi48-7 { background-color: #F5F1DE; } 111 | .inv48-7 { color: #F5F1DE; } 112 | .ansi48-8 { background-color: #7f7f7f; } 113 | .inv48-8 { color: #7f7f7f; } 114 | .ansi48-9 { background-color: #ff0000; } 115 | .inv48-9 { color: #ff0000; } 116 | .ansi48-10 { background-color: #00ff00; } 117 | .inv48-10 { color: #00ff00; } 118 | .ansi48-11 { background-color: #ffff00; } 119 | .inv48-11 { color: #ffff00; } 120 | .ansi48-12 { background-color: #5c5cff; } 121 | .inv48-12 { color: #5c5cff; } 122 | .ansi48-13 { background-color: #ff00ff; } 123 | .inv48-13 { color: #ff00ff; } 124 | .ansi48-14 { background-color: #00ffff; } 125 | .inv48-14 { color: #00ffff; } 126 | .ansi48-15 { background-color: #ffffff; } 127 | .inv48-15 { color: #ffffff; } 128 | .ansi38-16 { color: #000000; } 129 | .inv38-16 { background: #000000; } 130 | .ansi48-16 { background: #000000; } 131 | .inv48-16 { color: #000000; } 132 | .ansi38-17 { color: #00002a; } 133 | .inv38-17 { background: #00002a; } 134 | .ansi48-17 { background: #00002a; } 135 | .inv48-17 { color: #00002a; } 136 | .ansi38-18 { color: #000054; } 137 | .inv38-18 { background: #000054; } 138 | .ansi48-18 { background: #000054; } 139 | .inv48-18 { color: #000054; } 140 | .ansi38-19 { color: #00007e; } 141 | .inv38-19 { background: #00007e; } 142 | .ansi48-19 { background: #00007e; } 143 | .inv48-19 { color: #00007e; } 144 | .ansi38-20 { color: #0000a8; } 145 | .inv38-20 { background: #0000a8; } 146 | .ansi48-20 { background: #0000a8; } 147 | .inv48-20 { color: #0000a8; } 148 | .ansi38-21 { color: #0000d2; } 149 | .inv38-21 { background: #0000d2; } 150 | .ansi48-21 { background: #0000d2; } 151 | .inv48-21 { color: #0000d2; } 152 | .ansi38-52 { color: #2a0000; } 153 | .inv38-52 { background: #2a0000; } 154 | .ansi48-52 { background: #2a0000; } 155 | .inv48-52 { color: #2a0000; } 156 | .ansi38-53 { color: #2a002a; } 157 | .inv38-53 { background: #2a002a; } 158 | .ansi48-53 { background: #2a002a; } 159 | .inv48-53 { color: #2a002a; } 160 | .ansi38-54 { color: #2a0054; } 161 | .inv38-54 { background: #2a0054; } 162 | .ansi48-54 { background: #2a0054; } 163 | .inv48-54 { color: #2a0054; } 164 | .ansi38-55 { color: #2a007e; } 165 | .inv38-55 { background: #2a007e; } 166 | .ansi48-55 { background: #2a007e; } 167 | .inv48-55 { color: #2a007e; } 168 | .ansi38-56 { color: #2a00a8; } 169 | .inv38-56 { background: #2a00a8; } 170 | .ansi48-56 { background: #2a00a8; } 171 | .inv48-56 { color: #2a00a8; } 172 | .ansi38-57 { color: #2a00d2; } 173 | .inv38-57 { background: #2a00d2; } 174 | .ansi48-57 { background: #2a00d2; } 175 | .inv48-57 { color: #2a00d2; } 176 | .ansi38-88 { color: #540000; } 177 | .inv38-88 { background: #540000; } 178 | .ansi48-88 { background: #540000; } 179 | .inv48-88 { color: #540000; } 180 | .ansi38-89 { color: #54002a; } 181 | .inv38-89 { background: #54002a; } 182 | .ansi48-89 { background: #54002a; } 183 | .inv48-89 { color: #54002a; } 184 | .ansi38-90 { color: #540054; } 185 | .inv38-90 { background: #540054; } 186 | .ansi48-90 { background: #540054; } 187 | .inv48-90 { color: #540054; } 188 | .ansi38-91 { color: #54007e; } 189 | .inv38-91 { background: #54007e; } 190 | .ansi48-91 { background: #54007e; } 191 | .inv48-91 { color: #54007e; } 192 | .ansi38-92 { color: #5400a8; } 193 | .inv38-92 { background: #5400a8; } 194 | .ansi48-92 { background: #5400a8; } 195 | .inv48-92 { color: #5400a8; } 196 | .ansi38-93 { color: #5400d2; } 197 | .inv38-93 { background: #5400d2; } 198 | .ansi48-93 { background: #5400d2; } 199 | .inv48-93 { color: #5400d2; } 200 | .ansi38-124 { color: #7e0000; } 201 | .inv38-124 { background: #7e0000; } 202 | .ansi48-124 { background: #7e0000; } 203 | .inv48-124 { color: #7e0000; } 204 | .ansi38-125 { color: #7e002a; } 205 | .inv38-125 { background: #7e002a; } 206 | .ansi48-125 { background: #7e002a; } 207 | .inv48-125 { color: #7e002a; } 208 | .ansi38-126 { color: #7e0054; } 209 | .inv38-126 { background: #7e0054; } 210 | .ansi48-126 { background: #7e0054; } 211 | .inv48-126 { color: #7e0054; } 212 | .ansi38-127 { color: #7e007e; } 213 | .inv38-127 { background: #7e007e; } 214 | .ansi48-127 { background: #7e007e; } 215 | .inv48-127 { color: #7e007e; } 216 | .ansi38-128 { color: #7e00a8; } 217 | .inv38-128 { background: #7e00a8; } 218 | .ansi48-128 { background: #7e00a8; } 219 | .inv48-128 { color: #7e00a8; } 220 | .ansi38-129 { color: #7e00d2; } 221 | .inv38-129 { background: #7e00d2; } 222 | .ansi48-129 { background: #7e00d2; } 223 | .inv48-129 { color: #7e00d2; } 224 | .ansi38-160 { color: #a80000; } 225 | .inv38-160 { background: #a80000; } 226 | .ansi48-160 { background: #a80000; } 227 | .inv48-160 { color: #a80000; } 228 | .ansi38-161 { color: #a8002a; } 229 | .inv38-161 { background: #a8002a; } 230 | .ansi48-161 { background: #a8002a; } 231 | .inv48-161 { color: #a8002a; } 232 | .ansi38-162 { color: #a80054; } 233 | .inv38-162 { background: #a80054; } 234 | .ansi48-162 { background: #a80054; } 235 | .inv48-162 { color: #a80054; } 236 | .ansi38-163 { color: #a8007e; } 237 | .inv38-163 { background: #a8007e; } 238 | .ansi48-163 { background: #a8007e; } 239 | .inv48-163 { color: #a8007e; } 240 | .ansi38-164 { color: #a800a8; } 241 | .inv38-164 { background: #a800a8; } 242 | .ansi48-164 { background: #a800a8; } 243 | .inv48-164 { color: #a800a8; } 244 | .ansi38-165 { color: #a800d2; } 245 | .inv38-165 { background: #a800d2; } 246 | .ansi48-165 { background: #a800d2; } 247 | .inv48-165 { color: #a800d2; } 248 | .ansi38-196 { color: #d20000; } 249 | .inv38-196 { background: #d20000; } 250 | .ansi48-196 { background: #d20000; } 251 | .inv48-196 { color: #d20000; } 252 | .ansi38-197 { color: #d2002a; } 253 | .inv38-197 { background: #d2002a; } 254 | .ansi48-197 { background: #d2002a; } 255 | .inv48-197 { color: #d2002a; } 256 | .ansi38-198 { color: #d20054; } 257 | .inv38-198 { background: #d20054; } 258 | .ansi48-198 { background: #d20054; } 259 | .inv48-198 { color: #d20054; } 260 | .ansi38-199 { color: #d2007e; } 261 | .inv38-199 { background: #d2007e; } 262 | .ansi48-199 { background: #d2007e; } 263 | .inv48-199 { color: #d2007e; } 264 | .ansi38-200 { color: #d200a8; } 265 | .inv38-200 { background: #d200a8; } 266 | .ansi48-200 { background: #d200a8; } 267 | .inv48-200 { color: #d200a8; } 268 | .ansi38-201 { color: #d200d2; } 269 | .inv38-201 { background: #d200d2; } 270 | .ansi48-201 { background: #d200d2; } 271 | .inv48-201 { color: #d200d2; } 272 | .ansi38-22 { color: #002a00; } 273 | .inv38-22 { background: #002a00; } 274 | .ansi48-22 { background: #002a00; } 275 | .inv48-22 { color: #002a00; } 276 | .ansi38-23 { color: #002a2a; } 277 | .inv38-23 { background: #002a2a; } 278 | .ansi48-23 { background: #002a2a; } 279 | .inv48-23 { color: #002a2a; } 280 | .ansi38-24 { color: #002a54; } 281 | .inv38-24 { background: #002a54; } 282 | .ansi48-24 { background: #002a54; } 283 | .inv48-24 { color: #002a54; } 284 | .ansi38-25 { color: #002a7e; } 285 | .inv38-25 { background: #002a7e; } 286 | .ansi48-25 { background: #002a7e; } 287 | .inv48-25 { color: #002a7e; } 288 | .ansi38-26 { color: #002aa8; } 289 | .inv38-26 { background: #002aa8; } 290 | .ansi48-26 { background: #002aa8; } 291 | .inv48-26 { color: #002aa8; } 292 | .ansi38-27 { color: #002ad2; } 293 | .inv38-27 { background: #002ad2; } 294 | .ansi48-27 { background: #002ad2; } 295 | .inv48-27 { color: #002ad2; } 296 | .ansi38-58 { color: #2a2a00; } 297 | .inv38-58 { background: #2a2a00; } 298 | .ansi48-58 { background: #2a2a00; } 299 | .inv48-58 { color: #2a2a00; } 300 | .ansi38-59 { color: #2a2a2a; } 301 | .inv38-59 { background: #2a2a2a; } 302 | .ansi48-59 { background: #2a2a2a; } 303 | .inv48-59 { color: #2a2a2a; } 304 | .ansi38-60 { color: #2a2a54; } 305 | .inv38-60 { background: #2a2a54; } 306 | .ansi48-60 { background: #2a2a54; } 307 | .inv48-60 { color: #2a2a54; } 308 | .ansi38-61 { color: #2a2a7e; } 309 | .inv38-61 { background: #2a2a7e; } 310 | .ansi48-61 { background: #2a2a7e; } 311 | .inv48-61 { color: #2a2a7e; } 312 | .ansi38-62 { color: #2a2aa8; } 313 | .inv38-62 { background: #2a2aa8; } 314 | .ansi48-62 { background: #2a2aa8; } 315 | .inv48-62 { color: #2a2aa8; } 316 | .ansi38-63 { color: #2a2ad2; } 317 | .inv38-63 { background: #2a2ad2; } 318 | .ansi48-63 { background: #2a2ad2; } 319 | .inv48-63 { color: #2a2ad2; } 320 | .ansi38-94 { color: #542a00; } 321 | .inv38-94 { background: #542a00; } 322 | .ansi48-94 { background: #542a00; } 323 | .inv48-94 { color: #542a00; } 324 | .ansi38-95 { color: #542a2a; } 325 | .inv38-95 { background: #542a2a; } 326 | .ansi48-95 { background: #542a2a; } 327 | .inv48-95 { color: #542a2a; } 328 | .ansi38-96 { color: #542a54; } 329 | .inv38-96 { background: #542a54; } 330 | .ansi48-96 { background: #542a54; } 331 | .inv48-96 { color: #542a54; } 332 | .ansi38-97 { color: #542a7e; } 333 | .inv38-97 { background: #542a7e; } 334 | .ansi48-97 { background: #542a7e; } 335 | .inv48-97 { color: #542a7e; } 336 | .ansi38-98 { color: #542aa8; } 337 | .inv38-98 { background: #542aa8; } 338 | .ansi48-98 { background: #542aa8; } 339 | .inv48-98 { color: #542aa8; } 340 | .ansi38-99 { color: #542ad2; } 341 | .inv38-99 { background: #542ad2; } 342 | .ansi48-99 { background: #542ad2; } 343 | .inv48-99 { color: #542ad2; } 344 | .ansi38-130 { color: #7e2a00; } 345 | .inv38-130 { background: #7e2a00; } 346 | .ansi48-130 { background: #7e2a00; } 347 | .inv48-130 { color: #7e2a00; } 348 | .ansi38-131 { color: #7e2a2a; } 349 | .inv38-131 { background: #7e2a2a; } 350 | .ansi48-131 { background: #7e2a2a; } 351 | .inv48-131 { color: #7e2a2a; } 352 | .ansi38-132 { color: #7e2a54; } 353 | .inv38-132 { background: #7e2a54; } 354 | .ansi48-132 { background: #7e2a54; } 355 | .inv48-132 { color: #7e2a54; } 356 | .ansi38-133 { color: #7e2a7e; } 357 | .inv38-133 { background: #7e2a7e; } 358 | .ansi48-133 { background: #7e2a7e; } 359 | .inv48-133 { color: #7e2a7e; } 360 | .ansi38-134 { color: #7e2aa8; } 361 | .inv38-134 { background: #7e2aa8; } 362 | .ansi48-134 { background: #7e2aa8; } 363 | .inv48-134 { color: #7e2aa8; } 364 | .ansi38-135 { color: #7e2ad2; } 365 | .inv38-135 { background: #7e2ad2; } 366 | .ansi48-135 { background: #7e2ad2; } 367 | .inv48-135 { color: #7e2ad2; } 368 | .ansi38-166 { color: #a82a00; } 369 | .inv38-166 { background: #a82a00; } 370 | .ansi48-166 { background: #a82a00; } 371 | .inv48-166 { color: #a82a00; } 372 | .ansi38-167 { color: #a82a2a; } 373 | .inv38-167 { background: #a82a2a; } 374 | .ansi48-167 { background: #a82a2a; } 375 | .inv48-167 { color: #a82a2a; } 376 | .ansi38-168 { color: #a82a54; } 377 | .inv38-168 { background: #a82a54; } 378 | .ansi48-168 { background: #a82a54; } 379 | .inv48-168 { color: #a82a54; } 380 | .ansi38-169 { color: #a82a7e; } 381 | .inv38-169 { background: #a82a7e; } 382 | .ansi48-169 { background: #a82a7e; } 383 | .inv48-169 { color: #a82a7e; } 384 | .ansi38-170 { color: #a82aa8; } 385 | .inv38-170 { background: #a82aa8; } 386 | .ansi48-170 { background: #a82aa8; } 387 | .inv48-170 { color: #a82aa8; } 388 | .ansi38-171 { color: #a82ad2; } 389 | .inv38-171 { background: #a82ad2; } 390 | .ansi48-171 { background: #a82ad2; } 391 | .inv48-171 { color: #a82ad2; } 392 | .ansi38-202 { color: #d22a00; } 393 | .inv38-202 { background: #d22a00; } 394 | .ansi48-202 { background: #d22a00; } 395 | .inv48-202 { color: #d22a00; } 396 | .ansi38-203 { color: #d22a2a; } 397 | .inv38-203 { background: #d22a2a; } 398 | .ansi48-203 { background: #d22a2a; } 399 | .inv48-203 { color: #d22a2a; } 400 | .ansi38-204 { color: #d22a54; } 401 | .inv38-204 { background: #d22a54; } 402 | .ansi48-204 { background: #d22a54; } 403 | .inv48-204 { color: #d22a54; } 404 | .ansi38-205 { color: #d22a7e; } 405 | .inv38-205 { background: #d22a7e; } 406 | .ansi48-205 { background: #d22a7e; } 407 | .inv48-205 { color: #d22a7e; } 408 | .ansi38-206 { color: #d22aa8; } 409 | .inv38-206 { background: #d22aa8; } 410 | .ansi48-206 { background: #d22aa8; } 411 | .inv48-206 { color: #d22aa8; } 412 | .ansi38-207 { color: #d22ad2; } 413 | .inv38-207 { background: #d22ad2; } 414 | .ansi48-207 { background: #d22ad2; } 415 | .inv48-207 { color: #d22ad2; } 416 | .ansi38-28 { color: #005400; } 417 | .inv38-28 { background: #005400; } 418 | .ansi48-28 { background: #005400; } 419 | .inv48-28 { color: #005400; } 420 | .ansi38-29 { color: #00542a; } 421 | .inv38-29 { background: #00542a; } 422 | .ansi48-29 { background: #00542a; } 423 | .inv48-29 { color: #00542a; } 424 | .ansi38-30 { color: #005454; } 425 | .inv38-30 { background: #005454; } 426 | .ansi48-30 { background: #005454; } 427 | .inv48-30 { color: #005454; } 428 | .ansi38-31 { color: #00547e; } 429 | .inv38-31 { background: #00547e; } 430 | .ansi48-31 { background: #00547e; } 431 | .inv48-31 { color: #00547e; } 432 | .ansi38-32 { color: #0054a8; } 433 | .inv38-32 { background: #0054a8; } 434 | .ansi48-32 { background: #0054a8; } 435 | .inv48-32 { color: #0054a8; } 436 | .ansi38-33 { color: #0054d2; } 437 | .inv38-33 { background: #0054d2; } 438 | .ansi48-33 { background: #0054d2; } 439 | .inv48-33 { color: #0054d2; } 440 | .ansi38-64 { color: #2a5400; } 441 | .inv38-64 { background: #2a5400; } 442 | .ansi48-64 { background: #2a5400; } 443 | .inv48-64 { color: #2a5400; } 444 | .ansi38-65 { color: #2a542a; } 445 | .inv38-65 { background: #2a542a; } 446 | .ansi48-65 { background: #2a542a; } 447 | .inv48-65 { color: #2a542a; } 448 | .ansi38-66 { color: #2a5454; } 449 | .inv38-66 { background: #2a5454; } 450 | .ansi48-66 { background: #2a5454; } 451 | .inv48-66 { color: #2a5454; } 452 | .ansi38-67 { color: #2a547e; } 453 | .inv38-67 { background: #2a547e; } 454 | .ansi48-67 { background: #2a547e; } 455 | .inv48-67 { color: #2a547e; } 456 | .ansi38-68 { color: #2a54a8; } 457 | .inv38-68 { background: #2a54a8; } 458 | .ansi48-68 { background: #2a54a8; } 459 | .inv48-68 { color: #2a54a8; } 460 | .ansi38-69 { color: #2a54d2; } 461 | .inv38-69 { background: #2a54d2; } 462 | .ansi48-69 { background: #2a54d2; } 463 | .inv48-69 { color: #2a54d2; } 464 | .ansi38-100 { color: #545400; } 465 | .inv38-100 { background: #545400; } 466 | .ansi48-100 { background: #545400; } 467 | .inv48-100 { color: #545400; } 468 | .ansi38-101 { color: #54542a; } 469 | .inv38-101 { background: #54542a; } 470 | .ansi48-101 { background: #54542a; } 471 | .inv48-101 { color: #54542a; } 472 | .ansi38-102 { color: #545454; } 473 | .inv38-102 { background: #545454; } 474 | .ansi48-102 { background: #545454; } 475 | .inv48-102 { color: #545454; } 476 | .ansi38-103 { color: #54547e; } 477 | .inv38-103 { background: #54547e; } 478 | .ansi48-103 { background: #54547e; } 479 | .inv48-103 { color: #54547e; } 480 | .ansi38-104 { color: #5454a8; } 481 | .inv38-104 { background: #5454a8; } 482 | .ansi48-104 { background: #5454a8; } 483 | .inv48-104 { color: #5454a8; } 484 | .ansi38-105 { color: #5454d2; } 485 | .inv38-105 { background: #5454d2; } 486 | .ansi48-105 { background: #5454d2; } 487 | .inv48-105 { color: #5454d2; } 488 | .ansi38-136 { color: #7e5400; } 489 | .inv38-136 { background: #7e5400; } 490 | .ansi48-136 { background: #7e5400; } 491 | .inv48-136 { color: #7e5400; } 492 | .ansi38-137 { color: #7e542a; } 493 | .inv38-137 { background: #7e542a; } 494 | .ansi48-137 { background: #7e542a; } 495 | .inv48-137 { color: #7e542a; } 496 | .ansi38-138 { color: #7e5454; } 497 | .inv38-138 { background: #7e5454; } 498 | .ansi48-138 { background: #7e5454; } 499 | .inv48-138 { color: #7e5454; } 500 | .ansi38-139 { color: #7e547e; } 501 | .inv38-139 { background: #7e547e; } 502 | .ansi48-139 { background: #7e547e; } 503 | .inv48-139 { color: #7e547e; } 504 | .ansi38-140 { color: #7e54a8; } 505 | .inv38-140 { background: #7e54a8; } 506 | .ansi48-140 { background: #7e54a8; } 507 | .inv48-140 { color: #7e54a8; } 508 | .ansi38-141 { color: #7e54d2; } 509 | .inv38-141 { background: #7e54d2; } 510 | .ansi48-141 { background: #7e54d2; } 511 | .inv48-141 { color: #7e54d2; } 512 | .ansi38-172 { color: #a85400; } 513 | .inv38-172 { background: #a85400; } 514 | .ansi48-172 { background: #a85400; } 515 | .inv48-172 { color: #a85400; } 516 | .ansi38-173 { color: #a8542a; } 517 | .inv38-173 { background: #a8542a; } 518 | .ansi48-173 { background: #a8542a; } 519 | .inv48-173 { color: #a8542a; } 520 | .ansi38-174 { color: #a85454; } 521 | .inv38-174 { background: #a85454; } 522 | .ansi48-174 { background: #a85454; } 523 | .inv48-174 { color: #a85454; } 524 | .ansi38-175 { color: #a8547e; } 525 | .inv38-175 { background: #a8547e; } 526 | .ansi48-175 { background: #a8547e; } 527 | .inv48-175 { color: #a8547e; } 528 | .ansi38-176 { color: #a854a8; } 529 | .inv38-176 { background: #a854a8; } 530 | .ansi48-176 { background: #a854a8; } 531 | .inv48-176 { color: #a854a8; } 532 | .ansi38-177 { color: #a854d2; } 533 | .inv38-177 { background: #a854d2; } 534 | .ansi48-177 { background: #a854d2; } 535 | .inv48-177 { color: #a854d2; } 536 | .ansi38-208 { color: #d25400; } 537 | .inv38-208 { background: #d25400; } 538 | .ansi48-208 { background: #d25400; } 539 | .inv48-208 { color: #d25400; } 540 | .ansi38-209 { color: #d2542a; } 541 | .inv38-209 { background: #d2542a; } 542 | .ansi48-209 { background: #d2542a; } 543 | .inv48-209 { color: #d2542a; } 544 | .ansi38-210 { color: #d25454; } 545 | .inv38-210 { background: #d25454; } 546 | .ansi48-210 { background: #d25454; } 547 | .inv48-210 { color: #d25454; } 548 | .ansi38-211 { color: #d2547e; } 549 | .inv38-211 { background: #d2547e; } 550 | .ansi48-211 { background: #d2547e; } 551 | .inv48-211 { color: #d2547e; } 552 | .ansi38-212 { color: #d254a8; } 553 | .inv38-212 { background: #d254a8; } 554 | .ansi48-212 { background: #d254a8; } 555 | .inv48-212 { color: #d254a8; } 556 | .ansi38-213 { color: #d254d2; } 557 | .inv38-213 { background: #d254d2; } 558 | .ansi48-213 { background: #d254d2; } 559 | .inv48-213 { color: #d254d2; } 560 | .ansi38-34 { color: #007e00; } 561 | .inv38-34 { background: #007e00; } 562 | .ansi48-34 { background: #007e00; } 563 | .inv48-34 { color: #007e00; } 564 | .ansi38-35 { color: #007e2a; } 565 | .inv38-35 { background: #007e2a; } 566 | .ansi48-35 { background: #007e2a; } 567 | .inv48-35 { color: #007e2a; } 568 | .ansi38-36 { color: #007e54; } 569 | .inv38-36 { background: #007e54; } 570 | .ansi48-36 { background: #007e54; } 571 | .inv48-36 { color: #007e54; } 572 | .ansi38-37 { color: #007e7e; } 573 | .inv38-37 { background: #007e7e; } 574 | .ansi48-37 { background: #007e7e; } 575 | .inv48-37 { color: #007e7e; } 576 | .ansi38-38 { color: #007ea8; } 577 | .inv38-38 { background: #007ea8; } 578 | .ansi48-38 { background: #007ea8; } 579 | .inv48-38 { color: #007ea8; } 580 | .ansi38-39 { color: #007ed2; } 581 | .inv38-39 { background: #007ed2; } 582 | .ansi48-39 { background: #007ed2; } 583 | .inv48-39 { color: #007ed2; } 584 | .ansi38-70 { color: #2a7e00; } 585 | .inv38-70 { background: #2a7e00; } 586 | .ansi48-70 { background: #2a7e00; } 587 | .inv48-70 { color: #2a7e00; } 588 | .ansi38-71 { color: #2a7e2a; } 589 | .inv38-71 { background: #2a7e2a; } 590 | .ansi48-71 { background: #2a7e2a; } 591 | .inv48-71 { color: #2a7e2a; } 592 | .ansi38-72 { color: #2a7e54; } 593 | .inv38-72 { background: #2a7e54; } 594 | .ansi48-72 { background: #2a7e54; } 595 | .inv48-72 { color: #2a7e54; } 596 | .ansi38-73 { color: #2a7e7e; } 597 | .inv38-73 { background: #2a7e7e; } 598 | .ansi48-73 { background: #2a7e7e; } 599 | .inv48-73 { color: #2a7e7e; } 600 | .ansi38-74 { color: #2a7ea8; } 601 | .inv38-74 { background: #2a7ea8; } 602 | .ansi48-74 { background: #2a7ea8; } 603 | .inv48-74 { color: #2a7ea8; } 604 | .ansi38-75 { color: #2a7ed2; } 605 | .inv38-75 { background: #2a7ed2; } 606 | .ansi48-75 { background: #2a7ed2; } 607 | .inv48-75 { color: #2a7ed2; } 608 | .ansi38-106 { color: #547e00; } 609 | .inv38-106 { background: #547e00; } 610 | .ansi48-106 { background: #547e00; } 611 | .inv48-106 { color: #547e00; } 612 | .ansi38-107 { color: #547e2a; } 613 | .inv38-107 { background: #547e2a; } 614 | .ansi48-107 { background: #547e2a; } 615 | .inv48-107 { color: #547e2a; } 616 | .ansi38-108 { color: #547e54; } 617 | .inv38-108 { background: #547e54; } 618 | .ansi48-108 { background: #547e54; } 619 | .inv48-108 { color: #547e54; } 620 | .ansi38-109 { color: #547e7e; } 621 | .inv38-109 { background: #547e7e; } 622 | .ansi48-109 { background: #547e7e; } 623 | .inv48-109 { color: #547e7e; } 624 | .ansi38-110 { color: #547ea8; } 625 | .inv38-110 { background: #547ea8; } 626 | .ansi48-110 { background: #547ea8; } 627 | .inv48-110 { color: #547ea8; } 628 | .ansi38-111 { color: #547ed2; } 629 | .inv38-111 { background: #547ed2; } 630 | .ansi48-111 { background: #547ed2; } 631 | .inv48-111 { color: #547ed2; } 632 | .ansi38-142 { color: #7e7e00; } 633 | .inv38-142 { background: #7e7e00; } 634 | .ansi48-142 { background: #7e7e00; } 635 | .inv48-142 { color: #7e7e00; } 636 | .ansi38-143 { color: #7e7e2a; } 637 | .inv38-143 { background: #7e7e2a; } 638 | .ansi48-143 { background: #7e7e2a; } 639 | .inv48-143 { color: #7e7e2a; } 640 | .ansi38-144 { color: #7e7e54; } 641 | .inv38-144 { background: #7e7e54; } 642 | .ansi48-144 { background: #7e7e54; } 643 | .inv48-144 { color: #7e7e54; } 644 | .ansi38-145 { color: #7e7e7e; } 645 | .inv38-145 { background: #7e7e7e; } 646 | .ansi48-145 { background: #7e7e7e; } 647 | .inv48-145 { color: #7e7e7e; } 648 | .ansi38-146 { color: #7e7ea8; } 649 | .inv38-146 { background: #7e7ea8; } 650 | .ansi48-146 { background: #7e7ea8; } 651 | .inv48-146 { color: #7e7ea8; } 652 | .ansi38-147 { color: #7e7ed2; } 653 | .inv38-147 { background: #7e7ed2; } 654 | .ansi48-147 { background: #7e7ed2; } 655 | .inv48-147 { color: #7e7ed2; } 656 | .ansi38-178 { color: #a87e00; } 657 | .inv38-178 { background: #a87e00; } 658 | .ansi48-178 { background: #a87e00; } 659 | .inv48-178 { color: #a87e00; } 660 | .ansi38-179 { color: #a87e2a; } 661 | .inv38-179 { background: #a87e2a; } 662 | .ansi48-179 { background: #a87e2a; } 663 | .inv48-179 { color: #a87e2a; } 664 | .ansi38-180 { color: #a87e54; } 665 | .inv38-180 { background: #a87e54; } 666 | .ansi48-180 { background: #a87e54; } 667 | .inv48-180 { color: #a87e54; } 668 | .ansi38-181 { color: #a87e7e; } 669 | .inv38-181 { background: #a87e7e; } 670 | .ansi48-181 { background: #a87e7e; } 671 | .inv48-181 { color: #a87e7e; } 672 | .ansi38-182 { color: #a87ea8; } 673 | .inv38-182 { background: #a87ea8; } 674 | .ansi48-182 { background: #a87ea8; } 675 | .inv48-182 { color: #a87ea8; } 676 | .ansi38-183 { color: #a87ed2; } 677 | .inv38-183 { background: #a87ed2; } 678 | .ansi48-183 { background: #a87ed2; } 679 | .inv48-183 { color: #a87ed2; } 680 | .ansi38-214 { color: #d27e00; } 681 | .inv38-214 { background: #d27e00; } 682 | .ansi48-214 { background: #d27e00; } 683 | .inv48-214 { color: #d27e00; } 684 | .ansi38-215 { color: #d27e2a; } 685 | .inv38-215 { background: #d27e2a; } 686 | .ansi48-215 { background: #d27e2a; } 687 | .inv48-215 { color: #d27e2a; } 688 | .ansi38-216 { color: #d27e54; } 689 | .inv38-216 { background: #d27e54; } 690 | .ansi48-216 { background: #d27e54; } 691 | .inv48-216 { color: #d27e54; } 692 | .ansi38-217 { color: #d27e7e; } 693 | .inv38-217 { background: #d27e7e; } 694 | .ansi48-217 { background: #d27e7e; } 695 | .inv48-217 { color: #d27e7e; } 696 | .ansi38-218 { color: #d27ea8; } 697 | .inv38-218 { background: #d27ea8; } 698 | .ansi48-218 { background: #d27ea8; } 699 | .inv48-218 { color: #d27ea8; } 700 | .ansi38-219 { color: #d27ed2; } 701 | .inv38-219 { background: #d27ed2; } 702 | .ansi48-219 { background: #d27ed2; } 703 | .inv48-219 { color: #d27ed2; } 704 | .ansi38-40 { color: #00a800; } 705 | .inv38-40 { background: #00a800; } 706 | .ansi48-40 { background: #00a800; } 707 | .inv48-40 { color: #00a800; } 708 | .ansi38-41 { color: #00a82a; } 709 | .inv38-41 { background: #00a82a; } 710 | .ansi48-41 { background: #00a82a; } 711 | .inv48-41 { color: #00a82a; } 712 | .ansi38-42 { color: #00a854; } 713 | .inv38-42 { background: #00a854; } 714 | .ansi48-42 { background: #00a854; } 715 | .inv48-42 { color: #00a854; } 716 | .ansi38-43 { color: #00a87e; } 717 | .inv38-43 { background: #00a87e; } 718 | .ansi48-43 { background: #00a87e; } 719 | .inv48-43 { color: #00a87e; } 720 | .ansi38-44 { color: #00a8a8; } 721 | .inv38-44 { background: #00a8a8; } 722 | .ansi48-44 { background: #00a8a8; } 723 | .inv48-44 { color: #00a8a8; } 724 | .ansi38-45 { color: #00a8d2; } 725 | .inv38-45 { background: #00a8d2; } 726 | .ansi48-45 { background: #00a8d2; } 727 | .inv48-45 { color: #00a8d2; } 728 | .ansi38-76 { color: #2aa800; } 729 | .inv38-76 { background: #2aa800; } 730 | .ansi48-76 { background: #2aa800; } 731 | .inv48-76 { color: #2aa800; } 732 | .ansi38-77 { color: #2aa82a; } 733 | .inv38-77 { background: #2aa82a; } 734 | .ansi48-77 { background: #2aa82a; } 735 | .inv48-77 { color: #2aa82a; } 736 | .ansi38-78 { color: #2aa854; } 737 | .inv38-78 { background: #2aa854; } 738 | .ansi48-78 { background: #2aa854; } 739 | .inv48-78 { color: #2aa854; } 740 | .ansi38-79 { color: #2aa87e; } 741 | .inv38-79 { background: #2aa87e; } 742 | .ansi48-79 { background: #2aa87e; } 743 | .inv48-79 { color: #2aa87e; } 744 | .ansi38-80 { color: #2aa8a8; } 745 | .inv38-80 { background: #2aa8a8; } 746 | .ansi48-80 { background: #2aa8a8; } 747 | .inv48-80 { color: #2aa8a8; } 748 | .ansi38-81 { color: #2aa8d2; } 749 | .inv38-81 { background: #2aa8d2; } 750 | .ansi48-81 { background: #2aa8d2; } 751 | .inv48-81 { color: #2aa8d2; } 752 | .ansi38-112 { color: #54a800; } 753 | .inv38-112 { background: #54a800; } 754 | .ansi48-112 { background: #54a800; } 755 | .inv48-112 { color: #54a800; } 756 | .ansi38-113 { color: #54a82a; } 757 | .inv38-113 { background: #54a82a; } 758 | .ansi48-113 { background: #54a82a; } 759 | .inv48-113 { color: #54a82a; } 760 | .ansi38-114 { color: #54a854; } 761 | .inv38-114 { background: #54a854; } 762 | .ansi48-114 { background: #54a854; } 763 | .inv48-114 { color: #54a854; } 764 | .ansi38-115 { color: #54a87e; } 765 | .inv38-115 { background: #54a87e; } 766 | .ansi48-115 { background: #54a87e; } 767 | .inv48-115 { color: #54a87e; } 768 | .ansi38-116 { color: #54a8a8; } 769 | .inv38-116 { background: #54a8a8; } 770 | .ansi48-116 { background: #54a8a8; } 771 | .inv48-116 { color: #54a8a8; } 772 | .ansi38-117 { color: #54a8d2; } 773 | .inv38-117 { background: #54a8d2; } 774 | .ansi48-117 { background: #54a8d2; } 775 | .inv48-117 { color: #54a8d2; } 776 | .ansi38-148 { color: #7ea800; } 777 | .inv38-148 { background: #7ea800; } 778 | .ansi48-148 { background: #7ea800; } 779 | .inv48-148 { color: #7ea800; } 780 | .ansi38-149 { color: #7ea82a; } 781 | .inv38-149 { background: #7ea82a; } 782 | .ansi48-149 { background: #7ea82a; } 783 | .inv48-149 { color: #7ea82a; } 784 | .ansi38-150 { color: #7ea854; } 785 | .inv38-150 { background: #7ea854; } 786 | .ansi48-150 { background: #7ea854; } 787 | .inv48-150 { color: #7ea854; } 788 | .ansi38-151 { color: #7ea87e; } 789 | .inv38-151 { background: #7ea87e; } 790 | .ansi48-151 { background: #7ea87e; } 791 | .inv48-151 { color: #7ea87e; } 792 | .ansi38-152 { color: #7ea8a8; } 793 | .inv38-152 { background: #7ea8a8; } 794 | .ansi48-152 { background: #7ea8a8; } 795 | .inv48-152 { color: #7ea8a8; } 796 | .ansi38-153 { color: #7ea8d2; } 797 | .inv38-153 { background: #7ea8d2; } 798 | .ansi48-153 { background: #7ea8d2; } 799 | .inv48-153 { color: #7ea8d2; } 800 | .ansi38-184 { color: #a8a800; } 801 | .inv38-184 { background: #a8a800; } 802 | .ansi48-184 { background: #a8a800; } 803 | .inv48-184 { color: #a8a800; } 804 | .ansi38-185 { color: #a8a82a; } 805 | .inv38-185 { background: #a8a82a; } 806 | .ansi48-185 { background: #a8a82a; } 807 | .inv48-185 { color: #a8a82a; } 808 | .ansi38-186 { color: #a8a854; } 809 | .inv38-186 { background: #a8a854; } 810 | .ansi48-186 { background: #a8a854; } 811 | .inv48-186 { color: #a8a854; } 812 | .ansi38-187 { color: #a8a87e; } 813 | .inv38-187 { background: #a8a87e; } 814 | .ansi48-187 { background: #a8a87e; } 815 | .inv48-187 { color: #a8a87e; } 816 | .ansi38-188 { color: #a8a8a8; } 817 | .inv38-188 { background: #a8a8a8; } 818 | .ansi48-188 { background: #a8a8a8; } 819 | .inv48-188 { color: #a8a8a8; } 820 | .ansi38-189 { color: #a8a8d2; } 821 | .inv38-189 { background: #a8a8d2; } 822 | .ansi48-189 { background: #a8a8d2; } 823 | .inv48-189 { color: #a8a8d2; } 824 | .ansi38-220 { color: #d2a800; } 825 | .inv38-220 { background: #d2a800; } 826 | .ansi48-220 { background: #d2a800; } 827 | .inv48-220 { color: #d2a800; } 828 | .ansi38-221 { color: #d2a82a; } 829 | .inv38-221 { background: #d2a82a; } 830 | .ansi48-221 { background: #d2a82a; } 831 | .inv48-221 { color: #d2a82a; } 832 | .ansi38-222 { color: #d2a854; } 833 | .inv38-222 { background: #d2a854; } 834 | .ansi48-222 { background: #d2a854; } 835 | .inv48-222 { color: #d2a854; } 836 | .ansi38-223 { color: #d2a87e; } 837 | .inv38-223 { background: #d2a87e; } 838 | .ansi48-223 { background: #d2a87e; } 839 | .inv48-223 { color: #d2a87e; } 840 | .ansi38-224 { color: #d2a8a8; } 841 | .inv38-224 { background: #d2a8a8; } 842 | .ansi48-224 { background: #d2a8a8; } 843 | .inv48-224 { color: #d2a8a8; } 844 | .ansi38-225 { color: #d2a8d2; } 845 | .inv38-225 { background: #d2a8d2; } 846 | .ansi48-225 { background: #d2a8d2; } 847 | .inv48-225 { color: #d2a8d2; } 848 | .ansi38-46 { color: #00d200; } 849 | .inv38-46 { background: #00d200; } 850 | .ansi48-46 { background: #00d200; } 851 | .inv48-46 { color: #00d200; } 852 | .ansi38-47 { color: #00d22a; } 853 | .inv38-47 { background: #00d22a; } 854 | .ansi48-47 { background: #00d22a; } 855 | .inv48-47 { color: #00d22a; } 856 | .ansi38-48 { color: #00d254; } 857 | .inv38-48 { background: #00d254; } 858 | .ansi48-48 { background: #00d254; } 859 | .inv48-48 { color: #00d254; } 860 | .ansi38-49 { color: #00d27e; } 861 | .inv38-49 { background: #00d27e; } 862 | .ansi48-49 { background: #00d27e; } 863 | .inv48-49 { color: #00d27e; } 864 | .ansi38-50 { color: #00d2a8; } 865 | .inv38-50 { background: #00d2a8; } 866 | .ansi48-50 { background: #00d2a8; } 867 | .inv48-50 { color: #00d2a8; } 868 | .ansi38-51 { color: #00d2d2; } 869 | .inv38-51 { background: #00d2d2; } 870 | .ansi48-51 { background: #00d2d2; } 871 | .inv48-51 { color: #00d2d2; } 872 | .ansi38-82 { color: #2ad200; } 873 | .inv38-82 { background: #2ad200; } 874 | .ansi48-82 { background: #2ad200; } 875 | .inv48-82 { color: #2ad200; } 876 | .ansi38-83 { color: #2ad22a; } 877 | .inv38-83 { background: #2ad22a; } 878 | .ansi48-83 { background: #2ad22a; } 879 | .inv48-83 { color: #2ad22a; } 880 | .ansi38-84 { color: #2ad254; } 881 | .inv38-84 { background: #2ad254; } 882 | .ansi48-84 { background: #2ad254; } 883 | .inv48-84 { color: #2ad254; } 884 | .ansi38-85 { color: #2ad27e; } 885 | .inv38-85 { background: #2ad27e; } 886 | .ansi48-85 { background: #2ad27e; } 887 | .inv48-85 { color: #2ad27e; } 888 | .ansi38-86 { color: #2ad2a8; } 889 | .inv38-86 { background: #2ad2a8; } 890 | .ansi48-86 { background: #2ad2a8; } 891 | .inv48-86 { color: #2ad2a8; } 892 | .ansi38-87 { color: #2ad2d2; } 893 | .inv38-87 { background: #2ad2d2; } 894 | .ansi48-87 { background: #2ad2d2; } 895 | .inv48-87 { color: #2ad2d2; } 896 | .ansi38-118 { color: #54d200; } 897 | .inv38-118 { background: #54d200; } 898 | .ansi48-118 { background: #54d200; } 899 | .inv48-118 { color: #54d200; } 900 | .ansi38-119 { color: #54d22a; } 901 | .inv38-119 { background: #54d22a; } 902 | .ansi48-119 { background: #54d22a; } 903 | .inv48-119 { color: #54d22a; } 904 | .ansi38-120 { color: #54d254; } 905 | .inv38-120 { background: #54d254; } 906 | .ansi48-120 { background: #54d254; } 907 | .inv48-120 { color: #54d254; } 908 | .ansi38-121 { color: #54d27e; } 909 | .inv38-121 { background: #54d27e; } 910 | .ansi48-121 { background: #54d27e; } 911 | .inv48-121 { color: #54d27e; } 912 | .ansi38-122 { color: #54d2a8; } 913 | .inv38-122 { background: #54d2a8; } 914 | .ansi48-122 { background: #54d2a8; } 915 | .inv48-122 { color: #54d2a8; } 916 | .ansi38-123 { color: #54d2d2; } 917 | .inv38-123 { background: #54d2d2; } 918 | .ansi48-123 { background: #54d2d2; } 919 | .inv48-123 { color: #54d2d2; } 920 | .ansi38-154 { color: #7ed200; } 921 | .inv38-154 { background: #7ed200; } 922 | .ansi48-154 { background: #7ed200; } 923 | .inv48-154 { color: #7ed200; } 924 | .ansi38-155 { color: #7ed22a; } 925 | .inv38-155 { background: #7ed22a; } 926 | .ansi48-155 { background: #7ed22a; } 927 | .inv48-155 { color: #7ed22a; } 928 | .ansi38-156 { color: #7ed254; } 929 | .inv38-156 { background: #7ed254; } 930 | .ansi48-156 { background: #7ed254; } 931 | .inv48-156 { color: #7ed254; } 932 | .ansi38-157 { color: #7ed27e; } 933 | .inv38-157 { background: #7ed27e; } 934 | .ansi48-157 { background: #7ed27e; } 935 | .inv48-157 { color: #7ed27e; } 936 | .ansi38-158 { color: #7ed2a8; } 937 | .inv38-158 { background: #7ed2a8; } 938 | .ansi48-158 { background: #7ed2a8; } 939 | .inv48-158 { color: #7ed2a8; } 940 | .ansi38-159 { color: #7ed2d2; } 941 | .inv38-159 { background: #7ed2d2; } 942 | .ansi48-159 { background: #7ed2d2; } 943 | .inv48-159 { color: #7ed2d2; } 944 | .ansi38-190 { color: #a8d200; } 945 | .inv38-190 { background: #a8d200; } 946 | .ansi48-190 { background: #a8d200; } 947 | .inv48-190 { color: #a8d200; } 948 | .ansi38-191 { color: #a8d22a; } 949 | .inv38-191 { background: #a8d22a; } 950 | .ansi48-191 { background: #a8d22a; } 951 | .inv48-191 { color: #a8d22a; } 952 | .ansi38-192 { color: #a8d254; } 953 | .inv38-192 { background: #a8d254; } 954 | .ansi48-192 { background: #a8d254; } 955 | .inv48-192 { color: #a8d254; } 956 | .ansi38-193 { color: #a8d27e; } 957 | .inv38-193 { background: #a8d27e; } 958 | .ansi48-193 { background: #a8d27e; } 959 | .inv48-193 { color: #a8d27e; } 960 | .ansi38-194 { color: #a8d2a8; } 961 | .inv38-194 { background: #a8d2a8; } 962 | .ansi48-194 { background: #a8d2a8; } 963 | .inv48-194 { color: #a8d2a8; } 964 | .ansi38-195 { color: #a8d2d2; } 965 | .inv38-195 { background: #a8d2d2; } 966 | .ansi48-195 { background: #a8d2d2; } 967 | .inv48-195 { color: #a8d2d2; } 968 | .ansi38-226 { color: #d2d200; } 969 | .inv38-226 { background: #d2d200; } 970 | .ansi48-226 { background: #d2d200; } 971 | .inv48-226 { color: #d2d200; } 972 | .ansi38-227 { color: #d2d22a; } 973 | .inv38-227 { background: #d2d22a; } 974 | .ansi48-227 { background: #d2d22a; } 975 | .inv48-227 { color: #d2d22a; } 976 | .ansi38-228 { color: #d2d254; } 977 | .inv38-228 { background: #d2d254; } 978 | .ansi48-228 { background: #d2d254; } 979 | .inv48-228 { color: #d2d254; } 980 | .ansi38-229 { color: #d2d27e; } 981 | .inv38-229 { background: #d2d27e; } 982 | .ansi48-229 { background: #d2d27e; } 983 | .inv48-229 { color: #d2d27e; } 984 | .ansi38-230 { color: #d2d2a8; } 985 | .inv38-230 { background: #d2d2a8; } 986 | .ansi48-230 { background: #d2d2a8; } 987 | .inv48-230 { color: #d2d2a8; } 988 | .ansi38-231 { color: #d2d2d2; } 989 | .inv38-231 { background: #d2d2d2; } 990 | .ansi48-231 { background: #d2d2d2; } 991 | .inv48-231 { color: #d2d2d2; } 992 | .ansi38-232 { color: #080808; } 993 | .inv38-232 { background: #080808; } 994 | .ansi48-232 { background: #080808; } 995 | .inv48-232 { color: #080808; } 996 | .ansi38-233 { color: #121212; } 997 | .inv38-233 { background: #121212; } 998 | .ansi48-233 { background: #121212; } 999 | .inv48-233 { color: #121212; } 1000 | .ansi38-234 { color: #1c1c1c; } 1001 | .inv38-234 { background: #1c1c1c; } 1002 | .ansi48-234 { background: #1c1c1c; } 1003 | .inv48-234 { color: #1c1c1c; } 1004 | .ansi38-235 { color: #262626; } 1005 | .inv38-235 { background: #262626; } 1006 | .ansi48-235 { background: #262626; } 1007 | .inv48-235 { color: #262626; } 1008 | .ansi38-236 { color: #303030; } 1009 | .inv38-236 { background: #303030; } 1010 | .ansi48-236 { background: #303030; } 1011 | .inv48-236 { color: #303030; } 1012 | .ansi38-237 { color: #3a3a3a; } 1013 | .inv38-237 { background: #3a3a3a; } 1014 | .ansi48-237 { background: #3a3a3a; } 1015 | .inv48-237 { color: #3a3a3a; } 1016 | .ansi38-238 { color: #444444; } 1017 | .inv38-238 { background: #444444; } 1018 | .ansi48-238 { background: #444444; } 1019 | .inv48-238 { color: #444444; } 1020 | .ansi38-239 { color: #4e4e4e; } 1021 | .inv38-239 { background: #4e4e4e; } 1022 | .ansi48-239 { background: #4e4e4e; } 1023 | .inv48-239 { color: #4e4e4e; } 1024 | .ansi38-240 { color: #585858; } 1025 | .inv38-240 { background: #585858; } 1026 | .ansi48-240 { background: #585858; } 1027 | .inv48-240 { color: #585858; } 1028 | .ansi38-241 { color: #626262; } 1029 | .inv38-241 { background: #626262; } 1030 | .ansi48-241 { background: #626262; } 1031 | .inv48-241 { color: #626262; } 1032 | .ansi38-242 { color: #6c6c6c; } 1033 | .inv38-242 { background: #6c6c6c; } 1034 | .ansi48-242 { background: #6c6c6c; } 1035 | .inv48-242 { color: #6c6c6c; } 1036 | .ansi38-243 { color: #767676; } 1037 | .inv38-243 { background: #767676; } 1038 | .ansi48-243 { background: #767676; } 1039 | .inv48-243 { color: #767676; } 1040 | .ansi38-244 { color: #808080; } 1041 | .inv38-244 { background: #808080; } 1042 | .ansi48-244 { background: #808080; } 1043 | .inv48-244 { color: #808080; } 1044 | .ansi38-245 { color: #8a8a8a; } 1045 | .inv38-245 { background: #8a8a8a; } 1046 | .ansi48-245 { background: #8a8a8a; } 1047 | .inv48-245 { color: #8a8a8a; } 1048 | .ansi38-246 { color: #949494; } 1049 | .inv38-246 { background: #949494; } 1050 | .ansi48-246 { background: #949494; } 1051 | .inv48-246 { color: #949494; } 1052 | .ansi38-247 { color: #9e9e9e; } 1053 | .inv38-247 { background: #9e9e9e; } 1054 | .ansi48-247 { background: #9e9e9e; } 1055 | .inv48-247 { color: #9e9e9e; } 1056 | .ansi38-248 { color: #a8a8a8; } 1057 | .inv38-248 { background: #a8a8a8; } 1058 | .ansi48-248 { background: #a8a8a8; } 1059 | .inv48-248 { color: #a8a8a8; } 1060 | .ansi38-249 { color: #b2b2b2; } 1061 | .inv38-249 { background: #b2b2b2; } 1062 | .ansi48-249 { background: #b2b2b2; } 1063 | .inv48-249 { color: #b2b2b2; } 1064 | .ansi38-250 { color: #bcbcbc; } 1065 | .inv38-250 { background: #bcbcbc; } 1066 | .ansi48-250 { background: #bcbcbc; } 1067 | .inv48-250 { color: #bcbcbc; } 1068 | .ansi38-251 { color: #c6c6c6; } 1069 | .inv38-251 { background: #c6c6c6; } 1070 | .ansi48-251 { background: #c6c6c6; } 1071 | .inv48-251 { color: #c6c6c6; } 1072 | .ansi38-252 { color: #d0d0d0; } 1073 | .inv38-252 { background: #d0d0d0; } 1074 | .ansi48-252 { background: #d0d0d0; } 1075 | .inv48-252 { color: #d0d0d0; } 1076 | .ansi38-253 { color: #dadada; } 1077 | .inv38-253 { background: #dadada; } 1078 | .ansi48-253 { background: #dadada; } 1079 | .inv48-253 { color: #dadada; } 1080 | .ansi38-254 { color: #e4e4e4; } 1081 | .inv38-254 { background: #e4e4e4; } 1082 | .ansi48-254 { background: #e4e4e4; } 1083 | .inv48-254 { color: #e4e4e4; } 1084 | .ansi38-255 { color: #eeeeee; } 1085 | .inv38-255 { background: #eeeeee; } 1086 | .ansi48-255 { background: #eeeeee; } 1087 | .inv48-255 { color: #eeeeee; } 1088 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | ## Debug 2 | 3 | Somehow in the 27 years (and counting) of active development of python, no one thought to add a simple 4 | and readable way to print stuff during development. (If you know why this is, I'd love to hear). 5 | 6 | **The wait is over:** 7 | 8 | ```py 9 | {!examples/example.py!} 10 | ``` 11 | 12 | {{ example_html(examples/example.py) }} 13 | 14 | `debug` is like `print` after a good night's sleep and lots of coffee: 15 | 16 | * each output is prefixed with the file, line number and function where `debug` was called 17 | * the variable name or expression being printed is shown 18 | * each argument is printed "pretty" on a new line, see [prettier print](#prettier-print) 19 | * if `pygments` is installed the output is highlighted 20 | 21 | A more complex example of `debug` shows more of what it can do. 22 | 23 | ```py 24 | {!examples/complex.py!} 25 | ``` 26 | 27 | {{ example_html(examples/complex.py) }} 28 | 29 | ### Returning the arguments 30 | 31 | `debug` will return the arguments passed to it meaning you can insert `debug(...)` into code. 32 | 33 | The returned arguments work as follows: 34 | 35 | * if one non-keyword argument is passed to `debug()`, it is returned as-is 36 | * if multiple arguments are passed to `debug()`, they are returned as a tuple 37 | * if keyword arguments are passed to `debug()`, the `kwargs` dictionary is added to the returned tuple 38 | 39 | ```py 40 | {!examples/return_args.py!} 41 | ``` 42 | 43 | {{ example_html(examples/return_args.py) }} 44 | 45 | ## Other debug tools 46 | 47 | The debug namespace includes a number of other useful functions: 48 | 49 | * `debug.format()` same as calling `debug()` but returns a `DebugOutput` rather than printing the output 50 | * `debug.timer()` returns an instance of *devtool's* `Timer` class suitable for timing code execution 51 | * `debug.breakpoint()` introduces a breakpoint using `pdb` 52 | 53 | ```py 54 | {!examples/other.py!} 55 | ``` 56 | 57 | {{ example_html(examples/other.py) }} 58 | 59 | ### Prettier print 60 | 61 | Python comes with [pretty print](https://docs.python.org/3/library/pprint.html), problem is quite often 62 | it's not that pretty, it also doesn't cope well with non standard python objects (think numpy arrays or 63 | django querysets) which have their own pretty print functionality. 64 | 65 | To get round this *devtools* comes with prettier print, my take on pretty printing. You can see it in use above 66 | in `debug()`, but it can also be used directly: 67 | 68 | ```py 69 | {!examples/prettier.py!} 70 | ``` 71 | 72 | {{ example_html(examples/prettier.py) }} 73 | 74 | For more details on prettier printing, see 75 | [`prettier.py`](https://github.com/samuelcolvin/python-devtools/blob/main/devtools/prettier.py). 76 | 77 | ## ANSI terminal colours 78 | 79 | ```py 80 | {!examples/ansi_colours.py!} 81 | ``` 82 | 83 | For more details on ansi colours, see 84 | [ansi.py](https://github.com/samuelcolvin/python-devtools/blob/main/devtools/ansi.py). 85 | 86 | ## Usage without Import 87 | 88 | We all know the annoyance of running code only to discover a missing import, this can be particularly 89 | frustrating when the function you're using isn't used except during development. 90 | 91 | devtool's `debug` function can be used without import if you add `debug` to `__builtins__` 92 | in `sitecustomize.py`. 93 | 94 | Two ways to do this: 95 | 96 | ### Automatic install 97 | 98 | !!! warning 99 | This is experimental, please [create an issue](https://github.com/samuelcolvin/python-devtools/issues) 100 | if you encounter any problems. 101 | 102 | To install `debug` into `__builtins__` automatically, run: 103 | 104 | ```bash 105 | python -m devtools install 106 | ``` 107 | 108 | This command won't write to any files, but it should print a command for you to run to add/edit `sitecustomize.py`. 109 | 110 | ### Manual install 111 | 112 | To manually add `debug` to `__builtins__`, add the following to `sitecustomize.py` or any code 113 | which is always imported. 114 | 115 | ```py 116 | import sys 117 | # we don't install here for pytest as it breaks pytest, it is 118 | # installed later by a pytest fixture 119 | if not sys.argv[0].endswith('pytest'): 120 | import builtins 121 | try: 122 | from devtools import debug 123 | except ImportError: 124 | pass 125 | else: 126 | setattr(builtins, 'debug', debug) 127 | ``` 128 | 129 | The `ImportError` exception is important since you'll want python to run fine even if *devtools* isn't installed. 130 | 131 | This approach has another advantage: if you forget to remove `debug(...)` calls from your code, CI 132 | (which won't have devtools installed) should fail both on execution and linting, meaning you don't end up with 133 | extraneous debug calls in production code. 134 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: python-devtools 2 | site_description: Python's missing debug print command and other development tools. 3 | strict: true 4 | site_url: https://python-devtools.helpmanual.io/ 5 | 6 | theme: 7 | name: 'material' 8 | custom_dir: 'docs/theme' 9 | palette: 10 | primary: pink 11 | accent: pink 12 | 13 | repo_name: samuelcolvin/python-devtools 14 | repo_url: https://github.com/samuelcolvin/python-devtools 15 | 16 | extra: 17 | analytics: 18 | provider: google 19 | property: UA-62733018-4 20 | social: 21 | - icon: fontawesome/brands/github-alt 22 | link: https://github.com/samuelcolvin/python-devtools 23 | - icon: fontawesome/brands/twitter 24 | link: https://twitter.com/samuel_colvin 25 | 26 | extra_css: 27 | - 'theme/customization.css' 28 | 29 | nav: 30 | - Overview: index.md 31 | - install.md 32 | - usage.md 33 | - history.md 34 | 35 | markdown_extensions: 36 | - markdown.extensions.codehilite: 37 | guess_lang: false 38 | - markdown_include.include: 39 | base_path: docs 40 | - toc: 41 | permalink: 🔗 42 | - admonition 43 | - codehilite 44 | - extra 45 | - attr_list 46 | - pymdownx.highlight: 47 | anchor_linenums: true 48 | - pymdownx.inlinehilite 49 | - pymdownx.snippets 50 | - pymdownx.superfences 51 | 52 | plugins: 53 | - search 54 | - exclude: 55 | glob: 56 | - build/* 57 | - examples/* 58 | - requirements.txt 59 | - mkdocs-simple-hooks: 60 | hooks: 61 | on_pre_build: 'docs.plugins:on_pre_build' 62 | on_files: 'docs.plugins:on_files' 63 | on_page_markdown: 'docs.plugins:on_page_markdown' 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['hatchling'] 3 | build-backend = 'hatchling.build' 4 | 5 | [tool.hatch.version] 6 | path = 'devtools/version.py' 7 | 8 | [project] 9 | name = 'devtools' 10 | description = "Python's missing debug print command, and more." 11 | authors = [{name = 'Samuel Colvin', email = 's@muelcolvin.com'}] 12 | license = {file = 'LICENSE'} 13 | readme = 'README.md' 14 | classifiers = [ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Intended Audience :: Developers', 17 | 'Intended Audience :: Education', 18 | 'Intended Audience :: Information Technology', 19 | 'Intended Audience :: Science/Research', 20 | 'Intended Audience :: System Administrators', 21 | 'Operating System :: Unix', 22 | 'Operating System :: POSIX :: Linux', 23 | 'Environment :: Console', 24 | 'Environment :: MacOS X', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3 :: Only', 27 | 'Programming Language :: Python :: 3.7', 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 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | 'Topic :: Internet', 34 | 'Typing :: Typed', 35 | ] 36 | requires-python = '>=3.7' 37 | dependencies = [ 38 | 'executing>=1.1.1', 39 | 'asttokens>=2.0.0,<3.0.0', 40 | 'Pygments>=2.15.0', 41 | ] 42 | # keep this meaningless group around to avoid breaking installs using `pip install devtools[pygments]` 43 | optional-dependencies = {pygments = [] } 44 | dynamic = ['version'] 45 | 46 | [project.urls] 47 | Homepage = 'https://github.com/samuelcolvin/python-devtools' 48 | Documentation = 'https://python-devtools.helpmanual.io' 49 | Funding = 'https://github.com/sponsors/samuelcolvin' 50 | Source = 'https://github.com/samuelcolvin/python-devtools' 51 | Changelog = 'https://github.com/samuelcolvin/python-devtools/releases' 52 | 53 | [project.entry-points.pytest11] 54 | devtools = 'devtools.pytest_plugin' 55 | 56 | [tool.pytest.ini_options] 57 | testpaths = 'tests' 58 | filterwarnings = 'error' 59 | 60 | [tool.coverage.run] 61 | source = ['devtools'] 62 | branch = true 63 | omit = ['devtools/__main__.py'] 64 | 65 | [tool.coverage.report] 66 | precision = 2 67 | exclude_lines = [ 68 | 'pragma: no cover', 69 | 'raise NotImplementedError', 70 | 'raise NotImplemented', 71 | 'if TYPE_CHECKING:', 72 | 'if MYPY:', 73 | '@overload', 74 | ] 75 | 76 | [tool.black] 77 | color = true 78 | line-length = 120 79 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] 80 | skip-string-normalization = true 81 | extend-exclude = ['tests/test_expr_render.py'] 82 | 83 | [tool.ruff] 84 | line-length = 120 85 | exclude = ['cases_update'] 86 | extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I'] 87 | flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'} 88 | mccabe = { max-complexity = 14 } 89 | isort = { known-first-party = ['devtools'] } 90 | target-version = 'py37' 91 | 92 | [tool.mypy] 93 | show_error_codes = true 94 | strict = true 95 | warn_return_any = false 96 | 97 | [[tool.mypy.overrides]] 98 | module = ['executing.*', 'pygments.*'] 99 | ignore_missing_imports = true 100 | -------------------------------------------------------------------------------- /requirements/all.txt: -------------------------------------------------------------------------------- 1 | -r ./docs.txt 2 | -r ./linting.txt 3 | -r ./testing.txt 4 | -r ./pyproject.txt 5 | -------------------------------------------------------------------------------- /requirements/docs.in: -------------------------------------------------------------------------------- 1 | ansi2html 2 | mkdocs 3 | mkdocs-exclude 4 | mkdocs-material 5 | mkdocs-simple-hooks 6 | markdown-include 7 | ruff 8 | numpy 9 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/docs.txt --resolver=backtracking requirements/docs.in 6 | # 7 | ansi2html==1.8.0 8 | # via -r requirements/docs.in 9 | certifi==2023.7.22 10 | # via requests 11 | charset-normalizer==3.1.0 12 | # via requests 13 | click==8.1.3 14 | # via mkdocs 15 | colorama==0.4.6 16 | # via mkdocs-material 17 | ghp-import==2.1.0 18 | # via mkdocs 19 | idna==3.4 20 | # via requests 21 | jinja2==3.1.2 22 | # via 23 | # mkdocs 24 | # mkdocs-material 25 | markdown==3.3.7 26 | # via 27 | # markdown-include 28 | # mkdocs 29 | # mkdocs-material 30 | # pymdown-extensions 31 | markdown-include==0.8.1 32 | # via -r requirements/docs.in 33 | markupsafe==2.1.2 34 | # via jinja2 35 | mergedeep==1.3.4 36 | # via mkdocs 37 | mkdocs==1.4.2 38 | # via 39 | # -r requirements/docs.in 40 | # mkdocs-exclude 41 | # mkdocs-material 42 | # mkdocs-simple-hooks 43 | mkdocs-exclude==1.0.2 44 | # via -r requirements/docs.in 45 | mkdocs-material==9.1.5 46 | # via -r requirements/docs.in 47 | mkdocs-material-extensions==1.1.1 48 | # via mkdocs-material 49 | mkdocs-simple-hooks==0.1.5 50 | # via -r requirements/docs.in 51 | numpy==1.24.2 52 | # via -r requirements/docs.in 53 | packaging==23.0 54 | # via mkdocs 55 | pygments==2.15.0 56 | # via mkdocs-material 57 | pymdown-extensions==10.0 58 | # via mkdocs-material 59 | python-dateutil==2.8.2 60 | # via ghp-import 61 | pyyaml==6.0 62 | # via 63 | # mkdocs 64 | # pymdown-extensions 65 | # pyyaml-env-tag 66 | pyyaml-env-tag==0.1 67 | # via mkdocs 68 | regex==2023.3.23 69 | # via mkdocs-material 70 | requests==2.31.0 71 | # via mkdocs-material 72 | ruff==0.0.261 73 | # via -r requirements/docs.in 74 | six==1.16.0 75 | # via python-dateutil 76 | urllib3==1.26.15 77 | # via requests 78 | watchdog==3.0.0 79 | # via mkdocs 80 | -------------------------------------------------------------------------------- /requirements/linting.in: -------------------------------------------------------------------------------- 1 | black 2 | mypy 3 | ruff 4 | # required so mypy can find stubs 5 | sqlalchemy 6 | pytest 7 | -------------------------------------------------------------------------------- /requirements/linting.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/linting.txt --resolver=backtracking requirements/linting.in 6 | # 7 | attrs==22.2.0 8 | # via pytest 9 | black==23.3.0 10 | # via -r requirements/linting.in 11 | click==8.1.3 12 | # via black 13 | exceptiongroup==1.1.1 14 | # via pytest 15 | iniconfig==2.0.0 16 | # via pytest 17 | mypy==1.1.1 18 | # via -r requirements/linting.in 19 | mypy-extensions==1.0.0 20 | # via 21 | # black 22 | # mypy 23 | packaging==23.0 24 | # via 25 | # black 26 | # pytest 27 | pathspec==0.11.1 28 | # via black 29 | platformdirs==3.2.0 30 | # via black 31 | pluggy==1.0.0 32 | # via pytest 33 | pytest==7.2.2 34 | # via -r requirements/linting.in 35 | ruff==0.0.261 36 | # via -r requirements/linting.in 37 | sqlalchemy==2.0.8 38 | # via -r requirements/linting.in 39 | tomli==2.0.1 40 | # via 41 | # black 42 | # mypy 43 | # pytest 44 | typing-extensions==4.5.0 45 | # via 46 | # mypy 47 | # sqlalchemy 48 | -------------------------------------------------------------------------------- /requirements/pyproject.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/pyproject.txt --resolver=backtracking pyproject.toml 6 | # 7 | asttokens==2.2.1 8 | # via devtools (pyproject.toml) 9 | executing==1.2.0 10 | # via devtools (pyproject.toml) 11 | six==1.16.0 12 | # via asttokens 13 | -------------------------------------------------------------------------------- /requirements/testing.in: -------------------------------------------------------------------------------- 1 | coverage[toml] 2 | pytest 3 | pytest-mock 4 | pytest-pretty 5 | # these packages are used in tests so install the latest version 6 | # no binaries for 3.7 7 | asyncpg; python_version>='3.8' 8 | black 9 | multidict; python_version>='3.8' 10 | # no version is compatible with 3.7 and 3.11 11 | numpy; python_version>='3.8' 12 | pydantic 13 | sqlalchemy 14 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in 6 | # 7 | asyncpg==0.27.0 ; python_version >= "3.8" 8 | # via -r requirements/testing.in 9 | attrs==22.2.0 10 | # via pytest 11 | black==23.3.0 12 | # via -r requirements/testing.in 13 | click==8.1.3 14 | # via black 15 | coverage[toml]==7.2.2 16 | # via -r requirements/testing.in 17 | exceptiongroup==1.1.3 18 | # via pytest 19 | iniconfig==2.0.0 20 | # via pytest 21 | markdown-it-py==2.2.0 22 | # via rich 23 | mdurl==0.1.2 24 | # via markdown-it-py 25 | multidict==6.0.4 ; python_version >= "3.8" 26 | # via -r requirements/testing.in 27 | mypy-extensions==1.0.0 28 | # via black 29 | numpy==1.24.2 ; python_version >= "3.8" 30 | # via -r requirements/testing.in 31 | packaging==23.0 32 | # via 33 | # black 34 | # pytest 35 | pathspec==0.11.1 36 | # via black 37 | platformdirs==3.2.0 38 | # via black 39 | pluggy==1.0.0 40 | # via pytest 41 | pydantic==1.10.7 42 | # via -r requirements/testing.in 43 | pygments==2.15.0 44 | # via rich 45 | pytest==7.2.2 46 | # via 47 | # -r requirements/testing.in 48 | # pytest-mock 49 | # pytest-pretty 50 | pytest-mock==3.10.0 51 | # via -r requirements/testing.in 52 | pytest-pretty==1.2.0 53 | # via -r requirements/testing.in 54 | rich==13.3.3 55 | # via pytest-pretty 56 | sqlalchemy==2.0.8 57 | # via -r requirements/testing.in 58 | tomli==2.0.1 59 | # via 60 | # black 61 | # coverage 62 | # pytest 63 | typing-extensions==4.5.0 64 | # via 65 | # pydantic 66 | # sqlalchemy 67 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiangolo/python-devtools/ec406ffdd841f65b132e81f3d715321d3cfb5efa/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | pytest_plugins = ['pytester'] 4 | 5 | 6 | def pytest_sessionstart(session): 7 | os.environ.pop('PY_DEVTOOLS_HIGHLIGHT', None) 8 | -------------------------------------------------------------------------------- /tests/test_ansi.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | import pytest 4 | 5 | from devtools.ansi import sformat, sprint 6 | 7 | 8 | def test_colours(): 9 | v = sformat('hello', sformat.red) 10 | assert v == '\x1b[31mhello\x1b[0m' 11 | 12 | 13 | def test_no_reset(): 14 | v = sformat('hello', sformat.bold, reset=False) 15 | assert v == '\x1b[1mhello' 16 | 17 | 18 | def test_combine_styles(): 19 | v = sformat('hello', sformat.red, sformat.bold) 20 | assert v == '\x1b[31;1mhello\x1b[0m' 21 | 22 | 23 | def test_no_styles(): 24 | v = sformat('hello') 25 | assert v == 'hello\x1b[0m' 26 | 27 | 28 | def test_style_str(): 29 | v = sformat('hello', 'red') 30 | assert v == '\x1b[31mhello\x1b[0m' 31 | 32 | 33 | def test_non_str_input(): 34 | v = sformat(12.2, sformat.yellow, sformat.italic) 35 | assert v == '\x1b[33;3m12.2\x1b[0m' 36 | 37 | 38 | def test_invalid_style_str(): 39 | with pytest.raises(ValueError) as exc_info: 40 | sformat('x', 'mauve') 41 | assert exc_info.value.args[0] == 'invalid style "mauve"' 42 | 43 | 44 | def test_print_not_tty(): 45 | stream = io.StringIO() 46 | sprint('hello', sprint.green, file=stream) 47 | out = stream.getvalue() 48 | assert out == 'hello\n' 49 | 50 | 51 | def test_print_is_tty(): 52 | class TTYStream(io.StringIO): 53 | def isatty(self): 54 | return True 55 | 56 | stream = TTYStream() 57 | sprint('hello', sprint.green, file=stream) 58 | out = stream.getvalue() 59 | assert out == '\x1b[32mhello\x1b[0m\n', repr(out) 60 | 61 | 62 | def test_print_tty_error(): 63 | class TTYStream(io.StringIO): 64 | def isatty(self): 65 | raise RuntimeError('boom') 66 | 67 | stream = TTYStream() 68 | sprint('hello', sformat.green, file=stream) 69 | out = stream.getvalue() 70 | assert out == 'hello\n' 71 | 72 | 73 | def test_get_styles(): 74 | assert sformat.styles['bold'] == 1 75 | assert sformat.styles['not_bold'] == 22 76 | 77 | 78 | def test_repr_str(): 79 | assert repr(sformat) == '' 80 | assert repr(sformat.red) == '' 81 | 82 | assert str(sformat) == '' 83 | assert str(sformat.red) == 'Style.red' 84 | 85 | assert repr(sprint) == '' 86 | assert str(sprint) == '' 87 | -------------------------------------------------------------------------------- /tests/test_custom_pretty.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from devtools import pformat 4 | 5 | try: 6 | import pydantic 7 | except ImportError: 8 | pydantic = None 9 | 10 | 11 | def test_simple(): 12 | class CustomCls: 13 | def __pretty__(self, fmt, **kwargs): 14 | yield 'Thing(' 15 | yield 1 16 | for i in range(3): 17 | yield fmt(list(range(i))) 18 | yield ',' 19 | yield 0 20 | yield -1 21 | yield ')' 22 | 23 | my_cls = CustomCls() 24 | 25 | v = pformat(my_cls) 26 | assert ( 27 | v 28 | == """\ 29 | Thing( 30 | [], 31 | [0], 32 | [0, 1], 33 | )""" 34 | ) 35 | 36 | 37 | def test_skip(): 38 | class CustomCls: 39 | def __pretty__(self, fmt, skip_exc, **kwargs): 40 | raise skip_exc() 41 | yield 'Thing()' 42 | 43 | def __repr__(self): 44 | return '' 45 | 46 | my_cls = CustomCls() 47 | v = pformat(my_cls) 48 | assert v == '' 49 | 50 | 51 | def test_yield_other(): 52 | class CustomCls: 53 | def __pretty__(self, fmt, **kwargs): 54 | yield fmt('xxx') 55 | yield 123 56 | 57 | my_cls = CustomCls() 58 | v = pformat(my_cls) 59 | assert v == "'xxx'123" 60 | 61 | 62 | def test_pretty_not_func(): 63 | class Foobar: 64 | __pretty__ = 1 65 | 66 | assert '.Foobar object' in pformat(Foobar()) 67 | 68 | 69 | def test_pretty_class(): 70 | class Foobar: 71 | def __pretty__(self, fmt, **kwargs): 72 | yield 'xxx' 73 | 74 | assert pformat(Foobar()) == 'xxx' 75 | assert pformat(Foobar) == ".Foobar'>" 76 | 77 | 78 | @pytest.mark.skipif(pydantic is None, reason='numpy not installed') 79 | def test_pydantic_pretty(): 80 | class MyModel(pydantic.BaseModel): 81 | foobar: int = 1 82 | 83 | assert pformat(MyModel()) == 'MyModel(\n foobar=1,\n)' 84 | assert pformat(MyModel) == ".MyModel'>" 85 | -------------------------------------------------------------------------------- /tests/test_expr_render.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | import pytest 5 | 6 | from devtools import Debug, debug 7 | 8 | from .utils import normalise_output 9 | 10 | 11 | def foobar(a, b, c): 12 | return a + b + c 13 | 14 | 15 | def test_simple(): 16 | a = [1, 2, 3] 17 | v = debug.format(len(a)) 18 | s = normalise_output(str(v)) 19 | # print(s) 20 | assert ( 21 | 'tests/test_expr_render.py: test_simple\n' 22 | ' len(a): 3 (int)' 23 | ) == s 24 | 25 | 26 | def test_subscription(): 27 | a = {1: 2} 28 | v = debug.format(a[1]) 29 | s = normalise_output(str(v)) 30 | assert ( 31 | 'tests/test_expr_render.py: test_subscription\n' 32 | ' a[1]: 2 (int)' 33 | ) == s 34 | 35 | 36 | def test_exotic_types(): 37 | aa = [1, 2, 3] 38 | v = debug.format( 39 | sum(aa), 40 | 1 == 2, 41 | 1 < 2, 42 | 1 << 2, 43 | 't' if True else 'f', 44 | 1 or 2, 45 | [a for a in aa], 46 | {a for a in aa}, 47 | {a: a + 1 for a in aa}, 48 | (a for a in aa), 49 | ) 50 | s = normalise_output(str(v)) 51 | print(f'\n---\n{v}\n---') 52 | 53 | # Generator expression source changed in 3.8 to include parentheses, see: 54 | # https://github.com/gristlabs/asttokens/pull/50 55 | # https://bugs.python.org/issue31241 56 | genexpr_source = 'a for a in aa' 57 | if sys.version_info[:2] > (3, 7): 58 | genexpr_source = f'({genexpr_source})' 59 | 60 | assert ( 61 | "tests/test_expr_render.py: test_exotic_types\n" 62 | " sum(aa): 6 (int)\n" 63 | " 1 == 2: False (bool)\n" 64 | " 1 < 2: True (bool)\n" 65 | " 1 << 2: 4 (int)\n" 66 | " 't' if True else 'f': 't' (str) len=1\n" 67 | " 1 or 2: 1 (int)\n" 68 | " [a for a in aa]: [1, 2, 3] (list) len=3\n" 69 | " {a for a in aa}: {1, 2, 3} (set) len=3\n" 70 | " {a: a + 1 for a in aa}: {\n" 71 | " 1: 2,\n" 72 | " 2: 3,\n" 73 | " 3: 4,\n" 74 | " } (dict) len=3\n" 75 | f" {genexpr_source}: (\n" 76 | " 1,\n" 77 | " 2,\n" 78 | " 3,\n" 79 | " ) (generator)" 80 | ) == s 81 | 82 | 83 | def test_newline(): 84 | v = debug.format( 85 | foobar(1, 2, 3)) 86 | s = normalise_output(str(v)) 87 | # print(s) 88 | assert ( 89 | 'tests/test_expr_render.py: test_newline\n' 90 | ' foobar(1, 2, 3): 6 (int)' 91 | ) == s 92 | 93 | 94 | def test_trailing_bracket(): 95 | v = debug.format( 96 | foobar(1, 2, 3) 97 | ) 98 | s = normalise_output(str(v)) 99 | # print(s) 100 | assert ( 101 | 'tests/test_expr_render.py: test_trailing_bracket\n' 102 | ' foobar(1, 2, 3): 6 (int)' 103 | ) == s 104 | 105 | 106 | def test_multiline(): 107 | v = debug.format( 108 | foobar(1, 109 | 2, 110 | 3) 111 | ) 112 | s = normalise_output(str(v)) 113 | # print(s) 114 | assert ( 115 | 'tests/test_expr_render.py: test_multiline\n' 116 | ' foobar(1, 2, 3): 6 (int)' 117 | ) == s 118 | 119 | 120 | def test_multiline_trailing_bracket(): 121 | v = debug.format( 122 | foobar(1, 2, 3 123 | )) 124 | s = normalise_output(str(v)) 125 | # print(s) 126 | assert ( 127 | 'tests/test_expr_render.py: test_multiline_trailing_bracket\n' 128 | ' foobar(1, 2, 3 ): 6 (int)' 129 | ) == s 130 | 131 | 132 | @pytest.mark.skipif(sys.version_info < (3, 6), reason='kwarg order is not guaranteed for 3.5') 133 | def test_kwargs(): 134 | v = debug.format( 135 | foobar(1, 2, 3), 136 | a=6, 137 | b=7 138 | ) 139 | s = normalise_output(str(v)) 140 | assert ( 141 | 'tests/test_expr_render.py: test_kwargs\n' 142 | ' foobar(1, 2, 3): 6 (int)\n' 143 | ' a: 6 (int)\n' 144 | ' b: 7 (int)' 145 | ) == s 146 | 147 | 148 | @pytest.mark.skipif(sys.version_info < (3, 6), reason='kwarg order is not guaranteed for 3.5') 149 | def test_kwargs_multiline(): 150 | v = debug.format( 151 | foobar(1, 2, 152 | 3), 153 | a=6, 154 | b=7 155 | ) 156 | s = normalise_output(str(v)) 157 | assert ( 158 | 'tests/test_expr_render.py: test_kwargs_multiline\n' 159 | ' foobar(1, 2, 3): 6 (int)\n' 160 | ' a: 6 (int)\n' 161 | ' b: 7 (int)' 162 | ) == s 163 | 164 | 165 | def test_multiple_trailing_lines(): 166 | v = debug.format( 167 | foobar( 168 | 1, 2, 3 169 | ), 170 | ) 171 | s = normalise_output(str(v)) 172 | assert ( 173 | 'tests/test_expr_render.py: test_multiple_trailing_lines\n foobar( 1, 2, 3 ): 6 (int)' 174 | ) == s 175 | 176 | 177 | def test_very_nested_last_statement(): 178 | def func(): 179 | return debug.format( 180 | abs( 181 | abs( 182 | abs( 183 | abs( 184 | -1 185 | ) 186 | ) 187 | ) 188 | ) 189 | ) 190 | 191 | v = func() 192 | # check only the original code is included in the warning 193 | s = normalise_output(str(v)) 194 | assert s == ( 195 | 'tests/test_expr_render.py: test_very_nested_last_statement..func\n' 196 | ' abs( abs( abs( abs( -1 ) ) ) ): 1 (int)' 197 | ) 198 | 199 | 200 | def test_syntax_warning(): 201 | def func(): 202 | return debug.format( 203 | abs( 204 | abs( 205 | abs( 206 | abs( 207 | abs( 208 | -1 209 | ) 210 | ) 211 | ) 212 | ) 213 | ) 214 | ) 215 | 216 | v = func() 217 | # check only the original code is included in the warning 218 | s = normalise_output(str(v)) 219 | assert s == ( 220 | 'tests/test_expr_render.py: test_syntax_warning..func\n' 221 | ' abs( abs( abs( abs( abs( -1 ) ) ) ) ): 1 (int)' 222 | ) 223 | 224 | 225 | def test_no_syntax_warning(): 226 | # exceed the 4 extra lines which are normally checked 227 | debug_ = Debug(warnings=False) 228 | 229 | def func(): 230 | return debug_.format( 231 | abs( 232 | abs( 233 | abs( 234 | abs( 235 | abs( 236 | -1 237 | ) 238 | ) 239 | ) 240 | ) 241 | ) 242 | ) 243 | 244 | v = func() 245 | s = normalise_output(str(v)) 246 | assert s == ( 247 | 'tests/test_expr_render.py: test_no_syntax_warning..func\n' 248 | ' abs( abs( abs( abs( abs( -1 ) ) ) ) ): 1 (int)' 249 | ) 250 | 251 | 252 | def test_await(): 253 | async def foo(): 254 | return 1 255 | 256 | async def bar(): 257 | return debug.format(await foo()) 258 | 259 | loop = asyncio.new_event_loop() 260 | v = loop.run_until_complete(bar()) 261 | loop.close() 262 | s = normalise_output(str(v)) 263 | assert ( 264 | 'tests/test_expr_render.py: test_await..bar\n' 265 | ' await foo(): 1 (int)' 266 | ) == s 267 | 268 | 269 | def test_other_debug_arg(): 270 | debug.timer() 271 | v = debug.format([1, 2]) 272 | 273 | # check only the original code is included in the warning 274 | s = normalise_output(str(v)) 275 | assert s == ( 276 | 'tests/test_expr_render.py: test_other_debug_arg\n' 277 | ' [1, 2] (list) len=2' 278 | ) 279 | 280 | 281 | def test_other_debug_arg_not_literal(): 282 | debug.timer() 283 | x = 1 284 | y = 2 285 | v = debug.format([x, y]) 286 | 287 | s = normalise_output(str(v)) 288 | assert s == ( 289 | 'tests/test_expr_render.py: test_other_debug_arg_not_literal\n' 290 | ' [x, y]: [1, 2] (list) len=2' 291 | ) 292 | 293 | 294 | def test_executing_failure(): 295 | debug.timer() 296 | x = 1 297 | y = 2 298 | 299 | # executing fails inside a pytest assert ast the AST is modified 300 | assert normalise_output(str(debug.format([x, y]))) == ( 301 | 'tests/test_expr_render.py: test_executing_failure ' 302 | '(executing failed to find the calling node)\n' 303 | ' [1, 2] (list) len=2' 304 | ) 305 | 306 | 307 | def test_format_inside_error(): 308 | debug.timer() 309 | x = 1 310 | y = 2 311 | try: 312 | raise RuntimeError(debug.format([x, y])) 313 | except RuntimeError as e: 314 | v = str(e) 315 | 316 | s = normalise_output(str(v)) 317 | assert s == ( 318 | 'tests/test_expr_render.py: test_format_inside_error\n' 319 | ' [x, y]: [1, 2] (list) len=2' 320 | ) 321 | -------------------------------------------------------------------------------- /tests/test_insert_assert.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | from devtools.pytest_plugin import load_black 7 | 8 | pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason='requires Python 3.8+') 9 | 10 | 11 | config = "pytest_plugins = ['devtools.pytest_plugin']" 12 | # language=Python 13 | default_test = """\ 14 | def test_ok(): 15 | assert 1 + 2 == 3 16 | 17 | def test_string_assert(insert_assert): 18 | thing = 'foobar' 19 | insert_assert(thing)\ 20 | """ 21 | 22 | 23 | def test_insert_assert(pytester_pretty): 24 | os.environ.pop('CI', None) 25 | pytester_pretty.makeconftest(config) 26 | test_file = pytester_pretty.makepyfile(default_test) 27 | result = pytester_pretty.runpytest() 28 | result.assert_outcomes(passed=2) 29 | # print(result.outlines) 30 | assert test_file.read_text() == ( 31 | 'def test_ok():\n' 32 | ' assert 1 + 2 == 3\n' 33 | '\n' 34 | 'def test_string_assert(insert_assert):\n' 35 | " thing = 'foobar'\n" 36 | ' # insert_assert(thing)\n' 37 | ' assert thing == "foobar"' 38 | ) 39 | 40 | 41 | def test_insert_assert_no_pretty(pytester): 42 | os.environ.pop('CI', None) 43 | pytester.makeconftest(config) 44 | test_file = pytester.makepyfile(default_test) 45 | result = pytester.runpytest('-p', 'no:pretty') 46 | result.assert_outcomes(passed=2) 47 | assert test_file.read_text() == ( 48 | 'def test_ok():\n' 49 | ' assert 1 + 2 == 3\n' 50 | '\n' 51 | 'def test_string_assert(insert_assert):\n' 52 | " thing = 'foobar'\n" 53 | ' # insert_assert(thing)\n' 54 | ' assert thing == "foobar"' 55 | ) 56 | 57 | 58 | def test_insert_assert_print(pytester_pretty, capsys): 59 | os.environ.pop('CI', None) 60 | pytester_pretty.makeconftest(config) 61 | test_file = pytester_pretty.makepyfile(default_test) 62 | # assert r == 0 63 | result = pytester_pretty.runpytest('--insert-assert-print') 64 | result.assert_outcomes(passed=2) 65 | assert test_file.read_text() == default_test 66 | captured = capsys.readouterr() 67 | assert 'test_insert_assert_print.py - 6:' in captured.out 68 | assert 'Printed 1 insert_assert() call in 1 file\n' in captured.out 69 | 70 | 71 | def test_insert_assert_fail(pytester_pretty): 72 | os.environ.pop('CI', None) 73 | pytester_pretty.makeconftest(config) 74 | test_file = pytester_pretty.makepyfile(default_test) 75 | # assert r == 0 76 | result = pytester_pretty.runpytest() 77 | assert result.parseoutcomes() == {'passed': 2, 'warning': 1, 'insert': 1} 78 | assert test_file.read_text() != default_test 79 | 80 | 81 | def test_deep(pytester_pretty): 82 | os.environ.pop('CI', None) 83 | pytester_pretty.makeconftest(config) 84 | # language=Python 85 | test_file = pytester_pretty.makepyfile( 86 | """ 87 | def test_deep(insert_assert): 88 | insert_assert([{'a': i, 'b': 2 * 2} for i in range(3)]) 89 | """ 90 | ) 91 | result = pytester_pretty.runpytest() 92 | result.assert_outcomes(passed=1) 93 | assert test_file.read_text() == ( 94 | 'def test_deep(insert_assert):\n' 95 | " # insert_assert([{'a': i, 'b': 2 * 2} for i in range(3)])\n" 96 | ' assert [{"a": i, "b": 2 * 2} for i in range(3)] == [\n' 97 | ' {"a": 0, "b": 4},\n' 98 | ' {"a": 1, "b": 4},\n' 99 | ' {"a": 2, "b": 4},\n' 100 | ' ]' 101 | ) 102 | 103 | 104 | def test_enum(pytester_pretty, capsys): 105 | os.environ.pop('CI', None) 106 | pytester_pretty.makeconftest(config) 107 | # language=Python 108 | pytester_pretty.makepyfile( 109 | """ 110 | from enum import Enum 111 | 112 | class Foo(Enum): 113 | A = 1 114 | B = 2 115 | 116 | def test_deep(insert_assert): 117 | x = Foo.A 118 | insert_assert(x) 119 | """ 120 | ) 121 | result = pytester_pretty.runpytest('--insert-assert-print') 122 | result.assert_outcomes(passed=1) 123 | captured = capsys.readouterr() 124 | assert ' assert x == Foo.A\n' in captured.out 125 | 126 | 127 | def test_insert_assert_black(tmp_path): 128 | old_wd = os.getcwd() 129 | try: 130 | os.chdir(tmp_path) 131 | (tmp_path / 'pyproject.toml').write_text( 132 | """\ 133 | [tool.black] 134 | target-version = ["py39"] 135 | skip-string-normalization = true""" 136 | ) 137 | load_black.cache_clear() 138 | finally: 139 | os.chdir(old_wd) 140 | 141 | f = load_black() 142 | # no string normalization 143 | assert f("'foobar'") == "'foobar'\n" 144 | 145 | 146 | def test_insert_assert_repeat(pytester_pretty, capsys): 147 | os.environ.pop('CI', None) 148 | pytester_pretty.makeconftest(config) 149 | test_file = pytester_pretty.makepyfile( 150 | """\ 151 | import pytest 152 | 153 | @pytest.mark.parametrize('x', [1, 2, 3]) 154 | def test_string_assert(x, insert_assert): 155 | insert_assert(x)\ 156 | """ 157 | ) 158 | result = pytester_pretty.runpytest() 159 | result.assert_outcomes(passed=3) 160 | assert test_file.read_text() == ( 161 | 'import pytest\n' 162 | '\n' 163 | "@pytest.mark.parametrize('x', [1, 2, 3])\n" 164 | 'def test_string_assert(x, insert_assert):\n' 165 | ' # insert_assert(x)\n' 166 | ' assert x == 1' 167 | ) 168 | captured = capsys.readouterr() 169 | assert '2 insert skipped because an assert statement on that line had already be inserted!\n' in captured.out 170 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from collections.abc import Generator 4 | from pathlib import Path 5 | from subprocess import run 6 | 7 | import pytest 8 | 9 | from devtools import Debug, debug 10 | from devtools.ansi import strip_ansi 11 | 12 | from .utils import normalise_output 13 | 14 | 15 | def test_print(capsys): 16 | a = 1 17 | b = 2 18 | result = debug(a, b) 19 | stdout, stderr = capsys.readouterr() 20 | print(stdout) 21 | assert normalise_output(stdout) == ( 22 | 'tests/test_main.py: test_print\n' ' a: 1 (int)\n' ' b: 2 (int)\n' 23 | ) 24 | assert stderr == '' 25 | assert result == (1, 2) 26 | 27 | 28 | def test_print_kwargs(capsys): 29 | a = 1 30 | b = 2 31 | result = debug(a, b, foo=[1, 2, 3]) 32 | stdout, stderr = capsys.readouterr() 33 | print(stdout) 34 | assert normalise_output(stdout) == ( 35 | 'tests/test_main.py: test_print_kwargs\n' 36 | ' a: 1 (int)\n' 37 | ' b: 2 (int)\n' 38 | ' foo: [1, 2, 3] (list) len=3\n' 39 | ) 40 | assert stderr == '' 41 | assert result == (1, 2, {'foo': [1, 2, 3]}) 42 | 43 | 44 | def test_print_generator(capsys): 45 | gen = (i for i in [1, 2]) 46 | 47 | result = debug(gen) 48 | stdout, stderr = capsys.readouterr() 49 | print(stdout) 50 | assert normalise_output(stdout) == ( 51 | 'tests/test_main.py: test_print_generator\n' 52 | ' gen: (\n' 53 | ' 1,\n' 54 | ' 2,\n' 55 | ' ) (generator)\n' 56 | ) 57 | assert stderr == '' 58 | assert isinstance(result, Generator) 59 | # the generator got evaluated and is now empty, that's correct currently 60 | assert list(result) == [] 61 | 62 | 63 | def test_format(): 64 | a = b'i might bite' 65 | b = 'hello this is a test' 66 | v = debug.format(a, b) 67 | s = normalise_output(str(v)) 68 | print(s) 69 | assert s == ( 70 | "tests/test_main.py: test_format\n" 71 | " a: b'i might bite' (bytes) len=12\n" 72 | " b: 'hello this is a test' (str) len=20" 73 | ) 74 | 75 | 76 | @pytest.mark.xfail( 77 | sys.platform == 'win32', 78 | reason='Fatal Python error: _Py_HashRandomization_Init: failed to get random numbers to initialize Python', 79 | ) 80 | def test_print_subprocess(tmpdir): 81 | f = tmpdir.join('test.py') 82 | f.write( 83 | """\ 84 | from devtools import debug 85 | 86 | def test_func(v): 87 | debug('in test func', v) 88 | 89 | foobar = 'hello world' 90 | print('running debug...') 91 | debug(foobar) 92 | test_func(42) 93 | print('debug run.') 94 | """ 95 | ) 96 | env = {'PYTHONPATH': str(Path(__file__).parent.parent.resolve())} 97 | p = run([sys.executable, str(f)], capture_output=True, text=True, env=env) 98 | assert p.stderr == '' 99 | assert p.returncode == 0, (p.stderr, p.stdout) 100 | assert p.stdout.replace(str(f), '/path/to/test.py') == ( 101 | "running debug...\n" 102 | "/path/to/test.py:8 \n" 103 | " foobar: 'hello world' (str) len=11\n" 104 | "/path/to/test.py:4 test_func\n" 105 | " 'in test func' (str) len=12\n" 106 | " v: 42 (int)\n" 107 | "debug run.\n" 108 | ) 109 | 110 | 111 | def test_odd_path(mocker): 112 | # all valid calls 113 | mocked_relative_to = mocker.patch('pathlib.Path.relative_to') 114 | mocked_relative_to.side_effect = ValueError() 115 | v = debug.format('test') 116 | if sys.platform == 'win32': 117 | pattern = r'\w:\\.*?\\' 118 | else: 119 | pattern = r'/.*?/' 120 | pattern += r"test_main.py:\d{2,} test_odd_path\n 'test' \(str\) len=4" 121 | assert re.search(pattern, str(v)), v 122 | 123 | 124 | def test_small_call_frame(): 125 | debug_ = Debug(warnings=False) 126 | v = debug_.format( 127 | 1, 128 | 2, 129 | 3, 130 | ) 131 | assert normalise_output(str(v)) == ( 132 | 'tests/test_main.py: test_small_call_frame\n' ' 1 (int)\n' ' 2 (int)\n' ' 3 (int)' 133 | ) 134 | 135 | 136 | def test_small_call_frame_warning(): 137 | debug_ = Debug() 138 | v = debug_.format( 139 | 1, 140 | 2, 141 | 3, 142 | ) 143 | print(f'\n---\n{v}\n---') 144 | assert normalise_output(str(v)) == ( 145 | 'tests/test_main.py: test_small_call_frame_warning\n' ' 1 (int)\n' ' 2 (int)\n' ' 3 (int)' 146 | ) 147 | 148 | 149 | @pytest.mark.skipif(sys.version_info < (3, 6), reason='kwarg order is not guaranteed for 3.5') 150 | def test_kwargs(): 151 | a = 'variable' 152 | v = debug.format(first=a, second='literal') 153 | s = normalise_output(str(v)) 154 | print(s) 155 | assert s == ( 156 | "tests/test_main.py: test_kwargs\n" 157 | " first: 'variable' (str) len=8 variable=a\n" 158 | " second: 'literal' (str) len=7" 159 | ) 160 | 161 | 162 | def test_kwargs_orderless(): 163 | # for python3.5 164 | a = 'variable' 165 | v = debug.format(first=a, second='literal') 166 | s = normalise_output(str(v)) 167 | assert set(s.split('\n')) == { 168 | 'tests/test_main.py: test_kwargs_orderless', 169 | " first: 'variable' (str) len=8 variable=a", 170 | " second: 'literal' (str) len=7", 171 | } 172 | 173 | 174 | def test_simple_vars(): 175 | v = debug.format('test', 1, 2) 176 | s = normalise_output(str(v)) 177 | assert s == ( 178 | "tests/test_main.py: test_simple_vars\n" " 'test' (str) len=4\n" " 1 (int)\n" " 2 (int)" 179 | ) 180 | r = normalise_output(repr(v)) 181 | assert r == ( 182 | " test_simple_vars arguments: 'test' (str) len=4 1 (int) 2 (int)>" 183 | ) 184 | 185 | 186 | def test_attributes(): 187 | class Foo: 188 | x = 1 189 | 190 | class Bar: 191 | y = Foo() 192 | 193 | b = Bar() 194 | v = debug.format(b.y.x) 195 | assert 'test_attributes\n b.y.x: 1 (int)' in str(v) 196 | 197 | 198 | def test_eval(): 199 | v = eval('debug.format(1)') 200 | 201 | assert str(v) == ':1 (no code context for debug call, code inspection impossible)\n 1 (int)' 202 | 203 | 204 | def test_warnings_disabled(): 205 | debug_ = Debug(warnings=False) 206 | v1 = eval('debug_.format(1)') 207 | assert str(v1) == ':1 \n 1 (int)' 208 | v2 = debug_.format(1) 209 | assert 'test_warnings_disabled\n 1 (int)' in str(v2) 210 | 211 | 212 | def test_eval_kwargs(): 213 | v = eval('debug.format(1, apple="pear")') 214 | 215 | assert set(str(v).split('\n')) == { 216 | ':1 (no code context for debug call, code inspection impossible)', 217 | ' 1 (int)', 218 | " apple: 'pear' (str) len=4", 219 | } 220 | 221 | 222 | def test_exec(capsys): 223 | exec('a = 1\n' 'b = 2\n' 'debug(b, a + b)') 224 | 225 | stdout, stderr = capsys.readouterr() 226 | assert stdout == ( 227 | ':3 (no code context for debug call, code inspection impossible)\n' 228 | ' 2 (int)\n' 229 | ' 3 (int)\n' 230 | ) 231 | assert stderr == '' 232 | 233 | 234 | def test_colours(): 235 | v = debug.format(range(6)) 236 | s = v.str(True) 237 | assert s.startswith('\x1b[35mtests'), repr(s) 238 | s2 = normalise_output(strip_ansi(s)) 239 | assert s2 == normalise_output(v.str()), repr(s2) 240 | 241 | 242 | def test_colours_warnings(mocker): 243 | mocked_getframe = mocker.patch('sys._getframe') 244 | mocked_getframe.side_effect = ValueError() 245 | v = debug.format('x') 246 | s = normalise_output(v.str(True)) 247 | assert s.startswith('\x1b[35m'), repr(s) 248 | s2 = strip_ansi(s) 249 | assert s2 == v.str(), repr(s2) 250 | 251 | 252 | def test_inspect_error(mocker): 253 | mocked_getframe = mocker.patch('sys._getframe') 254 | mocked_getframe.side_effect = ValueError() 255 | v = debug.format('x') 256 | print(repr(str(v))) 257 | assert str(v) == ":0 (error parsing code, call stack too shallow)\n 'x' (str) len=1" 258 | 259 | 260 | def test_breakpoint(mocker): 261 | # not much else we can do here 262 | mocked_set_trace = mocker.patch('pdb.Pdb.set_trace') 263 | debug.breakpoint() 264 | assert mocked_set_trace.called 265 | 266 | 267 | @pytest.mark.xfail( 268 | sys.platform == 'win32' and sys.version_info >= (3, 9), 269 | reason='see https://github.com/alexmojaki/executing/issues/27', 270 | ) 271 | def test_starred_kwargs(): 272 | v = {'foo': 1, 'bar': 2} 273 | v = debug.format(**v) 274 | s = normalise_output(v.str()) 275 | assert set(s.split('\n')) == { 276 | 'tests/test_main.py: test_starred_kwargs', 277 | ' foo: 1 (int)', 278 | ' bar: 2 (int)', 279 | } 280 | 281 | 282 | @pytest.mark.skipif(sys.version_info < (3, 7), reason='error repr different before 3.7') 283 | def test_pretty_error(): 284 | class BadPretty: 285 | def __getattr__(self, item): 286 | raise RuntimeError('this is an error') 287 | 288 | b = BadPretty() 289 | v = debug.format(b) 290 | s = normalise_output(str(v)) 291 | assert s == ( 292 | "tests/test_main.py: test_pretty_error\n" 293 | " b: .BadPretty object at 0x> (BadPretty)\n" 294 | " !!! error pretty printing value: RuntimeError('this is an error')" 295 | ) 296 | 297 | 298 | def test_multiple_debugs(): 299 | debug.format([i * 2 for i in range(2)]) 300 | debug.format([i * 2 for i in range(2)]) 301 | v = debug.format([i * 2 for i in range(2)]) 302 | s = normalise_output(str(v)) 303 | assert s == ( 304 | 'tests/test_main.py: test_multiple_debugs\n' ' [i * 2 for i in range(2)]: [0, 2] (list) len=2' 305 | ) 306 | 307 | 308 | def test_return_args(capsys): 309 | assert debug('foo') == 'foo' 310 | assert debug('foo', 'bar') == ('foo', 'bar') 311 | assert debug('foo', 'bar', spam=123) == ('foo', 'bar', {'spam': 123}) 312 | assert debug(spam=123) == ({'spam': 123},) 313 | stdout, stderr = capsys.readouterr() 314 | print(stdout) 315 | -------------------------------------------------------------------------------- /tests/test_prettier.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import string 4 | import sys 5 | from collections import Counter, OrderedDict, namedtuple 6 | from dataclasses import dataclass 7 | from typing import List 8 | from unittest.mock import MagicMock 9 | 10 | import pytest 11 | 12 | from devtools.ansi import strip_ansi 13 | from devtools.prettier import PrettyFormat, pformat, pprint 14 | 15 | try: 16 | import numpy 17 | except ImportError: 18 | numpy = None 19 | 20 | try: 21 | from multidict import CIMultiDict, MultiDict 22 | except ImportError: 23 | CIMultiDict = None 24 | MultiDict = None 25 | 26 | try: 27 | from asyncpg.protocol.protocol import _create_record as Record 28 | except ImportError: 29 | Record = None 30 | 31 | try: 32 | from sqlalchemy import Column, Integer, String 33 | 34 | try: 35 | from sqlalchemy.orm import declarative_base 36 | except ImportError: 37 | from sqlalchemy.ext.declarative import declarative_base 38 | 39 | SQLAlchemyBase = declarative_base() 40 | except ImportError: 41 | SQLAlchemyBase = None 42 | 43 | 44 | def test_dict(): 45 | v = pformat({1: 2, 3: 4}) 46 | print(v) 47 | assert v == ('{\n' ' 1: 2,\n' ' 3: 4,\n' '}') 48 | 49 | 50 | def test_print(capsys): 51 | pprint({1: 2, 3: 4}) 52 | stdout, stderr = capsys.readouterr() 53 | assert strip_ansi(stdout) == ('{\n' ' 1: 2,\n' ' 3: 4,\n' '}\n') 54 | assert stderr == '' 55 | 56 | 57 | def test_colours(): 58 | v = pformat({1: 2, 3: 4}, highlight=True) 59 | assert v.startswith('\x1b'), repr(v) 60 | v2 = strip_ansi(v) 61 | assert v2 == pformat({1: 2, 3: 4}), repr(v2) 62 | 63 | 64 | def test_list(): 65 | v = pformat(list(range(6))) 66 | assert v == ('[\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' ' 5,\n' ']') 67 | 68 | 69 | def test_set(): 70 | v = pformat(set(range(5))) 71 | assert v == ('{\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' '}') 72 | 73 | 74 | def test_tuple(): 75 | v = pformat(tuple(range(5))) 76 | assert v == ('(\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' ')') 77 | 78 | 79 | def test_generator(): 80 | v = pformat(i for i in range(3)) 81 | assert v == ('(\n' ' 0,\n' ' 1,\n' ' 2,\n' ')') 82 | 83 | 84 | def test_named_tuple(): 85 | f = namedtuple('Foobar', ['foo', 'bar', 'spam']) 86 | v = pformat(f('x', 'y', 1)) 87 | assert v == ("Foobar(\n" " foo='x',\n" " bar='y',\n" " spam=1,\n" ")") 88 | 89 | 90 | def test_generator_no_yield(): 91 | pformat_ = PrettyFormat(yield_from_generators=False) 92 | v = pformat_(i for i in range(3)) 93 | assert v.startswith('. at ') 94 | 95 | 96 | def test_str(): 97 | pformat_ = PrettyFormat(width=12) 98 | v = pformat_(string.ascii_lowercase + '\n' + string.digits) 99 | print(repr(v)) 100 | assert v == ( 101 | "(\n" 102 | " 'abcde'\n" 103 | " 'fghij'\n" 104 | " 'klmno'\n" 105 | " 'pqrst'\n" 106 | " 'uvwxy'\n" 107 | " 'z\\n" 108 | "'\n" 109 | " '01234'\n" 110 | " '56789'\n" 111 | ")" 112 | ) 113 | 114 | 115 | def test_str_repr(): 116 | pformat_ = PrettyFormat(repr_strings=True) 117 | v = pformat_(string.ascii_lowercase + '\n' + string.digits) 118 | assert v == "'abcdefghijklmnopqrstuvwxyz\\n0123456789'" 119 | 120 | 121 | def test_bytes(): 122 | pformat_ = PrettyFormat(width=12) 123 | v = pformat_(string.ascii_lowercase.encode()) 124 | assert ( 125 | v 126 | == """( 127 | b'abcde' 128 | b'fghij' 129 | b'klmno' 130 | b'pqrst' 131 | b'uvwxy' 132 | b'z' 133 | )""" 134 | ) 135 | 136 | 137 | def test_short_bytes(): 138 | assert "b'abcdefghijklmnopqrstuvwxyz'" == pformat(string.ascii_lowercase.encode()) 139 | 140 | 141 | def test_bytearray(): 142 | pformat_ = PrettyFormat(width=18) 143 | v = pformat_(bytearray(string.ascii_lowercase.encode())) 144 | assert ( 145 | v 146 | == """\ 147 | bytearray( 148 | b'abcdefghijk' 149 | b'lmnopqrstuv' 150 | b'wxyz' 151 | )""" 152 | ) 153 | 154 | 155 | def test_bytearray_short(): 156 | v = pformat(bytearray(b'boo')) 157 | assert ( 158 | v 159 | == """\ 160 | bytearray( 161 | b'boo' 162 | )""" 163 | ) 164 | 165 | 166 | def test_map(): 167 | v = pformat(map(str.strip, ['x', 'y ', ' z'])) 168 | assert ( 169 | v 170 | == """\ 171 | map( 172 | 'x', 173 | 'y', 174 | 'z', 175 | )""" 176 | ) 177 | 178 | 179 | def test_filter(): 180 | v = pformat(filter(None, [1, 2, False, 3])) 181 | assert ( 182 | v 183 | == """\ 184 | filter( 185 | 1, 186 | 2, 187 | 3, 188 | )""" 189 | ) 190 | 191 | 192 | def test_counter(): 193 | c = Counter() 194 | c['x'] += 1 195 | c['x'] += 1 196 | c['y'] += 1 197 | v = pformat(c) 198 | assert ( 199 | v 200 | == """\ 201 | """ 205 | ) 206 | 207 | 208 | def test_dataclass(): 209 | @dataclass 210 | class FooDataclass: 211 | x: int 212 | y: List[int] 213 | 214 | f = FooDataclass(123, [1, 2, 3, 4]) 215 | v = pformat(f) 216 | print(v) 217 | assert ( 218 | v 219 | == """\ 220 | FooDataclass( 221 | x=123, 222 | y=[ 223 | 1, 224 | 2, 225 | 3, 226 | 4, 227 | ], 228 | )""" 229 | ) 230 | 231 | 232 | def test_nested_dataclasses(): 233 | @dataclass 234 | class FooDataclass: 235 | x: int 236 | 237 | @dataclass 238 | class BarDataclass: 239 | a: float 240 | b: FooDataclass 241 | 242 | f = FooDataclass(123) 243 | b = BarDataclass(10.0, f) 244 | v = pformat(b) 245 | print(v) 246 | assert ( 247 | v 248 | == """\ 249 | BarDataclass( 250 | a=10.0, 251 | b=FooDataclass( 252 | x=123, 253 | ), 254 | )""" 255 | ) 256 | 257 | 258 | def test_dataclass_slots(): 259 | try: 260 | dec = dataclass(slots=True) 261 | except TypeError: 262 | pytest.skip('dataclasses.slots not available') 263 | 264 | @dec 265 | class FooDataclass: 266 | x: int 267 | y: str 268 | 269 | f = FooDataclass(123, 'bar') 270 | v = pformat(f) 271 | print(v) 272 | assert ( 273 | v 274 | == """\ 275 | FooDataclass( 276 | x=123, 277 | y='bar', 278 | )""" 279 | ) 280 | 281 | 282 | @pytest.mark.skipif(numpy is None, reason='numpy not installed') 283 | def test_indent_numpy(): 284 | v = pformat({'numpy test': numpy.array(range(20))}) 285 | assert ( 286 | v 287 | == """{ 288 | 'numpy test': ( 289 | array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 290 | 17, 18, 19]) 291 | ), 292 | }""" 293 | ) 294 | 295 | 296 | @pytest.mark.skipif(numpy is None, reason='numpy not installed') 297 | def test_indent_numpy_short(): 298 | v = pformat({'numpy test': numpy.array(range(10))}) 299 | assert ( 300 | v 301 | == """{ 302 | 'numpy test': array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), 303 | }""" 304 | ) 305 | 306 | 307 | def test_ordered_dict(): 308 | v = pformat(OrderedDict([(1, 2), (3, 4), (5, 6)])) 309 | print(v) 310 | assert ( 311 | v 312 | == """\ 313 | OrderedDict([ 314 | (1, 2), 315 | (3, 4), 316 | (5, 6), 317 | ])""" 318 | ) 319 | 320 | 321 | def test_frozenset(): 322 | v = pformat(frozenset(range(3))) 323 | print(v) 324 | assert ( 325 | v 326 | == """\ 327 | frozenset({ 328 | 0, 329 | 1, 330 | 2, 331 | })""" 332 | ) 333 | 334 | 335 | def test_deep_objects(): 336 | f = namedtuple('Foobar', ['foo', 'bar', 'spam']) 337 | v = pformat(((f('x', 'y', OrderedDict([(1, 2), (3, 4), (5, 6)])), frozenset(range(3)), [1, 2, {1: 2}]), {1, 2, 3})) 338 | print(v) 339 | assert ( 340 | v 341 | == """\ 342 | ( 343 | ( 344 | Foobar( 345 | foo='x', 346 | bar='y', 347 | spam=OrderedDict([ 348 | (1, 2), 349 | (3, 4), 350 | (5, 6), 351 | ]), 352 | ), 353 | frozenset({ 354 | 0, 355 | 1, 356 | 2, 357 | }), 358 | [ 359 | 1, 360 | 2, 361 | {1: 2}, 362 | ], 363 | ), 364 | {1, 2, 3}, 365 | )""" 366 | ) 367 | 368 | 369 | def test_call_args(): 370 | m = MagicMock() 371 | m(1, 2, 3, a=4) 372 | v = pformat(m.call_args) 373 | 374 | assert ( 375 | v 376 | == """\ 377 | _Call( 378 | _fields=(1, 2, 3), 379 | {'a': 4}, 380 | )""" 381 | ) 382 | 383 | 384 | @pytest.mark.skipif(MultiDict is None, reason='MultiDict not installed') 385 | def test_multidict(): 386 | d = MultiDict({'a': 1, 'b': 2}) 387 | d.add('b', 3) 388 | v = pformat(d) 389 | assert set(v.split('\n')) == { 390 | '', 395 | } 396 | 397 | 398 | @pytest.mark.skipif(CIMultiDict is None, reason='MultiDict not installed') 399 | def test_cimultidict(): 400 | v = pformat(CIMultiDict({'a': 1, 'b': 2})) 401 | assert set(v.split('\n')) == { 402 | '', 406 | } 407 | 408 | 409 | def test_os_environ(): 410 | v = pformat(os.environ) 411 | assert v.startswith('<_Environ({') 412 | for key in os.environ: 413 | assert f" '{key}': " in v 414 | 415 | 416 | class Foo: 417 | a = 1 418 | 419 | def __init__(self): 420 | self.b = 2 421 | self.c = 3 422 | 423 | 424 | def test_dir(): 425 | assert pformat(vars(Foo())) == ("{\n" " 'b': 2,\n" " 'c': 3,\n" "}") 426 | 427 | 428 | def test_instance_dict(): 429 | assert pformat(Foo().__dict__) == ("{\n" " 'b': 2,\n" " 'c': 3,\n" "}") 430 | 431 | 432 | def test_class_dict(): 433 | s = pformat(Foo.__dict__) 434 | assert s.startswith('') 438 | 439 | 440 | def test_dictlike(): 441 | class Dictlike: 442 | _d = {'x': 4, 'y': 42, 3: None} 443 | 444 | def items(self): 445 | yield from self._d.items() 446 | 447 | def __getitem__(self, item): 448 | return self._d[item] 449 | 450 | assert pformat(Dictlike()) == ("") 451 | 452 | 453 | @pytest.mark.skipif(Record is None, reason='asyncpg not installed') 454 | def test_asyncpg_record(): 455 | r = Record({'a': 0, 'b': 1}, (41, 42)) 456 | assert dict(r) == {'a': 41, 'b': 42} 457 | assert pformat(r) == ("") 458 | 459 | 460 | def test_dict_type(): 461 | assert pformat(type({1: 2})) == "" 462 | 463 | 464 | @pytest.mark.skipif(SQLAlchemyBase is None, reason='sqlalchemy not installed') 465 | def test_sqlalchemy_object(): 466 | class User(SQLAlchemyBase): 467 | __tablename__ = 'users' 468 | id = Column(Integer, primary_key=True) 469 | name = Column(String) 470 | fullname = Column(String) 471 | nickname = Column(String) 472 | 473 | user = User() 474 | user.id = 1 475 | user.name = 'Test' 476 | user.fullname = 'Test For SQLAlchemy' 477 | user.nickname = 'test' 478 | assert pformat(user) == ( 479 | "User(\n" 480 | " fullname='Test For SQLAlchemy',\n" 481 | " id=1,\n" 482 | " name='Test',\n" 483 | " nickname='test',\n" 484 | ")" 485 | ) 486 | 487 | 488 | @pytest.mark.skipif(sys.version_info < (3, 9), reason='no indent on older versions') 489 | def test_ast_expr(): 490 | assert pformat(ast.parse('print(1, 2, round(3))', mode='eval')) == ( 491 | "Expression(" 492 | "\n body=Call(" 493 | "\n func=Name(id='print', ctx=Load())," 494 | "\n args=[" 495 | "\n Constant(value=1)," 496 | "\n Constant(value=2)," 497 | "\n Call(" 498 | "\n func=Name(id='round', ctx=Load())," 499 | "\n args=[" 500 | "\n Constant(value=3)]," 501 | "\n keywords=[])]," 502 | "\n keywords=[]))" 503 | ) 504 | 505 | 506 | @pytest.mark.skipif(sys.version_info < (3, 9), reason='no indent on older versions') 507 | def test_ast_module(): 508 | assert pformat(ast.parse('print(1, 2, round(3))')).startswith('Module(\n body=[') 509 | -------------------------------------------------------------------------------- /tests/test_timer.py: -------------------------------------------------------------------------------- 1 | import io 2 | import re 3 | import sys 4 | from time import sleep 5 | 6 | import pytest 7 | 8 | from devtools import debug 9 | from devtools.timer import Timer 10 | 11 | 12 | @pytest.mark.skipif(sys.platform != 'linux', reason='not on linux') 13 | def test_simple(): 14 | f = io.StringIO() 15 | t = debug.timer(name='foobar', file=f) 16 | with t: 17 | sleep(0.01) 18 | v = f.getvalue() 19 | assert re.fullmatch(r'foobar: 0\.01[012]s elapsed\n', v) 20 | 21 | 22 | @pytest.mark.skipif(sys.platform != 'linux', reason='not on linux') 23 | def test_multiple(): 24 | f = io.StringIO() 25 | t = Timer(file=f) 26 | for i in [0.001, 0.002, 0.003]: 27 | with t(i): 28 | sleep(i) 29 | t.summary() 30 | v = f.getvalue() 31 | assert re.sub(r'0\.00\d', '0.00X', v) == ( 32 | '0.00X: 0.00Xs elapsed\n' 33 | '0.00X: 0.00Xs elapsed\n' 34 | '0.00X: 0.00Xs elapsed\n' 35 | '3 times: mean=0.00Xs stdev=0.00Xs min=0.00Xs max=0.00Xs\n' 36 | ) 37 | 38 | 39 | def test_unfinished(): 40 | t = Timer().start() 41 | assert str(t.results[0]) == '-1.000s elapsed' 42 | 43 | 44 | @pytest.mark.skipif(sys.platform != 'linux', reason='not on linux') 45 | def test_multiple_not_verbose(): 46 | f = io.StringIO() 47 | t = Timer(file=f) 48 | for i in [0.01, 0.02, 0.03]: 49 | with t(verbose=False): 50 | sleep(i) 51 | t.summary(True) 52 | v = re.sub('[123]s', '0s', f.getvalue()) 53 | assert v == ( 54 | ' 0.010s elapsed\n' 55 | ' 0.020s elapsed\n' 56 | ' 0.030s elapsed\n' 57 | '3 times: mean=0.020s stdev=0.010s min=0.010s max=0.030s\n' 58 | ) 59 | 60 | 61 | def test_unfinished_summary(): 62 | f = io.StringIO() 63 | t = Timer(file=f).start() 64 | t.summary() 65 | v = f.getvalue() 66 | assert v == '1 times: mean=0.000s stdev=0.000s min=0.000s max=0.000s\n' 67 | 68 | 69 | def test_summary_not_started(): 70 | with pytest.raises(RuntimeError): 71 | Timer().summary() 72 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | import devtools.utils 6 | from devtools.utils import env_bool, env_true, use_highlight 7 | 8 | 9 | def test_env_true(): 10 | assert env_true('PATH') is False 11 | assert env_true('DOES_NOT_EXIST') is None 12 | 13 | 14 | def test_env_bool(monkeypatch): 15 | assert env_bool(False, 'VAR', None) is False 16 | monkeypatch.delenv('TEST_VARIABLE_NOT_EXIST', raising=False) 17 | assert env_bool(None, 'TEST_VARIABLE_NOT_EXIST', True) is True 18 | monkeypatch.setenv('TEST_VARIABLE_EXIST', 'bar') 19 | assert env_bool(None, 'TEST_VARIABLE_EXIST', True) is False 20 | 21 | 22 | def test_use_highlight_manually_set(monkeypatch): 23 | monkeypatch.delenv('TEST_DONT_USE_HIGHLIGHT', raising=False) 24 | assert use_highlight(highlight=True) is True 25 | assert use_highlight(highlight=False) is False 26 | 27 | monkeypatch.setenv('PY_DEVTOOLS_HIGHLIGHT', 'True') 28 | assert use_highlight() is True 29 | 30 | monkeypatch.setenv('PY_DEVTOOLS_HIGHLIGHT', 'False') 31 | assert use_highlight() is False 32 | 33 | 34 | @pytest.mark.skipif(sys.platform == 'win32', reason='windows os') 35 | def test_use_highlight_auto_not_win(monkeypatch): 36 | monkeypatch.delenv('TEST_DONT_USE_HIGHLIGHT', raising=False) 37 | monkeypatch.setattr(devtools.utils, 'isatty', lambda _=None: True) 38 | assert use_highlight() is True 39 | 40 | 41 | @pytest.mark.skipif(sys.platform != 'win32', reason='not windows os') 42 | def test_use_highlight_auto_win(monkeypatch): 43 | monkeypatch.delenv('TEST_DONT_USE_HIGHLIGHT', raising=False) 44 | monkeypatch.setattr(devtools.utils, 'isatty', lambda _=None: True) 45 | 46 | assert use_highlight() is True 47 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def normalise_output(s): 5 | s = re.sub(r':\d{2,}', ':', s) 6 | s = re.sub(r'(at 0x)\w+', r'\1', s) 7 | s = s.replace('\\', '/') 8 | return s 9 | --------------------------------------------------------------------------------