Skip to content

Commit 9ed64c2

Browse files
authored
changes to warnings (#30)
* changes to warnings * fix tests * more tests * improve coverage
1 parent 5dabb18 commit 9ed64c2

File tree

4 files changed

+72
-69
lines changed

4 files changed

+72
-69
lines changed

devtools/debug.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import os
44
import pdb
55
import re
6-
import warnings
76
from pathlib import Path
87
from textwrap import dedent, indent
9-
from typing import Generator, List, Optional, Tuple, Type
8+
from typing import Generator, List, Optional, Tuple
109

1110
from .ansi import isatty, sformat
1211
from .prettier import PrettyFormat, env_true
@@ -57,13 +56,14 @@ class DebugOutput:
5756
Represents the output of a debug command.
5857
"""
5958
arg_class = DebugArgument
60-
__slots__ = 'filename', 'lineno', 'frame', 'arguments'
59+
__slots__ = 'filename', 'lineno', 'frame', 'arguments', 'warning'
6160

62-
def __init__(self, *, filename: str, lineno: int, frame: str, arguments: List[DebugArgument]):
61+
def __init__(self, *, filename: str, lineno: int, frame: str, arguments: List[DebugArgument], warning=None):
6362
self.filename = filename
6463
self.lineno = lineno
6564
self.frame = frame
6665
self.arguments = arguments
66+
self.warning = warning
6767

6868
def str(self, highlight=False) -> str:
6969
if highlight:
@@ -72,8 +72,12 @@ def str(self, highlight=False) -> str:
7272
sformat(self.lineno, sformat.green),
7373
sformat(self.frame, sformat.green, sformat.italic)
7474
)
75+
if self.warning:
76+
prefix += sformat(' ({})'.format(self.warning), sformat.dim)
7577
else:
7678
prefix = '{0.filename}:{0.lineno} {0.frame}'.format(self)
79+
if self.warning:
80+
prefix += ' ({})'.format(self.warning)
7781
return prefix + '\n ' + '\n '.join(a.str(highlight) for a in self.arguments)
7882

7983
def __str__(self) -> str:
@@ -127,15 +131,15 @@ def _process(self, args, kwargs, func_regex) -> DebugOutput:
127131
curframe = inspect.currentframe()
128132
try:
129133
frames = inspect.getouterframes(curframe, context=self._frame_context_length)
130-
except IndexError as e:
134+
except IndexError:
131135
# NOTICE: we should really catch all conceivable errors here, if you find one please report.
132136
# IndexError happens in odd situations such as code called from within jinja templates
133-
self._warn('error parsing code, {0.__class__.__name__}: {0}'.format(e), SyntaxWarning)
134137
return self.output_class(
135138
filename='<unknown>',
136139
lineno=0,
137140
frame='',
138-
arguments=list(self._args_inspection_failed(args, kwargs))
141+
arguments=list(self._args_inspection_failed(args, kwargs)),
142+
warning=self._show_warnings and 'error parsing code, IndexError',
139143
)
140144
# BEWARE: this must be called by a method which in turn is called "directly" for the frame to be correct
141145
call_frame = frames[2]
@@ -150,22 +154,23 @@ def _process(self, args, kwargs, func_regex) -> DebugOutput:
150154
pass
151155

152156
if call_frame.code_context:
153-
func_ast, code_lines, lineno = self._parse_code(call_frame, func_regex, filename)
157+
func_ast, code_lines, lineno, warning = self._parse_code(call_frame, func_regex, filename)
154158
if func_ast:
155159
arguments = list(self._process_args(func_ast, code_lines, args, kwargs))
156160
else:
157161
# parsing failed
158162
arguments = list(self._args_inspection_failed(args, kwargs))
159163
else:
160164
lineno = call_frame.lineno
161-
self._warn('no code context for debug call, code inspection impossible')
165+
warning = 'no code context for debug call, code inspection impossible'
162166
arguments = list(self._args_inspection_failed(args, kwargs))
163167

164168
return self.output_class(
165169
filename=filename,
166170
lineno=lineno,
167171
frame=call_frame.function,
168-
arguments=arguments
172+
arguments=arguments,
173+
warning=self._show_warnings and warning,
169174
)
170175

171176
def _args_inspection_failed(self, args, kwargs):
@@ -206,17 +211,22 @@ def _process_args(self, func_ast, code_lines, args, kwargs) -> Generator[DebugAr
206211
for name, value in kwargs.items():
207212
yield self.output_class.arg_class(value, name=name, variable=kw_arg_names.get(name))
208213

209-
def _parse_code(self, call_frame, func_regex, filename) -> Tuple[Optional[ast.AST], Optional[List[str]], int]:
214+
def _parse_code(
215+
self, call_frame, func_regex, filename
216+
) -> Tuple[Optional[ast.AST], Optional[List[str]], int, Optional[str]]:
210217
call_lines = []
211218
for line in range(call_frame.index, -1, -1):
212-
new_line = call_frame.code_context[line]
219+
try:
220+
new_line = call_frame.code_context[line]
221+
except IndexError: # pragma: no cover
222+
return None, None, line, 'error passing code. line not found'
213223
call_lines.append(new_line)
214224
if re.search(func_regex, new_line):
215225
break
216226
call_lines.reverse()
217227
lineno = call_frame.lineno - len(call_lines) + 1
218228

219-
original_code = code = dedent(''.join(call_lines))
229+
code = dedent(''.join(call_lines))
220230
func_ast = None
221231
tail_index = call_frame.index
222232
try:
@@ -238,18 +248,16 @@ def _parse_code(self, call_frame, func_regex, filename) -> Tuple[Optional[ast.AS
238248
break
239249

240250
if not func_ast:
241-
self._warn('error passing code:\n"{}"\nError: {}'.format(original_code, e1), SyntaxWarning)
242-
return None, None, lineno
251+
return None, None, lineno, 'error passing code. Error: {}'.format(e1)
243252

244253
if not isinstance(func_ast, ast.Call):
245-
self._warn('error passing code, found {} not Call'.format(func_ast.__class__), SyntaxWarning)
246-
return None, None, lineno
254+
return None, None, lineno, 'error passing code, found {} not Call'.format(func_ast.__class__)
247255

248256
code_lines = [l for l in code.split('\n') if l]
249257
# this removes the trailing bracket from the lines of code meaning it doesn't appear in the
250258
# representation of the last argument
251259
code_lines[-1] = code_lines[-1][:-1]
252-
return func_ast, code_lines, lineno
260+
return func_ast, code_lines, lineno, None
253261

254262
@staticmethod
255263
def _wrap_parse(code, filename):
@@ -271,9 +279,5 @@ def _get_offsets(func_ast):
271279
for kw in func_ast.keywords:
272280
yield kw.value.lineno - 2, kw.value.col_offset - len(kw.arg) - 2
273281

274-
def _warn(self, msg, category: Type[Warning] = RuntimeWarning):
275-
if self._show_warnings:
276-
warnings.warn(msg, category)
277-
278282

279283
debug = Debug()

tests/test_expr_render.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -169,47 +169,39 @@ def test_multiple_trailing_lines():
169169

170170
def test_syntax_warning():
171171
# exceed the 4 extra lines which are normally checked
172-
with pytest.warns(SyntaxWarning) as warning_checker:
173-
v = debug.format(
172+
v = debug.format(
173+
abs(
174174
abs(
175175
abs(
176176
abs(
177-
abs(
178-
-1
179-
)
177+
-1
180178
)
181179
)
182180
)
183181
)
184-
assert len(warning_checker) == 1
185-
warning = warning_checker.list[0]
186-
print(warning.message)
187-
assert 'Error: unexpected EOF while parsing (test_expr_render.py' in str(warning.message)
182+
)
188183
# check only the original code is included in the warning
189-
assert '-1\n"' in str(warning.message)
190184
s = re.sub(r':\d{2,}', ':<line no>', str(v))
191-
assert (
192-
'tests/test_expr_render.py:<line no> test_syntax_warning\n 1 (int)'
193-
) == s
185+
assert s.startswith('tests/test_expr_render.py:<line no> test_syntax_warning (error passing code. '
186+
'Error: unexpected EOF while')
194187

195188

196189
def test_no_syntax_warning():
197190
# exceed the 4 extra lines which are normally checked
198191
debug_ = Debug(warnings=False)
199-
with pytest.warns(None) as warning_checker:
200-
v = debug_.format(
192+
v = debug_.format(
193+
abs(
201194
abs(
202195
abs(
203196
abs(
204-
abs(
205-
-1
206-
)
197+
-1
207198
)
208199
)
209200
)
210201
)
211-
assert 'test_no_syntax_warning\n 1 (int)' in str(v)
212-
assert len(warning_checker) == 0
202+
)
203+
assert '(error passing code' not in str(v)
204+
assert 'test_no_syntax_warning' in str(v)
213205

214206

215207
def test_await():

tests/test_main.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ def test_small_call_frame():
9090

9191
def test_small_call_frame_warning():
9292
debug_ = Debug(frame_context_length=2)
93-
with pytest.warns(SyntaxWarning):
94-
v = debug_.format(
95-
1,
96-
2,
97-
3,
98-
)
93+
v = debug_.format(
94+
1,
95+
2,
96+
3,
97+
)
9998
assert re.sub(r':\d{2,}', ':<line no>', str(v)) == (
100-
'tests/test_main.py:<line no> test_small_call_frame_warning\n'
101-
' 1 (int)\n'
102-
' 2 (int)\n'
103-
' 3 (int)'
99+
"tests/test_main.py:<line no> test_small_call_frame_warning "
100+
"(error passing code, found <class '_ast.Tuple'> not Call)\n"
101+
" 1 (int)\n"
102+
" 2 (int)\n"
103+
" 3 (int)"
104104
)
105105

106106

@@ -157,10 +157,9 @@ class Bar:
157157

158158

159159
def test_eval():
160-
with pytest.warns(RuntimeWarning):
161-
v = eval('debug.format(1)')
160+
v = eval('debug.format(1)')
162161

163-
assert str(v) == '<string>:1 <module>\n 1 (int)'
162+
assert str(v) == '<string>:1 <module> (no code context for debug call, code inspection impossible)\n 1 (int)'
164163

165164

166165
def test_warnings_disabled():
@@ -174,27 +173,25 @@ def test_warnings_disabled():
174173

175174

176175
def test_eval_kwargs():
177-
with pytest.warns(RuntimeWarning):
178-
v = eval('debug.format(1, apple="pear")')
176+
v = eval('debug.format(1, apple="pear")')
179177

180178
assert set(str(v).split('\n')) == {
181-
"<string>:1 <module>",
179+
"<string>:1 <module> (no code context for debug call, code inspection impossible)",
182180
" 1 (int)",
183181
" apple: 'pear' (str) len=4",
184182
}
185183

186184

187185
def test_exec(capsys):
188-
with pytest.warns(RuntimeWarning):
189-
exec(
190-
'a = 1\n'
191-
'b = 2\n'
192-
'debug(b, a + b)'
193-
)
186+
exec(
187+
'a = 1\n'
188+
'b = 2\n'
189+
'debug(b, a + b)'
190+
)
194191

195192
stdout, stderr = capsys.readouterr()
196193
assert stdout == (
197-
'<string>:3 <module>\n'
194+
'<string>:3 <module> (no code context for debug call, code inspection impossible)\n'
198195
' 2 (int)\n'
199196
' 3 (int)\n'
200197
)
@@ -209,12 +206,21 @@ def test_colours():
209206
assert s2 == v.str(), repr(s2)
210207

211208

209+
def test_colours_warnings(mocker):
210+
mocked_getouterframes = mocker.patch('inspect.getouterframes')
211+
mocked_getouterframes.side_effect = IndexError()
212+
v = debug.format('x')
213+
s = re.sub(r':\d{2,}', ':<line no>', v.str(True))
214+
assert s.startswith('\x1b[35m<unknown>'), repr(s)
215+
s2 = strip_ansi(s)
216+
assert s2 == v.str(), repr(s2)
217+
218+
212219
def test_inspect_error(mocker):
213220
mocked_getouterframes = mocker.patch('inspect.getouterframes')
214221
mocked_getouterframes.side_effect = IndexError()
215-
with pytest.warns(SyntaxWarning):
216-
v = debug.format('x')
217-
assert str(v) == "<unknown>:0 \n 'x' (str) len=1"
222+
v = debug.format('x')
223+
assert str(v) == "<unknown>:0 (error parsing code, IndexError)\n 'x' (str) len=1"
218224

219225

220226
def test_breakpoint(mocker):

tests/test_timer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import io
2+
import re
23
from time import sleep
34

45
import pytest
@@ -12,7 +13,7 @@ def test_simple():
1213
with t:
1314
sleep(0.01)
1415
v = f.getvalue()
15-
assert v == 'foobar: 0.010s elapsed\n'
16+
assert re.fullmatch(r'foobar: 0\.01[012]s elapsed\n', v)
1617

1718

1819
def test_multiple():

0 commit comments

Comments
 (0)