From ec406ffdd841f65b132e81f3d715321d3cfb5efa Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 3 Sep 2023 17:50:32 +0100 Subject: [PATCH] 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'