Skip to content

Commit b68445c

Browse files
committed
test with smaller call stack
1 parent dcd7a2b commit b68445c

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

devtools/debug.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ast
22
import inspect
3+
import os
34
import re
45
import warnings
56
from pathlib import Path
@@ -10,6 +11,10 @@
1011
CWD = Path('.').resolve()
1112

1213

14+
def env_true(var_name, alt='FALSE'):
15+
return os.getenv(var_name, alt).upper() in {'1', 'TRUE'}
16+
17+
1318
class DebugArgument:
1419
__slots__ = 'value', 'name', 'extra'
1520

@@ -64,14 +69,20 @@ def __repr__(self) -> str:
6469

6570
class Debug:
6671
output_class = DebugOutput
67-
# 50 lines should be enough to make sure we always get the entire function definition
68-
frame_context_length = 50
6972
complex_nodes = (
7073
ast.Call, ast.Attribute,
7174
ast.IfExp, ast.BoolOp, ast.BinOp, ast.Compare,
7275
ast.DictComp, ast.ListComp, ast.SetComp, ast.GeneratorExp
7376
)
7477

78+
def __init__(self, *, warnings: Optional[bool]=None, frame_context_length: int=50):
79+
if warnings is None:
80+
self._warnings = env_true('PY_DEVTOOLS_WARNINGS', 'TRUE')
81+
else:
82+
self._warnings = warnings
83+
# 50 lines should be enough to make sure we always get the entire function definition
84+
self._frame_context_length = frame_context_length
85+
7586
def __call__(self, *args, **kwargs):
7687
print(self._process(args, kwargs, r'debug *\('), flush=True)
7788

@@ -80,7 +91,7 @@ def format(self, *args, **kwargs):
8091

8192
def _process(self, args, kwargs, func_regex):
8293
curframe = inspect.currentframe()
83-
frames = inspect.getouterframes(curframe, context=self.frame_context_length)
94+
frames = inspect.getouterframes(curframe, context=self._frame_context_length)
8495
# BEWARE: this must be call by a method which in turn is called "directly" for the frame to be correct
8596
call_frame = frames[2]
8697

@@ -102,7 +113,8 @@ def _process(self, args, kwargs, func_regex):
102113
arguments = list(self._args_inspection_failed(args, kwargs))
103114
else:
104115
lineno = call_frame.lineno
105-
warnings.warn('no code context for debug call, code inspection impossible', RuntimeWarning)
116+
if self._warnings:
117+
warnings.warn('no code context for debug call, code inspection impossible', RuntimeWarning)
106118
arguments = list(self._args_inspection_failed(args, kwargs))
107119

108120
return self.output_class(
@@ -182,9 +194,15 @@ def _parse_code(self, call_frame, func_regex, filename) -> Tuple[Optional[ast.AS
182194
break
183195

184196
if not func_ast:
185-
warnings.warn('error passing code:\n"{}"\nError: {}'.format(original_code, e1), SyntaxWarning)
197+
if self._warnings:
198+
warnings.warn('error passing code:\n"{}"\nError: {}'.format(original_code, e1), SyntaxWarning)
186199
return None, None, lineno
187200

201+
if not isinstance(func_ast, ast.Call):
202+
if self._warnings:
203+
warnings.warn('error passing code, found {} not Call'.format(func_ast.__class__), SyntaxWarning)
204+
return None, None, lineno
205+
188206
code_lines = [l for l in code.split('\n') if l]
189207
# this removes the trailing bracket from the lines of code meaning it doesn't appear in the
190208
# representation of the last argument

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ filterwarnings =
88

99
[flake8]
1010
max-line-length = 120
11-
max-complexity = 10
11+
max-complexity = 12
1212

1313
[bdist_wheel]
1414
python-tag = py35,py36

tests/test_expr_render.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from devtools import debug
6+
from devtools import Debug, debug
77

88

99
def foobar(a, b, c):
@@ -167,3 +167,22 @@ def test_syntax_warning():
167167
assert (
168168
'tests/test_expr_render.py:<line no> test_syntax_warning: 1 (int)'
169169
) == s
170+
171+
172+
def test_no_syntax_warning():
173+
# exceed the 4 extra lines which are normally checked
174+
debug_ = Debug(warnings=False)
175+
with pytest.warns(None) as warning_checker:
176+
v = debug_.format(
177+
abs(
178+
abs(
179+
abs(
180+
abs(
181+
-1
182+
)
183+
)
184+
)
185+
)
186+
)
187+
assert 'test_no_syntax_warning: 1 (int)' in str(v)
188+
assert len(warning_checker) == 0

tests/test_main.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77

8-
from devtools import debug
8+
from devtools import Debug, debug
99

1010

1111
def test_print(capsys):
@@ -63,6 +63,45 @@ def test_func(v):
6363
)
6464

6565

66+
def test_odd_path(mocker):
67+
# all valid calls
68+
mocked_relative_to = mocker.patch('pathlib.Path.relative_to')
69+
mocked_relative_to.side_effect = ValueError()
70+
v = debug.format('test')
71+
assert re.search('/.*?/test_main.py:\d{2,} test_odd_path: "test" \(str\) len=4', str(v)), v
72+
73+
74+
def test_small_call_frame():
75+
debug_ = Debug(warnings=False, frame_context_length=2)
76+
v = debug_.format(
77+
1,
78+
2,
79+
3,
80+
)
81+
assert re.sub(':\d{2,}', ':<line no>', str(v)) == (
82+
'tests/test_main.py:<line no> test_small_call_frame\n'
83+
' 1 (int)\n'
84+
' 2 (int)\n'
85+
' 3 (int)'
86+
)
87+
88+
89+
def test_small_call_frame_warning():
90+
debug_ = Debug(frame_context_length=2)
91+
with pytest.warns(SyntaxWarning):
92+
v = debug_.format(
93+
1,
94+
2,
95+
3,
96+
)
97+
assert re.sub(':\d{2,}', ':<line no>', str(v)) == (
98+
'tests/test_main.py:<line no> test_small_call_frame_warning\n'
99+
' 1 (int)\n'
100+
' 2 (int)\n'
101+
' 3 (int)'
102+
)
103+
104+
66105
@pytest.mark.skipif(sys.version_info < (3, 6), reason='kwarg order is not guaranteed for 3.5')
67106
def test_kwargs():
68107
a = 'variable'
@@ -122,6 +161,16 @@ def test_eval():
122161
assert str(v) == '<string>:1 <module>: 1 (int)'
123162

124163

164+
def test_warnings_disabled():
165+
debug_ = Debug(warnings=False)
166+
with pytest.warns(None) as warnings:
167+
v1 = eval('debug_.format(1)')
168+
assert str(v1) == '<string>:1 <module>: 1 (int)'
169+
v2 = debug_.format(1)
170+
assert 'test_warnings_disabled: 1 (int)' in str(v2)
171+
assert len(warnings) == 0
172+
173+
125174
@pytest.mark.skipif(sys.version_info < (3, 6), reason='kwarg order is not guaranteed for 3.5')
126175
def test_eval_kwargs():
127176
with pytest.warns(RuntimeWarning):

0 commit comments

Comments
 (0)