Show source path, especially when multiple files

This commit is contained in:
Ram Rachum 2019-08-09 18:09:56 +03:00
parent 7392765ada
commit 297b3cd8d7
13 changed files with 161 additions and 20 deletions

View file

@ -37,6 +37,7 @@ number_to_bits(6)
The output to stderr is:
```
Source path:... /my_code/foo.py
Starting var:.. number = 6
15:29:11.327032 call 4 def number_to_bits(number):
15:29:11.327032 line 5 if number:

View file

@ -40,16 +40,16 @@ class UnavailableSource(object):
return u'SOURCE IS UNAVAILABLE'
source_cache = {}
source_and_path_cache = {}
def get_source_from_frame(frame):
def get_path_and_source_from_frame(frame):
globs = frame.f_globals or {}
module_name = globs.get('__name__')
file_name = frame.f_code.co_filename
cache_key = (module_name, file_name)
try:
return source_cache[cache_key]
return source_and_path_cache[cache_key]
except KeyError:
pass
loader = globs.get('__loader__')
@ -98,8 +98,9 @@ def get_source_from_frame(frame):
source = [pycompat.text_type(sline, encoding, 'replace') for sline in
source]
source_cache[cache_key] = source
return source
result = (file_name, source)
source_and_path_cache[cache_key] = result
return result
def get_write_function(output, overwrite):
@ -218,6 +219,7 @@ class Tracer:
pycompat.collections_abc.Iterable) for x in custom_repr):
custom_repr = (custom_repr,)
self.custom_repr = custom_repr
self.last_source_path = None
def __call__(self, function):
if DISABLED:
@ -323,6 +325,21 @@ class Tracer:
# #
### Finished checking whether we should trace this line. ##############
now_string = datetime_module.datetime.now().time().isoformat()
line_no = frame.f_lineno
source_path, source = get_path_and_source_from_frame(frame)
if self.last_source_path != source_path:
self.write(u'{indent}Source path:... {source_path}'.
format(**locals()))
self.last_source_path = source_path
source_line = source[line_no - 1]
thread_info = ""
if self.thread_info:
current_thread = threading.current_thread()
thread_info = "{ident}-{name} ".format(
ident=current_thread.ident, name=current_thread.getName())
thread_info = self.set_thread_info_padding(thread_info)
### Reporting newish and modified variables: ##########################
# #
old_local_reprs = self.frame_to_local_reprs.get(frame, {})
@ -343,15 +360,6 @@ class Tracer:
# #
### Finished newish and modified variables. ###########################
now_string = datetime_module.datetime.now().time().isoformat()
line_no = frame.f_lineno
source_line = get_source_from_frame(frame)[line_no - 1]
thread_info = ""
if self.thread_info:
current_thread = threading.current_thread()
thread_info = "{ident}-{name} ".format(
ident=current_thread.ident, name=current_thread.getName())
thread_info = self.set_thread_info_padding(thread_info)
### Dealing with misplaced function definition: #######################
# #
@ -360,8 +368,7 @@ class Tracer:
# function definition is found.
for candidate_line_no in itertools.count(line_no):
try:
candidate_source_line = \
get_source_from_frame(frame)[candidate_line_no - 1]
candidate_source_line = source[candidate_line_no - 1]
except IndexError:
# End of source file reached without finding a function
# definition. Fall back to original source line.

View file

@ -22,6 +22,7 @@ def main():
expected_output = '''
Source path:... Whatever
12:18:08.017782 call 17 def main():
12:18:08.018142 line 18 try:
12:18:08.018181 line 19 bar()

View file

@ -24,10 +24,12 @@ def f5():
expected_output = '''
Source path:... Whatever
21:10:42.298924 call 5 def main():
21:10:42.299158 line 6 f2()
21:10:42.299205 call 9 def f2():
21:10:42.299246 line 10 f3()
Source path:... Whatever
21:10:42.299305 call 18 def f4():
21:10:42.299348 line 19 f5()
21:10:42.299386 call 22 def f5():

View file

@ -14,8 +14,9 @@ def mul(a, b):
def main():
factorial(4)
expected_output = '''
Source path:... Whatever
Starting var:.. x = 4
20:28:17.875295 call 5 def factorial(x):
20:28:17.875509 line 6 if x <= 1:

View file

@ -16,7 +16,7 @@ from pysnooper import pycompat
from pysnooper.variables import needs_parentheses
from .utils import (assert_output, assert_sample_output, VariableEntry,
CallEntry, LineEntry, ReturnEntry, OpcodeEntry,
ReturnValueEntry, ExceptionEntry)
ReturnValueEntry, ExceptionEntry, SourcePathEntry)
from . import mini_toolbox
@ -36,6 +36,7 @@ def test_chinese():
assert_output(
output,
(
SourcePathEntry(),
CallEntry(),
LineEntry(),
VariableEntry('a'),

View file

View file

@ -0,0 +1,6 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
def bar_function(y):
x = 7 * y
return x

View file

@ -0,0 +1,11 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
import pysnooper
from .bar import bar_function
@pysnooper.snoop(depth=2)
def foo_function():
z = bar_function(3)
return z

View file

@ -0,0 +1,51 @@
# Copyright 2019 Ram Rachum and collaborators.
# This program is distributed under the MIT license.
import io
import textwrap
import threading
import types
import os
import sys
from pysnooper.utils import truncate
import pytest
import pysnooper
from pysnooper.variables import needs_parentheses
from ..utils import (assert_output, assert_sample_output, VariableEntry,
CallEntry, LineEntry, ReturnEntry, OpcodeEntry,
ReturnValueEntry, ExceptionEntry, SourcePathEntry)
from .. import mini_toolbox
from .multiple_files import foo
def test_multiple_files():
with mini_toolbox.OutputCapturer(stdout=False,
stderr=True) as output_capturer:
result = foo.foo_function()
assert result == 21
output = output_capturer.string_io.getvalue()
assert_output(
output,
(
SourcePathEntry(source_path_regex=r'.*foo\.py$'),
CallEntry(),
LineEntry(),
SourcePathEntry(source_path_regex=r'.*bar\.py$'),
VariableEntry(),
CallEntry(),
LineEntry(),
VariableEntry(),
LineEntry(),
ReturnEntry(),
ReturnValueEntry(),
SourcePathEntry(source_path_regex=r'.*foo\.py$'),
VariableEntry(),
LineEntry(),
ReturnEntry(),
ReturnValueEntry(),
)
)

View file

@ -15,7 +15,7 @@ import pysnooper
from pysnooper.variables import needs_parentheses
from .utils import (assert_output, assert_sample_output, VariableEntry,
CallEntry, LineEntry, ReturnEntry, OpcodeEntry,
ReturnValueEntry, ExceptionEntry)
ReturnValueEntry, ExceptionEntry, SourcePathEntry)
from . import mini_toolbox
@ -34,6 +34,7 @@ def test_string_io():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry('x = 7'),
@ -63,6 +64,7 @@ def test_thread_info():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry('x = 7'),
@ -105,6 +107,7 @@ def test_multi_thread_info():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):',
thread_info_regex=thread_info_regex.format(
@ -174,6 +177,7 @@ def test_callable():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry('x = 7'),
@ -215,6 +219,7 @@ def test_watch():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('Foo'),
VariableEntry('io.__name__', "'io'"),
CallEntry('def my_function():'),
@ -261,6 +266,7 @@ def test_watch_explode():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('Foo'),
CallEntry('def my_function():'),
LineEntry(),
@ -315,6 +321,7 @@ def test_variables_classes():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('WithSlots'),
CallEntry('def my_function():'),
LineEntry(),
@ -360,6 +367,7 @@ def test_single_watch_no_comma():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('Foo'),
CallEntry('def my_function():'),
LineEntry('foo = Foo()'),
@ -392,6 +400,7 @@ def test_long_variable():
assert_output(
output,
(
SourcePathEntry(),
CallEntry('def my_function():'),
LineEntry('foo = list(range(1000))'),
VariableEntry('foo', value_regex=regex),
@ -419,6 +428,7 @@ def test_repr_exception():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('Bad'),
CallEntry('def my_function():'),
LineEntry('bad = Bad()'),
@ -455,6 +465,7 @@ def test_depth():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry(),
VariableEntry(),
CallEntry('def f1(x1):'),
@ -510,6 +521,7 @@ def test_method_and_prefix():
assert_output(
output,
(
SourcePathEntry(prefix='ZZZ'),
VariableEntry('self', prefix='ZZZ'),
VariableEntry('self.x', '2', prefix='ZZZ'),
CallEntry('def square(self):', prefix='ZZZ'),
@ -542,6 +554,7 @@ def test_file_output():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('_foo', value_regex="u?'baba'"),
CallEntry('def my_function(_foo):'),
LineEntry('x = 7'),
@ -577,6 +590,7 @@ def test_confusing_decorator_lines():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry(),
@ -606,6 +620,7 @@ def test_lambda():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('x', '7'),
CallEntry(source_regex='^my_function = pysnooper.*'),
LineEntry(source_regex='^my_function = pysnooper.*'),
@ -638,6 +653,7 @@ def test_unavailable_source():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry(stage='starting'),
CallEntry('SOURCE IS UNAVAILABLE'),
LineEntry('SOURCE IS UNAVAILABLE'),
@ -666,6 +682,7 @@ def test_no_overwrite_by_default():
assert_output(
shortened_output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry('x = 7'),
@ -698,6 +715,7 @@ def test_overwrite():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('foo', value_regex="u?'baba'"),
CallEntry('def my_function(foo):'),
LineEntry('x = 7'),
@ -793,6 +811,7 @@ def test_with_block():
output,
(
# In first with
SourcePathEntry(),
VariableEntry('x', '2'),
VariableEntry('bar1'),
VariableEntry('bar2'),
@ -897,6 +916,7 @@ def test_with_block_depth():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry(),
VariableEntry(),
VariableEntry(),
@ -948,6 +968,7 @@ def test_cellvars():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry(),
VariableEntry(),
VariableEntry(),
@ -1002,6 +1023,7 @@ def test_var_order():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry(),
VariableEntry(),
@ -1087,6 +1109,7 @@ def test_generator():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('x1', '0'),
VariableEntry(),
CallEntry(),
@ -1162,6 +1185,7 @@ def test_custom_repr():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('x', '10000'),
CallEntry(),
LineEntry(),
@ -1188,6 +1212,7 @@ def test_custom_repr_single():
assert_output(
output,
(
SourcePathEntry(),
VariableEntry('x', '10000'),
CallEntry(),
LineEntry(),

View file

@ -167,6 +167,31 @@ class ReturnValueEntry(_BaseValueEntry):
else:
return True
class SourcePathEntry(_BaseValueEntry):
def __init__(self, source_path=None, source_path_regex=None, prefix=''):
_BaseValueEntry.__init__(self, prefix=prefix)
if source_path is not None:
assert source_path_regex is None
self.source_path = source_path
self.source_path_regex = (None if source_path_regex is None else
re.compile(source_path_regex))
_preamble_pattern = re.compile(
r"""^Source path$"""
)
def _check_preamble(self, preamble):
return bool(self._preamble_pattern.match(preamble))
def _check_content(self, source_path):
if self.source_path is not None:
return source_path == self.source_path
elif self.source_path_regex is not None:
return self.source_path_regex.match(source_path)
else:
return True
class _BaseEventEntry(_BaseEntry):
def __init__(self, source=None, source_regex=None, thread_info=None,
@ -278,7 +303,15 @@ def assert_sample_output(module):
time_pattern = re.sub(r'\d', r'\\d', time)
def normalise(out):
return re.sub(time_pattern, time, out).strip()
out = re.sub(time_pattern, time, out).strip()
out = re.sub(
r'^( *)Source path:\.\.\. .*$',
r'\1Source path:... Whatever',
out,
flags=re.MULTILINE
)
return out
output = output_capturer.string_io.getvalue()
@ -290,3 +323,5 @@ def assert_sample_output(module):
except AssertionError:
print('\n\nActual Output:\n\n' + output) # to copy paste into expected_output
raise # show pytest diff (may need -vv flag to see in full)