diff --git a/README.md b/README.md index 7584f73..32b6573 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/pysnooper/tracer.py b/pysnooper/tracer.py index fb6eddc..9b090a8 100644 --- a/pysnooper/tracer.py +++ b/pysnooper/tracer.py @@ -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. diff --git a/tests/samples/exception.py b/tests/samples/exception.py index 8b36042..aa663de 100644 --- a/tests/samples/exception.py +++ b/tests/samples/exception.py @@ -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() diff --git a/tests/samples/indentation.py b/tests/samples/indentation.py index ed254ca..9ea0318 100644 --- a/tests/samples/indentation.py +++ b/tests/samples/indentation.py @@ -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(): diff --git a/tests/samples/recursion.py b/tests/samples/recursion.py index b4a78f3..1570d1a 100644 --- a/tests/samples/recursion.py +++ b/tests/samples/recursion.py @@ -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: diff --git a/tests/test_chinese.py b/tests/test_chinese.py index e92b15a..828ad1c 100644 --- a/tests/test_chinese.py +++ b/tests/test_chinese.py @@ -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'), diff --git a/tests/test_multiple_files/__init__.py b/tests/test_multiple_files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_multiple_files/multiple_files/__init__.py b/tests/test_multiple_files/multiple_files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_multiple_files/multiple_files/bar.py b/tests/test_multiple_files/multiple_files/bar.py new file mode 100644 index 0000000..a3e6dfd --- /dev/null +++ b/tests/test_multiple_files/multiple_files/bar.py @@ -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 diff --git a/tests/test_multiple_files/multiple_files/foo.py b/tests/test_multiple_files/multiple_files/foo.py new file mode 100644 index 0000000..a5525f1 --- /dev/null +++ b/tests/test_multiple_files/multiple_files/foo.py @@ -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 \ No newline at end of file diff --git a/tests/test_multiple_files/test_multiple_files.py b/tests/test_multiple_files/test_multiple_files.py new file mode 100644 index 0000000..5bd233a --- /dev/null +++ b/tests/test_multiple_files/test_multiple_files.py @@ -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(), + ) + ) + + diff --git a/tests/test_pysnooper.py b/tests/test_pysnooper.py index 930dd5d..b6f03e1 100644 --- a/tests/test_pysnooper.py +++ b/tests/test_pysnooper.py @@ -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(), diff --git a/tests/utils.py b/tests/utils.py index 5772f8b..6c03e74 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -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) + +