Skip to content

Commit f24fa21

Browse files
committed
add breakpoint() and catch getouterframes error
1 parent 4abd287 commit f24fa21

3 files changed

Lines changed: 42 additions & 11 deletions

File tree

devtools/debug.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import ast
22
import inspect
33
import os
4+
import pdb
45
import re
56
import warnings
67
from pathlib import Path
78
from textwrap import dedent
8-
from typing import Generator, List, Optional, Tuple
9+
from typing import Generator, List, Optional, Tuple, Type
910

1011
from .ansi import isatty, sformat
1112
from .prettier import PrettyFormat
@@ -99,7 +100,7 @@ def __init__(self, *,
99100
colour: Optional[bool]=None,
100101
highlight: Optional[bool]=None,
101102
frame_context_length: int=50):
102-
self._warnings = self._env_bool(warnings, 'PY_DEVTOOLS_WARNINGS')
103+
self._show_warnings = self._env_bool(warnings, 'PY_DEVTOOLS_WARNINGS')
103104
self._colour = self._env_bool(colour, 'PY_DEVTOOLS_COLOUR')
104105
self._highlight = self._env_bool(highlight, 'PY_DEVTOOLS_HIGHLIGHT')
105106
# 50 lines should be enough to make sure we always get the entire function definition
@@ -121,10 +122,24 @@ def __call__(self, *args, file_=None, flush_=True, **kwargs) -> None:
121122
def format(self, *args, **kwargs) -> DebugOutput:
122123
return self._process(args, kwargs, r'debug.format *\(')
123124

125+
def breakpoint(self):
126+
pdb.Pdb(skip=['devtools.*']).set_trace()
127+
124128
def _process(self, args, kwargs, func_regex) -> DebugOutput:
125129
curframe = inspect.currentframe()
126-
frames = inspect.getouterframes(curframe, context=self._frame_context_length)
127-
# BEWARE: this must be call by a method which in turn is called "directly" for the frame to be correct
130+
try:
131+
frames = inspect.getouterframes(curframe, context=self._frame_context_length)
132+
except IndexError as e:
133+
# NOTICE: we should really catch all conceivable errors here, if you find one please report.
134+
# IndexError happens in odd situations such as code called from within jinja templates
135+
self._warn('error parsing code, {0.__class__.__name__}: {0}'.format(e), SyntaxWarning)
136+
return self.output_class(
137+
filename='<unknown>',
138+
lineno=0,
139+
frame='',
140+
arguments=list(self._args_inspection_failed(args, kwargs))
141+
)
142+
# BEWARE: this must be called by a method which in turn is called "directly" for the frame to be correct
128143
call_frame = frames[2]
129144

130145
filename = call_frame.filename
@@ -145,8 +160,7 @@ def _process(self, args, kwargs, func_regex) -> DebugOutput:
145160
arguments = list(self._args_inspection_failed(args, kwargs))
146161
else:
147162
lineno = call_frame.lineno
148-
if self._warnings:
149-
warnings.warn('no code context for debug call, code inspection impossible', RuntimeWarning)
163+
self._warn('no code context for debug call, code inspection impossible')
150164
arguments = list(self._args_inspection_failed(args, kwargs))
151165

152166
return self.output_class(
@@ -226,13 +240,11 @@ def _parse_code(self, call_frame, func_regex, filename) -> Tuple[Optional[ast.AS
226240
break
227241

228242
if not func_ast:
229-
if self._warnings:
230-
warnings.warn('error passing code:\n"{}"\nError: {}'.format(original_code, e1), SyntaxWarning)
243+
self._warn('error passing code:\n"{}"\nError: {}'.format(original_code, e1), SyntaxWarning)
231244
return None, None, lineno
232245

233246
if not isinstance(func_ast, ast.Call):
234-
if self._warnings:
235-
warnings.warn('error passing code, found {} not Call'.format(func_ast.__class__), SyntaxWarning)
247+
self._warn('error passing code, found {} not Call'.format(func_ast.__class__), SyntaxWarning)
236248
return None, None, lineno
237249

238250
code_lines = [l for l in code.split('\n') if l]
@@ -253,5 +265,9 @@ def _get_offsets(cls, func_ast):
253265
for kw in func_ast.keywords:
254266
yield kw.value.lineno - 1, kw.value.col_offset - len(kw.arg) - 1
255267

268+
def _warn(self, msg, category: Type[Warning]=RuntimeWarning):
269+
if self._show_warnings:
270+
warnings.warn(msg, category)
271+
256272

257273
debug = Debug()

devtools/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
__all__ = ['VERSION']
44

5-
VERSION = StrictVersion('0.1.0')
5+
VERSION = StrictVersion('0.1.1')

tests/test_main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,18 @@ def test_colours():
208208
assert s.startswith('\x1b[35mtests'), repr(s)
209209
s2 = strip_ansi(s)
210210
assert s2 == v.str(), repr(s2)
211+
212+
213+
def test_inspect_error(mocker):
214+
mocked_getouterframes = mocker.patch('inspect.getouterframes')
215+
mocked_getouterframes.side_effect = IndexError()
216+
with pytest.warns(SyntaxWarning):
217+
v = debug.format('x')
218+
assert str(v) == "<unknown>:0 \n 'x' (str) len=1"
219+
220+
221+
def test_breakpoint(mocker):
222+
# not much else we can do here
223+
mocked_set_trace = mocker.patch('pdb.Pdb.set_trace')
224+
debug.breakpoint()
225+
assert mocked_set_trace.called

0 commit comments

Comments
 (0)