From fae0ce0c769a348c7ff88e98fb960e9d9756c3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Beraud?= Date: Wed, 8 May 2019 15:52:21 +0200 Subject: [PATCH] Add support for thread identifiers Display thread infos (identifier and name) on output to help user to track execution on apps who use threading. --- README.md | 6 +++ pysnooper/tracer.py | 21 +++++++- tests/test_pysnooper.py | 109 ++++++++++++++++++++++++++++++++++++++++ tests/utils.py | 22 ++++++-- 4 files changed, 153 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8588db2..331488c 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,12 @@ Start all snoop lines with a prefix, to grep for them easily: @pysnooper.snoop(prefix='ZZZ ') ``` +On multi-threaded apps identify which thread are snooped in output:: + +```python +@pysnooper.snoop(thread_info=True) +``` + # Installation # You can install **PySnooper** by: diff --git a/pysnooper/tracer.py b/pysnooper/tracer.py index f9d322e..5bdebd9 100644 --- a/pysnooper/tracer.py +++ b/pysnooper/tracer.py @@ -129,6 +129,7 @@ class Tracer: depth=1, prefix='', overwrite=False, + thread_info=False, ): ''' Snoop on the function, writing everything it's doing to stderr. @@ -163,6 +164,9 @@ class Tracer: @pysnooper.snoop(prefix='ZZZ ') + On multi-threaded apps identify which thread are snooped in output:: + + @pysnooper.snoop(thread_info=True) ''' self._write, self.truncate = get_write_and_truncate_functions(output) @@ -183,6 +187,8 @@ class Tracer: self.prefix = prefix self.overwrite = overwrite self._did_overwrite = False + self.thread_info = thread_info + self.thread_info_padding = 0 assert self.depth >= 1 self.target_codes = set() self.target_frames = set() @@ -223,6 +229,13 @@ class Tracer: def _is_internal_frame(self, frame): return frame.f_code.co_filename == Tracer.__enter__.__code__.co_filename + def set_thread_info_padding(self, thread_info): + current_thread_len = len(thread_info) + self.thread_info_padding = max(self.thread_info_padding, + current_thread_len) + return thread_info.ljust(self.thread_info_padding) + + def trace(self, frame, event, arg): ### Checking whether we should trace this line: ####################### @@ -285,6 +298,12 @@ class Tracer: 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: ####################### # # @@ -308,7 +327,7 @@ class Tracer: # # ### Finished dealing with misplaced function definition. ############## - self.write(u'{indent}{now_string} {event:9} ' + self.write(u'{indent}{now_string} {thread_info}{event:9} ' u'{line_no:4} {source_line}'.format(**locals())) if event == 'return': diff --git a/tests/test_pysnooper.py b/tests/test_pysnooper.py index 2cbc477..4142708 100644 --- a/tests/test_pysnooper.py +++ b/tests/test_pysnooper.py @@ -3,6 +3,7 @@ import io import textwrap +import threading import types from python_toolbox import sys_tools, temp_file_tools @@ -42,6 +43,114 @@ def test_string_io(): ) +def test_thread_info(): + + @pysnooper.snoop(thread_info=True) + def my_function(foo): + x = 7 + y = 8 + return y + x + + with sys_tools.OutputCapturer(stdout=False, + stderr=True) as output_capturer: + result = my_function('baba') + assert result == 15 + output = output_capturer.string_io.getvalue() + assert_output( + output, + ( + VariableEntry('foo', value_regex="u?'baba'"), + CallEntry('def my_function(foo):'), + LineEntry('x = 7'), + VariableEntry('x', '7'), + LineEntry('y = 8'), + VariableEntry('y', '8'), + LineEntry('return y + x'), + ReturnEntry('return y + x'), + ReturnValueEntry('15'), + ) + ) + + +def test_multi_thread_info(): + + @pysnooper.snoop(thread_info=True) + def my_function(foo): + x = 7 + y = 8 + return y + x + + with sys_tools.OutputCapturer(stdout=False, + stderr=True) as output_capturer: + my_function('baba') + t1 = threading.Thread(target=my_function, name="test123",args=['bubu']) + t1.start() + t1.join() + t1 = threading.Thread(target=my_function, name="bibi",args=['bibi']) + t1.start() + t1.join() + output = output_capturer.string_io.getvalue() + calls = [line for line in output.split("\n") if "call" in line] + main_thread = calls[0] + assert len(main_thread) == len(calls[1]) + assert len(main_thread) == len(calls[2]) + main_thread_call_str = main_thread.find("call") + assert main_thread_call_str == calls[1].find("call") + assert main_thread_call_str == calls[2].find("call") + thread_info_regex = '([0-9]+-{name}+[ ]+)' + assert_output( + output, + ( + VariableEntry('foo', value_regex="u?'baba'"), + CallEntry('def my_function(foo):', + thread_info_regex=thread_info_regex.format( + name="MainThread")), + LineEntry('x = 7', + thread_info_regex=thread_info_regex.format( + name="MainThread")), + VariableEntry('x', '7'), + LineEntry('y = 8', + thread_info_regex=thread_info_regex.format( + name="MainThread")), + VariableEntry('y', '8'), + LineEntry('return y + x', + thread_info_regex=thread_info_regex.format( + name="MainThread")), + ReturnEntry('return y + x'), + ReturnValueEntry('15'), + VariableEntry('foo', value_regex="u?'bubu'"), + CallEntry('def my_function(foo):', + thread_info_regex=thread_info_regex.format( + name="test123")), + LineEntry('x = 7', + thread_info_regex=thread_info_regex.format( + name="test123")), + VariableEntry('x', '7'), + LineEntry('y = 8', + thread_info_regex=thread_info_regex.format( + name="test123")), + VariableEntry('y', '8'), + LineEntry('return y + x', + thread_info_regex=thread_info_regex.format( + name="test123")), + ReturnEntry('return y + x'), + ReturnValueEntry('15'), + VariableEntry('foo', value_regex="u?'bibi'"), + CallEntry('def my_function(foo):', + thread_info_regex=thread_info_regex.format(name='bibi')), + LineEntry('x = 7', + thread_info_regex=thread_info_regex.format(name='bibi')), + VariableEntry('x', '7'), + LineEntry('y = 8', + thread_info_regex=thread_info_regex.format(name='bibi')), + VariableEntry('y', '8'), + LineEntry('return y + x', + thread_info_regex=thread_info_regex.format(name='bibi')), + ReturnEntry('return y + x'), + ReturnValueEntry('15'), + ) + ) + def test_callable(): string_io = io.StringIO() diff --git a/tests/utils.py b/tests/utils.py index 0217ace..40d6781 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -73,7 +73,7 @@ class _BaseValueEntry(_BaseEntry): class VariableEntry(_BaseValueEntry): def __init__(self, name=None, value=None, stage=None, prefix='', - name_regex=None, value_regex=None, ): + name_regex=None, value_regex=None): _BaseValueEntry.__init__(self, prefix=prefix) if name is not None: assert name_regex is None @@ -165,7 +165,8 @@ class ReturnValueEntry(_BaseValueEntry): class _BaseEventEntry(_BaseEntry): - def __init__(self, source=None, source_regex=None, prefix=''): + def __init__(self, source=None, source_regex=None, thread_info=None, + thread_info_regex=None, prefix=''): _BaseEntry.__init__(self, prefix=prefix) if type(self) is _BaseEventEntry: raise TypeError @@ -173,6 +174,7 @@ class _BaseEventEntry(_BaseEntry): assert source_regex is None self.line_pattern = re.compile( r"""^%s(?P(?: {4})*)[0-9:.]{15} """ + r"""(?P[0-9]+-[0-9A-Za-z_-]+[ ]+)?""" r"""(?P[a-z_]*) +(?P[0-9]*) """ r"""+(?P.*)$""" % (re.escape(self.prefix,)) ) @@ -180,6 +182,9 @@ class _BaseEventEntry(_BaseEntry): self.source = source self.source_regex = (None if source_regex is None else re.compile(source_regex)) + self.thread_info = thread_info + self.thread_info_regex = (None if thread_info_regex is None else + re.compile(thread_info_regex)) @caching.CachedProperty def event_name(self): @@ -193,13 +198,22 @@ class _BaseEventEntry(_BaseEntry): else: return True + def _check_thread_info(self, thread_info): + if self.thread_info is not None: + return thread_info == self.thread_info + elif self.thread_info_regex is not None: + return self.thread_info_regex.match(thread_info) + else: + return True + def check(self, s): match = self.line_pattern.match(s) if not match: return False - _, event_name, _, source = match.groups() + _, thread_info, event_name, _, source = match.groups() return (event_name == self.event_name and - self._check_source(source)) + self._check_source(source) and + self._check_thread_info(thread_info)) class CallEntry(_BaseEventEntry):