PySnooper/pysnooper/tracer.py

244 lines
9.3 KiB
Python
Raw Normal View History

2019-04-24 09:10:46 +00:00
# Copyright 2019 Ram Rachum and collaborators.
2019-04-21 13:53:18 +00:00
# This program is distributed under the MIT license.
import types
import sys
import re
import collections
2019-04-24 11:02:50 +00:00
try:
2019-04-24 10:51:25 +00:00
from collections import ChainMap
2019-04-24 11:02:50 +00:00
except ImportError:
2019-04-24 10:51:25 +00:00
from ConfigParser import _Chainmap as ChainMap
2019-04-21 13:53:18 +00:00
import datetime as datetime_module
2019-04-24 09:36:37 +00:00
import itertools
2019-04-21 13:53:18 +00:00
from .third_party import six
2019-04-21 17:54:55 +00:00
2019-04-23 08:11:38 +00:00
MAX_VARIABLE_LENGTH = 100
2019-04-24 19:28:43 +00:00
ipython_filename_pattern = re.compile('^<ipython-input-([0-9]+)-.*>$')
2019-04-23 08:11:38 +00:00
2019-04-21 17:54:55 +00:00
def get_shortish_repr(item):
try:
r = repr(item)
except Exception:
r = 'REPR FAILED'
2019-04-24 12:30:51 +00:00
r = r.replace('\r', '').replace('\n', '')
2019-04-23 08:11:38 +00:00
if len(r) > MAX_VARIABLE_LENGTH:
r = '{truncated_r}...'.format(truncated_r=r[:MAX_VARIABLE_LENGTH])
2019-04-21 13:53:18 +00:00
return r
2019-04-21 17:54:55 +00:00
def get_local_reprs(frame, variables=()):
result = {key: get_shortish_repr(value) for key, value
in frame.f_locals.items()}
2019-04-24 10:51:25 +00:00
locals_and_globals = ChainMap(frame.f_locals, frame.f_globals)
2019-04-21 13:53:18 +00:00
for variable in variables:
steps = variable.split('.')
step_iterator = iter(steps)
try:
current = locals_and_globals[next(step_iterator)]
for step in step_iterator:
current = getattr(current, step)
except (KeyError, AttributeError):
continue
result[variable] = get_shortish_repr(current)
2019-04-21 13:53:18 +00:00
return result
2019-04-24 19:12:53 +00:00
class UnavailableSource(object):
def __getitem__(self, i):
content = 'SOURCE IS UNAVAILABLE'
if six.PY2:
content = content.decode()
return content
2019-04-21 13:53:18 +00:00
source_cache_by_module_name = {}
source_cache_by_file_name = {}
2019-04-21 17:54:55 +00:00
def get_source_from_frame(frame):
2019-04-21 13:53:18 +00:00
module_name = frame.f_globals.get('__name__') or ''
if module_name:
try:
return source_cache_by_module_name[module_name]
except KeyError:
pass
file_name = frame.f_code.co_filename
if file_name:
try:
return source_cache_by_file_name[file_name]
except KeyError:
pass
function = frame.f_code.co_name
loader = frame.f_globals.get('__loader__')
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
source = None
if hasattr(loader, 'get_source'):
try:
source = loader.get_source(module_name)
except ImportError:
pass
if source is not None:
source = source.splitlines()
if source is None:
2019-04-24 19:28:43 +00:00
ipython_filename_match = ipython_filename_pattern.match(file_name)
if ipython_filename_match:
entry_number = int(ipython_filename_match.group(1))
try:
import IPython
ipython_shell = IPython.get_ipython()
((_, _, source_chunk),) = ipython_shell.history_manager. \
get_range(0, entry_number, entry_number + 1)
source = source_chunk.splitlines()
except Exception:
pass
else:
try:
with open(file_name, 'rb') as fp:
source = fp.read().splitlines()
except (OSError, IOError):
pass
2019-04-21 13:53:18 +00:00
if source is None:
2019-04-24 19:12:53 +00:00
source = UnavailableSource()
2019-04-21 13:53:18 +00:00
# If we just read the source from a file, or if the loader did not
# apply tokenize.detect_encoding to decode the source into a
# string, then we should do that ourselves.
if isinstance(source[0], bytes):
encoding = 'ascii'
for line in source[:2]:
# File coding may be specified. Match pattern from PEP-263
# (https://www.python.org/dev/peps/pep-0263/)
match = re.search(br'coding[:=]\s*([-\w.]+)', line)
if match:
encoding = match.group(1).decode('ascii')
break
2019-04-21 17:54:55 +00:00
source = [six.text_type(sline, encoding, 'replace') for sline in
source]
2019-04-21 13:53:18 +00:00
if module_name:
source_cache_by_module_name[module_name] = source
if file_name:
source_cache_by_file_name[file_name] = source
return source
class Tracer:
2019-04-21 18:35:43 +00:00
def __init__(self, target_code_object, write, variables=(), depth=1,
prefix=''):
2019-04-21 13:53:18 +00:00
self.target_code_object = target_code_object
2019-04-21 18:35:43 +00:00
self._write = write
2019-04-21 13:53:18 +00:00
self.variables = variables
self.frame_to_old_local_reprs = collections.defaultdict(lambda: {})
self.frame_to_local_reprs = collections.defaultdict(lambda: {})
self.depth = depth
2019-04-21 18:35:43 +00:00
self.prefix = prefix
2019-04-21 13:53:18 +00:00
assert self.depth >= 1
2019-04-21 18:39:32 +00:00
2019-04-21 18:35:43 +00:00
def write(self, s):
s = '{self.prefix}{s}\n'.format(**locals())
if isinstance(s, bytes): # Python 2 compatibility
s = s.decode()
self._write(s)
2019-04-21 13:53:18 +00:00
def __enter__(self):
self.original_trace_function = sys.gettrace()
sys.settrace(self.trace)
def __exit__(self, exc_type, exc_value, exc_traceback):
sys.settrace(self.original_trace_function)
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
2019-04-21 17:54:55 +00:00
def trace(self, frame, event, arg):
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
### Checking whether we should trace this line: #######################
# #
# We should trace this line either if it's in the decorated function,
# or the user asked to go a few levels deeper and we're within that
# number of levels deeper.
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
if frame.f_code is not self.target_code_object:
if self.depth == 1:
# We did the most common and quickest check above, because the
# trace function runs so incredibly often, therefore it's
# crucial to hyper-optimize it for the common case.
return self.trace
else:
_frame_candidate = frame
for i in range(1, self.depth):
_frame_candidate = _frame_candidate.f_back
if _frame_candidate is None:
return self.trace
elif _frame_candidate.f_code is self.target_code_object:
indent = ' ' * 4 * i
break
else:
return self.trace
else:
indent = ''
# #
### Finished checking whether we should trace this line. ##############
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
### Reporting newish and modified variables: ##########################
# #
self.frame_to_old_local_reprs[frame] = old_local_reprs = \
self.frame_to_local_reprs[frame]
self.frame_to_local_reprs[frame] = local_reprs = \
get_local_reprs(frame, variables=self.variables)
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
modified_local_reprs = {}
newish_local_reprs = {}
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
for key, value in local_reprs.items():
if key not in old_local_reprs:
newish_local_reprs[key] = value
elif old_local_reprs[key] != value:
modified_local_reprs[key] = value
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
newish_string = ('Starting var:.. ' if event == 'call' else
'New var:....... ')
2019-04-24 12:57:20 +00:00
for name, value_repr in sorted(newish_local_reprs.items()):
2019-04-21 13:53:18 +00:00
self.write('{indent}{newish_string}{name} = {value_repr}'.format(
**locals()))
2019-04-24 12:57:20 +00:00
for name, value_repr in sorted(modified_local_reprs.items()):
2019-04-21 13:53:18 +00:00
self.write('{indent}Modified var:.. {name} = {value_repr}'.format(
**locals()))
# #
### Finished newish and modified variables. ###########################
2019-04-21 18:39:32 +00:00
2019-04-21 13:53:18 +00:00
now_string = datetime_module.datetime.now().time().isoformat()
line_no = frame.f_lineno
source_line = get_source_from_frame(frame)[line_no - 1]
2019-04-24 09:36:37 +00:00
### Dealing with misplaced function definition: #######################
# #
if event == 'call' and source_line.lstrip().startswith('@'):
# If a function decorator is found, skip lines until an actual
# 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]
except IndexError:
# End of source file reached without finding a function
# definition. Fall back to original source line.
break
if candidate_source_line.lstrip().startswith('def'):
# Found the def line!
line_no = candidate_line_no
source_line = candidate_source_line
break
# #
### Finished dealing with misplaced function definition. ##############
2019-04-21 13:53:18 +00:00
self.write('{indent}{now_string} {event:9} '
'{line_no:4} {source_line}'.format(**locals()))
if event == 'return':
return_value_repr = get_shortish_repr(arg)
self.write('{indent}Return value:.. {return_value_repr}'.
format(**locals()))
2019-04-21 13:53:18 +00:00
return self.trace