Add support for thread identifiers

Display thread infos (identifier and name) on output to help
user to track execution on apps who use threading.
This commit is contained in:
Hervé Beraud 2019-05-08 15:52:21 +02:00 committed by Ram Rachum
parent 2801ce0c0e
commit fae0ce0c76
4 changed files with 153 additions and 5 deletions

View file

@ -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:

View file

@ -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':

View file

@ -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()

View file

@ -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<indent>(?: {4})*)[0-9:.]{15} """
r"""(?P<thread_info>[0-9]+-[0-9A-Za-z_-]+[ ]+)?"""
r"""(?P<event_name>[a-z_]*) +(?P<line_number>[0-9]*) """
r"""+(?P<source>.*)$""" % (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):