From 0c5834196a8a305956ee15d4f4e665bbf87cd0f1 Mon Sep 17 00:00:00 2001 From: "Itamar.Raviv" Date: Tue, 19 Nov 2019 18:08:54 +0200 Subject: [PATCH] Add normalize flag to remove machine-specific data This allows for diffing between multiple PySnooper outputs. --- README.md | 6 + pysnooper/tracer.py | 23 ++- pysnooper/utils.py | 13 +- pysnooper/variables.py | 14 +- tests/test_pysnooper.py | 328 ++++++++++++++++++++++++++++------------ tests/utils.py | 29 +++- 6 files changed, 301 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index ac82704..d5fb417 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,12 @@ Start all snoop lines with a prefix, to grep for them easily: @pysnooper.snoop(prefix='ZZZ ') ``` +Remove all machine-related data (paths, timestamps, memory addresses) to compare with other traces easily: + +```python +@pysnooper.snoop(normalize=True) +``` + On multi-threaded apps identify which thread are snooped in output: ```python diff --git a/pysnooper/tracer.py b/pysnooper/tracer.py index 3ea6580..be177bf 100644 --- a/pysnooper/tracer.py +++ b/pysnooper/tracer.py @@ -22,19 +22,19 @@ if pycompat.PY2: ipython_filename_pattern = re.compile('^$') -def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None): +def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None, normalize=False): code = frame.f_code vars_order = (code.co_varnames + code.co_cellvars + code.co_freevars + tuple(frame.f_locals.keys())) result_items = [(key, utils.get_shortish_repr(value, custom_repr, - max_length)) + max_length, normalize)) for key, value in frame.f_locals.items()] result_items.sort(key=lambda key_value: vars_order.index(key_value[0])) result = collections.OrderedDict(result_items) for variable in watch: - result.update(sorted(variable.items(frame))) + result.update(sorted(variable.items(frame, normalize))) return result @@ -201,7 +201,7 @@ class Tracer: ''' def __init__(self, output=None, watch=(), watch_explode=(), depth=1, prefix='', overwrite=False, thread_info=False, custom_repr=(), - max_variable_length=100): + max_variable_length=100, normalize=False): self._write = get_write_function(output, overwrite) self.watch = [ @@ -226,6 +226,7 @@ class Tracer: self.custom_repr = custom_repr self.last_source_path = None self.max_variable_length = max_variable_length + self.normalize = normalize def __call__(self, function_or_class): if DISABLED: @@ -351,9 +352,10 @@ class Tracer: ### Finished checking whether we should trace this line. ############## now = datetime_module.datetime.now().time() - now_string = pycompat.time_isoformat(now, timespec='microseconds') + now_string = pycompat.time_isoformat(now, timespec='microseconds') if not self.normalize else ' ' * 15 line_no = frame.f_lineno source_path, source = get_path_and_source_from_frame(frame) + source_path = source_path if not self.normalize else os.path.basename(source_path) if self.last_source_path != source_path: self.write(u'{indent}Source path:... {source_path}'. format(**locals())) @@ -361,6 +363,9 @@ class Tracer: source_line = source[line_no - 1] thread_info = "" if self.thread_info: + if self.normalize: + raise NotImplementedError("normalize is not supported with " + "thread_info") current_thread = threading.current_thread() thread_info = "{ident}-{name} ".format( ident=current_thread.ident, name=current_thread.getName()) @@ -372,7 +377,9 @@ class Tracer: self.frame_to_local_reprs[frame] = local_reprs = \ get_local_reprs(frame, watch=self.watch, custom_repr=self.custom_repr, - max_length=self.max_variable_length) + max_length=self.max_variable_length, + normalize=self.normalize, + ) newish_string = ('Starting var:.. ' if event == 'call' else 'New var:....... ') @@ -437,7 +444,9 @@ class Tracer: if not ended_by_exception: return_value_repr = utils.get_shortish_repr(arg, custom_repr=self.custom_repr, - max_length=self.max_variable_length) + max_length=self.max_variable_length, + normalize=self.normalize, + ) self.write('{indent}Return value:.. {return_value_repr}'. format(**locals())) diff --git a/pysnooper/utils.py b/pysnooper/utils.py index 15dbc3b..ff9b9e8 100644 --- a/pysnooper/utils.py +++ b/pysnooper/utils.py @@ -2,6 +2,7 @@ # This program is distributed under the MIT license. import abc +import re import sys from .pycompat import ABC, string_types, collections_abc @@ -55,13 +56,23 @@ def get_repr_function(item, custom_repr): return repr -def get_shortish_repr(item, custom_repr=(), max_length=None): +DEFAULT_REPR_RE = re.compile(r' at 0x[a-f0-9A-F]{4,}') + + +def normalize_repr(item_repr): + """Remove memory address (0x...) from a default python repr""" + return DEFAULT_REPR_RE.sub('', item_repr) + + +def get_shortish_repr(item, custom_repr=(), max_length=None, normalize=False): repr_function = get_repr_function(item, custom_repr) try: r = repr_function(item) except Exception: r = 'REPR FAILED' r = r.replace('\r', '').replace('\n', '') + if normalize: + r = normalize_repr(r) if max_length: r = truncate(r, max_length) return r diff --git a/pysnooper/variables.py b/pysnooper/variables.py index fe0176a..2229c38 100644 --- a/pysnooper/variables.py +++ b/pysnooper/variables.py @@ -27,15 +27,15 @@ class BaseVariable(pycompat.ABC): else: self.unambiguous_source = source - def items(self, frame): + def items(self, frame, normalize=False): try: main_value = eval(self.code, frame.f_globals or {}, frame.f_locals) except Exception: return () - return self._items(main_value) + return self._items(main_value, normalize) @abc.abstractmethod - def _items(self, key): + def _items(self, key, normalize=False): raise NotImplementedError @property @@ -51,8 +51,8 @@ class BaseVariable(pycompat.ABC): class CommonVariable(BaseVariable): - def _items(self, main_value): - result = [(self.source, utils.get_shortish_repr(main_value))] + def _items(self, main_value, normalize=False): + result = [(self.source, utils.get_shortish_repr(main_value, normalize=normalize))] for key in self._safe_keys(main_value): try: if key in self.exclude: @@ -122,7 +122,7 @@ class Indices(Keys): class Exploding(BaseVariable): - def _items(self, main_value): + def _items(self, main_value, normalize=False): if isinstance(main_value, Mapping): cls = Keys elif isinstance(main_value, Sequence): @@ -130,4 +130,4 @@ class Exploding(BaseVariable): else: cls = Attrs - return cls(self.source, self.exclude)._items(main_value) + return cls(self.source, self.exclude)._items(main_value, normalize) diff --git a/tests/test_pysnooper.py b/tests/test_pysnooper.py index 24b0868..44852f4 100644 --- a/tests/test_pysnooper.py +++ b/tests/test_pysnooper.py @@ -159,13 +159,14 @@ def test_multi_thread_info(): ) -def test_callable(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_callable(normalize): string_io = io.StringIO() def write(msg): string_io.write(msg) - @pysnooper.snoop(write) + @pysnooper.snoop(write, normalize=normalize) def my_function(foo): x = 7 y = 8 @@ -187,13 +188,13 @@ def test_callable(): LineEntry('return y + x'), ReturnEntry('return y + x'), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) - -def test_watch(): - +@pytest.mark.parametrize("normalize", (True, False)) +def test_watch(normalize): class Foo(object): def __init__(self): self.x = 2 @@ -205,7 +206,7 @@ def test_watch(): 'foo.x', 'io.__name__', 'len(foo.__dict__["x"] * "abc")', - )) + ), normalize=normalize) def my_function(): foo = Foo() for i in range(2): @@ -240,18 +241,19 @@ def test_watch(): LineEntry(), ReturnEntry(), ReturnValueEntry('None') - ) + ), + normalize=normalize, ) -def test_watch_explode(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_watch_explode(normalize): class Foo: def __init__(self, x, y): self.x = x self.y = y - - @pysnooper.snoop(watch_explode=('_d', '_point', 'lst + []')) + @pysnooper.snoop(watch_explode=('_d', '_point', 'lst + []'), normalize=normalize) def my_function(): _d = {'a': 1, 'b': 2, 'c': 'ignore'} _point = Foo(x=3, y=4) @@ -290,11 +292,13 @@ def test_watch_explode(): VariableEntry('lst + []'), ReturnEntry(), ReturnValueEntry('None') - ) + ), + normalize=normalize, ) -def test_variables_classes(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_variables_classes(normalize): class WithSlots(object): __slots__ = ('x', 'y') @@ -307,7 +311,7 @@ def test_variables_classes(): pysnooper.Attrs('_d'), # doesn't have attributes pysnooper.Attrs('_s'), pysnooper.Indices('_lst')[-3:], - )) + ), normalize=normalize) def my_function(): _d = {'a': 1, 'b': 2, 'c': 'ignore'} _s = WithSlots() @@ -339,13 +343,13 @@ def test_variables_classes(): VariableEntry('_lst[999]', '999'), ReturnEntry(), ReturnValueEntry('None') - ) + ), + normalize=normalize, ) - -def test_single_watch_no_comma(): - +@pytest.mark.parametrize("normalize", (True, False)) +def test_single_watch_no_comma(normalize): class Foo(object): def __init__(self): self.x = 2 @@ -353,7 +357,7 @@ def test_single_watch_no_comma(): def square(self): self.x **= 2 - @pysnooper.snoop(watch='foo') + @pysnooper.snoop(watch='foo', normalize=normalize) def my_function(): foo = Foo() for i in range(2): @@ -381,12 +385,14 @@ def test_single_watch_no_comma(): LineEntry(), ReturnEntry(), ReturnValueEntry('None') - ) + ), + normalize=normalize, ) -def test_long_variable(): - @pysnooper.snoop() +@pytest.mark.parametrize("normalize", (True, False)) +def test_long_variable(normalize): + @pysnooper.snoop(normalize=normalize) def my_function(): foo = list(range(1000)) return foo @@ -407,13 +413,14 @@ def test_long_variable(): LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex) - ) + ), + normalize=normalize, ) - -def test_long_variable_with_custom_max_variable_length(): - @pysnooper.snoop(max_variable_length=200) +@pytest.mark.parametrize("normalize", (True, False)) +def test_long_variable_with_custom_max_variable_length(normalize): + @pysnooper.snoop(max_variable_length=200, normalize=normalize) def my_function(): foo = list(range(1000)) return foo @@ -434,12 +441,14 @@ def test_long_variable_with_custom_max_variable_length(): LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex) - ) + ), + normalize=normalize, ) -def test_long_variable_with_infinite_max_variable_length(): - @pysnooper.snoop(max_variable_length=None) +@pytest.mark.parametrize("normalize", (True, False)) +def test_long_variable_with_infinite_max_variable_length(normalize): + @pysnooper.snoop(max_variable_length=None, normalize=normalize) def my_function(): foo = list(range(1000)) return foo @@ -460,17 +469,18 @@ def test_long_variable_with_infinite_max_variable_length(): LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex) - ) + ), + normalize=normalize, ) - -def test_repr_exception(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_repr_exception(normalize): class Bad(object): def __repr__(self): 1 / 0 - @pysnooper.snoop() + @pysnooper.snoop(normalize=normalize) def my_function(): bad = Bad() @@ -489,11 +499,13 @@ def test_repr_exception(): VariableEntry('bad', value='REPR FAILED'), ReturnEntry(), ReturnValueEntry('None') - ) + ), + normalize=normalize, ) -def test_depth(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_depth(normalize): string_io = io.StringIO() def f4(x4): @@ -508,7 +520,7 @@ def test_depth(): result2 = f3(x2) return result2 - @pysnooper.snoop(string_io, depth=3) + @pysnooper.snoop(string_io, depth=3, normalize=normalize) def f1(x1): result1 = f2(x1) return result1 @@ -549,16 +561,18 @@ def test_depth(): LineEntry(), ReturnEntry(), ReturnValueEntry('20'), - ) + ), + normalize=normalize, ) -def test_method_and_prefix(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_method_and_prefix(normalize): class Baz(object): def __init__(self): self.x = 2 - @pysnooper.snoop(watch=('self.x',), prefix='ZZZ') + @pysnooper.snoop(watch=('self.x',), prefix='ZZZ', normalize=normalize) def square(self): foo = 7 self.x **= 2 @@ -587,15 +601,17 @@ def test_method_and_prefix(): ReturnEntry(prefix='ZZZ'), ReturnValueEntry(prefix='ZZZ'), ), - prefix='ZZZ' + prefix='ZZZ', + normalize=normalize, ) -def test_file_output(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_file_output(normalize): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: path = folder / 'foo.log' - @pysnooper.snoop(path) + @pysnooper.snoop(path, normalize=normalize) def my_function(_foo): x = 7 y = 8 @@ -618,18 +634,20 @@ def test_file_output(): LineEntry('return y + x'), ReturnEntry('return y + x'), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) -def test_confusing_decorator_lines(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_confusing_decorator_lines(normalize): string_io = io.StringIO() def empty_decorator(function): return function @empty_decorator - @pysnooper.snoop(string_io, + @pysnooper.snoop(string_io, normalize=normalize, depth=2) # Multi-line decorator for extra confusion! @empty_decorator @empty_decorator @@ -661,13 +679,15 @@ def test_confusing_decorator_lines(): # back in my_function ReturnEntry(), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) -def test_lambda(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_lambda(normalize): string_io = io.StringIO() - my_function = pysnooper.snoop(string_io)(lambda x: x ** 2) + my_function = pysnooper.snoop(string_io, normalize=normalize)(lambda x: x ** 2) result = my_function(7) assert result == 49 output = string_io.getvalue() @@ -680,7 +700,8 @@ def test_lambda(): LineEntry(source_regex='^my_function = pysnooper.*'), ReturnEntry(source_regex='^my_function = pysnooper.*'), ReturnValueEntry('49'), - ) + ), + normalize=normalize, ) @@ -820,9 +841,10 @@ def test_needs_parentheses(): assert needs_parentheses('x if z else y') -def test_with_block(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_with_block(normalize): # Testing that a single Tracer can handle many mixed uses - snoop = pysnooper.snoop() + snoop = pysnooper.snoop(normalize=normalize) def foo(x): if x == 0: @@ -940,10 +962,12 @@ def test_with_block(): ReturnEntry('qux()'), ReturnValueEntry('None'), ), + normalize=normalize, ) -def test_with_block_depth(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_with_block_depth(normalize): string_io = io.StringIO() def f4(x4): @@ -960,7 +984,7 @@ def test_with_block_depth(): def f1(x1): str(3) - with pysnooper.snoop(string_io, depth=3): + with pysnooper.snoop(string_io, depth=3, normalize=normalize): result1 = f2(x1) return result1 @@ -974,6 +998,7 @@ def test_with_block_depth(): VariableEntry(), VariableEntry(), VariableEntry(), + VariableEntry(), LineEntry('result1 = f2(x1)'), VariableEntry(), @@ -995,10 +1020,13 @@ def test_with_block_depth(): LineEntry(), ReturnEntry(), ReturnValueEntry('20'), - ) + ), + normalize=normalize, ) -def test_cellvars(): + +@pytest.mark.parametrize("normalize", (True, False)) +def test_cellvars(normalize): string_io = io.StringIO() def f2(a): @@ -1012,7 +1040,7 @@ def test_cellvars(): return f3(a) def f1(a): - with pysnooper.snoop(string_io, depth=4): + with pysnooper.snoop(string_io, depth=4, normalize=normalize): result1 = f2(a) return result1 @@ -1026,6 +1054,7 @@ def test_cellvars(): VariableEntry(), VariableEntry(), VariableEntry(), + VariableEntry(), LineEntry('result1 = f2(a)'), VariableEntry(), @@ -1057,10 +1086,13 @@ def test_cellvars(): ReturnValueEntry(), ReturnEntry(), ReturnValueEntry(), - ) + ), + normalize=normalize, ) -def test_var_order(): + +@pytest.mark.parametrize("normalize", (True, False)) +def test_var_order(normalize): string_io = io.StringIO() def f(one, two, three, four): @@ -1070,7 +1102,7 @@ def test_var_order(): five, six, seven = 5, 6, 7 - with pysnooper.snoop(string_io, depth=2): + with pysnooper.snoop(string_io, depth=2, normalize=normalize): result = f(1, 2, 3, 4) output = string_io.getvalue() @@ -1080,6 +1112,7 @@ def test_var_order(): SourcePathEntry(), VariableEntry(), VariableEntry(), + VariableEntry(), LineEntry('result = f(1, 2, 3, 4)'), VariableEntry("one", "1"), @@ -1100,7 +1133,8 @@ def test_var_order(): VariableEntry("seven", "7"), ReturnEntry(), ReturnValueEntry(), - ) + ), + normalize=normalize, ) @@ -1209,7 +1243,8 @@ def test_generator(): ) -def test_custom_repr(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_custom_repr(normalize): string_io = io.StringIO() def large(l): @@ -1227,7 +1262,8 @@ def test_custom_repr(): @pysnooper.snoop(string_io, custom_repr=( (large, print_list_size), (dict, print_dict), - (evil_condition, lambda x: 'I am evil'))) + (evil_condition, lambda x: 'I am evil')), + normalize=normalize,) def sum_to_x(x): l = list(range(x)) a = {'1': 1, '2': 2} @@ -1249,13 +1285,16 @@ def test_custom_repr(): LineEntry(), ReturnEntry(), ReturnValueEntry('49995000'), - ) + ), + normalize=normalize, ) -def test_custom_repr_single(): + +@pytest.mark.parametrize("normalize", (True, False)) +def test_custom_repr_single(normalize): string_io = io.StringIO() - @pysnooper.snoop(string_io, custom_repr=(list, lambda l: 'foofoo!')) + @pysnooper.snoop(string_io, custom_repr=(list, lambda l: 'foofoo!'), normalize=normalize) def sum_to_x(x): l = list(range(x)) return 7 @@ -1274,7 +1313,8 @@ def test_custom_repr_single(): LineEntry(), ReturnEntry(), ReturnValueEntry('7'), - ) + ), + normalize=normalize, ) @@ -1297,10 +1337,11 @@ def test_disable(): assert not output -def test_class(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_class(normalize): string_io = io.StringIO() - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) class MyClass(object): def __init__(self): self.x = 7 @@ -1317,12 +1358,12 @@ def test_class(): output, ( SourcePathEntry(), - VariableEntry('self', value_regex="u?.+MyClass object at"), + VariableEntry('self', value_regex="u?.+MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), - VariableEntry('self', value_regex="u?.+MyClass object at"), + VariableEntry('self', value_regex="u?.+MyClass object"), VariableEntry('foo', value_regex="u?'baba'"), CallEntry('def my_method(self, foo):'), LineEntry('y = 8'), @@ -1330,10 +1371,13 @@ def test_class(): LineEntry('return y + self.x'), ReturnEntry('return y + self.x'), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) -def test_class_with_decorated_method(): + +@pytest.mark.parametrize("normalize", (True, False)) +def test_class_with_decorated_method(normalize): string_io = io.StringIO() def decorator(function): @@ -1342,7 +1386,7 @@ def test_class_with_decorated_method(): return result return wrapper - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) class MyClass(object): def __init__(self): self.x = 7 @@ -1360,25 +1404,27 @@ def test_class_with_decorated_method(): output, ( SourcePathEntry(), - VariableEntry('self', value_regex="u?.+MyClass object at"), + VariableEntry('self', value_regex="u?.+MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), VariableEntry('args', value_regex=r"\(<.+>, 'baba'\)"), VariableEntry('kwargs', value_regex=r"\{\}"), - VariableEntry('function', value_regex="u?.+my_method at"), + VariableEntry('function', value_regex="u?.+my_method"), CallEntry('def wrapper(*args, **kwargs):'), LineEntry('result = function(*args, **kwargs)'), VariableEntry('result', '15'), LineEntry('return result'), ReturnEntry('return result'), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) -def test_class_with_decorated_method_and_snoop_applied_to_method(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_class_with_decorated_method_and_snoop_applied_to_method(normalize): string_io = io.StringIO() def decorator(function): @@ -1387,13 +1433,13 @@ def test_class_with_decorated_method_and_snoop_applied_to_method(): return result return wrapper - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) class MyClass(object): def __init__(self): self.x = 7 @decorator - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) def my_method(self, foo): y = 8 return y + self.x @@ -1406,18 +1452,18 @@ def test_class_with_decorated_method_and_snoop_applied_to_method(): output, ( SourcePathEntry(), - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), VariableEntry('args', value_regex=r"u?\(<.+>, 'baba'\)"), VariableEntry('kwargs', value_regex=r"u?\{\}"), - VariableEntry('function', value_regex="u?.*my_method at"), + VariableEntry('function', value_regex="u?.*my_method"), CallEntry('def wrapper(*args, **kwargs):'), LineEntry('result = function(*args, **kwargs)'), SourcePathEntry(), - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), VariableEntry('foo', value_regex="u?'baba'"), CallEntry('def my_method(self, foo):'), LineEntry('y = 8'), @@ -1429,14 +1475,16 @@ def test_class_with_decorated_method_and_snoop_applied_to_method(): LineEntry('return result'), ReturnEntry('return result'), ReturnValueEntry('15'), - ) + ), + normalize=normalize, ) -def test_class_with_property(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_class_with_property(normalize): string_io = io.StringIO() - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) class MyClass(object): def __init__(self): self._x = 0 @@ -1478,37 +1526,39 @@ def test_class_with_property(): output, ( SourcePathEntry(), - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def __init__(self):'), LineEntry('self._x = 0'), ReturnEntry('self._x = 0'), ReturnValueEntry('None'), # Called from getter - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), # Called from setter - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), # Called from deleter - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), - ) + ), + normalize=normalize, ) -def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(): +@pytest.mark.parametrize("normalize", (True, False)) +def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(normalize): string_io = io.StringIO() class UnsnoopedBaseClass(object): @@ -1518,7 +1568,7 @@ def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(): def method_on_base_class(self): self.method_on_base_class_was_called = True - @pysnooper.snoop(string_io) + @pysnooper.snoop(string_io, normalize=normalize) class MyClass(UnsnoopedBaseClass): def method_on_child_class(self): self.method_on_base_class() @@ -1534,10 +1584,100 @@ def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(): output, ( SourcePathEntry(), - VariableEntry('self', value_regex="u?.*MyClass object at"), + VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def method_on_child_class(self):'), LineEntry('self.method_on_base_class()'), ReturnEntry('self.method_on_base_class()'), ReturnValueEntry('None'), - ) + ), + normalize=normalize, ) + + +def test_normalize(): + string_io = io.StringIO() + + class A: + def __init__(self, a): + self.a = a + + @pysnooper.snoop(string_io, normalize=True) + def add(): + a = A(19) + b = A(22) + res = a.a + b.a + return res + + add() + output = string_io.getvalue() + assert_output( + output, + ( + SourcePathEntry('test_pysnooper.py'), + VariableEntry('A', value_regex=r""), + CallEntry('def add():'), + LineEntry('a = A(19)'), + VariableEntry('a', value_regex=r"<.*\.A (?:object|instance)>"), + LineEntry('b = A(22)'), + VariableEntry('b', value_regex=r"<.*\.A (?:object|instance)>"), + LineEntry('res = a.a + b.a'), + VariableEntry('res', value="41"), + LineEntry('return res'), + ReturnEntry('return res'), + ReturnValueEntry('41'), + ) + ) + + +def test_normalize_prefix(): + string_io = io.StringIO() + _prefix = 'ZZZZ' + + class A: + def __init__(self, a): + self.a = a + + @pysnooper.snoop(string_io, normalize=True, prefix=_prefix) + def add(): + a = A(19) + b = A(22) + res = a.a + b.a + return res + + add() + output = string_io.getvalue() + assert_output( + output, + ( + SourcePathEntry('test_pysnooper.py', prefix=_prefix), + VariableEntry('A', value_regex=r"", prefix=_prefix), + CallEntry('def add():', prefix=_prefix), + LineEntry('a = A(19)', prefix=_prefix), + VariableEntry('a', value_regex=r"<.*\.A (?:object|instance)>", prefix=_prefix), + LineEntry('b = A(22)', prefix=_prefix), + VariableEntry('b', value_regex=r"<.*\.A (?:object|instance)>", prefix=_prefix), + LineEntry('res = a.a + b.a', prefix=_prefix), + VariableEntry('res', value="41", prefix=_prefix), + LineEntry('return res', prefix=_prefix), + ReturnEntry('return res', prefix=_prefix), + ReturnValueEntry('41', prefix=_prefix), + ) + ) + + +def test_normalize_thread_info(): + string_io = io.StringIO() + + class A: + def __init__(self, a): + self.a = a + + @pysnooper.snoop(string_io, normalize=True, thread_info=True) + def add(): + a = A(19) + b = A(22) + res = a.a + b.a + return res + + with pytest.raises(NotImplementedError): + add() diff --git a/tests/utils.py b/tests/utils.py index 3f7d376..48e98cc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,10 +1,12 @@ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. - +import os import re import abc import inspect +from pysnooper.utils import DEFAULT_REPR_RE + try: from itertools import zip_longest except ImportError: @@ -202,7 +204,7 @@ class _BaseEventEntry(_BaseEntry): if source is not None: assert source_regex is None self.line_pattern = re.compile( - r"""^%s(?P(?: {4})*)[0-9:.]{15} """ + r"""^%s(?P(?: {4})*)(?:(?:[0-9:.]{15})|(?: {15})) """ r"""(?P[0-9]+-[0-9A-Za-z_-]+[ ]+)?""" r"""(?P[a-z_]*) +(?P[0-9]*) """ r"""+(?P.*)$""" % (re.escape(self.prefix,)) @@ -269,7 +271,25 @@ class OutputFailure(Exception): pass -def assert_output(output, expected_entries, prefix=None): +def verify_normalize(lines, prefix): + time_re = re.compile(r"[0-9:.]{15}") + src_re = re.compile(r'^(?: *)Source path:\.\.\. (.*)$') + for line in lines: + if DEFAULT_REPR_RE.search(line): + msg = "normalize is active, memory address should not appear" + raise OutputFailure(line, msg) + no_prefix = line.replace(prefix if prefix else '', '').strip() + if time_re.match(no_prefix): + msg = "normalize is active, time should not appear" + raise OutputFailure(line, msg) + m = src_re.match(line) + if m: + if not os.path.basename(m.group(1)) == m.group(1): + msg = "normalize is active, path should be only basename" + raise OutputFailure(line, msg) + + +def assert_output(output, expected_entries, prefix=None, normalize=False): lines = tuple(filter(None, output.split('\n'))) if prefix is not None: @@ -277,6 +297,9 @@ def assert_output(output, expected_entries, prefix=None): if not line.startswith(prefix): raise OutputFailure(line) + if normalize: + verify_normalize(lines, prefix) + any_mismatch = False result = '' template = u'\n{line!s:%s} {expected_entry} {arrow}' % max(map(len, lines))