From 1e3e9427c46f69dcec897d507e04a53d4281aaab Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 10:35:20 +0100 Subject: [PATCH 01/36] uprev deps (#106) * uprev deps * drop 3.6, uprev, fix warnings * fix netlify, xfail on 3.10 * fix mkdocs * remove 3.6 comments * linting --- .github/workflows/ci.yml | 10 +++------- Makefile | 4 ++-- README.md | 4 ++-- devtools/prettier.py | 2 +- devtools/utils.py | 10 +++------- docs/install.md | 4 ++-- docs/requirements.txt | 11 +++++------ docs/usage.md | 2 +- mkdocs.yml | 13 ++++++++++--- runtime.txt | 2 +- setup.py | 3 +-- tests/requirements-linting.txt | 12 ++++++------ tests/requirements.txt | 14 ++++++-------- tests/test_main.py | 11 +++++------ tests/test_prettier.py | 2 -- 15 files changed, 48 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c200a7..b8353a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: '3.10' - run: pip install -U pip wheel - run: pip install -r tests/requirements-linting.txt @@ -31,11 +31,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos, windows] - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10.0-rc.1'] - exclude: - # numpy currently get's upset with macos and python 3.10 - - os: macos - python-version: '3.10.0-rc.1' + python-version: ['3.7', '3.8', '3.9', '3.10'] env: PYTHON: ${{ matrix.python-version }} @@ -95,7 +91,7 @@ jobs: - name: set up python uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: '3.10' - name: install run: make install diff --git a/Makefile b/Makefile index fe19741..bc3b058 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,11 @@ lint: .PHONY: test test: - pytest --cov=devtools --cov-fail-under 0 + coverage run -m pytest .PHONY: testcov testcov: - pytest --cov=devtools --cov-fail-under 0 + coverage run -m pytest @echo "building coverage html" @coverage html diff --git a/README.md b/README.md index ab64a57..05383e4 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ pip install devtools[pygments] `pygments` is not required but if it's installed, output will be highlighted and easier to read. -`devtools` has no other required dependencies except python 3.6, 3.7, 3.8 or 3.9. -If you've got python 3.6+ and `pip` installed, you're good to go. +`devtools` has no other required dependencies except python 3.7, 3.8, 3.9 or 3.10. +If you've got python 3.7+ and `pip` installed, you're good to go. ## Usage diff --git a/devtools/prettier.py b/devtools/prettier.py index 36f2d57..d4e2b1f 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -74,7 +74,7 @@ def __init__( ((list, set, frozenset), self._format_list_like), (bytearray, self._format_bytearray), (generator_types, self._format_generator), - # put this last as the check can be slow + # put these last as the check can be slow (LaxMapping, self._format_dict), (DataClassType, self._format_dataclass), (SQLAlchemyClassType, self._format_sqlalchemy_class), diff --git a/devtools/utils.py b/devtools/utils.py index 034ece8..5956073 100644 --- a/devtools/utils.py +++ b/devtools/utils.py @@ -133,13 +133,9 @@ class LaxMapping(metaclass=MetaLaxMapping): class MetaDataClassType(type): def __instancecheck__(self, instance: 'Any') -> bool: - try: - from dataclasses import _is_dataclass_instance - except ImportError: - # python 3.6 - return False - else: - return _is_dataclass_instance(instance) + from dataclasses import is_dataclass + + return is_dataclass(instance) class DataClassType(metaclass=MetaDataClassType): diff --git a/docs/install.md b/docs/install.md index d81e5f6..36cc597 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,5 +6,5 @@ pip install devtools[pygments] `pygments` is not required but if it's installed, output will be highlighted and easier to read. -`devtools` has no other required dependencies except python 3.6, 3.7, 3.8, or 3.9. -If you've got python 3.6+ and `pip` installed, you're good to go. +`devtools` has no other required dependencies except python 3.7, 3.8, 3.9 or 3.10. +If you've got python 3.7+ and `pip` installed, you're good to go. diff --git a/docs/requirements.txt b/docs/requirements.txt index 20804a2..7ecdc7d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,6 @@ -ansi2html==1.5.2 -mkdocs==1.1.2 -markdown==3.2.2 +ansi2html==1.8.0 +mkdocs==1.3.1 mkdocs-exclude==1.0.2 -mkdocs-material==5.5.0 -markdown-include==0.5.1 -pygments==2.7.4 +mkdocs-material==8.3.9 +markdown-include==0.7.0 +pygments==2.12.0 diff --git a/docs/usage.md b/docs/usage.md index ad92c16..3e5a490 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -89,7 +89,7 @@ We all know the annoyance of running code only to discover a missing import, thi frustrating when the function you're using isn't used except during development. You can setup your environment to make `debug` available at all times by editing `sitecustomize.py`, -with ubuntu and python3.6 this file can be found at `/usr/lib/python3.6/sitecustomize.py` but you might +with ubuntu and python3.8 this file can be found at `/usr/lib/python3.8/sitecustomize.py` but you might need to look elsewhere depending on your OS/python version. Add the following to `sitecustomize.py` diff --git a/mkdocs.yml b/mkdocs.yml index d6de97d..268ac76 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,9 +12,16 @@ theme: repo_name: samuelcolvin/python-devtools repo_url: https://github.com/samuelcolvin/python-devtools -google_analytics: -- 'UA-62733018-4' -- 'auto' + +extra: + analytics: + provider: google + property: UA-62733018-4 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/samuelcolvin/python-devtools + - icon: fontawesome/brands/twitter + link: https://twitter.com/samuel_colvin extra_css: - 'theme/customization.css' diff --git a/runtime.txt b/runtime.txt index 475ba51..cc1923a 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -3.7 +3.8 diff --git a/setup.py b/setup.py index d699598..7bf8f5b 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,6 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -49,7 +48,7 @@ url='https://github.com/samuelcolvin/python-devtools', license='MIT', packages=['devtools'], - python_requires='>=3.6', + python_requires='>=3.7', install_requires=[ 'executing>=0.8.0,<1.0.0', 'asttokens>=2.0.0,<3.0.0', diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 593bd2e..cf57c5c 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,6 +1,6 @@ -black==20.8b1 -flake8==3.9.2 -isort==5.9.3 -mypy==0.910 -pycodestyle==2.7.0 -pyflakes==2.3.1 +black==22.6.0 +flake8==4.0.1 +isort==5.10.1 +mypy==0.971 +pycodestyle==2.8.0 +pyflakes==2.4.0 diff --git a/tests/requirements.txt b/tests/requirements.txt index 3570e26..56a5dfa 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,12 +1,10 @@ -coverage==5.5 -Pygments==2.7.4 -pytest==6.2.5 -pytest-cov==2.12.1 -pytest-mock==3.6.1 -pytest-sugar==0.9.4 -pytest-toolbox==0.4 +coverage==6.4.2 +Pygments==2.12.0 +pytest==7.1.2 +pytest-mock==3.8.2 +pytest-sugar==0.9.5 pydantic asyncpg numpy multidict -sqlalchemy \ No newline at end of file +sqlalchemy diff --git a/tests/test_main.py b/tests/test_main.py index 667a79a..5693061 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -177,6 +177,7 @@ def test_kwargs_orderless(): } +@pytest.mark.xfail(sys.version_info >= (3, 10), reason='https://github.com/alexmojaki/executing/issues/40') def test_simple_vars(): v = debug.format('test', 1, 2) s = normalise_output(str(v)) @@ -212,12 +213,10 @@ def test_eval(): def test_warnings_disabled(): debug_ = Debug(warnings=False) - with pytest.warns(None) as warnings: - v1 = eval('debug_.format(1)') - assert str(v1) == ':1 \n 1 (int)' - v2 = debug_.format(1) - assert 'test_warnings_disabled\n 1 (int)' in str(v2) - assert len(warnings) == 0 + v1 = eval('debug_.format(1)') + assert str(v1) == ':1 \n 1 (int)' + v2 = debug_.format(1) + assert 'test_warnings_disabled\n 1 (int)' in str(v2) def test_eval_kwargs(): diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 34c507d..7ec2b66 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -1,6 +1,5 @@ import os import string -import sys from collections import Counter, OrderedDict, namedtuple from dataclasses import dataclass from typing import List @@ -220,7 +219,6 @@ def test_counter(): })>""" -@pytest.mark.skipif(sys.version_info > (3, 7), reason='no datalcasses before 3.6') def test_dataclass(): @dataclass class FooDataclass: From 3838cc8a1539488767ab4c94f706a09d3ffd54ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Eren=20=C3=96zt=C3=BCrk?= Date: Tue, 26 Jul 2022 12:50:18 +0300 Subject: [PATCH 02/36] fix format of nested dataclasses (#99) * fix format of nested dataclasses * import sys * unskip Co-authored-by: Samuel Colvin --- devtools/prettier.py | 4 +--- tests/test_prettier.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/devtools/prettier.py b/devtools/prettier.py index d4e2b1f..1154668 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -230,9 +230,7 @@ def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_ne self._str_lines(lines, indent_current, indent_new) def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int): - from dataclasses import asdict - - self._format_fields(value, asdict(value).items(), indent_current, indent_new) + self._format_fields(value, value.__dict__.items(), indent_current, indent_new) def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int): fields = [ diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 7ec2b66..6dea882 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -240,6 +240,29 @@ class FooDataclass: )""" +def test_nested_dataclasses(): + @dataclass + class FooDataclass: + x: int + + @dataclass + class BarDataclass: + a: float + b: FooDataclass + + f = FooDataclass(123) + b = BarDataclass(10.0, f) + v = pformat(b) + print(v) + assert v == """\ +BarDataclass( + a=10.0, + b=FooDataclass( + x=123, + ), +)""" + + @pytest.mark.skipif(numpy is None, reason='numpy not installed') def test_indent_numpy(): v = pformat({'numpy test': numpy.array(range(20))}) From cd5d511b6005caf41838e402218616fa76fa017a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 15:18:52 +0100 Subject: [PATCH 03/36] Moving to `pyproject.toml` and complete type hints (#107) * moving to pyproject.toml * fixing mkdocs * tweak pyproject.toml * switch to hatch/hatchling * linting * revert removal of editable builds * bump * fix docs build * lint plugins.py * add history file * complete type hints * fix tests * revert "str->as_str" name change * tweak HISTORY * tweak pyproject.toml --- .github/workflows/ci.yml | 7 +-- .gitignore | 1 + HISTORY.md | 12 +++++ MANIFEST.in | 3 -- Makefile | 9 ++-- devtools/__init__.py | 2 + devtools/ansi.py | 28 +++++++---- devtools/debug.py | 40 +++++++++------ devtools/prettier.py | 68 ++++++++++++++------------ devtools/py.typed | 0 devtools/timer.py | 35 +++++++------ devtools/utils.py | 19 +++++--- devtools/version.py | 2 - docs/build/gen_html.py | 32 ------------ docs/build/main.py | 28 ----------- docs/index.md | 4 +- docs/plugins.py | 83 +++++++++++++++++++++++++++++++ docs/requirements.txt | 1 + docs/usage.md | 10 ++-- mkdocs.yml | 10 ++++ pyproject.toml | 89 ++++++++++++++++++++++++++++++++++ setup.cfg | 28 ----------- setup.py | 60 ----------------------- tests/requirements-linting.txt | 2 +- tests/requirements.txt | 3 +- 25 files changed, 327 insertions(+), 249 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 devtools/py.typed delete mode 100644 docs/build/gen_html.py delete mode 100755 docs/build/main.py create mode 100755 docs/plugins.py create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8353a3..59a9382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,13 +94,10 @@ jobs: python-version: '3.10' - name: install - run: make install - - - name: set version - run: VERSION_PATH='devtools/version.py' python <(curl -Ls https://git.io/JT3rm) + run: pip install -U build twine setuptools - name: build - run: python setup.py sdist bdist_wheel + run: python -m build - run: twine check dist/* diff --git a/.gitignore b/.gitignore index 1b7451b..a0f90dd 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ old-version/ *.swp /site/ /site.zip +/build/ diff --git a/HISTORY.md b/HISTORY.md index 61dcf05..6f8c2c5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,11 @@ +## v0.8.0 (2021-09-29) + +* test with python 3.10 #91 +* display `SQLAlchemy` objects nicely #94 +* fix tests on windows #93 +* show function `qualname` #95 +* cache pygments loading (significant speedup) #96 + ## v0.7.0 (2021-09-03) * switch to [`executing`](https://pypi.org/project/executing/) and [`asttokens`](https://pypi.org/project/asttokens/) @@ -9,6 +17,10 @@ * display `dataclasses` properly, #88 * uprev test dependencies, #81, #83, #90 +## v0.6.1 (2020-10-22) + +compatibility with python 3.8.6 + ## v0.6.0 (2020-07-29) * improve `__pretty__` to work better with pydantic classes, #52 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 5f8d242..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include LICENSE -include README.md -include HISTORY.md diff --git a/Makefile b/Makefile index bc3b058..a899e5a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .DEFAULT_GOAL := all -isort = isort devtools tests -black = black -S -l 120 --target-version py37 devtools +isort = isort devtools tests docs/plugins.py +black = black -S -l 120 --target-version py37 devtools docs/plugins.py .PHONY: install install: @@ -15,9 +15,10 @@ format: .PHONY: lint lint: - flake8 devtools/ tests/ + flake8 --max-complexity 10 --max-line-length 120 --ignore E203,W503 devtools tests docs/plugins.py $(isort) --check-only --df $(black) --check --diff + mypy devtools .PHONY: test test: @@ -50,12 +51,10 @@ clean: .PHONY: docs docs: flake8 --max-line-length=80 docs/examples/ - python docs/build/main.py mkdocs build .PHONY: docs-serve docs-serve: - python docs/build/main.py mkdocs serve .PHONY: publish-docs diff --git a/devtools/__init__.py b/devtools/__init__.py index 6808bf5..d6a6564 100644 --- a/devtools/__init__.py +++ b/devtools/__init__.py @@ -4,3 +4,5 @@ from .prettier import * from .timer import * from .version import VERSION + +__version__ = VERSION diff --git a/devtools/ansi.py b/devtools/ansi.py index e0e7b3d..daa4c9d 100644 --- a/devtools/ansi.py +++ b/devtools/ansi.py @@ -6,10 +6,10 @@ MYPY = False if MYPY: - from typing import Any, Union + from typing import Any, Mapping, Union -def strip_ansi(value): +def strip_ansi(value: str) -> str: import re return re.sub('\033\\[((?:\\d|;)*)([a-zA-Z])', '', value) @@ -62,7 +62,7 @@ class Style(IntEnum): # this is a meta value used for the "Style" instance which is the "style" function function = -1 - def __call__(self, input: 'Any', *styles: 'Style', reset: bool = True, apply: bool = True) -> str: + def __call__(self, input: 'Any', *styles: 'Union[Style, int, str]', reset: bool = True, apply: bool = True) -> str: """ Styles text with ANSI styles and returns the new string. @@ -91,7 +91,7 @@ def __call__(self, input: 'Any', *styles: 'Style', reset: bool = True, apply: bo s = self.styles[s] except KeyError: raise ValueError(f'invalid style "{s}"') - codes.append(_style_as_int(s.value)) + codes.append(_style_as_int(s.value)) # type: ignore if codes: r = _as_ansi(';'.join(codes)) + text @@ -103,16 +103,16 @@ def __call__(self, input: 'Any', *styles: 'Style', reset: bool = True, apply: bo return r @property - def styles(self): + def styles(self) -> 'Mapping[str, Style]': return self.__class__.__members__ - def __repr__(self): + def __repr__(self) -> str: if self == self.function: return '' else: return super().__repr__() - def __str__(self): + def __str__(self) -> str: if self == self.function: return repr(self) else: @@ -139,14 +139,22 @@ class StylePrint: for that mistake. """ - def __call__(self, input, *styles, reset=True, flush=True, file=None, **print_kwargs): + def __call__( + self, + input: str, + *styles: 'Union[Style, int, str]', + reset: bool = True, + flush: bool = True, + file: 'Any' = None, + **print_kwargs: 'Any', + ) -> None: text = sformat(input, *styles, reset=reset, apply=isatty(file)) print(text, flush=flush, file=file, **print_kwargs) - def __getattr__(self, item): + def __getattr__(self, item: str) -> str: return getattr(sformat, item) - def __repr__(self): + def __repr__(self) -> str: return '' diff --git a/devtools/debug.py b/devtools/debug.py index c56588c..657859c 100644 --- a/devtools/debug.py +++ b/devtools/debug.py @@ -10,7 +10,7 @@ MYPY = False if MYPY: from types import FrameType - from typing import Any, Generator, List, Optional + from typing import Any, Generator, List, Optional, Union pformat = PrettyFormat( indent_step=int(os.getenv('PY_DEVTOOLS_INDENT', 4)), @@ -18,12 +18,14 @@ width=int(os.getenv('PY_DEVTOOLS_WIDTH', 120)), yield_from_generators=env_true('PY_DEVTOOLS_YIELD_FROM_GEN', True), ) +# required for type hinting because I (stupidly) added methods called `str` +StrType = str class DebugArgument: __slots__ = 'value', 'name', 'extra' - def __init__(self, value, *, name=None, **extra): + def __init__(self, value: 'Any', *, name: 'Optional[str]' = None, **extra: 'Any') -> None: self.value = value self.name = name self.extra = [] @@ -35,7 +37,7 @@ def __init__(self, value, *, name=None, **extra): self.extra.append(('len', length)) self.extra += [(k, v) for k, v in extra.items() if v is not None] - def str(self, highlight=False) -> str: + def str(self, highlight: bool = False) -> StrType: s = '' if self.name and not is_literal(self.name): s = f'{sformat(self.name, sformat.blue, apply=highlight)}: ' @@ -54,7 +56,7 @@ def str(self, highlight=False) -> str: s += suffix return s - def __str__(self) -> str: + def __str__(self) -> StrType: return self.str() @@ -66,14 +68,22 @@ class DebugOutput: arg_class = DebugArgument __slots__ = 'filename', 'lineno', 'frame', 'arguments', 'warning' - def __init__(self, *, filename: str, lineno: int, frame: str, arguments: 'List[DebugArgument]', warning=None): + def __init__( + self, + *, + filename: str, + lineno: int, + frame: str, + arguments: 'List[DebugArgument]', + warning: 'Union[None, str, bool]' = None, + ) -> None: self.filename = filename self.lineno = lineno self.frame = frame self.arguments = arguments self.warning = warning - def str(self, highlight=False) -> str: + def str(self, highlight: bool = False) -> StrType: if highlight: prefix = ( f'{sformat(self.filename, sformat.magenta)}:{sformat(self.lineno, sformat.green)} ' @@ -87,10 +97,10 @@ def str(self, highlight=False) -> str: prefix += f' ({self.warning})' return f'{prefix}\n ' + '\n '.join(a.str(highlight) for a in self.arguments) - def __str__(self) -> str: + def __str__(self) -> StrType: return self.str() - def __repr__(self) -> str: + def __repr__(self) -> StrType: arguments = ' '.join(str(a) for a in self.arguments) return f'' @@ -102,7 +112,7 @@ def __init__(self, *, warnings: 'Optional[bool]' = None, highlight: 'Optional[bo self._show_warnings = env_bool(warnings, 'PY_DEVTOOLS_WARNINGS', True) self._highlight = highlight - def __call__(self, *args, file_=None, flush_=True, **kwargs) -> 'Any': + def __call__(self, *args: 'Any', file_: 'Any' = None, flush_: bool = True, **kwargs: 'Any') -> 'Any': d_out = self._process(args, kwargs) s = d_out.str(use_highlight(self._highlight, file_)) print(s, file=file_, flush=flush_) @@ -113,18 +123,18 @@ def __call__(self, *args, file_=None, flush_=True, **kwargs) -> 'Any': else: return args - def format(self, *args, **kwargs) -> DebugOutput: + def format(self, *args: 'Any', **kwargs: 'Any') -> DebugOutput: return self._process(args, kwargs) - def breakpoint(self): + def breakpoint(self) -> None: import pdb pdb.Pdb(skip=['devtools.*']).set_trace() - def timer(self, name=None, *, verbose=True, file=None, dp=3) -> Timer: + def timer(self, name: 'Optional[str]' = None, *, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> Timer: return Timer(name=name, verbose=verbose, file=file, dp=dp) - def _process(self, args, kwargs) -> DebugOutput: + def _process(self, args: 'Any', kwargs: 'Any') -> DebugOutput: """ BEWARE: this must be called from a function exactly 2 levels below the top of the stack. """ @@ -181,13 +191,13 @@ def _process(self, args, kwargs) -> DebugOutput: warning=self._show_warnings and warning, ) - def _args_inspection_failed(self, args, kwargs): + def _args_inspection_failed(self, args: 'Any', kwargs: 'Any') -> 'Generator[DebugArgument, None, None]': for arg in args: yield self.output_class.arg_class(arg) for name, value in kwargs.items(): yield self.output_class.arg_class(value, name=name) - def _process_args(self, ex, args, kwargs) -> 'Generator[DebugArgument, None, None]': + def _process_args(self, ex: 'Any', args: 'Any', kwargs: 'Any') -> 'Generator[DebugArgument, None, None]': import ast func_ast = ex.node diff --git a/devtools/prettier.py b/devtools/prettier.py index 1154668..bb5974c 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -15,7 +15,7 @@ __all__ = 'PrettyFormat', 'pformat', 'pprint' MYPY = False if MYPY: - from typing import Any, Iterable, Tuple, Union + from typing import Any, Callable, Iterable, List, Set, Tuple, Union PARENTHESES_LOOKUP = [ (list, '[', ']'), @@ -27,7 +27,7 @@ PRETTY_KEY = '__prettier_formatted_value__' -def fmt(v): +def fmt(v: 'Any') -> 'Any': return {PRETTY_KEY: v} @@ -36,11 +36,11 @@ class SkipPretty(Exception): @cache -def get_pygments(): +def get_pygments() -> 'Tuple[Any, Any, Any]': try: - import pygments - from pygments.formatters import Terminal256Formatter - from pygments.lexers import PythonLexer + import pygments # type: ignore + from pygments.formatters import Terminal256Formatter # type: ignore + from pygments.lexers import PythonLexer # type: ignore except ImportError: # pragma: no cover return None, None, None else: @@ -54,12 +54,12 @@ def get_pygments(): class PrettyFormat: def __init__( self, - indent_step=4, - indent_char=' ', - repr_strings=False, - simple_cutoff=10, - width=120, - yield_from_generators=True, + indent_step: int = 4, + indent_char: str = ' ', + repr_strings: bool = False, + simple_cutoff: int = 10, + width: int = 120, + yield_from_generators: bool = True, ): self._indent_step = indent_step self._c = indent_char @@ -67,7 +67,7 @@ def __init__( self._repr_generators = not yield_from_generators self._simple_cutoff = simple_cutoff self._width = width - self._type_lookup = [ + self._type_lookup: 'List[Tuple[Any, Callable[[Any, str, int, int], None]]]' = [ (dict, self._format_dict), ((str, bytes), self._format_str_bytes), (tuple, self._format_tuples), @@ -80,7 +80,7 @@ def __init__( (SQLAlchemyClassType, self._format_sqlalchemy_class), ] - def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, highlight: bool = False): + def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, highlight: bool = False) -> str: self._stream = io.StringIO() self._format(value, indent_current=indent, indent_first=indent_first) s = self._stream.getvalue() @@ -90,7 +90,7 @@ def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, s = pygments.highlight(s, lexer=pyg_lexer, formatter=pyg_formatter).rstrip('\n') return s - def _format(self, value: 'Any', indent_current: int, indent_first: bool): + def _format(self, value: 'Any', indent_current: int, indent_first: bool) -> None: if indent_first: self._stream.write(indent_current * self._c) @@ -110,7 +110,7 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool): except SkipPretty: pass else: - return + return None value_repr = repr(value) if len(value_repr) <= self._simple_cutoff and not isinstance(value, generator_types): @@ -120,11 +120,11 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool): for t, func in self._type_lookup: if isinstance(value, t): func(value, value_repr, indent_current, indent_new) - return + return None self._format_raw(value, value_repr, indent_current, indent_new) - def _render_pretty(self, gen, indent: int): + def _render_pretty(self, gen: 'Iterable[Any]', indent: int) -> None: prefix = False for v in gen: if isinstance(v, int) and v in {-1, 0, 1}: @@ -144,7 +144,7 @@ def _render_pretty(self, gen, indent: int): # shouldn't happen but will self._stream.write(repr(v)) - def _format_dict(self, value: 'Any', _: str, indent_current: int, indent_new: int): + def _format_dict(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: open_, before_, split_, after_, close_ = '{\n', indent_new * self._c, ': ', ',\n', '}' if isinstance(value, OrderedDict): open_, split_, after_, close_ = 'OrderedDict([\n', ', ', '),\n', '])' @@ -161,7 +161,9 @@ def _format_dict(self, value: 'Any', _: str, indent_current: int, indent_new: in self._stream.write(after_) self._stream.write(indent_current * self._c + close_) - def _format_list_like(self, value: 'Union[list, tuple, set]', _: str, indent_current: int, indent_new: int): + def _format_list_like( + self, value: 'Union[List[Any], Tuple[Any, ...], Set[Any]]', _: str, indent_current: int, indent_new: int + ) -> None: open_, close_ = '(', ')' for t, *oc in PARENTHESES_LOOKUP: if isinstance(value, t): @@ -174,16 +176,18 @@ def _format_list_like(self, value: 'Union[list, tuple, set]', _: str, indent_cur self._stream.write(',\n') self._stream.write(indent_current * self._c + close_) - def _format_tuples(self, value: tuple, value_repr: str, indent_current: int, indent_new: int): + def _format_tuples(self, value: 'Tuple[Any, ...]', value_repr: str, indent_current: int, indent_new: int) -> None: fields = getattr(value, '_fields', None) if fields: # named tuple self._format_fields(value, zip(fields, value), indent_current, indent_new) else: # normal tuples are just like other similar iterables - return self._format_list_like(value, value_repr, indent_current, indent_new) + self._format_list_like(value, value_repr, indent_current, indent_new) - def _format_str_bytes(self, value: 'Union[str, bytes]', value_repr: str, indent_current: int, indent_new: int): + def _format_str_bytes( + self, value: 'Union[str, bytes]', value_repr: str, indent_current: int, indent_new: int + ) -> None: if self._repr_strings: self._stream.write(value_repr) else: @@ -193,14 +197,14 @@ def _format_str_bytes(self, value: 'Union[str, bytes]', value_repr: str, indent_ else: self._stream.write(value_repr) - def _str_lines(self, lines: 'Iterable[str]', indent_current: int, indent_new: int) -> None: + def _str_lines(self, lines: 'Iterable[Union[str, bytes]]', indent_current: int, indent_new: int) -> None: self._stream.write('(\n') prefix = indent_new * self._c for line in lines: self._stream.write(prefix + repr(line) + '\n') self._stream.write(indent_current * self._c + ')') - def _wrap_lines(self, s, indent_new) -> 'Generator[str, None, None]': + def _wrap_lines(self, s: 'Union[str, bytes]', indent_new: int) -> 'Generator[Union[str, bytes], None, None]': width = self._width - indent_new - 3 for line in s.splitlines(True): start = 0 @@ -209,7 +213,9 @@ def _wrap_lines(self, s, indent_new) -> 'Generator[str, None, None]': start = pos yield line[start:] - def _format_generator(self, value: Generator, value_repr: str, indent_current: int, indent_new: int): + def _format_generator( + self, value: 'Generator[Any, None, None]', value_repr: str, indent_current: int, indent_new: int + ) -> None: if self._repr_generators: self._stream.write(value_repr) else: @@ -224,15 +230,15 @@ def _format_generator(self, value: Generator, value_repr: str, indent_current: i self._stream.write(',\n') self._stream.write(indent_current * self._c + ')') - def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_new: int): + def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: self._stream.write('bytearray') lines = self._wrap_lines(bytes(value), indent_new) self._str_lines(lines, indent_current, indent_new) - def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int): + def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: self._format_fields(value, value.__dict__.items(), indent_current, indent_new) - def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int): + def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: fields = [ (field, getattr(value, field)) for field in dir(value) @@ -240,7 +246,7 @@ def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, in ] self._format_fields(value, fields, indent_current, indent_new) - def _format_raw(self, _: 'Any', value_repr: str, indent_current: int, indent_new: int): + def _format_raw(self, _: 'Any', value_repr: str, indent_current: int, indent_new: int) -> None: lines = value_repr.splitlines(True) if len(lines) > 1 or (len(value_repr) + indent_current) >= self._width: self._stream.write('(\n') @@ -274,6 +280,6 @@ def _format_fields( force_highlight = env_true('PY_DEVTOOLS_HIGHLIGHT', None) -def pprint(s, file=None): +def pprint(s: 'Any', file: 'Any' = None) -> None: highlight = isatty(file) if force_highlight is None else force_highlight print(pformat(s, highlight=highlight), file=file, flush=True) diff --git a/devtools/py.typed b/devtools/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/devtools/timer.py b/devtools/timer.py index 3182d1f..dd6c139 100644 --- a/devtools/timer.py +++ b/devtools/timer.py @@ -2,30 +2,37 @@ __all__ = ('Timer',) +MYPY = False +if MYPY: + from typing import Any, List, Optional, Set + +# required for type hinting because I (stupidly) added methods called `str` +StrType = str + class TimerResult: - def __init__(self, name=None, verbose=True): + def __init__(self, name: 'Optional[str]' = None, verbose: bool = True) -> None: self._name = name self.verbose = verbose - self.finish = None + self.finish: 'Optional[float]' = None self.start = time() - def capture(self): + def capture(self) -> None: self.finish = time() - def elapsed(self): + def elapsed(self) -> float: if self.finish: return self.finish - self.start else: return -1 - def str(self, dp=3): + def str(self, dp: int = 3) -> StrType: if self._name: return f'{self._name}: {self.elapsed():0.{dp}f}s elapsed' else: return f'{self.elapsed():0.{dp}f}s elapsed' - def __str__(self): + def __str__(self) -> StrType: return self.str() @@ -33,25 +40,25 @@ def __str__(self): class Timer: - def __init__(self, name=None, verbose=True, file=None, dp=3): + def __init__(self, name: 'Optional[str]' = None, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> None: self.file = file self.dp = dp self._name = name self._verbose = verbose - self.results = [] + self.results: 'List[TimerResult]' = [] - def __call__(self, name=None, verbose=None): + def __call__(self, name: 'Optional[str]' = None, verbose: 'Optional[bool]' = None) -> 'Timer': if name: self._name = name if verbose is not None: self._verbose = verbose return self - def start(self, name=None, verbose=None): + def start(self, name: 'Optional[str]' = None, verbose: 'Optional[bool]' = None) -> 'Timer': self.results.append(TimerResult(name or self._name, self._verbose if verbose is None else verbose)) return self - def capture(self, verbose=None): + def capture(self, verbose: 'Optional[bool]' = None) -> 'TimerResult': r = self.results[-1] r.capture() print_ = r.verbose if verbose is None else verbose @@ -59,7 +66,7 @@ def capture(self, verbose=None): print(r.str(self.dp), file=self.file, flush=True) return r - def summary(self, verbose=False): + def summary(self, verbose: bool = False) -> 'Set[float]': times = set() for r in self.results: if not r.finish: @@ -84,9 +91,9 @@ def summary(self, verbose=False): raise RuntimeError('timer not started') return times - def __enter__(self): + def __enter__(self) -> 'Timer': self.start() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, *args: 'Any') -> None: self.capture() diff --git a/devtools/utils.py b/devtools/utils.py index 5956073..994027e 100644 --- a/devtools/utils.py +++ b/devtools/utils.py @@ -14,10 +14,14 @@ MYPY = False if MYPY: - from typing import Any, Optional + from typing import Any, Optional, no_type_check +else: + def no_type_check(x: 'Any') -> 'Any': + return x -def isatty(stream=None): + +def isatty(stream: 'Any' = None) -> bool: stream = stream or sys.stdout try: return stream.isatty() @@ -25,7 +29,7 @@ def isatty(stream=None): return False -def env_true(var_name: str, alt: 'Optional[bool]' = None) -> 'Optional[bool]': +def env_true(var_name: str, alt: 'Optional[bool]' = None) -> 'Any': env = os.getenv(var_name, None) if env: return env.upper() in {'1', 'TRUE'} @@ -40,6 +44,7 @@ def env_bool(value: 'Optional[bool]', env_name: str, env_default: 'Optional[bool return value +@no_type_check def activate_win_color() -> bool: # pragma: no cover """ Activate ANSI support on windows consoles. @@ -88,14 +93,14 @@ def _set_conout_mode(new_mode, mask=0xFFFFFFFF): mode = mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING try: _set_conout_mode(mode, mask) - except WindowsError as e: + except WindowsError as e: # type: ignore if e.winerror == ERROR_INVALID_PARAMETER: return False raise return True -def use_highlight(highlight: 'Optional[bool]' = None, file_=None) -> bool: +def use_highlight(highlight: 'Optional[bool]' = None, file_: 'Any' = None) -> bool: highlight = env_bool(highlight, 'PY_DEVTOOLS_HIGHLIGHT', None) if highlight is not None: @@ -106,7 +111,7 @@ def use_highlight(highlight: 'Optional[bool]' = None, file_=None) -> bool: return isatty(file_) -def is_literal(s): +def is_literal(s: 'Any') -> bool: import ast try: @@ -145,7 +150,7 @@ class DataClassType(metaclass=MetaDataClassType): class MetaSQLAlchemyClassType(type): def __instancecheck__(self, instance: 'Any') -> bool: try: - from sqlalchemy.ext.declarative import DeclarativeMeta + from sqlalchemy.ext.declarative import DeclarativeMeta # type: ignore except ImportError: return False else: diff --git a/devtools/version.py b/devtools/version.py index 34672f0..2810d0a 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1,3 +1 @@ -__all__ = ('VERSION',) - VERSION = '0.7.0' diff --git a/docs/build/gen_html.py b/docs/build/gen_html.py deleted file mode 100644 index cabba3b..0000000 --- a/docs/build/gen_html.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import subprocess -import sys -from pathlib import Path - -from ansi2html import Ansi2HTMLConverter - -EX_DIR = Path(__file__).parent / '..' / 'examples' - - -def gen_examples_html(): - os.environ.update(PY_DEVTOOLS_HIGHLIGHT='true', PY_DEVTOOLS_WIDTH='80') - conv = Ansi2HTMLConverter() - fast = 'FAST' in os.environ - - for f in EX_DIR.iterdir(): - if f.suffix != '.py' or f.name == 'sitecustomize.py': - continue - output_file = EX_DIR / f'{f.stem}.html' - if fast and output_file.exists(): - print(f'HTML file already exists for {f}, skipping') - continue - - print(f'generating output for: {f}') - p = subprocess.run((sys.executable, str(f)), stdout=subprocess.PIPE, check=True) - html = conv.convert(p.stdout.decode(), full=False).strip('\r\n') - html = html.replace('docs/build/../examples/', '') - output_file.write_text(f'
\n{html}\n
\n') - - -if __name__ == '__main__': - gen_examples_html() diff --git a/docs/build/main.py b/docs/build/main.py deleted file mode 100755 index 9c36ae4..0000000 --- a/docs/build/main.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import re -import sys -from importlib.machinery import SourceFileLoader -from pathlib import Path - -THIS_DIR = Path(__file__).parent -PROJECT_ROOT = THIS_DIR / '..' / '..' - - -def main(): - history = (PROJECT_ROOT / 'HISTORY.md').read_text() - history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/python-devtools/issues/\1)', history) - history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) - history = re.sub('@@', '@', history) - - (PROJECT_ROOT / 'docs/.history.md').write_text(history) - - version = SourceFileLoader('version', str(PROJECT_ROOT / 'devtools/version.py')).load_module() - (PROJECT_ROOT / 'docs/.version.md').write_text(f'Documentation for version: **v{version.VERSION}**\n') - - sys.path.append(str(THIS_DIR.resolve())) - from gen_html import gen_examples_html - return gen_examples_html() - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/docs/index.md b/docs/index.md index 5098d6a..8a0b125 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ [![versions](https://img.shields.io/pypi/pyversions/devtools.svg)](https://github.com/samuelcolvin/python-devtools) [![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/master/LICENSE) -{!.version.md!} +{{ version }} **Python's missing debug print command and other development tools.** @@ -14,6 +14,6 @@ {!examples/example.py!} ``` -{!examples/example.html!} +{{ example_html(examples/example.py) }} Python devtools can do much more, see [Usage](./usage.md) for examples. diff --git a/docs/plugins.py b/docs/plugins.py new file mode 100755 index 0000000..6e51aef --- /dev/null +++ b/docs/plugins.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import logging +import os +import re +import subprocess +import sys +from importlib.machinery import SourceFileLoader +from pathlib import Path + +from ansi2html import Ansi2HTMLConverter +from mkdocs.config import Config +from mkdocs.structure.files import Files +from mkdocs.structure.pages import Page + +THIS_DIR = Path(__file__).parent +PROJECT_ROOT = THIS_DIR / '..' + +logger = logging.getLogger('mkdocs.test_examples') + +# see mkdocs.yml for how these methods ar used +__all__ = 'on_pre_build', 'on_page_markdown', 'on_files' + + +def on_pre_build(config: Config): + build_history() + + +def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str: + markdown = set_version(markdown, page) + return gen_example_html(markdown) + + +def on_files(files: Files, config: Config) -> Files: + return remove_files(files) + + +def build_history(): + history = (PROJECT_ROOT / 'HISTORY.md').read_text() + history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/python-devtools/issues/\1)', history) + history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) + history = re.sub('@@', '@', history) + (THIS_DIR / '.history.md').write_text(history) + + +def gen_example_html(markdown: str): + return re.sub(r'{{ *example_html\((.*?)\) *}}', gen_examples_html, markdown) + + +def gen_examples_html(m: re.Match) -> str: + sys.path.append(str(THIS_DIR.resolve())) + + os.environ.update(PY_DEVTOOLS_HIGHLIGHT='true', PY_DEVTOOLS_WIDTH='80') + conv = Ansi2HTMLConverter() + name = THIS_DIR / Path(m.group(1)) + + logger.info("running %s to generate HTML...", name) + p = subprocess.run((sys.executable, str(name)), stdout=subprocess.PIPE, check=True) + html = conv.convert(p.stdout.decode(), full=False).strip('\r\n') + html = html.replace('docs/build/../examples/', '') + return f'
\n{html}\n
\n' + + +def set_version(markdown: str, page: Page) -> str: + if page.abs_url == '/': + version = SourceFileLoader('version', str(PROJECT_ROOT / 'devtools/version.py')).load_module() + version_str = f'Documentation for version: **{version}**' + markdown = re.sub(r'{{ *version *}}', version_str, markdown) + return markdown + + +def remove_files(files: Files) -> Files: + to_remove = [] + for file in files: + if file.src_path in {'plugins.py', 'requirements.txt'}: + to_remove.append(file) + elif file.src_path.startswith('__pycache__/'): + to_remove.append(file) + + logger.debug('removing files: %s', [f.src_path for f in to_remove]) + for f in to_remove: + files.remove(f) + + return files diff --git a/docs/requirements.txt b/docs/requirements.txt index 7ecdc7d..540379d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,5 +2,6 @@ ansi2html==1.8.0 mkdocs==1.3.1 mkdocs-exclude==1.0.2 mkdocs-material==8.3.9 +mkdocs-simple-hooks==0.1.5 markdown-include==0.7.0 pygments==2.12.0 diff --git a/docs/usage.md b/docs/usage.md index 3e5a490..eb8f3e1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,7 +9,7 @@ and readable way to print stuff during development. (If you know why this is, I' {!examples/example.py!} ``` -{!examples/example.html!} +{{ example_html(examples/example.py) }} `debug` is like `print` after a good night's sleep and lots of coffee: @@ -24,7 +24,7 @@ A more complex example of `debug` shows more of what it can do. {!examples/complex.py!} ``` -{!examples/complex.html!} +{{ example_html(examples/complex.py) }} ### Returning the arguments @@ -40,7 +40,7 @@ The returned arguments work as follows: {!examples/return_args.py!} ``` -{!examples/return_args.html!} +{{ example_html(examples/return_args.py) }} ## Other debug tools @@ -54,7 +54,7 @@ The debug namespace includes a number of other useful functions: {!examples/other.py!} ``` -{!examples/other.html!} +{{ example_html(examples/other.py) }} ### Prettier print @@ -69,7 +69,7 @@ in `debug()`, but it can also be used directly: {!examples/prettier.py!} ``` -{!examples/prettier.html!} +{{ example_html(examples/prettier.py) }} For more details on prettier printing, see [`prettier.py`](https://github.com/samuelcolvin/python-devtools/blob/master/devtools/prettier.py). diff --git a/mkdocs.yml b/mkdocs.yml index 268ac76..b088763 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,11 @@ markdown_extensions: - codehilite - extra - attr_list +- pymdownx.highlight: + anchor_linenums: true +- pymdownx.inlinehilite +- pymdownx.snippets +- pymdownx.superfences plugins: - search @@ -51,3 +56,8 @@ plugins: - build/* - examples/* - requirements.txt +- mkdocs-simple-hooks: + hooks: + on_pre_build: 'docs.plugins:on_pre_build' + on_files: 'docs.plugins:on_files' + on_page_markdown: 'docs.plugins:on_page_markdown' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..80ecf52 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = ['hatchling'] +build-backend = 'hatchling.build' + +[tool.hatch.version] +path = 'devtools/version.py' + +[project] +name = 'devtools' +description = "Python's missing debug print command, and more." +authors = [{name = 'Samuel Colvin', email = 's@muelcolvin.com'}] +license = {file = 'LICENSE'} +readme = 'README.md' +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Science/Research', + 'Intended Audience :: System Administrators', + 'Operating System :: Unix', + 'Operating System :: POSIX :: Linux', + 'Environment :: Console', + 'Environment :: MacOS X', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Internet', + 'Typing :: Typed', +] +requires-python = '>=3.7' +dependencies = [ + 'executing>=0.8.0,<1.0.0', + 'asttokens>=2.0.0,<3.0.0', +] +optional-dependencies = {pygments = ['Pygments>=2.2.0'] } +dynamic = ['version'] + +[project.urls] +Homepage = 'https://github.com/samuelcolvin/python-devtools' +Documentation = 'https://python-devtools.helpmanual.io' +Funding = 'https://github.com/sponsors/samuelcolvin' +Source = 'https://github.com/samuelcolvin/python-devtools' +Changelog = 'https://github.com/samuelcolvin/python-devtools/releases' + +[tool.pytest.ini_options] +testpaths = 'tests' +filterwarnings = 'error' + +[tool.coverage.run] +source = ['devtools'] +branch = true + +[tool.coverage.report] +precision = 2 +exclude_lines = [ + 'pragma: no cover', + 'raise NotImplementedError', + 'raise NotImplemented', + 'if TYPE_CHECKING:', + 'if MYPY:', + '@overload', +] + +[tool.black] +color = true +line-length = 120 +target-version = ['py37', 'py38', 'py39', 'py310'] +skip-string-normalization = true + +[tool.isort] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +combine_as_imports = true +color_output = true + +[tool.mypy] +strict = true +warn_return_any = false + +[[tool.mypy.overrides]] +module = ['executing.*'] +ignore_missing_imports = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f58b3c4..0000000 --- a/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[tool:pytest] -testpaths = tests -filterwarnings = error - -[flake8] -max-line-length = 120 -max-complexity = 12 - -[coverage:run] -source = devtools -branch = True - -[coverage:report] -precision = 2 -exclude_lines = - pragma: no cover - raise NotImplementedError - raise NotImplemented - if MYPY: - @overload - -[isort] -line_length=120 -known_first_party=devtools -multi_line_output=3 -include_trailing_comma=True -force_grid_wrap=0 -combine_as_imports=True diff --git a/setup.py b/setup.py deleted file mode 100644 index 7bf8f5b..0000000 --- a/setup.py +++ /dev/null @@ -1,60 +0,0 @@ -import re -from importlib.machinery import SourceFileLoader -from pathlib import Path -from setuptools import setup - -description = "Python's missing debug print command and other development tools." -THIS_DIR = Path(__file__).resolve().parent -try: - history = (THIS_DIR / 'HISTORY.md').read_text() - history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/python-devtools/issues/\1)', history) - history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) - history = re.sub('@@', '@', history) - - long_description = (THIS_DIR / 'README.md').read_text() + '\n\n' + history -except FileNotFoundError: - long_description = description + '.\n\nSee https://python-devtools.helpmanual.io/ for documentation.' - -# avoid loading the package before requirements are installed: -version = SourceFileLoader('version', 'devtools/version.py').load_module() - -setup( - name='devtools', - version=str(version.VERSION), - description=description, - long_description=long_description, - long_description_content_type='text/markdown', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Unix', - 'Operating System :: POSIX :: Linux', - 'Environment :: MacOS X', - 'Topic :: Software Development :: Libraries :: Python Modules', - ], - author='Samuel Colvin', - author_email='s@muelcolvin.com', - url='https://github.com/samuelcolvin/python-devtools', - license='MIT', - packages=['devtools'], - python_requires='>=3.7', - install_requires=[ - 'executing>=0.8.0,<1.0.0', - 'asttokens>=2.0.0,<3.0.0', - ], - extras_require={ - 'pygments': ['Pygments>=2.2.0'], - }, - zip_safe=True, -) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index cf57c5c..b84d0b2 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,6 +1,6 @@ black==22.6.0 flake8==4.0.1 -isort==5.10.1 +isort[colors]==5.10.1 mypy==0.971 pycodestyle==2.8.0 pyflakes==2.4.0 diff --git a/tests/requirements.txt b/tests/requirements.txt index 56a5dfa..bf6b169 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,9 @@ -coverage==6.4.2 +coverage[toml]==6.4.2 Pygments==2.12.0 pytest==7.1.2 pytest-mock==3.8.2 pytest-sugar==0.9.5 +# these packages are used in tests so install the latest version pydantic asyncpg numpy From 352dc9e3cb82ba3281a42b87053a1dd5cc32b384 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 15:23:03 +0100 Subject: [PATCH 04/36] rename master -> main --- .github/workflows/ci.yml | 2 +- README.md | 8 ++++---- docs/index.md | 6 +++--- docs/usage.md | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59a9382..89f98f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - master + - main tags: - '**' pull_request: {} diff --git a/README.md b/README.md index 05383e4..80cb404 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # python devtools -[![CI](https://github.com/samuelcolvin/python-devtools/workflows/CI/badge.svg?event=push)](https://github.com/samuelcolvin/python-devtools/actions?query=event%3Apush+branch%3Amaster+workflow%3ACI) -[![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/master/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) +[![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) +[![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/main/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) [![pypi](https://img.shields.io/pypi/v/devtools.svg)](https://pypi.python.org/pypi/devtools) [![versions](https://img.shields.io/pypi/pyversions/devtools.svg)](https://github.com/samuelcolvin/python-devtools) -[![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/master/LICENSE) +[![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/main/LICENSE) **Python's missing debug print command and other development tools.** @@ -57,7 +57,7 @@ debug(data) outputs: -![python-devtools demo](https://raw.githubusercontent.com/samuelcolvin/python-devtools/master/demo.py.png) +![python-devtools demo](https://raw.githubusercontent.com/samuelcolvin/python-devtools/main/demo.py.png) ## Usage without Import diff --git a/docs/index.md b/docs/index.md index 8a0b125..b6bc7b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,10 @@ # Python devtools -[![CI](https://github.com/samuelcolvin/python-devtools/workflows/CI/badge.svg?event=push)](https://github.com/samuelcolvin/python-devtools/actions?query=event%3Apush+branch%3Amaster+workflow%3ACI) -[![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/master/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) +[![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) +[![Coverage](https://codecov.io/gh/samuelcolvin/python-devtools/branch/main/graph/badge.svg)](https://codecov.io/gh/samuelcolvin/python-devtools) [![pypi](https://img.shields.io/pypi/v/devtools.svg)](https://pypi.python.org/pypi/devtools) [![versions](https://img.shields.io/pypi/pyversions/devtools.svg)](https://github.com/samuelcolvin/python-devtools) -[![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/master/LICENSE) +[![license](https://img.shields.io/github/license/samuelcolvin/python-devtools.svg)](https://github.com/samuelcolvin/python-devtools/blob/main/LICENSE) {{ version }} diff --git a/docs/usage.md b/docs/usage.md index eb8f3e1..8445c6f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -72,7 +72,7 @@ in `debug()`, but it can also be used directly: {{ example_html(examples/prettier.py) }} For more details on prettier printing, see -[`prettier.py`](https://github.com/samuelcolvin/python-devtools/blob/master/devtools/prettier.py). +[`prettier.py`](https://github.com/samuelcolvin/python-devtools/blob/main/devtools/prettier.py). ## ANSI terminal colours @@ -81,7 +81,7 @@ For more details on prettier printing, see ``` For more details on ansi colours, see -[ansi.py](https://github.com/samuelcolvin/python-devtools/blob/master/devtools/ansi.py). +[ansi.py](https://github.com/samuelcolvin/python-devtools/blob/main/devtools/ansi.py). ## Usage without import From 4cd875cef1f972a5c798d712ae3a342c10f415ea Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 16:47:05 +0100 Subject: [PATCH 05/36] add install command (#108) * add install command * improve usage without import docs * fix linting, tweak __main__.py --- README.md | 14 +++----- devtools/__main__.py | 64 ++++++++++++++++++++++++++++++++++ docs/examples/sitecustomize.py | 8 ----- docs/plugins.py | 8 +++-- docs/usage.md | 36 +++++++++++++++---- pyproject.toml | 1 + 6 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 devtools/__main__.py delete mode 100644 docs/examples/sitecustomize.py diff --git a/README.md b/README.md index 80cb404..80218cc 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,8 @@ outputs: ## Usage without Import -modify `/usr/lib/python3.8/sitecustomize.py` making `debug` available in any python 3.8 code +devtools can be used without `from devtools import debug` if you add `debug` into `__builtins__` +in `sitecustomize.py`. -```py -# add devtools debug to builtins -try: - from devtools import debug -except ImportError: - pass -else: - __builtins__['debug'] = debug -``` +For instructions on adding `debug` to `__builtins__`, +see the [installation docs](https://python-devtools.helpmanual.io/usage/#usage-without-import). diff --git a/devtools/__main__.py b/devtools/__main__.py new file mode 100644 index 0000000..bf84fbf --- /dev/null +++ b/devtools/__main__.py @@ -0,0 +1,64 @@ +import os +import sys +from pathlib import Path + +from .version import VERSION + +# language=python +install_code = """ +# add devtools `debug` function to builtins +try: + from devtools import debug +except ImportError: + pass +else: + __builtins__['debug'] = debug +""" + + +def print_code() -> int: + print(install_code) + return 0 + + +def install() -> int: + print('[WARNING: this command is experimental, report issues at github.com/samuelcolvin/python-devtools]\n') + + if 'debug' in __builtins__.__dict__: + print('Looks like devtools is already installed.') + return 0 + + try: + import sitecustomize # type: ignore + except ImportError: + paths = [Path(p) for p in sys.path] + try: + path = next(p for p in paths if p.is_dir() and p.name == 'site-packages') + except StopIteration: + # what else makes sense to try? + print(f'unable to file a suitable path to save `sitecustomize.py` to from sys.path: {paths}') + return 1 + else: + install_path = path / 'sitecustomize.py' + else: + install_path = Path(sitecustomize.__file__) + + print(f'Found path "{install_path}" to install devtools into __builtins__') + print('To install devtools, run the following command:\n') + if os.access(install_path, os.W_OK): + print(f' python -m devtools print-code >> {install_path}\n') + else: + print(f' python -m devtools print-code | sudo tee -a {install_path} > /dev/null\n') + print('Note: "sudo" is required because the path is not writable by the current user.') + + return 0 + + +if __name__ == '__main__': + if 'install' in sys.argv: + sys.exit(install()) + elif 'print-code' in sys.argv: + sys.exit(print_code()) + else: + print(f'python-devtools v{VERSION}, CLI usage: python -m devtools [install|print-code]') + sys.exit(1) diff --git a/docs/examples/sitecustomize.py b/docs/examples/sitecustomize.py deleted file mode 100644 index a1c2e28..0000000 --- a/docs/examples/sitecustomize.py +++ /dev/null @@ -1,8 +0,0 @@ -... - -try: - from devtools import debug -except ImportError: - pass -else: - __builtins__['debug'] = debug diff --git a/docs/plugins.py b/docs/plugins.py index 6e51aef..d3ec8c4 100755 --- a/docs/plugins.py +++ b/docs/plugins.py @@ -39,7 +39,9 @@ def build_history(): history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/python-devtools/issues/\1)', history) history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) history = re.sub('@@', '@', history) - (THIS_DIR / '.history.md').write_text(history) + output_file = THIS_DIR / '.history.md' + if not output_file.exists() or history != output_file.read_text(): + (THIS_DIR / '.history.md').write_text(history) def gen_example_html(markdown: str): @@ -71,10 +73,12 @@ def set_version(markdown: str, page: Page) -> str: def remove_files(files: Files) -> Files: to_remove = [] for file in files: - if file.src_path in {'plugins.py', 'requirements.txt'}: + if file.src_path == 'requirements.txt': to_remove.append(file) elif file.src_path.startswith('__pycache__/'): to_remove.append(file) + elif file.src_path.endswith('.py'): + to_remove.append(file) logger.debug('removing files: %s', [f.src_path for f in to_remove]) for f in to_remove: diff --git a/docs/usage.md b/docs/usage.md index 8445c6f..ee5c15a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -83,19 +83,43 @@ For more details on prettier printing, see For more details on ansi colours, see [ansi.py](https://github.com/samuelcolvin/python-devtools/blob/main/devtools/ansi.py). -## Usage without import +## Usage without Import We all know the annoyance of running code only to discover a missing import, this can be particularly frustrating when the function you're using isn't used except during development. -You can setup your environment to make `debug` available at all times by editing `sitecustomize.py`, -with ubuntu and python3.8 this file can be found at `/usr/lib/python3.8/sitecustomize.py` but you might -need to look elsewhere depending on your OS/python version. +devtool's `debug` function can be used without import if you add `debug` to `__builtins__` +in `sitecustomize.py`. -Add the following to `sitecustomize.py` +Two ways to do this: + +### Automatic install + +!!! warning + This is experimental, please [create an issue](https://github.com/samuelcolvin/python-devtools/issues) + if you encounter any problems. + +To install `debug` into `__builtins__` automatically, run: + +```bash +python -m devtools install +``` + +This command won't write to any files, but it should print a command for you to run to add/edit `sitecustomize.py`. + +### Manual install + +To manually add `debug` to `__builtins__`, add the following to `sitecustomize.py` or any code +which is always imported. ```py -{!examples/sitecustomize.py!} +# add devtools `debug` function to builtins +try: + from devtools import debug +except ImportError: + pass +else: + __builtins__['debug'] = debug ``` The `ImportError` exception is important since you'll want python to run fine even if *devtools* isn't installed. diff --git a/pyproject.toml b/pyproject.toml index 80ecf52..2153541 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ filterwarnings = 'error' [tool.coverage.run] source = ['devtools'] branch = true +omit = ['devtools/__main__.py'] [tool.coverage.report] precision = 2 From b4c4275de315ec19e1d365c6eeabcdb154f71f36 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 16:54:09 +0100 Subject: [PATCH 06/36] prepare for release --- HISTORY.md | 6 ++++++ devtools/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 6f8c2c5..fcbebe3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,9 @@ +## v0.9.0 (2022-07-26) + +* fix format of nested dataclasses, #99 thanks @aliereno +* Moving to `pyproject.toml`, complete type hints and test with mypy, #107 +* add `install` command to add `debug` to `__builtins__`, #108 + ## v0.8.0 (2021-09-29) * test with python 3.10 #91 diff --git a/devtools/version.py b/devtools/version.py index 2810d0a..b9b28aa 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.7.0' +VERSION = '0.9.0' From fe1cac92394cb9f2b904da4aa4e2895d920459c1 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 17:04:27 +0100 Subject: [PATCH 07/36] fix deploy --- .github/workflows/ci.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89f98f7..69abdd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: python-version: '3.10' - name: install - run: pip install -U build twine setuptools + run: make install - name: build run: python -m build diff --git a/Makefile b/Makefile index a899e5a..15225e9 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ black = black -S -l 120 --target-version py37 devtools docs/plugins.py .PHONY: install install: - python -m pip install -U setuptools pip wheel twine + python -m pip install -U setuptools pip wheel twine build pip install -U -r requirements.txt pip install -e . From 297e9229fd6189588e515ed6cb2e99b2ad6c7ae5 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 26 Jul 2022 17:06:09 +0100 Subject: [PATCH 08/36] fix version in docs --- docs/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins.py b/docs/plugins.py index d3ec8c4..aa46876 100755 --- a/docs/plugins.py +++ b/docs/plugins.py @@ -65,7 +65,7 @@ def gen_examples_html(m: re.Match) -> str: def set_version(markdown: str, page: Page) -> str: if page.abs_url == '/': version = SourceFileLoader('version', str(PROJECT_ROOT / 'devtools/version.py')).load_module() - version_str = f'Documentation for version: **{version}**' + version_str = f'Documentation for version: **v{version.VERSION}**' markdown = re.sub(r'{{ *version *}}', version_str, markdown) return markdown From 0909f014e0d248b2a8f616525c2f3569e7251fc1 Mon Sep 17 00:00:00 2001 From: 0xsirsaif Date: Wed, 27 Jul 2022 11:16:56 +0200 Subject: [PATCH 09/36] Use secure builtins standard module, instead of the __builtins__ (#109) * Use secure builtins standard module, instead of the __builtins__ * use single quotes, and update the docs example * single quotes again, use hasattr() --- devtools/__main__.py | 6 ++++-- docs/usage.md | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/devtools/__main__.py b/devtools/__main__.py index bf84fbf..726f825 100644 --- a/devtools/__main__.py +++ b/devtools/__main__.py @@ -1,3 +1,4 @@ +import builtins import os import sys from pathlib import Path @@ -7,12 +8,13 @@ # language=python install_code = """ # add devtools `debug` function to builtins +import builtins try: from devtools import debug except ImportError: pass else: - __builtins__['debug'] = debug + setattr(builtins, 'debug', debug) """ @@ -24,7 +26,7 @@ def print_code() -> int: def install() -> int: print('[WARNING: this command is experimental, report issues at github.com/samuelcolvin/python-devtools]\n') - if 'debug' in __builtins__.__dict__: + if hasattr(builtins, 'debug'): print('Looks like devtools is already installed.') return 0 diff --git a/docs/usage.md b/docs/usage.md index ee5c15a..68bb6cb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -114,12 +114,13 @@ which is always imported. ```py # add devtools `debug` function to builtins +import builtins try: from devtools import debug except ImportError: pass else: - __builtins__['debug'] = debug + setattr(builtins, 'debug', debug) ``` The `ImportError` exception is important since you'll want python to run fine even if *devtools* isn't installed. From f4dfed484ce246943da324f0e24a13d80fbc0558 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 29 Jul 2022 12:26:53 +0100 Subject: [PATCH 10/36] upgrade executing to fix 3.10 (#110) --- pyproject.toml | 2 +- tests/test_main.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2153541..c21820a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ ] requires-python = '>=3.7' dependencies = [ - 'executing>=0.8.0,<1.0.0', + 'executing>=0.9.1,<1.0.0', 'asttokens>=2.0.0,<3.0.0', ] optional-dependencies = {pygments = ['Pygments>=2.2.0'] } diff --git a/tests/test_main.py b/tests/test_main.py index 5693061..f0cce7d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -177,7 +177,6 @@ def test_kwargs_orderless(): } -@pytest.mark.xfail(sys.version_info >= (3, 10), reason='https://github.com/alexmojaki/executing/issues/40') def test_simple_vars(): v = debug.format('test', 1, 2) s = normalise_output(str(v)) From 5efff5fdcf8539e7a4f539d018f785ba4d0e59c4 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 1 Aug 2022 13:13:33 +0100 Subject: [PATCH 11/36] Fix windows build (#111) * Fix windows build * try again :sleeping: --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69abdd2..3980217 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,6 @@ jobs: with: python-version: '3.10' - - run: pip install -U pip wheel - run: pip install -r tests/requirements-linting.txt - run: pip install . @@ -48,7 +47,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - run: pip install -U pip wheel - run: pip install -r tests/requirements.txt - run: pip install . - run: pip freeze From ba0507a369311b1e5d2b99599dd56905bdbf9358 Mon Sep 17 00:00:00 2001 From: Riley <44530786+staticf0x@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:23:41 +0100 Subject: [PATCH 12/36] Allow executing dependency to be >1.0.0 (#115) * Allow executing dependency to be >1.0.0 * Constrain executing on >=1.1.1 * Remove upper bound --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c21820a..ebe9dd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ ] requires-python = '>=3.7' dependencies = [ - 'executing>=0.9.1,<1.0.0', + 'executing>=1.1.1', 'asttokens>=2.0.0,<3.0.0', ] optional-dependencies = {pygments = ['Pygments>=2.2.0'] } From 560b9884919bf06cef6e91125b4f9af93299f6fe Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Fri, 25 Nov 2022 13:25:10 +0300 Subject: [PATCH 13/36] more precise timer summary (#113) * fix: use all timers in summary * fix: lint * chore: lint --- devtools/timer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/devtools/timer.py b/devtools/timer.py index dd6c139..822ea44 100644 --- a/devtools/timer.py +++ b/devtools/timer.py @@ -1,10 +1,10 @@ -from time import time +from time import perf_counter __all__ = ('Timer',) MYPY = False if MYPY: - from typing import Any, List, Optional, Set + from typing import Any, List, Optional # required for type hinting because I (stupidly) added methods called `str` StrType = str @@ -15,10 +15,10 @@ def __init__(self, name: 'Optional[str]' = None, verbose: bool = True) -> None: self._name = name self.verbose = verbose self.finish: 'Optional[float]' = None - self.start = time() + self.start = perf_counter() def capture(self) -> None: - self.finish = time() + self.finish = perf_counter() def elapsed(self) -> float: if self.finish: @@ -66,14 +66,14 @@ def capture(self, verbose: 'Optional[bool]' = None) -> 'TimerResult': print(r.str(self.dp), file=self.file, flush=True) return r - def summary(self, verbose: bool = False) -> 'Set[float]': - times = set() + def summary(self, verbose: bool = False) -> 'List[float]': + times = [] for r in self.results: if not r.finish: r.capture() if verbose: print(f' {r.str(self.dp)}', file=self.file) - times.add(r.elapsed()) + times.append(r.elapsed()) if times: from statistics import mean, stdev From 90337c7f463fb941b6aa561d0fd640638375b34b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 25 Nov 2022 11:37:17 +0000 Subject: [PATCH 14/36] Python 3.11 (#118) * add 3.11 * add check job * fix test swith 3.11 * fix docs build --- .github/workflows/ci.yml | 32 +++++++++++++++++++++----------- .gitignore | 4 +--- README.md | 2 +- devtools/ansi.py | 5 ++++- docs/install.md | 2 +- docs/requirements.txt | 2 +- pyproject.toml | 1 + tests/requirements.txt | 10 +++++----- 8 files changed, 35 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3980217..7cd8862 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: '3.10' @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos, windows] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] env: PYTHON: ${{ matrix.python-version }} @@ -40,10 +40,10 @@ jobs: runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -56,7 +56,7 @@ jobs: - run: coverage xml - - uses: codecov/codecov-action@v2.0.3 + - uses: codecov/codecov-action@v3 with: file: ./coverage.xml env_vars: EXTRAS,PYTHON,OS @@ -69,25 +69,35 @@ jobs: - run: coverage xml - - uses: codecov/codecov-action@v2.0.3 + - uses: codecov/codecov-action@v3 with: file: ./coverage.xml env_vars: EXTRAS,PYTHON,OS env: EXTRAS: no + # https://github.com/marketplace/actions/alls-green#why used for branch protection checks + check: + if: always() + needs: [test, lint] + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + deploy: needs: - - test - - lint + - check if: "success() && startsWith(github.ref, 'refs/tags/')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.10' diff --git a/.gitignore b/.gitignore index a0f90dd..c6e6fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .idea/ env/ -env35/ -env36/ -env37/ +env*/ *.py[cod] *.egg-info/ dist/ diff --git a/README.md b/README.md index 80218cc..6a78b47 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ pip install devtools[pygments] `pygments` is not required but if it's installed, output will be highlighted and easier to read. -`devtools` has no other required dependencies except python 3.7, 3.8, 3.9 or 3.10. +`devtools` has no other required dependencies except python 3.7, 3.8, 3.9, 3.10 or 3.11. If you've got python 3.7+ and `pip` installed, you're good to go. ## Usage diff --git a/devtools/ansi.py b/devtools/ansi.py index daa4c9d..e31a2d1 100644 --- a/devtools/ansi.py +++ b/devtools/ansi.py @@ -116,7 +116,10 @@ def __str__(self) -> str: if self == self.function: return repr(self) else: - return super().__str__() + # this matches `super().__str__()` in python 3.7 - 3.10 + # required since IntEnum.__str__ was changed in 3.11, + # see https://docs.python.org/3/library/enum.html#enum.IntEnum + return f'{self.__class__.__name__}.{self._name_}' def _style_as_int(v: 'Union[Style, int]') -> str: diff --git a/docs/install.md b/docs/install.md index 36cc597..94630bb 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,5 +6,5 @@ pip install devtools[pygments] `pygments` is not required but if it's installed, output will be highlighted and easier to read. -`devtools` has no other required dependencies except python 3.7, 3.8, 3.9 or 3.10. +`devtools` has no other required dependencies except python 3.7, 3.8, 3.9, 3.10 or 3.11. If you've got python 3.7+ and `pip` installed, you're good to go. diff --git a/docs/requirements.txt b/docs/requirements.txt index 540379d..3a7c1d2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ mkdocs-exclude==1.0.2 mkdocs-material==8.3.9 mkdocs-simple-hooks==0.1.5 markdown-include==0.7.0 -pygments==2.12.0 +pygments==2.13.0 diff --git a/pyproject.toml b/pyproject.toml index ebe9dd4..87bff20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Internet', 'Typing :: Typed', diff --git a/tests/requirements.txt b/tests/requirements.txt index bf6b169..cd9c6b2 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,8 @@ -coverage[toml]==6.4.2 -Pygments==2.12.0 -pytest==7.1.2 -pytest-mock==3.8.2 -pytest-sugar==0.9.5 +coverage[toml]==6.5.0 +Pygments==2.13.0 +pytest==7.2.0 +pytest-mock==3.10.0 +pytest-pretty==0.0.1 # these packages are used in tests so install the latest version pydantic asyncpg From 8811bfed6166284179a9b09a4a18390224bf61c8 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 28 Nov 2022 10:39:26 +0000 Subject: [PATCH 15/36] uprev --- devtools/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/version.py b/devtools/version.py index b9b28aa..ea301cb 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.9.0' +VERSION = '0.10.0' From abed0a58929311475d74f48d6080c948661e0879 Mon Sep 17 00:00:00 2001 From: Victor Naumov Date: Mon, 6 Feb 2023 17:22:45 +0100 Subject: [PATCH 16/36] added support for sqlalchemy2 (#120) * added support for sqlalchemy2 * added declarative_base support for sqlalchemy 2.0 * fixed sqlalchemy.exc.MovedIn20Warning. using sqlalchemy.orm.declarative_base * make check happy * simplified the sqlalchemy check * making black happy * added support of deferred fields * yet another sqlalchemy check * made mypy happier --------- Co-authored-by: victor naumov --- devtools/prettier.py | 13 ++++++++++++- devtools/utils.py | 12 +++++++++++- tests/test_prettier.py | 6 +++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/devtools/prettier.py b/devtools/prettier.py index bb5974c..fe01128 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -12,6 +12,11 @@ cache = lru_cache() +try: + from sqlalchemy import inspect as sa_inspect # type: ignore +except ImportError: + sa_inspect = None + __all__ = 'PrettyFormat', 'pformat', 'pprint' MYPY = False if MYPY: @@ -239,8 +244,14 @@ def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_ne self._format_fields(value, value.__dict__.items(), indent_current, indent_new) def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: + if sa_inspect is not None: + state = sa_inspect(value) + deferred = state.unloaded + else: + deferred = set() + fields = [ - (field, getattr(value, field)) + (field, getattr(value, field) if field not in deferred else "") for field in dir(value) if not (field.startswith('_') or field in ['metadata', 'registry']) ] diff --git a/devtools/utils.py b/devtools/utils.py index 994027e..c0ac1a3 100644 --- a/devtools/utils.py +++ b/devtools/utils.py @@ -149,13 +149,23 @@ class DataClassType(metaclass=MetaDataClassType): class MetaSQLAlchemyClassType(type): def __instancecheck__(self, instance: 'Any') -> bool: + try: + from sqlalchemy.orm import DeclarativeBase # type: ignore + except ImportError: + pass + else: + if isinstance(instance, DeclarativeBase): + return True + try: from sqlalchemy.ext.declarative import DeclarativeMeta # type: ignore except ImportError: - return False + pass else: return isinstance(instance.__class__, DeclarativeMeta) + return False + class SQLAlchemyClassType(metaclass=MetaSQLAlchemyClassType): pass diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 6dea882..657e1b7 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -28,7 +28,11 @@ try: from sqlalchemy import Column, Integer, String - from sqlalchemy.ext.declarative import declarative_base + try: + from sqlalchemy.orm import declarative_base + except ImportError: + from sqlalchemy.ext.declarative import declarative_base + SQLAlchemyBase = declarative_base() except ImportError: SQLAlchemyBase = None From 8f087b210bc41a0a02be689c582050a796e19867 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 12:41:16 +0100 Subject: [PATCH 17/36] switch to ruff (#124) * switch to ruff * revert tests/test_expr_render.py * fix pyproject.toml, etc. * switch to pinned dependencies * switch to 3.7 deps * skip some tests on 3.7, add pre-commit --- .github/workflows/ci.yml | 13 ++- .pre-commit-config.yaml | 25 +++++ HISTORY.md | 2 +- Makefile | 31 ++++-- README.md | 2 +- devtools/debug.py | 2 +- devtools/prettier.py | 6 +- devtools/utils.py | 6 +- docs/plugins.py | 2 +- docs/usage.md | 2 +- pyproject.toml | 19 ++-- requirements.txt | 3 - requirements/all.txt | 4 + requirements/docs.in | 7 ++ requirements/docs.txt | 67 ++++++++++++ requirements/linting.in | 5 + requirements/linting.txt | 34 +++++++ requirements/pyproject.txt | 12 +++ requirements/testing.in | 11 ++ requirements/testing.txt | 55 ++++++++++ tests/requirements-linting.txt | 6 -- tests/requirements.txt | 11 -- tests/test_custom_pretty.py | 5 +- tests/test_expr_render.py | 6 +- tests/test_main.py | 54 ++++------ tests/test_prettier.py | 181 ++++++++++++++------------------- 26 files changed, 378 insertions(+), 193 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 requirements.txt create mode 100644 requirements/all.txt create mode 100644 requirements/docs.in create mode 100644 requirements/docs.txt create mode 100644 requirements/linting.in create mode 100644 requirements/linting.txt create mode 100644 requirements/pyproject.txt create mode 100644 requirements/testing.in create mode 100644 requirements/testing.txt delete mode 100644 tests/requirements-linting.txt delete mode 100644 tests/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cd8862..ad2c060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,13 @@ jobs: with: python-version: '3.10' - - run: pip install -r tests/requirements-linting.txt - - run: pip install . + - run: pip install -r requirements/linting.txt -r requirements/pyproject.txt + + - run: mypy devtools - - run: make lint + - uses: pre-commit/action@v3.0.0 + with: + extra_args: --all-files --verbose test: name: test py${{ matrix.python-version }} on ${{ matrix.os }} @@ -47,7 +50,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - - run: pip install -r tests/requirements.txt + - run: pip install -r requirements/testing.txt -r requirements/pyproject.txt - run: pip install . - run: pip freeze @@ -102,7 +105,7 @@ jobs: python-version: '3.10' - name: install - run: make install + run: pip install build twine - name: build run: python -m build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..89b5ad4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: local + hooks: + - id: ruff + name: Ruff + entry: ruff + args: [--fix, --exit-non-zero-on-fix] + types: [python] + language: system + files: ^devtools/|^tests/ + - id: black + name: Black + entry: black + types: [python] + language: system + files: ^devtools/|^tests/ + exclude: test_expr_render.py diff --git a/HISTORY.md b/HISTORY.md index fcbebe3..2ebbb56 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,7 +14,7 @@ ## v0.7.0 (2021-09-03) -* switch to [`executing`](https://pypi.org/project/executing/) and [`asttokens`](https://pypi.org/project/asttokens/) +* switch to [`executing`](https://pypi.org/project/executing/) and [`asttokens`](https://pypi.org/project/asttokens/) for finding and printing debug arguments, #82, thanks @alexmojaki * correct changelog links, #76, thanks @Cielquan * return `debug()` arguments, #87 diff --git a/Makefile b/Makefile index 15225e9..3a464eb 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,36 @@ .DEFAULT_GOAL := all -isort = isort devtools tests docs/plugins.py -black = black -S -l 120 --target-version py37 devtools docs/plugins.py +sources = devtools tests docs/plugins.py .PHONY: install install: - python -m pip install -U setuptools pip wheel twine build - pip install -U -r requirements.txt + python -m pip install -U pip pre-commit + pip install -U -r requirements/all.txt pip install -e . + pre-commit install + +.PHONY: refresh-lockfiles +refresh-lockfiles: + find requirements/ -name '*.txt' ! -name 'all.txt' -type f -delete + make update-lockfiles + +.PHONY: update-lockfiles +update-lockfiles: + @echo "Updating requirements/*.txt files using pip-compile" + pip-compile -q --resolver backtracking -o requirements/linting.txt requirements/linting.in + pip-compile -q --resolver backtracking -o requirements/testing.txt requirements/testing.in + pip-compile -q --resolver backtracking -o requirements/docs.txt requirements/docs.in + pip-compile -q --resolver backtracking -o requirements/pyproject.txt pyproject.toml + pip install --dry-run -r requirements/all.txt .PHONY: format format: - $(isort) - $(black) + black $(sources) + ruff $(sources) --fix --exit-zero .PHONY: lint lint: - flake8 --max-complexity 10 --max-line-length 120 --ignore E203,W503 devtools tests docs/plugins.py - $(isort) --check-only --df - $(black) --check --diff + black $(sources) --check --diff + ruff $(sources) mypy devtools .PHONY: test diff --git a/README.md b/README.md index 6a78b47..55e69e0 100644 --- a/README.md +++ b/README.md @@ -64,5 +64,5 @@ outputs: devtools can be used without `from devtools import debug` if you add `debug` into `__builtins__` in `sitecustomize.py`. -For instructions on adding `debug` to `__builtins__`, +For instructions on adding `debug` to `__builtins__`, see the [installation docs](https://python-devtools.helpmanual.io/usage/#usage-without-import). diff --git a/devtools/debug.py b/devtools/debug.py index 657859c..5ea836a 100644 --- a/devtools/debug.py +++ b/devtools/debug.py @@ -178,7 +178,7 @@ def _process(self, args: 'Any', kwargs: 'Any') -> DebugOutput: ex = source.executing(call_frame) function = ex.code_qualname() if not ex.node: - warning = "executing failed to find the calling node" + warning = 'executing failed to find the calling node' arguments = list(self._args_inspection_failed(args, kwargs)) else: arguments = list(self._process_args(ex, args, kwargs)) diff --git a/devtools/prettier.py b/devtools/prettier.py index fe01128..7489d33 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -13,9 +13,9 @@ cache = lru_cache() try: - from sqlalchemy import inspect as sa_inspect # type: ignore + from sqlalchemy import inspect as sa_inspect except ImportError: - sa_inspect = None + sa_inspect = None # type: ignore[assignment] __all__ = 'PrettyFormat', 'pformat', 'pprint' MYPY = False @@ -251,7 +251,7 @@ def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, in deferred = set() fields = [ - (field, getattr(value, field) if field not in deferred else "") + (field, getattr(value, field) if field not in deferred else '') for field in dir(value) if not (field.startswith('_') or field in ['metadata', 'registry']) ] diff --git a/devtools/utils.py b/devtools/utils.py index c0ac1a3..2a96765 100644 --- a/devtools/utils.py +++ b/devtools/utils.py @@ -93,7 +93,7 @@ def _set_conout_mode(new_mode, mask=0xFFFFFFFF): mode = mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING try: _set_conout_mode(mode, mask) - except WindowsError as e: # type: ignore + except OSError as e: if e.winerror == ERROR_INVALID_PARAMETER: return False raise @@ -150,7 +150,7 @@ class DataClassType(metaclass=MetaDataClassType): class MetaSQLAlchemyClassType(type): def __instancecheck__(self, instance: 'Any') -> bool: try: - from sqlalchemy.orm import DeclarativeBase # type: ignore + from sqlalchemy.orm import DeclarativeBase except ImportError: pass else: @@ -158,7 +158,7 @@ def __instancecheck__(self, instance: 'Any') -> bool: return True try: - from sqlalchemy.ext.declarative import DeclarativeMeta # type: ignore + from sqlalchemy.ext.declarative import DeclarativeMeta except ImportError: pass else: diff --git a/docs/plugins.py b/docs/plugins.py index aa46876..5f40bf1 100755 --- a/docs/plugins.py +++ b/docs/plugins.py @@ -55,7 +55,7 @@ def gen_examples_html(m: re.Match) -> str: conv = Ansi2HTMLConverter() name = THIS_DIR / Path(m.group(1)) - logger.info("running %s to generate HTML...", name) + logger.info('running %s to generate HTML...', name) p = subprocess.run((sys.executable, str(name)), stdout=subprocess.PIPE, check=True) html = conv.convert(p.stdout.decode(), full=False).strip('\r\n') html = html.replace('docs/build/../examples/', '') diff --git a/docs/usage.md b/docs/usage.md index 68bb6cb..0388a2b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -96,7 +96,7 @@ Two ways to do this: ### Automatic install !!! warning - This is experimental, please [create an issue](https://github.com/samuelcolvin/python-devtools/issues) + This is experimental, please [create an issue](https://github.com/samuelcolvin/python-devtools/issues) if you encounter any problems. To install `debug` into `__builtins__` automatically, run: diff --git a/pyproject.toml b/pyproject.toml index 87bff20..193aabc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,18 +71,21 @@ exclude_lines = [ [tool.black] color = true line-length = 120 -target-version = ['py37', 'py38', 'py39', 'py310'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] skip-string-normalization = true +extend-exclude = ['tests/test_expr_render.py'] -[tool.isort] -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -combine_as_imports = true -color_output = true +[tool.ruff] +line-length = 120 +exclude = ['cases_update'] +extend-select = ['Q', 'RUF100', 'C90', 'UP', 'I'] +flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'} +mccabe = { max-complexity = 14 } +isort = { known-first-party = ['devtools'] } +target-version = 'py37' [tool.mypy] +show_error_codes = true strict = true warn_return_any = false diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 357dec2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ --r docs/requirements.txt --r tests/requirements-linting.txt --r tests/requirements.txt diff --git a/requirements/all.txt b/requirements/all.txt new file mode 100644 index 0000000..3e6af75 --- /dev/null +++ b/requirements/all.txt @@ -0,0 +1,4 @@ +-r ./docs.txt +-r ./linting.txt +-r ./testing.txt +-r ./pyproject.txt diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 0000000..f4c60e6 --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,7 @@ +ansi2html==1.8.0 +mkdocs==1.3.1 +mkdocs-exclude==1.0.2 +mkdocs-material==8.3.9 +mkdocs-simple-hooks==0.1.5 +markdown-include==0.7.0 +pygments diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..d39c76c --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,67 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements/docs.txt --resolver=backtracking requirements/docs.in +# +ansi2html==1.8.0 + # via -r requirements/docs.in +click==8.1.3 + # via mkdocs +ghp-import==2.1.0 + # via mkdocs +importlib-metadata==6.1.0 + # via mkdocs +jinja2==3.1.2 + # via + # mkdocs + # mkdocs-material +markdown==3.3.7 + # via + # markdown-include + # mkdocs + # mkdocs-material + # pymdown-extensions +markdown-include==0.7.0 + # via -r requirements/docs.in +markupsafe==2.1.2 + # via jinja2 +mergedeep==1.3.4 + # via mkdocs +mkdocs==1.3.1 + # via + # -r requirements/docs.in + # mkdocs-exclude + # mkdocs-material + # mkdocs-simple-hooks +mkdocs-exclude==1.0.2 + # via -r requirements/docs.in +mkdocs-material==8.3.9 + # via -r requirements/docs.in +mkdocs-material-extensions==1.1.1 + # via mkdocs-material +mkdocs-simple-hooks==0.1.5 + # via -r requirements/docs.in +packaging==23.0 + # via mkdocs +pygments==2.14.0 + # via + # -r requirements/docs.in + # mkdocs-material +pymdown-extensions==9.10 + # via mkdocs-material +python-dateutil==2.8.2 + # via ghp-import +pyyaml==6.0 + # via + # mkdocs + # pymdown-extensions + # pyyaml-env-tag +pyyaml-env-tag==0.1 + # via mkdocs +six==1.16.0 + # via python-dateutil +watchdog==3.0.0 + # via mkdocs +zipp==3.15.0 + # via importlib-metadata diff --git a/requirements/linting.in b/requirements/linting.in new file mode 100644 index 0000000..44df076 --- /dev/null +++ b/requirements/linting.in @@ -0,0 +1,5 @@ +black +mypy==0.971 +ruff +# required so mypy can find stubs +sqlalchemy diff --git a/requirements/linting.txt b/requirements/linting.txt new file mode 100644 index 0000000..92279cd --- /dev/null +++ b/requirements/linting.txt @@ -0,0 +1,34 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements/linting.txt --resolver=backtracking requirements/linting.in +# +black==23.3.0 + # via -r requirements/linting.in +click==8.1.3 + # via black +mypy==0.971 + # via -r requirements/linting.in +mypy-extensions==1.0.0 + # via + # black + # mypy +packaging==23.0 + # via black +pathspec==0.11.1 + # via black +platformdirs==3.2.0 + # via black +ruff==0.0.261 + # via -r requirements/linting.in +sqlalchemy==2.0.8 + # via -r requirements/linting.in +tomli==2.0.1 + # via + # black + # mypy +typing-extensions==4.5.0 + # via + # mypy + # sqlalchemy diff --git a/requirements/pyproject.txt b/requirements/pyproject.txt new file mode 100644 index 0000000..d385c38 --- /dev/null +++ b/requirements/pyproject.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements/pyproject.txt --resolver=backtracking pyproject.toml +# +asttokens==2.2.1 + # via devtools (pyproject.toml) +executing==1.2.0 + # via devtools (pyproject.toml) +six==1.16.0 + # via asttokens diff --git a/requirements/testing.in b/requirements/testing.in new file mode 100644 index 0000000..7b1ad14 --- /dev/null +++ b/requirements/testing.in @@ -0,0 +1,11 @@ +coverage[toml] +pygments +pytest +pytest-mock +pytest-pretty +# these packages are used in tests so install the latest version +pydantic +asyncpg +numpy; python_version>='3.8' +multidict; python_version>='3.8' +sqlalchemy diff --git a/requirements/testing.txt b/requirements/testing.txt new file mode 100644 index 0000000..b62eaba --- /dev/null +++ b/requirements/testing.txt @@ -0,0 +1,55 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in +# +asyncpg==0.27.0 + # via -r requirements/testing.in +attrs==22.2.0 + # via pytest +coverage[toml]==7.2.2 + # via -r requirements/testing.in +exceptiongroup==1.1.1 + # via pytest +iniconfig==2.0.0 + # via pytest +markdown-it-py==2.2.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +multidict==6.0.4 ; python_version >= "3.8" + # via -r requirements/testing.in +numpy==1.24.2 ; python_version >= "3.8" + # via -r requirements/testing.in +packaging==23.0 + # via pytest +pluggy==1.0.0 + # via pytest +pydantic==1.10.7 + # via -r requirements/testing.in +pygments==2.14.0 + # via + # -r requirements/testing.in + # rich +pytest==7.2.2 + # via + # -r requirements/testing.in + # pytest-mock + # pytest-pretty +pytest-mock==3.10.0 + # via -r requirements/testing.in +pytest-pretty==1.1.1 + # via -r requirements/testing.in +rich==13.3.3 + # via pytest-pretty +sqlalchemy==2.0.8 + # via -r requirements/testing.in +tomli==2.0.1 + # via + # coverage + # pytest +typing-extensions==4.5.0 + # via + # pydantic + # sqlalchemy diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt deleted file mode 100644 index b84d0b2..0000000 --- a/tests/requirements-linting.txt +++ /dev/null @@ -1,6 +0,0 @@ -black==22.6.0 -flake8==4.0.1 -isort[colors]==5.10.1 -mypy==0.971 -pycodestyle==2.8.0 -pyflakes==2.4.0 diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index cd9c6b2..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -coverage[toml]==6.5.0 -Pygments==2.13.0 -pytest==7.2.0 -pytest-mock==3.10.0 -pytest-pretty==0.0.1 -# these packages are used in tests so install the latest version -pydantic -asyncpg -numpy -multidict -sqlalchemy diff --git a/tests/test_custom_pretty.py b/tests/test_custom_pretty.py index d63392b..552e5d3 100644 --- a/tests/test_custom_pretty.py +++ b/tests/test_custom_pretty.py @@ -23,12 +23,15 @@ def __pretty__(self, fmt, **kwargs): my_cls = CustomCls() v = pformat(my_cls) - assert v == """\ + assert ( + v + == """\ Thing( [], [0], [0, 1], )""" + ) def test_skip(): diff --git a/tests/test_expr_render.py b/tests/test_expr_render.py index 6b30bad..eed9469 100644 --- a/tests/test_expr_render.py +++ b/tests/test_expr_render.py @@ -48,14 +48,14 @@ def test_exotic_types(): (a for a in aa), ) s = normalise_output(str(v)) - print('\n---\n{}\n---'.format(v)) + print(f'\n---\n{v}\n---') # Generator expression source changed in 3.8 to include parentheses, see: # https://github.com/gristlabs/asttokens/pull/50 # https://bugs.python.org/issue31241 - genexpr_source = "a for a in aa" + genexpr_source = 'a for a in aa' if sys.version_info[:2] > (3, 7): - genexpr_source = f"({genexpr_source})" + genexpr_source = f'({genexpr_source})' assert ( "tests/test_expr_render.py: test_exotic_types\n" diff --git a/tests/test_main.py b/tests/test_main.py index f0cce7d..1057313 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,7 +2,7 @@ import sys from collections.abc import Generator from pathlib import Path -from subprocess import PIPE, run +from subprocess import run import pytest @@ -19,9 +19,7 @@ def test_print(capsys): stdout, stderr = capsys.readouterr() print(stdout) assert normalise_output(stdout) == ( - 'tests/test_main.py: test_print\n' - ' a: 1 (int)\n' - ' b: 2 (int)\n' + 'tests/test_main.py: test_print\n' ' a: 1 (int)\n' ' b: 2 (int)\n' ) assert stderr == '' assert result == (1, 2) @@ -64,7 +62,7 @@ def test_print_generator(capsys): def test_format(): a = b'i might bite' - b = "hello this is a test" + b = 'hello this is a test' v = debug.format(a, b) s = normalise_output(str(v)) print(s) @@ -81,7 +79,8 @@ def test_format(): ) def test_print_subprocess(tmpdir): f = tmpdir.join('test.py') - f.write("""\ + f.write( + """\ from devtools import debug def test_func(v): @@ -92,9 +91,10 @@ def test_func(v): debug(foobar) test_func(42) print('debug run.') - """) + """ + ) env = {'PYTHONPATH': str(Path(__file__).parent.parent.resolve())} - p = run([sys.executable, str(f)], stdout=PIPE, stderr=PIPE, universal_newlines=True, env=env) + p = run([sys.executable, str(f)], capture_output=True, text=True, env=env) assert p.stderr == '' assert p.returncode == 0, (p.stderr, p.stdout) assert p.stdout.replace(str(f), '/path/to/test.py') == ( @@ -113,10 +113,10 @@ def test_odd_path(mocker): mocked_relative_to = mocker.patch('pathlib.Path.relative_to') mocked_relative_to.side_effect = ValueError() v = debug.format('test') - if sys.platform == "win32": - pattern = r"\w:\\.*?\\" + if sys.platform == 'win32': + pattern = r'\w:\\.*?\\' else: - pattern = r"/.*?/" + pattern = r'/.*?/' pattern += r"test_main.py:\d{2,} test_odd_path\n 'test' \(str\) len=4" assert re.search(pattern, str(v)), v @@ -129,10 +129,7 @@ def test_small_call_frame(): 3, ) assert normalise_output(str(v)) == ( - 'tests/test_main.py: test_small_call_frame\n' - ' 1 (int)\n' - ' 2 (int)\n' - ' 3 (int)' + 'tests/test_main.py: test_small_call_frame\n' ' 1 (int)\n' ' 2 (int)\n' ' 3 (int)' ) @@ -143,12 +140,9 @@ def test_small_call_frame_warning(): 2, 3, ) - print('\n---\n{}\n---'.format(v)) + print(f'\n---\n{v}\n---') assert normalise_output(str(v)) == ( - 'tests/test_main.py: test_small_call_frame_warning\n' - ' 1 (int)\n' - ' 2 (int)\n' - ' 3 (int)' + 'tests/test_main.py: test_small_call_frame_warning\n' ' 1 (int)\n' ' 2 (int)\n' ' 3 (int)' ) @@ -171,7 +165,7 @@ def test_kwargs_orderless(): v = debug.format(first=a, second='literal') s = normalise_output(str(v)) assert set(s.split('\n')) == { - "tests/test_main.py: test_kwargs_orderless", + 'tests/test_main.py: test_kwargs_orderless', " first: 'variable' (str) len=8 variable=a", " second: 'literal' (str) len=7", } @@ -181,10 +175,7 @@ def test_simple_vars(): v = debug.format('test', 1, 2) s = normalise_output(str(v)) assert s == ( - "tests/test_main.py: test_simple_vars\n" - " 'test' (str) len=4\n" - " 1 (int)\n" - " 2 (int)" + "tests/test_main.py: test_simple_vars\n" " 'test' (str) len=4\n" " 1 (int)\n" " 2 (int)" ) r = normalise_output(repr(v)) assert r == ( @@ -222,18 +213,14 @@ def test_eval_kwargs(): v = eval('debug.format(1, apple="pear")') assert set(str(v).split('\n')) == { - ":1 (no code context for debug call, code inspection impossible)", - " 1 (int)", + ':1 (no code context for debug call, code inspection impossible)', + ' 1 (int)', " apple: 'pear' (str) len=4", } def test_exec(capsys): - exec( - 'a = 1\n' - 'b = 2\n' - 'debug(b, a + b)' - ) + exec('a = 1\n' 'b = 2\n' 'debug(b, a + b)') stdout, stderr = capsys.readouterr() assert stdout == ( @@ -314,8 +301,7 @@ def test_multiple_debugs(): v = debug.format([i * 2 for i in range(2)]) s = normalise_output(str(v)) assert s == ( - 'tests/test_main.py: test_multiple_debugs\n' - ' [i * 2 for i in range(2)]: [0, 2] (list) len=2' + 'tests/test_main.py: test_multiple_debugs\n' ' [i * 2 for i in range(2)]: [0, 2] (list) len=2' ) diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 657e1b7..364447b 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -28,6 +28,7 @@ try: from sqlalchemy import Column, Integer, String + try: from sqlalchemy.orm import declarative_base except ImportError: @@ -41,21 +42,13 @@ def test_dict(): v = pformat({1: 2, 3: 4}) print(v) - assert v == ( - '{\n' - ' 1: 2,\n' - ' 3: 4,\n' - '}') + assert v == ('{\n' ' 1: 2,\n' ' 3: 4,\n' '}') def test_print(capsys): pprint({1: 2, 3: 4}) stdout, stderr = capsys.readouterr() - assert strip_ansi(stdout) == ( - '{\n' - ' 1: 2,\n' - ' 3: 4,\n' - '}\n') + assert strip_ansi(stdout) == ('{\n' ' 1: 2,\n' ' 3: 4,\n' '}\n') assert stderr == '' @@ -68,64 +61,33 @@ def test_colours(): def test_list(): v = pformat(list(range(6))) - assert v == ( - '[\n' - ' 0,\n' - ' 1,\n' - ' 2,\n' - ' 3,\n' - ' 4,\n' - ' 5,\n' - ']') + assert v == ('[\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' ' 5,\n' ']') def test_set(): v = pformat(set(range(5))) - assert v == ( - '{\n' - ' 0,\n' - ' 1,\n' - ' 2,\n' - ' 3,\n' - ' 4,\n' - '}') + assert v == ('{\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' '}') def test_tuple(): v = pformat(tuple(range(5))) - assert v == ( - '(\n' - ' 0,\n' - ' 1,\n' - ' 2,\n' - ' 3,\n' - ' 4,\n' - ')') + assert v == ('(\n' ' 0,\n' ' 1,\n' ' 2,\n' ' 3,\n' ' 4,\n' ')') def test_generator(): - v = pformat((i for i in range(3))) - assert v == ( - '(\n' - ' 0,\n' - ' 1,\n' - ' 2,\n' - ')') + v = pformat(i for i in range(3)) + assert v == ('(\n' ' 0,\n' ' 1,\n' ' 2,\n' ')') def test_named_tuple(): f = namedtuple('Foobar', ['foo', 'bar', 'spam']) v = pformat(f('x', 'y', 1)) - assert v == ("Foobar(\n" - " foo='x',\n" - " bar='y',\n" - " spam=1,\n" - ")") + assert v == ("Foobar(\n" " foo='x',\n" " bar='y',\n" " spam=1,\n" ")") def test_generator_no_yield(): pformat_ = PrettyFormat(yield_from_generators=False) - v = pformat_((i for i in range(3))) + v = pformat_(i for i in range(3)) assert v.startswith('. at ') @@ -157,7 +119,9 @@ def test_str_repr(): def test_bytes(): pformat_ = PrettyFormat(width=12) v = pformat_(string.ascii_lowercase.encode()) - assert v == """( + assert ( + v + == """( b'abcde' b'fghij' b'klmno' @@ -165,6 +129,7 @@ def test_bytes(): b'uvwxy' b'z' )""" + ) def test_short_bytes(): @@ -174,40 +139,52 @@ def test_short_bytes(): def test_bytearray(): pformat_ = PrettyFormat(width=18) v = pformat_(bytearray(string.ascii_lowercase.encode())) - assert v == """\ + assert ( + v + == """\ bytearray( b'abcdefghijk' b'lmnopqrstuv' b'wxyz' )""" + ) def test_bytearray_short(): v = pformat(bytearray(b'boo')) - assert v == """\ + assert ( + v + == """\ bytearray( b'boo' )""" + ) def test_map(): v = pformat(map(str.strip, ['x', 'y ', ' z'])) - assert v == """\ + assert ( + v + == """\ map( 'x', 'y', 'z', )""" + ) def test_filter(): v = pformat(filter(None, [1, 2, False, 3])) - assert v == """\ + assert ( + v + == """\ filter( 1, 2, 3, )""" + ) def test_counter(): @@ -216,11 +193,14 @@ def test_counter(): c['x'] += 1 c['y'] += 1 v = pformat(c) - assert v == """\ + assert ( + v + == """\ """ + ) def test_dataclass(): @@ -232,7 +212,9 @@ class FooDataclass: f = FooDataclass(123, [1, 2, 3, 4]) v = pformat(f) print(v) - assert v == """\ + assert ( + v + == """\ FooDataclass( x=123, y=[ @@ -242,6 +224,7 @@ class FooDataclass: 4, ], )""" + ) def test_nested_dataclasses(): @@ -258,68 +241,78 @@ class BarDataclass: b = BarDataclass(10.0, f) v = pformat(b) print(v) - assert v == """\ + assert ( + v + == """\ BarDataclass( a=10.0, b=FooDataclass( x=123, ), )""" + ) @pytest.mark.skipif(numpy is None, reason='numpy not installed') def test_indent_numpy(): v = pformat({'numpy test': numpy.array(range(20))}) - assert v == """{ + assert ( + v + == """{ 'numpy test': ( array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) ), }""" + ) @pytest.mark.skipif(numpy is None, reason='numpy not installed') def test_indent_numpy_short(): v = pformat({'numpy test': numpy.array(range(10))}) - assert v == """{ + assert ( + v + == """{ 'numpy test': array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), }""" + ) def test_ordered_dict(): v = pformat(OrderedDict([(1, 2), (3, 4), (5, 6)])) print(v) - assert v == """\ + assert ( + v + == """\ OrderedDict([ (1, 2), (3, 4), (5, 6), ])""" + ) def test_frozenset(): v = pformat(frozenset(range(3))) print(v) - assert v == """\ + assert ( + v + == """\ frozenset({ 0, 1, 2, })""" + ) def test_deep_objects(): f = namedtuple('Foobar', ['foo', 'bar', 'spam']) - v = pformat(( - ( - f('x', 'y', OrderedDict([(1, 2), (3, 4), (5, 6)])), - frozenset(range(3)), - [1, 2, {1: 2}] - ), - {1, 2, 3} - )) + v = pformat(((f('x', 'y', OrderedDict([(1, 2), (3, 4), (5, 6)])), frozenset(range(3)), [1, 2, {1: 2}]), {1, 2, 3})) print(v) - assert v == """\ + assert ( + v + == """\ ( ( Foobar( @@ -344,6 +337,7 @@ def test_deep_objects(): ), {1, 2, 3}, )""" + ) def test_call_args(): @@ -351,11 +345,14 @@ def test_call_args(): m(1, 2, 3, a=4) v = pformat(m.call_args) - assert v == """\ + assert ( + v + == """\ _Call( _fields=(1, 2, 3), {'a': 4}, )""" + ) @pytest.mark.skipif(MultiDict is None, reason='MultiDict not installed') @@ -364,11 +361,11 @@ def test_multidict(): d.add('b', 3) v = pformat(d) assert set(v.split('\n')) == { - "", + '})>', } @@ -376,10 +373,10 @@ def test_multidict(): def test_cimultidict(): v = pformat(CIMultiDict({'a': 1, 'b': 2})) assert set(v.split('\n')) == { - "", + '})>', } @@ -399,21 +396,11 @@ def __init__(self): def test_dir(): - assert pformat(vars(Foo())) == ( - "{\n" - " 'b': 2,\n" - " 'c': 3,\n" - "}" - ) + assert pformat(vars(Foo())) == ("{\n" " 'b': 2,\n" " 'c': 3,\n" "}") def test_instance_dict(): - assert pformat(Foo().__dict__) == ( - "{\n" - " 'b': 2,\n" - " 'c': 3,\n" - "}" - ) + assert pformat(Foo().__dict__) == ("{\n" " 'b': 2,\n" " 'c': 3,\n" "}") def test_class_dict(): @@ -434,25 +421,14 @@ def items(self): def __getitem__(self, item): return self._d[item] - assert pformat(Dictlike()) == ( - "" - ) + assert pformat(Dictlike()) == ("") @pytest.mark.skipif(Record is None, reason='asyncpg not installed') def test_asyncpg_record(): r = Record({'a': 0, 'b': 1}, (41, 42)) assert dict(r) == {'a': 41, 'b': 42} - assert pformat(r) == ( - "" - ) + assert pformat(r) == ("") def test_dict_type(): @@ -467,11 +443,12 @@ class User(SQLAlchemyBase): name = Column(String) fullname = Column(String) nickname = Column(String) + user = User() user.id = 1 - user.name = "Test" - user.fullname = "Test For SQLAlchemy" - user.nickname = "test" + user.name = 'Test' + user.fullname = 'Test For SQLAlchemy' + user.nickname = 'test' assert pformat(user) == ( "User(\n" " fullname='Test For SQLAlchemy',\n" From 3d5ff6597fe3fc176dfc83062a9cf4e1203cb83a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 12:45:32 +0100 Subject: [PATCH 18/36] update licence and history --- HISTORY.md | 9 +++++++++ LICENSE | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 2ebbb56..1aa231e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,12 @@ +## v0.10.0 (2022-11-28) + +* Use secure builtins standard module, instead of the `__builtins__` by @0xsirsaif in #109 +* upgrade executing to fix 3.10 by @samuelcolvin in #110 +* Fix windows build by @samuelcolvin in #111 +* Allow executing dependency to be >1.0.0 by @staticf0x in #115 +* more precise timer summary by @banteg in #113 +* Python 3.11 by @samuelcolvin in #118 + ## v0.9.0 (2022-07-26) * fix format of nested dataclasses, #99 thanks @aliereno diff --git a/LICENSE b/LICENSE index 3338ce9..bdd9b15 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Samuel Colvin +Copyright (c) 2017 to present Samuel Colvin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f0e0fb2b139e1980c87363a754b6a42bb1a9fbc4 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 13:48:42 +0100 Subject: [PATCH 19/36] support displaying ast types (#125) * support displaying ast types * support 3.7 & 3.8 --- devtools/prettier.py | 13 +++++++++++++ requirements/testing.in | 4 +++- requirements/testing.txt | 2 +- tests/test_prettier.py | 25 +++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/devtools/prettier.py b/devtools/prettier.py index 7489d33..e378030 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -1,3 +1,4 @@ +import ast import io import os from collections import OrderedDict @@ -80,6 +81,7 @@ def __init__( (bytearray, self._format_bytearray), (generator_types, self._format_generator), # put these last as the check can be slow + (ast.AST, self._format_ast_expression), (LaxMapping, self._format_dict), (DataClassType, self._format_dataclass), (SQLAlchemyClassType, self._format_sqlalchemy_class), @@ -240,6 +242,17 @@ def _format_bytearray(self, value: 'Any', _: str, indent_current: int, indent_ne lines = self._wrap_lines(bytes(value), indent_new) self._str_lines(lines, indent_current, indent_new) + def _format_ast_expression(self, value: ast.AST, _: str, indent_current: int, indent_new: int) -> None: + try: + s = ast.dump(value, indent=self._indent_step) + except TypeError: + # no indent before 3.9 + s = ast.dump(value) + lines = s.splitlines(True) + self._stream.write(lines[0]) + for line in lines[1:]: + self._stream.write(indent_current * self._c + line) + def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: self._format_fields(value, value.__dict__.items(), indent_current, indent_new) diff --git a/requirements/testing.in b/requirements/testing.in index 7b1ad14..1976d79 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -5,7 +5,9 @@ pytest-mock pytest-pretty # these packages are used in tests so install the latest version pydantic -asyncpg +# no binaries for 3.7 +asyncpg; python_version>='3.8' +# no version is compatible with 3.7 and 3.11 numpy; python_version>='3.8' multidict; python_version>='3.8' sqlalchemy diff --git a/requirements/testing.txt b/requirements/testing.txt index b62eaba..2ce58bf 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in # -asyncpg==0.27.0 +asyncpg==0.27.0 ; python_version >= "3.8" # via -r requirements/testing.in attrs==22.2.0 # via pytest diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 364447b..6a0247c 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -1,5 +1,7 @@ +import ast import os import string +import sys from collections import Counter, OrderedDict, namedtuple from dataclasses import dataclass from typing import List @@ -457,3 +459,26 @@ class User(SQLAlchemyBase): " nickname='test',\n" ")" ) + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='no indent on older versions') +def test_ast_expr(): + assert pformat(ast.parse('print(1, 2, round(3))', mode='eval')) == ( + "Expression(" + "\n body=Call(" + "\n func=Name(id='print', ctx=Load())," + "\n args=[" + "\n Constant(value=1)," + "\n Constant(value=2)," + "\n Call(" + "\n func=Name(id='round', ctx=Load())," + "\n args=[" + "\n Constant(value=3)]," + "\n keywords=[])]," + "\n keywords=[]))" + ) + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='no indent on older versions') +def test_ast_module(): + assert pformat(ast.parse('print(1, 2, round(3))')).startswith('Module(\n body=[') From 61c6b67472f7a0e818968ab73dcd56762f91d419 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 18:59:07 +0100 Subject: [PATCH 20/36] Insert assert (#126) * support displaying ast types * support 3.7 & 3.8 * skip tests on older python * add insert_assert pytest fixture * use newest pytest-pretty * try to fix CI * fix mypy and black * add pytest to for mypy * fix mypy * change code to install debug in fixture * tweak install instructions --- .github/workflows/ci.yml | 5 +- devtools/__main__.py | 27 ++-- devtools/prettier.py | 6 +- devtools/pytest_plugin.py | 301 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +- requirements/linting.in | 1 + requirements/linting.txt | 15 +- requirements/testing.in | 5 +- requirements/testing.txt | 17 +- tests/conftest.py | 2 + tests/test_insert_assert.py | 169 ++++++++++++++++++++ 11 files changed, 531 insertions(+), 22 deletions(-) create mode 100644 devtools/pytest_plugin.py create mode 100644 tests/test_insert_assert.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad2c060..e7b81b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: - '**' pull_request: {} +env: + COLUMNS: 150 + jobs: lint: runs-on: ubuntu-latest @@ -51,7 +54,7 @@ jobs: python-version: ${{ matrix.python-version }} - run: pip install -r requirements/testing.txt -r requirements/pyproject.txt - - run: pip install . + - run: pip freeze - name: test with extras diff --git a/devtools/__main__.py b/devtools/__main__.py index 726f825..bfc6155 100644 --- a/devtools/__main__.py +++ b/devtools/__main__.py @@ -1,5 +1,4 @@ import builtins -import os import sys from pathlib import Path @@ -8,13 +7,17 @@ # language=python install_code = """ # add devtools `debug` function to builtins -import builtins -try: - from devtools import debug -except ImportError: - pass -else: - setattr(builtins, 'debug', debug) +import sys +# we don't install here for pytest as it breaks pytest, it is +# installed later by a pytest fixture +if not sys.argv[0].endswith('pytest'): + import builtins + try: + from devtools import debug + except ImportError: + pass + else: + setattr(builtins, 'debug', debug) """ @@ -47,11 +50,11 @@ def install() -> int: print(f'Found path "{install_path}" to install devtools into __builtins__') print('To install devtools, run the following command:\n') - if os.access(install_path, os.W_OK): - print(f' python -m devtools print-code >> {install_path}\n') - else: + print(f' python -m devtools print-code >> {install_path}\n') + if not install_path.is_relative_to(Path.home()): + print('or maybe\n') print(f' python -m devtools print-code | sudo tee -a {install_path} > /dev/null\n') - print('Note: "sudo" is required because the path is not writable by the current user.') + print('Note: "sudo" might be required because the path is in your home directory.') return 0 diff --git a/devtools/prettier.py b/devtools/prettier.py index e378030..4f274de 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -44,9 +44,9 @@ class SkipPretty(Exception): @cache def get_pygments() -> 'Tuple[Any, Any, Any]': try: - import pygments # type: ignore - from pygments.formatters import Terminal256Formatter # type: ignore - from pygments.lexers import PythonLexer # type: ignore + import pygments + from pygments.formatters import Terminal256Formatter + from pygments.lexers import PythonLexer except ImportError: # pragma: no cover return None, None, None else: diff --git a/devtools/pytest_plugin.py b/devtools/pytest_plugin.py new file mode 100644 index 0000000..f80efd3 --- /dev/null +++ b/devtools/pytest_plugin.py @@ -0,0 +1,301 @@ +from __future__ import annotations as _annotations + +import ast +import builtins +import sys +import textwrap +from contextvars import ContextVar +from dataclasses import dataclass +from enum import Enum +from functools import lru_cache +from itertools import groupby +from pathlib import Path +from types import FrameType +from typing import TYPE_CHECKING, Any, Callable, Generator, Sized + +import pytest +from executing import Source + +from . import debug + +if TYPE_CHECKING: + pass + +__all__ = ('insert_assert',) + + +@dataclass +class ToReplace: + file: Path + start_line: int + end_line: int | None + code: str + + +to_replace: list[ToReplace] = [] +insert_assert_calls: ContextVar[int] = ContextVar('insert_assert_calls', default=0) +insert_assert_summary: ContextVar[list[str]] = ContextVar('insert_assert_summary') + + +def insert_assert(value: Any) -> int: + call_frame: FrameType = sys._getframe(1) + if sys.version_info < (3, 8): # pragma: no cover + raise RuntimeError('insert_assert() requires Python 3.8+') + + format_code = load_black() + ex = Source.for_frame(call_frame).executing(call_frame) + if ex.node is None: # pragma: no cover + python_code = format_code(str(custom_repr(value))) + raise RuntimeError( + f'insert_assert() was unable to find the frame from which it was called, called with:\n{python_code}' + ) + ast_arg = ex.node.args[0] # type: ignore[attr-defined] + if isinstance(ast_arg, ast.Name): + arg = ast_arg.id + else: + arg = ' '.join(map(str.strip, ex.source.asttokens().get_text(ast_arg).splitlines())) + + python_code = format_code(f'# insert_assert({arg})\nassert {arg} == {custom_repr(value)}') + + python_code = textwrap.indent(python_code, ex.node.col_offset * ' ') + to_replace.append(ToReplace(Path(call_frame.f_code.co_filename), ex.node.lineno, ex.node.end_lineno, python_code)) + calls = insert_assert_calls.get() + 1 + insert_assert_calls.set(calls) + return calls + + +def pytest_addoption(parser: Any) -> None: + parser.addoption( + '--insert-assert-print', + action='store_true', + default=False, + help='Print statements that would be substituted for insert_assert(), instead of writing to files', + ) + parser.addoption( + '--insert-assert-fail', + action='store_true', + default=False, + help='Fail tests which include one or more insert_assert() calls', + ) + + +@pytest.fixture(scope='session', autouse=True) +def insert_assert_add_to_builtins() -> None: + try: + setattr(builtins, 'insert_assert', insert_assert) + # we also install debug here since the default script doesn't install it + setattr(builtins, 'debug', debug) + except TypeError: + # happens on pypy + pass + + +@pytest.fixture(autouse=True) +def insert_assert_maybe_fail(pytestconfig: pytest.Config) -> Generator[None, None, None]: + insert_assert_calls.set(0) + yield + print_instead = pytestconfig.getoption('insert_assert_print') + if not print_instead: + count = insert_assert_calls.get() + if count: + pytest.fail(f'devtools-insert-assert: {count} assert{plural(count)} will be inserted', pytrace=False) + + +@pytest.fixture(name='insert_assert') +def insert_assert_fixture() -> Callable[[Any], int]: + return insert_assert + + +def pytest_report_teststatus(report: pytest.TestReport, config: pytest.Config) -> Any: + if report.when == 'teardown' and report.failed and 'devtools-insert-assert:' in repr(report.longrepr): + return 'insert assert', 'i', ('INSERT ASSERT', {'cyan': True}) + + +@pytest.fixture(scope='session', autouse=True) +def insert_assert_session(pytestconfig: pytest.Config) -> Generator[None, None, None]: + """ + Actual logic for updating code examples. + """ + try: + __builtins__['insert_assert'] = insert_assert + except TypeError: + # happens on pypy + pass + + yield + + if not to_replace: + return None + + print_instead = pytestconfig.getoption('insert_assert_print') + + highlight = None + if print_instead: + highlight = get_pygments() + + files = 0 + dup_count = 0 + summary = [] + for file, group in groupby(to_replace, key=lambda tr: tr.file): + # we have to substitute lines in reverse order to avoid messing up line numbers + lines = file.read_text().splitlines() + duplicates: set[int] = set() + for tr in sorted(group, key=lambda x: x.start_line, reverse=True): + if print_instead: + hr = '-' * 80 + code = highlight(tr.code) if highlight else tr.code + line_no = f'{tr.start_line}' if tr.start_line == tr.end_line else f'{tr.start_line}-{tr.end_line}' + summary.append(f'{file} - {line_no}:\n{hr}\n{code}{hr}\n') + else: + if tr.start_line in duplicates: + dup_count += 1 + else: + duplicates.add(tr.start_line) + lines[tr.start_line - 1 : tr.end_line] = tr.code.splitlines() + if not print_instead: + file.write_text('\n'.join(lines)) + files += 1 + prefix = 'Printed' if print_instead else 'Replaced' + summary.append( + f'{prefix} {len(to_replace)} insert_assert() call{plural(to_replace)} in {files} file{plural(files)}' + ) + if dup_count: + summary.append( + f'\n{dup_count} insert skipped because an assert statement on that line had already be inserted!' + ) + + insert_assert_summary.set(summary) + to_replace.clear() + + +def pytest_terminal_summary() -> None: + summary = insert_assert_summary.get(None) + if summary: + print('\n'.join(summary)) + + +def custom_repr(value: Any) -> Any: + if isinstance(value, (list, tuple, set, frozenset)): + return value.__class__(map(custom_repr, value)) + elif isinstance(value, dict): + return value.__class__((custom_repr(k), custom_repr(v)) for k, v in value.items()) + if isinstance(value, Enum): + return PlainRepr(f'{value.__class__.__name__}.{value.name}') + else: + return PlainRepr(repr(value)) + + +class PlainRepr(str): + """ + String class where repr doesn't include quotes. + """ + + def __repr__(self) -> str: + return str(self) + + +def plural(v: int | Sized) -> str: + if isinstance(v, (int, float)): + n = v + else: + n = len(v) + return '' if n == 1 else 's' + + +@lru_cache(maxsize=None) +def load_black() -> Callable[[str], str]: + """ + Build black configuration from "pyproject.toml". + + Black doesn't have a nice self-contained API for reading pyproject.toml, hence all this. + """ + try: + from black import format_file_contents + from black.files import find_pyproject_toml, parse_pyproject_toml + from black.mode import Mode, TargetVersion + from black.parsing import InvalidInput + except ImportError: + return lambda x: x + + def convert_target_version(target_version_config: Any) -> set[Any] | None: + if target_version_config is not None: + return None + elif not isinstance(target_version_config, list): + raise ValueError('Config key "target_version" must be a list') + else: + return {TargetVersion[tv.upper()] for tv in target_version_config} + + @dataclass + class ConfigArg: + config_name: str + keyword_name: str + converter: Callable[[Any], Any] + + config_mapping: list[ConfigArg] = [ + ConfigArg('target_version', 'target_versions', convert_target_version), + ConfigArg('line_length', 'line_length', int), + ConfigArg('skip_string_normalization', 'string_normalization', lambda x: not x), + ConfigArg('skip_magic_trailing_commas', 'magic_trailing_comma', lambda x: not x), + ] + + config_str = find_pyproject_toml((str(Path.cwd()),)) + mode_ = None + fast = False + if config_str: + try: + config = parse_pyproject_toml(config_str) + except (OSError, ValueError) as e: + raise ValueError(f'Error reading configuration file: {e}') + + if config: + kwargs = dict() + for config_arg in config_mapping: + try: + value = config[config_arg.config_name] + except KeyError: + pass + else: + value = config_arg.converter(value) + if value is not None: + kwargs[config_arg.keyword_name] = value + + mode_ = Mode(**kwargs) + fast = bool(config.get('fast')) + + mode = mode_ or Mode() + + def format_code(code: str) -> str: + try: + return format_file_contents(code, fast=fast, mode=mode) + except InvalidInput as e: + print('black error, you will need to format the code manually,', e) + return code + + return format_code + + +# isatty() is false inside pytest, hence calling this now +try: + std_out_istty = sys.stdout.isatty() +except Exception: + std_out_istty = False + + +@lru_cache(maxsize=None) +def get_pygments() -> Callable[[str], str] | None: # pragma: no cover + if not std_out_istty: + return None + try: + import pygments + from pygments.formatters import Terminal256Formatter + from pygments.lexers import PythonLexer + except ImportError as e: # pragma: no cover + print(e) + return None + else: + pyg_lexer, terminal_formatter = PythonLexer(), Terminal256Formatter() + + def highlight(code: str) -> str: + return pygments.highlight(code, lexer=pyg_lexer, formatter=terminal_formatter) + + return highlight diff --git a/pyproject.toml b/pyproject.toml index 193aabc..7e7c87f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,9 @@ Funding = 'https://github.com/sponsors/samuelcolvin' Source = 'https://github.com/samuelcolvin/python-devtools' Changelog = 'https://github.com/samuelcolvin/python-devtools/releases' +[project.entry-points.pytest11] +devtools = 'devtools.pytest_plugin' + [tool.pytest.ini_options] testpaths = 'tests' filterwarnings = 'error' @@ -90,5 +93,5 @@ strict = true warn_return_any = false [[tool.mypy.overrides]] -module = ['executing.*'] +module = ['executing.*', 'pygments.*'] ignore_missing_imports = true diff --git a/requirements/linting.in b/requirements/linting.in index 44df076..e284107 100644 --- a/requirements/linting.in +++ b/requirements/linting.in @@ -3,3 +3,4 @@ mypy==0.971 ruff # required so mypy can find stubs sqlalchemy +pytest diff --git a/requirements/linting.txt b/requirements/linting.txt index 92279cd..21c1a03 100644 --- a/requirements/linting.txt +++ b/requirements/linting.txt @@ -4,10 +4,16 @@ # # pip-compile --output-file=requirements/linting.txt --resolver=backtracking requirements/linting.in # +attrs==22.2.0 + # via pytest black==23.3.0 # via -r requirements/linting.in click==8.1.3 # via black +exceptiongroup==1.1.1 + # via pytest +iniconfig==2.0.0 + # via pytest mypy==0.971 # via -r requirements/linting.in mypy-extensions==1.0.0 @@ -15,11 +21,17 @@ mypy-extensions==1.0.0 # black # mypy packaging==23.0 - # via black + # via + # black + # pytest pathspec==0.11.1 # via black platformdirs==3.2.0 # via black +pluggy==1.0.0 + # via pytest +pytest==7.2.2 + # via -r requirements/linting.in ruff==0.0.261 # via -r requirements/linting.in sqlalchemy==2.0.8 @@ -28,6 +40,7 @@ tomli==2.0.1 # via # black # mypy + # pytest typing-extensions==4.5.0 # via # mypy diff --git a/requirements/testing.in b/requirements/testing.in index 1976d79..0543021 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -4,10 +4,11 @@ pytest pytest-mock pytest-pretty # these packages are used in tests so install the latest version -pydantic # no binaries for 3.7 asyncpg; python_version>='3.8' +black +multidict; python_version>='3.8' # no version is compatible with 3.7 and 3.11 numpy; python_version>='3.8' -multidict; python_version>='3.8' +pydantic sqlalchemy diff --git a/requirements/testing.txt b/requirements/testing.txt index 2ce58bf..9fb1f82 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -8,6 +8,10 @@ asyncpg==0.27.0 ; python_version >= "3.8" # via -r requirements/testing.in attrs==22.2.0 # via pytest +black==23.3.0 + # via -r requirements/testing.in +click==8.1.3 + # via black coverage[toml]==7.2.2 # via -r requirements/testing.in exceptiongroup==1.1.1 @@ -20,10 +24,18 @@ mdurl==0.1.2 # via markdown-it-py multidict==6.0.4 ; python_version >= "3.8" # via -r requirements/testing.in +mypy-extensions==1.0.0 + # via black numpy==1.24.2 ; python_version >= "3.8" # via -r requirements/testing.in packaging==23.0 - # via pytest + # via + # black + # pytest +pathspec==0.11.1 + # via black +platformdirs==3.2.0 + # via black pluggy==1.0.0 # via pytest pydantic==1.10.7 @@ -39,7 +51,7 @@ pytest==7.2.2 # pytest-pretty pytest-mock==3.10.0 # via -r requirements/testing.in -pytest-pretty==1.1.1 +pytest-pretty==1.2.0 # via -r requirements/testing.in rich==13.3.3 # via pytest-pretty @@ -47,6 +59,7 @@ sqlalchemy==2.0.8 # via -r requirements/testing.in tomli==2.0.1 # via + # black # coverage # pytest typing-extensions==4.5.0 diff --git a/tests/conftest.py b/tests/conftest.py index 2591e75..2a62df2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ import os +pytest_plugins = ['pytester'] + def pytest_sessionstart(session): os.environ.pop('PY_DEVTOOLS_HIGHLIGHT', None) diff --git a/tests/test_insert_assert.py b/tests/test_insert_assert.py new file mode 100644 index 0000000..299cee8 --- /dev/null +++ b/tests/test_insert_assert.py @@ -0,0 +1,169 @@ +import os +import sys + +import pytest + +from devtools.pytest_plugin import load_black + +pytestmark = pytest.mark.skipif(sys.version_info < (3, 8), reason='requires Python 3.8+') + + +config = "pytest_plugins = ['devtools.pytest_plugin']" +# language=Python +default_test = """\ +def test_ok(): + assert 1 + 2 == 3 + +def test_string_assert(insert_assert): + thing = 'foobar' + insert_assert(thing)\ +""" + + +def test_insert_assert(pytester_pretty): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + test_file = pytester_pretty.makepyfile(default_test) + result = pytester_pretty.runpytest() + result.assert_outcomes(passed=2) + # print(result.outlines) + assert test_file.read_text() == ( + 'def test_ok():\n' + ' assert 1 + 2 == 3\n' + '\n' + 'def test_string_assert(insert_assert):\n' + " thing = 'foobar'\n" + ' # insert_assert(thing)\n' + ' assert thing == "foobar"' + ) + + +def test_insert_assert_no_pretty(pytester): + os.environ.pop('CI', None) + pytester.makeconftest(config) + test_file = pytester.makepyfile(default_test) + result = pytester.runpytest('-p', 'no:pretty') + result.assert_outcomes(passed=2) + assert test_file.read_text() == ( + 'def test_ok():\n' + ' assert 1 + 2 == 3\n' + '\n' + 'def test_string_assert(insert_assert):\n' + " thing = 'foobar'\n" + ' # insert_assert(thing)\n' + ' assert thing == "foobar"' + ) + + +def test_insert_assert_print(pytester_pretty, capsys): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + test_file = pytester_pretty.makepyfile(default_test) + # assert r == 0 + result = pytester_pretty.runpytest('--insert-assert-print') + result.assert_outcomes(passed=2) + assert test_file.read_text() == default_test + captured = capsys.readouterr() + assert 'test_insert_assert_print.py - 6:' in captured.out + assert 'Printed 1 insert_assert() call in 1 file\n' in captured.out + + +def test_insert_assert_fail(pytester_pretty): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + test_file = pytester_pretty.makepyfile(default_test) + # assert r == 0 + result = pytester_pretty.runpytest() + assert result.parseoutcomes() == {'passed': 2, 'warning': 1, 'insert': 1} + assert test_file.read_text() != default_test + + +def test_deep(pytester_pretty): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + # language=Python + test_file = pytester_pretty.makepyfile( + """ + def test_deep(insert_assert): + insert_assert([{'a': i, 'b': 2 * 2} for i in range(3)]) + """ + ) + result = pytester_pretty.runpytest() + result.assert_outcomes(passed=1) + assert test_file.read_text() == ( + 'def test_deep(insert_assert):\n' + " # insert_assert([{'a': i, 'b': 2 * 2} for i in range(3)])\n" + ' assert [{"a": i, "b": 2 * 2} for i in range(3)] == [\n' + ' {"a": 0, "b": 4},\n' + ' {"a": 1, "b": 4},\n' + ' {"a": 2, "b": 4},\n' + ' ]' + ) + + +def test_enum(pytester_pretty, capsys): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + # language=Python + pytester_pretty.makepyfile( + """ +from enum import Enum + +class Foo(Enum): + A = 1 + B = 2 + +def test_deep(insert_assert): + x = Foo.A + insert_assert(x) + """ + ) + result = pytester_pretty.runpytest('--insert-assert-print') + result.assert_outcomes(passed=1) + captured = capsys.readouterr() + assert ' assert x == Foo.A\n' in captured.out + + +def test_insert_assert_black(tmp_path): + old_wd = os.getcwd() + try: + os.chdir(tmp_path) + (tmp_path / 'pyproject.toml').write_text( + """\ +[tool.black] +target-version = ["py39"] +skip-string-normalization = true""" + ) + load_black.cache_clear() + finally: + os.chdir(old_wd) + + f = load_black() + # no string normalization + assert f("'foobar'") == "'foobar'\n" + + +def test_insert_assert_repeat(pytester_pretty, capsys): + os.environ.pop('CI', None) + pytester_pretty.makeconftest(config) + test_file = pytester_pretty.makepyfile( + """\ +import pytest + +@pytest.mark.parametrize('x', [1, 2, 3]) +def test_string_assert(x, insert_assert): + insert_assert(x)\ +""" + ) + result = pytester_pretty.runpytest() + result.assert_outcomes(passed=3) + assert test_file.read_text() == ( + 'import pytest\n' + '\n' + "@pytest.mark.parametrize('x', [1, 2, 3])\n" + 'def test_string_assert(x, insert_assert):\n' + ' # insert_assert(x)\n' + ' assert x == 1' + ) + captured = capsys.readouterr() + assert '2 insert skipped because an assert statement on that line had already be inserted!\n' in captured.out From f416c9b8df4f087b2302cd0fe3f878ffecd40661 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 19:00:13 +0100 Subject: [PATCH 21/36] uprev --- devtools/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/version.py b/devtools/version.py index ea301cb..d3c01ed 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.10.0' +VERSION = '0.11.0' From 71edb0d6957895615ac258a2080e9424e99b0e99 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 5 Apr 2023 19:04:44 +0100 Subject: [PATCH 22/36] uprev mypy --- requirements/linting.in | 2 +- requirements/linting.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/linting.in b/requirements/linting.in index e284107..41aa444 100644 --- a/requirements/linting.in +++ b/requirements/linting.in @@ -1,5 +1,5 @@ black -mypy==0.971 +mypy ruff # required so mypy can find stubs sqlalchemy diff --git a/requirements/linting.txt b/requirements/linting.txt index 21c1a03..bd6c1fc 100644 --- a/requirements/linting.txt +++ b/requirements/linting.txt @@ -14,7 +14,7 @@ exceptiongroup==1.1.1 # via pytest iniconfig==2.0.0 # via pytest -mypy==0.971 +mypy==1.1.1 # via -r requirements/linting.in mypy-extensions==1.0.0 # via From b6a98196677637323dc89bfb28cb1dc4e602ef44 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 6 Apr 2023 00:45:13 +0100 Subject: [PATCH 23/36] build docs on CI (#127) * build docs on ci * fix docs build * add numpy --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++--- Makefile | 2 +- devtools/__init__.py | 11 +++++----- docs/examples/ansi_colours.py | 2 +- docs/examples/complex.py | 3 ++- docs/examples/other.py | 2 +- docs/examples/prettier.py | 3 ++- requirements/docs.in | 14 +++++++----- requirements/docs.txt | 28 ++++++++++++++++++------ 9 files changed, 80 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7b81b9..bcfe8bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,26 @@ jobs: with: extra_args: --all-files --verbose + docs-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - run: pip install -r requirements/docs.txt -r requirements/pyproject.txt + - run: pip install . + + - run: make docs + + - name: Store docs site + uses: actions/upload-artifact@v3 + with: + name: docs + path: site + test: name: test py${{ matrix.python-version }} on ${{ matrix.os }} strategy: @@ -85,7 +105,7 @@ jobs: # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() - needs: [test, lint] + needs: [test, lint, docs-build] runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed @@ -107,6 +127,17 @@ jobs: with: python-version: '3.10' + - name: get docs + uses: actions/download-artifact@v3 + with: + name: docs + path: site + + - name: check GITHUB_REF matches package version + uses: samuelcolvin/check-python-version@v3 + with: + version_file_path: devtools/version.py + - name: install run: pip install build twine @@ -122,6 +153,10 @@ jobs: TWINE_PASSWORD: ${{ secrets.pypi_token }} - name: publish docs - run: make publish-docs + if: '!fromJSON(steps.check-tag.outputs.IS_PRERELEASE)' + uses: cloudflare/wrangler-action@2.0.0 + with: + apiToken: ${{ secrets.cloudflare_api_token }} + command: pages publish --project-name=python-devtools --branch=main site env: - NETLIFY: ${{ secrets.netlify_token }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.cloudflare_account_id }} diff --git a/Makefile b/Makefile index 3a464eb..eecc748 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ clean: .PHONY: docs docs: - flake8 --max-line-length=80 docs/examples/ + ruff --line-length=80 docs/examples/ mkdocs build .PHONY: docs-serve diff --git a/devtools/__init__.py b/devtools/__init__.py index d6a6564..b607d19 100644 --- a/devtools/__init__.py +++ b/devtools/__init__.py @@ -1,8 +1,9 @@ -# flake8: noqa -from .ansi import * -from .debug import * -from .prettier import * -from .timer import * +from .ansi import sformat, sprint +from .debug import Debug, debug +from .prettier import PrettyFormat, pformat, pprint +from .timer import Timer from .version import VERSION __version__ = VERSION + +__all__ = 'sformat', 'sprint', 'Debug', 'debug', 'PrettyFormat', 'pformat', 'pprint', 'Timer', 'VERSION' diff --git a/docs/examples/ansi_colours.py b/docs/examples/ansi_colours.py index 1e7c594..a89e9ca 100644 --- a/docs/examples/ansi_colours.py +++ b/docs/examples/ansi_colours.py @@ -1,4 +1,4 @@ -from devtools import sprint, sformat +from devtools import sformat, sprint sprint('this is red', sprint.red) diff --git a/docs/examples/complex.py b/docs/examples/complex.py index ea84b3b..55f1d55 100644 --- a/docs/examples/complex.py +++ b/docs/examples/complex.py @@ -1,6 +1,7 @@ -from devtools import debug import numpy as np +from devtools import debug + foo = { 'foo': np.array(range(20)), 'bar': [{'a': i, 'b': {j for j in range(1 + i * 2)}} for i in range(3)], diff --git a/docs/examples/other.py b/docs/examples/other.py index 0e569f8..7763972 100644 --- a/docs/examples/other.py +++ b/docs/examples/other.py @@ -20,7 +20,7 @@ # if used repeatedly a summary is available t3 = debug.timer() for i in [1e4, 1e6, 1e7]: - with t3('sum {}'.format(i), verbose=False): + with t3(f'sum {i}', verbose=False): sum(range(int(i))) t3.summary(verbose=True) diff --git a/docs/examples/prettier.py b/docs/examples/prettier.py index af1e4b4..0be9736 100644 --- a/docs/examples/prettier.py +++ b/docs/examples/prettier.py @@ -1,6 +1,7 @@ -from devtools import PrettyFormat, pprint, pformat import numpy as np +from devtools import PrettyFormat, pformat, pprint + v = { 'foo': {'whatever': [3, 2, 1]}, 'sentence': 'hello\nworld', diff --git a/requirements/docs.in b/requirements/docs.in index f4c60e6..be48235 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -1,7 +1,9 @@ -ansi2html==1.8.0 -mkdocs==1.3.1 -mkdocs-exclude==1.0.2 -mkdocs-material==8.3.9 -mkdocs-simple-hooks==0.1.5 -markdown-include==0.7.0 +ansi2html +mkdocs +mkdocs-exclude +mkdocs-material +mkdocs-simple-hooks +markdown-include pygments +ruff +numpy diff --git a/requirements/docs.txt b/requirements/docs.txt index d39c76c..2d242be 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -6,12 +6,18 @@ # ansi2html==1.8.0 # via -r requirements/docs.in +certifi==2022.12.7 + # via requests +charset-normalizer==3.1.0 + # via requests click==8.1.3 # via mkdocs +colorama==0.4.6 + # via mkdocs-material ghp-import==2.1.0 # via mkdocs -importlib-metadata==6.1.0 - # via mkdocs +idna==3.4 + # via requests jinja2==3.1.2 # via # mkdocs @@ -22,13 +28,13 @@ markdown==3.3.7 # mkdocs # mkdocs-material # pymdown-extensions -markdown-include==0.7.0 +markdown-include==0.8.1 # via -r requirements/docs.in markupsafe==2.1.2 # via jinja2 mergedeep==1.3.4 # via mkdocs -mkdocs==1.3.1 +mkdocs==1.4.2 # via # -r requirements/docs.in # mkdocs-exclude @@ -36,12 +42,14 @@ mkdocs==1.3.1 # mkdocs-simple-hooks mkdocs-exclude==1.0.2 # via -r requirements/docs.in -mkdocs-material==8.3.9 +mkdocs-material==9.1.5 # via -r requirements/docs.in mkdocs-material-extensions==1.1.1 # via mkdocs-material mkdocs-simple-hooks==0.1.5 # via -r requirements/docs.in +numpy==1.24.2 + # via -r requirements/docs.in packaging==23.0 # via mkdocs pygments==2.14.0 @@ -59,9 +67,15 @@ pyyaml==6.0 # pyyaml-env-tag pyyaml-env-tag==0.1 # via mkdocs +regex==2023.3.23 + # via mkdocs-material +requests==2.28.2 + # via mkdocs-material +ruff==0.0.261 + # via -r requirements/docs.in six==1.16.0 # via python-dateutil +urllib3==1.26.15 + # via requests watchdog==3.0.0 # via mkdocs -zipp==3.15.0 - # via importlib-metadata From 88e020616448f41d1a379aa3e4429047c4e10f84 Mon Sep 17 00:00:00 2001 From: Tom Hamilton Stubber Date: Thu, 27 Apr 2023 18:06:00 +0100 Subject: [PATCH 24/36] Update usage to reflect the recent addition of the pytest plugin (#128) --- docs/usage.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 0388a2b..3efce3e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -113,14 +113,17 @@ To manually add `debug` to `__builtins__`, add the following to `sitecustomize.p which is always imported. ```py -# add devtools `debug` function to builtins -import builtins -try: - from devtools import debug -except ImportError: - pass -else: - setattr(builtins, 'debug', debug) +import sys +# we don't install here for pytest as it breaks pytest, it is +# installed later by a pytest fixture +if not sys.argv[0].endswith('pytest'): + import builtins + try: + from devtools import debug + except ImportError: + pass + else: + setattr(builtins, 'debug', debug) ``` The `ImportError` exception is important since you'll want python to run fine even if *devtools* isn't installed. From 1172e81db7a05742e6f9a3db1ccec16045565b2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 16:06:17 +0100 Subject: [PATCH 25/36] Bump requests from 2.28.2 to 2.31.0 in /requirements (#130) Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0) --- updated-dependencies: - dependency-name: requests dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/docs.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index 2d242be..c7e43ac 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -5,7 +5,7 @@ # pip-compile --output-file=requirements/docs.txt --resolver=backtracking requirements/docs.in # ansi2html==1.8.0 - # via -r requirements/docs.in + # via -r docs.in certifi==2022.12.7 # via requests charset-normalizer==3.1.0 @@ -29,32 +29,32 @@ markdown==3.3.7 # mkdocs-material # pymdown-extensions markdown-include==0.8.1 - # via -r requirements/docs.in + # via -r docs.in markupsafe==2.1.2 # via jinja2 mergedeep==1.3.4 # via mkdocs mkdocs==1.4.2 # via - # -r requirements/docs.in + # -r docs.in # mkdocs-exclude # mkdocs-material # mkdocs-simple-hooks mkdocs-exclude==1.0.2 - # via -r requirements/docs.in + # via -r docs.in mkdocs-material==9.1.5 - # via -r requirements/docs.in + # via -r docs.in mkdocs-material-extensions==1.1.1 # via mkdocs-material mkdocs-simple-hooks==0.1.5 - # via -r requirements/docs.in + # via -r docs.in numpy==1.24.2 - # via -r requirements/docs.in + # via -r docs.in packaging==23.0 # via mkdocs pygments==2.14.0 # via - # -r requirements/docs.in + # -r docs.in # mkdocs-material pymdown-extensions==9.10 # via mkdocs-material @@ -69,10 +69,10 @@ pyyaml-env-tag==0.1 # via mkdocs regex==2023.3.23 # via mkdocs-material -requests==2.28.2 +requests==2.31.0 # via mkdocs-material ruff==0.0.261 - # via -r requirements/docs.in + # via -r docs.in six==1.16.0 # via python-dateutil urllib3==1.26.15 From 2341ef37359b7d42a2c307207c50253343539d13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:00:48 +0100 Subject: [PATCH 26/36] Bump pygments from 2.13.0 to 2.15.0 in /docs (#133) Bumps [pygments](https://github.com/pygments/pygments) from 2.13.0 to 2.15.0. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.13.0...2.15.0) --- updated-dependencies: - dependency-name: pygments dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 3a7c1d2..d091120 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ mkdocs-exclude==1.0.2 mkdocs-material==8.3.9 mkdocs-simple-hooks==0.1.5 markdown-include==0.7.0 -pygments==2.13.0 +pygments==2.15.0 From 967121ff0b847756ef813c3ba80b4ab6d5ecde1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:01:07 +0100 Subject: [PATCH 27/36] Bump pygments from 2.14.0 to 2.15.0 in /requirements (#134) Bumps [pygments](https://github.com/pygments/pygments) from 2.14.0 to 2.15.0. - [Release notes](https://github.com/pygments/pygments/releases) - [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) - [Commits](https://github.com/pygments/pygments/compare/2.14.0...2.15.0) --- updated-dependencies: - dependency-name: pygments dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/docs.txt | 2 +- requirements/testing.txt | 33 ++++++++++++++------------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index c7e43ac..dc0af30 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -52,7 +52,7 @@ numpy==1.24.2 # via -r docs.in packaging==23.0 # via mkdocs -pygments==2.14.0 +pygments==2.15.0 # via # -r docs.in # mkdocs-material diff --git a/requirements/testing.txt b/requirements/testing.txt index 9fb1f82..ace238f 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -5,17 +5,17 @@ # pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in # asyncpg==0.27.0 ; python_version >= "3.8" - # via -r requirements/testing.in + # via -r testing.in attrs==22.2.0 # via pytest black==23.3.0 - # via -r requirements/testing.in + # via -r testing.in click==8.1.3 # via black coverage[toml]==7.2.2 - # via -r requirements/testing.in -exceptiongroup==1.1.1 - # via pytest + # via -r testing.in +greenlet==2.0.2 + # via sqlalchemy iniconfig==2.0.0 # via pytest markdown-it-py==2.2.0 @@ -23,11 +23,11 @@ markdown-it-py==2.2.0 mdurl==0.1.2 # via markdown-it-py multidict==6.0.4 ; python_version >= "3.8" - # via -r requirements/testing.in + # via -r testing.in mypy-extensions==1.0.0 # via black numpy==1.24.2 ; python_version >= "3.8" - # via -r requirements/testing.in + # via -r testing.in packaging==23.0 # via # black @@ -39,29 +39,24 @@ platformdirs==3.2.0 pluggy==1.0.0 # via pytest pydantic==1.10.7 - # via -r requirements/testing.in -pygments==2.14.0 + # via -r testing.in +pygments==2.15.0 # via - # -r requirements/testing.in + # -r testing.in # rich pytest==7.2.2 # via - # -r requirements/testing.in + # -r testing.in # pytest-mock # pytest-pretty pytest-mock==3.10.0 - # via -r requirements/testing.in + # via -r testing.in pytest-pretty==1.2.0 - # via -r requirements/testing.in + # via -r testing.in rich==13.3.3 # via pytest-pretty sqlalchemy==2.0.8 - # via -r requirements/testing.in -tomli==2.0.1 - # via - # black - # coverage - # pytest + # via -r testing.in typing-extensions==4.5.0 # via # pydantic From 3a6f5b991f4f1d5726f0308a92fc14554e2cef67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:01:21 +0100 Subject: [PATCH 28/36] Bump pymdown-extensions from 9.10 to 10.0 in /requirements (#129) Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.10 to 10.0. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.10...10.0) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index dc0af30..2782252 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -56,7 +56,7 @@ pygments==2.15.0 # via # -r docs.in # mkdocs-material -pymdown-extensions==9.10 +pymdown-extensions==10.0 # via mkdocs-material python-dateutil==2.8.2 # via ghp-import From c17c31021143f4d3feac0c47b897ae22b1aefb06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:01:29 +0100 Subject: [PATCH 29/36] Bump certifi from 2022.12.7 to 2023.7.22 in /requirements (#135) Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs.txt b/requirements/docs.txt index 2782252..85ac479 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -6,7 +6,7 @@ # ansi2html==1.8.0 # via -r docs.in -certifi==2022.12.7 +certifi==2023.7.22 # via requests charset-normalizer==3.1.0 # via requests From 00047cb12c3b5245d9ad569e41b8b0d4c06bc8e5 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 17 Aug 2023 12:02:57 +0100 Subject: [PATCH 30/36] support dataclasses with slots (#136) --- devtools/prettier.py | 7 ++++++- tests/test_prettier.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/devtools/prettier.py b/devtools/prettier.py index 4f274de..c45bc6a 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -254,7 +254,12 @@ def _format_ast_expression(self, value: ast.AST, _: str, indent_current: int, in self._stream.write(indent_current * self._c + line) def _format_dataclass(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: - self._format_fields(value, value.__dict__.items(), indent_current, indent_new) + try: + field_items = value.__dict__.items() + except AttributeError: + # slots + field_items = ((f, getattr(value, f)) for f in value.__slots__) + self._format_fields(value, field_items, indent_current, indent_new) def _format_sqlalchemy_class(self, value: 'Any', _: str, indent_current: int, indent_new: int) -> None: if sa_inspect is not None: diff --git a/tests/test_prettier.py b/tests/test_prettier.py index 6a0247c..298dc58 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -255,6 +255,30 @@ class BarDataclass: ) +def test_dataclass_slots(): + try: + dec = dataclass(slots=True) + except TypeError: + pytest.skip('dataclasses.slots not available') + + @dec + class FooDataclass: + x: int + y: str + + f = FooDataclass(123, 'bar') + v = pformat(f) + print(v) + assert ( + v + == """\ +FooDataclass( + x=123, + y='bar', +)""" + ) + + @pytest.mark.skipif(numpy is None, reason='numpy not installed') def test_indent_numpy(): v = pformat({'numpy test': numpy.array(range(20))}) From 6eac2ca7e63114d4bb44f6e1366673d16b0eaef6 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 17 Aug 2023 12:18:53 +0100 Subject: [PATCH 31/36] Make `Pygments` required (#137) --- README.md | 5 +---- docs/install.md | 6 ++---- pyproject.toml | 4 +++- requirements/docs.in | 1 - requirements/docs.txt | 20 +++++++++----------- requirements/testing.in | 1 - requirements/testing.txt | 33 ++++++++++++++++++--------------- 7 files changed, 33 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 55e69e0..c660d28 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,9 @@ For more information, see [documentation](https://python-devtools.helpmanual.io/ Just ```bash -pip install devtools[pygments] +pip install devtools ``` -`pygments` is not required but if it's installed, output will be highlighted and easier to read. - -`devtools` has no other required dependencies except python 3.7, 3.8, 3.9, 3.10 or 3.11. If you've got python 3.7+ and `pip` installed, you're good to go. ## Usage diff --git a/docs/install.md b/docs/install.md index 94630bb..a1ceba7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,10 +1,8 @@ Installation is as simple as: ```bash -pip install devtools[pygments] +pip install devtools ``` -`pygments` is not required but if it's installed, output will be highlighted and easier to read. - -`devtools` has no other required dependencies except python 3.7, 3.8, 3.9, 3.10 or 3.11. +`devtools` has [very few dependencies](https://github.com/samuelcolvin/python-devtools/blob/main/pyproject.toml#L37). If you've got python 3.7+ and `pip` installed, you're good to go. diff --git a/pyproject.toml b/pyproject.toml index 7e7c87f..28a9ecf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,10 @@ requires-python = '>=3.7' dependencies = [ 'executing>=1.1.1', 'asttokens>=2.0.0,<3.0.0', + 'Pygments>=2.15.0', ] -optional-dependencies = {pygments = ['Pygments>=2.2.0'] } +# keep this meaningless group around to avoid breaking installs using `pip install devtools[pygments]` +optional-dependencies = {pygments = [] } dynamic = ['version'] [project.urls] diff --git a/requirements/docs.in b/requirements/docs.in index be48235..caf159b 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -4,6 +4,5 @@ mkdocs-exclude mkdocs-material mkdocs-simple-hooks markdown-include -pygments ruff numpy diff --git a/requirements/docs.txt b/requirements/docs.txt index 85ac479..b2d5b9f 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -5,7 +5,7 @@ # pip-compile --output-file=requirements/docs.txt --resolver=backtracking requirements/docs.in # ansi2html==1.8.0 - # via -r docs.in + # via -r requirements/docs.in certifi==2023.7.22 # via requests charset-normalizer==3.1.0 @@ -29,33 +29,31 @@ markdown==3.3.7 # mkdocs-material # pymdown-extensions markdown-include==0.8.1 - # via -r docs.in + # via -r requirements/docs.in markupsafe==2.1.2 # via jinja2 mergedeep==1.3.4 # via mkdocs mkdocs==1.4.2 # via - # -r docs.in + # -r requirements/docs.in # mkdocs-exclude # mkdocs-material # mkdocs-simple-hooks mkdocs-exclude==1.0.2 - # via -r docs.in + # via -r requirements/docs.in mkdocs-material==9.1.5 - # via -r docs.in + # via -r requirements/docs.in mkdocs-material-extensions==1.1.1 # via mkdocs-material mkdocs-simple-hooks==0.1.5 - # via -r docs.in + # via -r requirements/docs.in numpy==1.24.2 - # via -r docs.in + # via -r requirements/docs.in packaging==23.0 # via mkdocs pygments==2.15.0 - # via - # -r docs.in - # mkdocs-material + # via mkdocs-material pymdown-extensions==10.0 # via mkdocs-material python-dateutil==2.8.2 @@ -72,7 +70,7 @@ regex==2023.3.23 requests==2.31.0 # via mkdocs-material ruff==0.0.261 - # via -r docs.in + # via -r requirements/docs.in six==1.16.0 # via python-dateutil urllib3==1.26.15 diff --git a/requirements/testing.in b/requirements/testing.in index 0543021..6a3232a 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -1,5 +1,4 @@ coverage[toml] -pygments pytest pytest-mock pytest-pretty diff --git a/requirements/testing.txt b/requirements/testing.txt index ace238f..a3b4099 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -5,17 +5,17 @@ # pip-compile --output-file=requirements/testing.txt --resolver=backtracking requirements/testing.in # asyncpg==0.27.0 ; python_version >= "3.8" - # via -r testing.in + # via -r requirements/testing.in attrs==22.2.0 # via pytest black==23.3.0 - # via -r testing.in + # via -r requirements/testing.in click==8.1.3 # via black coverage[toml]==7.2.2 - # via -r testing.in -greenlet==2.0.2 - # via sqlalchemy + # via -r requirements/testing.in +exceptiongroup==1.1.3 + # via pytest iniconfig==2.0.0 # via pytest markdown-it-py==2.2.0 @@ -23,11 +23,11 @@ markdown-it-py==2.2.0 mdurl==0.1.2 # via markdown-it-py multidict==6.0.4 ; python_version >= "3.8" - # via -r testing.in + # via -r requirements/testing.in mypy-extensions==1.0.0 # via black numpy==1.24.2 ; python_version >= "3.8" - # via -r testing.in + # via -r requirements/testing.in packaging==23.0 # via # black @@ -39,24 +39,27 @@ platformdirs==3.2.0 pluggy==1.0.0 # via pytest pydantic==1.10.7 - # via -r testing.in + # via -r requirements/testing.in pygments==2.15.0 - # via - # -r testing.in - # rich + # via rich pytest==7.2.2 # via - # -r testing.in + # -r requirements/testing.in # pytest-mock # pytest-pretty pytest-mock==3.10.0 - # via -r testing.in + # via -r requirements/testing.in pytest-pretty==1.2.0 - # via -r testing.in + # via -r requirements/testing.in rich==13.3.3 # via pytest-pretty sqlalchemy==2.0.8 - # via -r testing.in + # via -r requirements/testing.in +tomli==2.0.1 + # via + # black + # coverage + # pytest typing-extensions==4.5.0 # via # pydantic From e4937c984d516827577f4a909174d36339414a9a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 17 Aug 2023 12:20:46 +0100 Subject: [PATCH 32/36] Uprev 0.12.0 (#138) --- HISTORY.md | 14 ++++++++++++++ devtools/version.py | 2 +- runtime.txt | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 runtime.txt diff --git a/HISTORY.md b/HISTORY.md index 1aa231e..8cf5103 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,17 @@ +## v0.12.0 (2023-08-17) + +* build docs on CI by @samuelcolvin in #127 +* Update usage to reflect the recent addition of the pytest plugin by @tomhamiltonstubber in #128 +* support dataclasses with slots by @samuelcolvin in #136 +* Make `Pygments` required #137 + +## v0.11.0 (2023-04-05) + +* added support for sqlalchemy2 by @the-vty in #120 +* switch to ruff by @samuelcolvin in #124 +* support displaying ast types by @samuelcolvin in #125 +* Insert assert by @samuelcolvin in #126 + ## v0.10.0 (2022-11-28) * Use secure builtins standard module, instead of the `__builtins__` by @0xsirsaif in #109 diff --git a/devtools/version.py b/devtools/version.py index d3c01ed..bf3b89a 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.11.0' +VERSION = '0.12.0' diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index cc1923a..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -3.8 From f080d39dfe8ff86ef3d2483616cd70c2fe8b5cc9 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 17 Aug 2023 15:49:53 +0100 Subject: [PATCH 33/36] fix docs publish --- .github/workflows/ci.yml | 3 ++- HISTORY.md | 4 ++++ devtools/version.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcfe8bf..3df53d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,7 +134,8 @@ jobs: path: site - name: check GITHUB_REF matches package version - uses: samuelcolvin/check-python-version@v3 + id: check-tag + uses: samuelcolvin/check-python-version@v4.1 with: version_file_path: devtools/version.py diff --git a/HISTORY.md b/HISTORY.md index 8cf5103..ddf0bda 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,7 @@ +## v0.12.1 (2023-08-17) + +fix docs release + ## v0.12.0 (2023-08-17) * build docs on CI by @samuelcolvin in #127 diff --git a/devtools/version.py b/devtools/version.py index bf3b89a..b0ec321 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.12.0' +VERSION = '0.12.1' From ec406ffdd841f65b132e81f3d715321d3cfb5efa Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 3 Sep 2023 17:50:32 +0100 Subject: [PATCH 34/36] install debug into `builtins` via `DebugProxy` (#139) --- Makefile | 2 +- devtools/__main__.py | 51 +++++++++++++++++++++++++++----------------- devtools/debug.py | 21 ++++++++++++------ devtools/version.py | 2 +- 4 files changed, 48 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index eecc748..51b877c 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ update-lockfiles: .PHONY: format format: black $(sources) - ruff $(sources) --fix --exit-zero + ruff $(sources) --fix-only .PHONY: lint lint: diff --git a/devtools/__main__.py b/devtools/__main__.py index bfc6155..2fbc79a 100644 --- a/devtools/__main__.py +++ b/devtools/__main__.py @@ -7,17 +7,32 @@ # language=python install_code = """ # add devtools `debug` function to builtins -import sys -# we don't install here for pytest as it breaks pytest, it is -# installed later by a pytest fixture -if not sys.argv[0].endswith('pytest'): - import builtins - try: - from devtools import debug - except ImportError: - pass - else: - setattr(builtins, 'debug', debug) +# we don't want to import devtools until it's required since it breaks pytest, hence this proxy +class DebugProxy: + def __init__(self): + self._debug = None + + def _import_debug(self): + if self._debug is None: + from devtools import debug + self._debug = debug + + def __call__(self, *args, **kwargs): + self._import_debug() + kwargs['frame_depth_'] = 3 + return self._debug(*args, **kwargs) + + def format(self, *args, **kwargs): + self._import_debug() + kwargs['frame_depth_'] = 3 + return self._debug.format(*args, **kwargs) + + def __getattr__(self, item): + self._import_debug() + return getattr(self._debug, item) + +import builtins +setattr(builtins, 'debug', DebugProxy()) """ @@ -27,12 +42,6 @@ def print_code() -> int: def install() -> int: - print('[WARNING: this command is experimental, report issues at github.com/samuelcolvin/python-devtools]\n') - - if hasattr(builtins, 'debug'): - print('Looks like devtools is already installed.') - return 0 - try: import sitecustomize # type: ignore except ImportError: @@ -48,7 +57,11 @@ def install() -> int: else: install_path = Path(sitecustomize.__file__) - print(f'Found path "{install_path}" to install devtools into __builtins__') + if hasattr(builtins, 'debug'): + print(f'Looks like devtools is already installed, probably in `{install_path}`.') + return 0 + + print(f'Found path `{install_path}` to install devtools into `builtins`') print('To install devtools, run the following command:\n') print(f' python -m devtools print-code >> {install_path}\n') if not install_path.is_relative_to(Path.home()): @@ -65,5 +78,5 @@ def install() -> int: elif 'print-code' in sys.argv: sys.exit(print_code()) else: - print(f'python-devtools v{VERSION}, CLI usage: python -m devtools [install|print-code]') + print(f'python-devtools v{VERSION}, CLI usage: `python -m devtools install|print-code`') sys.exit(1) diff --git a/devtools/debug.py b/devtools/debug.py index 5ea836a..89cda24 100644 --- a/devtools/debug.py +++ b/devtools/debug.py @@ -112,8 +112,15 @@ def __init__(self, *, warnings: 'Optional[bool]' = None, highlight: 'Optional[bo self._show_warnings = env_bool(warnings, 'PY_DEVTOOLS_WARNINGS', True) self._highlight = highlight - def __call__(self, *args: 'Any', file_: 'Any' = None, flush_: bool = True, **kwargs: 'Any') -> 'Any': - d_out = self._process(args, kwargs) + def __call__( + self, + *args: 'Any', + file_: 'Any' = None, + flush_: bool = True, + frame_depth_: int = 2, + **kwargs: 'Any', + ) -> 'Any': + d_out = self._process(args, kwargs, frame_depth_) s = d_out.str(use_highlight(self._highlight, file_)) print(s, file=file_, flush=flush_) if kwargs: @@ -123,8 +130,8 @@ def __call__(self, *args: 'Any', file_: 'Any' = None, flush_: bool = True, **kwa else: return args - def format(self, *args: 'Any', **kwargs: 'Any') -> DebugOutput: - return self._process(args, kwargs) + def format(self, *args: 'Any', frame_depth_: int = 2, **kwargs: 'Any') -> DebugOutput: + return self._process(args, kwargs, frame_depth_) def breakpoint(self) -> None: import pdb @@ -134,13 +141,13 @@ def breakpoint(self) -> None: def timer(self, name: 'Optional[str]' = None, *, verbose: bool = True, file: 'Any' = None, dp: int = 3) -> Timer: return Timer(name=name, verbose=verbose, file=file, dp=dp) - def _process(self, args: 'Any', kwargs: 'Any') -> DebugOutput: + def _process(self, args: 'Any', kwargs: 'Any', frame_depth: int) -> DebugOutput: """ - BEWARE: this must be called from a function exactly 2 levels below the top of the stack. + BEWARE: this must be called from a function exactly `frame_depth` levels below the top of the stack. """ # HELP: any errors other than ValueError from _getframe? If so please submit an issue try: - call_frame: 'FrameType' = sys._getframe(2) + call_frame: 'FrameType' = sys._getframe(frame_depth) except ValueError: # "If [ValueError] is deeper than the call stack, ValueError is raised" return self.output_class( diff --git a/devtools/version.py b/devtools/version.py index b0ec321..a4ba93b 100644 --- a/devtools/version.py +++ b/devtools/version.py @@ -1 +1 @@ -VERSION = '0.12.1' +VERSION = '0.12.2' From 2aea99d95f4766fbca86139f72a1e4985d10a30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Fri, 26 Jan 2024 20:10:51 +0100 Subject: [PATCH 35/36] BUG FIX: Use `Path.relative_to()` for Python 3.8 compatibility (#150) --- devtools/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devtools/__main__.py b/devtools/__main__.py index 2fbc79a..a5fa2f5 100644 --- a/devtools/__main__.py +++ b/devtools/__main__.py @@ -64,7 +64,9 @@ def install() -> int: print(f'Found path `{install_path}` to install devtools into `builtins`') print('To install devtools, run the following command:\n') print(f' python -m devtools print-code >> {install_path}\n') - if not install_path.is_relative_to(Path.home()): + try: + install_path.relative_to(Path.home()) + except ValueError: print('or maybe\n') print(f' python -m devtools print-code | sudo tee -a {install_path} > /dev/null\n') print('Note: "sudo" might be required because the path is in your home directory.') From 5022f1f6f1c55d871b1db208c9c1b2ab0df488fe Mon Sep 17 00:00:00 2001 From: Kinuax Date: Fri, 24 Jan 2025 20:11:42 +0100 Subject: [PATCH 36/36] Fix macos-latest and 3.7 tests (#155) --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3df53d6..e131174 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,15 +55,21 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu, macos, windows] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + exclude: + - os: macos-latest + python-version: '3.7' + include: + - os: macos-13 + python-version: '3.7' env: PYTHON: ${{ matrix.python-version }} OS: ${{ matrix.os }} EXTRAS: yes - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3