Repository URL to install this package:
|
Version:
0.8.0 ▾
|
devtools
/
debug.py
|
|---|
import os
import sys
from .ansi import sformat
from .prettier import PrettyFormat
from .timer import Timer
from .utils import env_bool, env_true, is_literal, use_highlight
__all__ = 'Debug', 'debug'
MYPY = False
if MYPY:
from types import FrameType
from typing import Any, Generator, List, Optional
pformat = PrettyFormat(
indent_step=int(os.getenv('PY_DEVTOOLS_INDENT', 4)),
simple_cutoff=int(os.getenv('PY_DEVTOOLS_SIMPLE_CUTOFF', 10)),
width=int(os.getenv('PY_DEVTOOLS_WIDTH', 120)),
yield_from_generators=env_true('PY_DEVTOOLS_YIELD_FROM_GEN', True),
)
class DebugArgument:
__slots__ = 'value', 'name', 'extra'
def __init__(self, value, *, name=None, **extra):
self.value = value
self.name = name
self.extra = []
try:
length = len(value)
except TypeError:
pass
else:
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:
s = ''
if self.name and not is_literal(self.name):
s = f'{sformat(self.name, sformat.blue, apply=highlight)}: '
suffix = sformat(
f" ({self.value.__class__.__name__}){''.join(f' {k}={v}' for k, v in self.extra)}",
sformat.dim,
apply=highlight,
)
try:
s += pformat(self.value, indent=4, highlight=highlight)
except Exception as exc:
v = sformat(f'!!! error pretty printing value: {exc!r}', sformat.yellow, apply=highlight)
s += f'{self.value!r}{suffix}\n {v}'
else:
s += suffix
return s
def __str__(self) -> str:
return self.str()
class DebugOutput:
"""
Represents the output of a debug command.
"""
arg_class = DebugArgument
__slots__ = 'filename', 'lineno', 'frame', 'arguments', 'warning'
def __init__(self, *, filename: str, lineno: int, frame: str, arguments: 'List[DebugArgument]', warning=None):
self.filename = filename
self.lineno = lineno
self.frame = frame
self.arguments = arguments
self.warning = warning
def str(self, highlight=False) -> str:
if highlight:
prefix = (
f'{sformat(self.filename, sformat.magenta)}:{sformat(self.lineno, sformat.green)} '
f'{sformat(self.frame, sformat.green, sformat.italic)}'
)
if self.warning:
prefix += sformat(f' ({self.warning})', sformat.dim)
else:
prefix = f'{self.filename}:{self.lineno} {self.frame}'
if self.warning:
prefix += f' ({self.warning})'
return f'{prefix}\n ' + '\n '.join(a.str(highlight) for a in self.arguments)
def __str__(self) -> str:
return self.str()
def __repr__(self) -> str:
arguments = ' '.join(str(a) for a in self.arguments)
return f'<DebugOutput {self.filename}:{self.lineno} {self.frame} arguments: {arguments}>'
class Debug:
output_class = DebugOutput
def __init__(self, *, warnings: 'Optional[bool]' = None, highlight: 'Optional[bool]' = None):
self._show_warnings = env_bool(warnings, 'PY_DEVTOOLS_WARNINGS', True)
self._highlight = highlight
def __call__(self, *args, file_=None, flush_=True, **kwargs) -> 'Any':
d_out = self._process(args, kwargs)
s = d_out.str(use_highlight(self._highlight, file_))
print(s, file=file_, flush=flush_)
if kwargs:
return (*args, kwargs)
elif len(args) == 1:
return args[0]
else:
return args
def format(self, *args, **kwargs) -> DebugOutput:
return self._process(args, kwargs)
def breakpoint(self):
import pdb
pdb.Pdb(skip=['devtools.*']).set_trace()
def timer(self, name=None, *, verbose=True, file=None, dp=3) -> Timer:
return Timer(name=name, verbose=verbose, file=file, dp=dp)
def _process(self, args, kwargs) -> DebugOutput:
"""
BEWARE: this must be called from a function exactly 2 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)
except ValueError:
# "If [ValueError] is deeper than the call stack, ValueError is raised"
return self.output_class(
filename='<unknown>',
lineno=0,
frame='',
arguments=list(self._args_inspection_failed(args, kwargs)),
warning=self._show_warnings and 'error parsing code, call stack too shallow',
)
function = call_frame.f_code.co_name
from pathlib import Path
path = Path(call_frame.f_code.co_filename)
if path.is_absolute():
# make the path relative
cwd = Path('.').resolve()
try:
path = path.relative_to(cwd)
except ValueError:
# happens if filename path is not within CWD
pass
lineno = call_frame.f_lineno
warning = None
import executing
source = executing.Source.for_frame(call_frame)
if not source.text:
warning = 'no code context for debug call, code inspection impossible'
arguments = list(self._args_inspection_failed(args, kwargs))
else:
ex = source.executing(call_frame)
function = ex.code_qualname()
if not ex.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))
return self.output_class(
filename=str(path),
lineno=lineno,
frame=function,
arguments=arguments,
warning=self._show_warnings and warning,
)
def _args_inspection_failed(self, args, kwargs):
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]':
import ast
func_ast = ex.node
atok = ex.source.asttokens()
for arg, ast_arg in zip(args, func_ast.args):
if isinstance(ast_arg, ast.Name):
yield self.output_class.arg_class(arg, name=ast_arg.id)
else:
name = ' '.join(map(str.strip, atok.get_text(ast_arg).splitlines()))
yield self.output_class.arg_class(arg, name=name)
kw_arg_names = {}
for kw in func_ast.keywords:
if isinstance(kw.value, ast.Name):
kw_arg_names[kw.arg] = kw.value.id
for name, value in kwargs.items():
yield self.output_class.arg_class(value, name=name, variable=kw_arg_names.get(name))
debug = Debug()