mirror of https://github.com/cool-RR/PySnooper.git
Initial commit
This commit is contained in:
commit
cb2d96ddfa
|
@ -0,0 +1,7 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
__pycache__
|
||||
|
||||
.pytest_cache
|
||||
|
||||
*.wpu
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ram Rachum
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,83 @@
|
|||
# PySnooper - Never use print for debugging again #
|
||||
|
||||
**PySnooper** is a poor man's debugger.
|
||||
|
||||
You're trying to figure out why your Python code isn't doing what you think it should be doing. You'd love to use a full-fledged debugger with breakpoints and watches, but you can't be bothered to set one up right now.
|
||||
|
||||
You're looking at a section of Python code. You want to know which lines are running and which aren't, and what the values of the local variables are.
|
||||
|
||||
Most people would use a `print` line. Probably several of them, in strategic locations, some of them showing the values of variables. Then they'd use the output of the prints to figure out which code ran when and what was in the variables.
|
||||
|
||||
**PySnooper** lets you do the same, except instead of carefully crafting the right `print` lines, you just add one decorator line to the function you're interested in. You'll get a play-by-play log of your function, including which lines ran and when, and exactly when local variables were changed.
|
||||
|
||||
What makes **PySnooper** stand out from all other code intelligence tools? You can use it in your shitty, sprawling enterprise codebase without having to do any setup. Just slap the decorator on, as shown below, and redirect the output to a dedicated log file by specifying its path as the first argument.
|
||||
|
||||
# Example #
|
||||
|
||||
We're writing a function that converts a number to binary, by returing a list of bits. Let's snoop on it by adding the `@pysnooper.snoop()` decorator:
|
||||
|
||||
import pysnooper
|
||||
|
||||
@pysnooper.snoop()
|
||||
def number_to_bits(number):
|
||||
if number:
|
||||
bits = []
|
||||
while number:
|
||||
number, remainder = divmod(number, 2)
|
||||
bits.insert(0, remainder)
|
||||
return bits
|
||||
else:
|
||||
return [0]
|
||||
|
||||
number_to_bits(6)
|
||||
|
||||
The output to stderr is:
|
||||
|
||||
==> number = 6
|
||||
00:24:15.284000 call 3 @pysnooper.snoop()
|
||||
00:24:15.284000 line 5 if number:
|
||||
00:24:15.284000 line 6 bits = []
|
||||
==> bits = []
|
||||
00:24:15.284000 line 7 while number:
|
||||
00:24:15.284000 line 8 number, remainder = divmod(number, 2)
|
||||
==> number = 3
|
||||
==> remainder = 0
|
||||
00:24:15.284000 line 9 bits.insert(0, remainder)
|
||||
==> bits = [0]
|
||||
00:24:15.284000 line 7 while number:
|
||||
00:24:15.284000 line 8 number, remainder = divmod(number, 2)
|
||||
==> number = 1
|
||||
==> remainder = 1
|
||||
00:24:15.284000 line 9 bits.insert(0, remainder)
|
||||
==> bits = [1, 0]
|
||||
00:24:15.284000 line 7 while number:
|
||||
00:24:15.284000 line 8 number, remainder = divmod(number, 2)
|
||||
==> number = 0
|
||||
00:24:15.284000 line 9 bits.insert(0, remainder)
|
||||
==> bits = [1, 1, 0]
|
||||
00:24:15.284000 line 7 while number:
|
||||
00:24:15.284000 line 10 return bits
|
||||
00:24:15.284000 return 10 return bits
|
||||
|
||||
|
||||
# Features #
|
||||
|
||||
If stderr is not easily accessible for you, you can redirect the output to a file easily:
|
||||
|
||||
@pysnooper.snoop('/my/log/file.log')
|
||||
|
||||
Want to see values of some variables that aren't local variables?
|
||||
|
||||
@pysnooper.snoop(variables=('foo.bar', 'self.whatever'))
|
||||
|
||||
|
||||
# Installation #
|
||||
|
||||
Use `pip`:
|
||||
|
||||
pip install pysnooper
|
||||
|
||||
|
||||
# Copyright #
|
||||
|
||||
Copyright (c) 2019 Ram Rachum, released under the MIT license.
|
|
@ -0,0 +1,21 @@
|
|||
#!wing
|
||||
#!version=7.0
|
||||
##################################################################
|
||||
# Wing project file #
|
||||
##################################################################
|
||||
[project attributes]
|
||||
proj.directory-list = [{'dirloc': loc('../..'),
|
||||
'excludes': (),
|
||||
'filter': u'*',
|
||||
'include_hidden': False,
|
||||
'recursive': True,
|
||||
'watch_for_changes': True}]
|
||||
proj.file-type = 'shared'
|
||||
proj.home-dir = loc('../..')
|
||||
proj.launch-config = {loc('../../../../../Dropbox/Scripts and shortcuts/_simplify3d_add_m600.py'): ('p'\
|
||||
'roject',
|
||||
(u'"C:\\Users\\Administrator\\Dropbox\\Desktop\\foo.gcode"',
|
||||
''))}
|
||||
testing.auto-test-file-specs = (('regex',
|
||||
'pysnooper/tests.*/test[^./]*.py.?$'),)
|
||||
testing.test-framework = {None: ':internal pytest'}
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright 2019 Ram Rachum.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
from .pysnooper import snoop
|
|
@ -0,0 +1,177 @@
|
|||
# Copyright 2019 Ram Rachum.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pathlib
|
||||
import inspect
|
||||
import types
|
||||
import typing
|
||||
import datetime as datetime_module
|
||||
import re
|
||||
import collections
|
||||
|
||||
import decorator
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_write_function(output) -> typing.Callable:
|
||||
if output is None:
|
||||
def write(s):
|
||||
stderr = sys.stderr
|
||||
stderr.write(s)
|
||||
stderr.write('\n')
|
||||
elif isinstance(output, (os.PathLike, str)):
|
||||
output_path = pathlib.Path(output)
|
||||
def write(s):
|
||||
with output_path.open('a') as output_file:
|
||||
output_file.write(s)
|
||||
output_file.write('\n')
|
||||
else:
|
||||
assert isinstance(output, utils.WritableStream)
|
||||
def write(s):
|
||||
output.write(s)
|
||||
output.write('\n')
|
||||
|
||||
return write
|
||||
|
||||
|
||||
class Tracer:
|
||||
def __init__(self, target_code_object: types.CodeType, write: callable, *,
|
||||
variables: typing.Sequence=()):
|
||||
self.target_code_object = target_code_object
|
||||
self.write = write
|
||||
self.variables = variables
|
||||
self.old_local_reprs = {}
|
||||
self.local_reprs = {}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def trace(self: Tracer, frame: types.FrameType, event: str,
|
||||
arg: typing.Any) -> typing.Callable:
|
||||
if frame.f_code != self.target_code_object:
|
||||
return self.trace
|
||||
self.old_local_reprs, self.local_reprs = \
|
||||
self.local_reprs, get_local_reprs(frame, variables=self.variables)
|
||||
modified_local_reprs = {
|
||||
key: value for key, value in self.local_reprs.items()
|
||||
if (key not in self.old_local_reprs) or
|
||||
(self.old_local_reprs[key] != value)
|
||||
}
|
||||
for name, value_repr in modified_local_reprs.items():
|
||||
self.write(f' ==> {name} = {value_repr}')
|
||||
# x = repr((frame.f_code.co_stacksize, frame, event, arg))
|
||||
now_string = datetime_module.datetime.now().time().isoformat()
|
||||
source_line = get_source_from_frame(frame)[frame.f_lineno - 1]
|
||||
self.write(f'{now_string} {event:9} '
|
||||
f'{frame.f_lineno:4} {source_line}')
|
||||
return self.trace
|
||||
|
||||
|
||||
|
||||
source_cache_by_module_name = {}
|
||||
source_cache_by_file_name = {}
|
||||
def get_source_from_frame(frame: types.FrameType) -> str:
|
||||
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__')
|
||||
|
||||
source: typing.Union[None, str] = 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:
|
||||
try:
|
||||
with open(file_name, 'rb') as fp:
|
||||
source = fp.read().splitlines()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
if source is None:
|
||||
raise NotImplementedError
|
||||
|
||||
# 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
|
||||
source = [str(sline, encoding, 'replace') for sline in source]
|
||||
|
||||
if module_name:
|
||||
source_cache_by_module_name[module_name] = source
|
||||
if file_name:
|
||||
source_cache_by_file_name[file_name] = source
|
||||
return source
|
||||
|
||||
def get_local_reprs(frame: types.FrameType, *, variables: typing.Sequence=()) -> dict:
|
||||
result = {}
|
||||
for key, value in frame.f_locals.items():
|
||||
try:
|
||||
result[key] = get_shortish_repr(value)
|
||||
except Exception:
|
||||
continue
|
||||
locals_and_globals = collections.ChainMap(frame.f_locals, frame.f_globals)
|
||||
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
|
||||
try:
|
||||
result[variable] = get_shortish_repr(current)
|
||||
except Exception:
|
||||
continue
|
||||
return result
|
||||
|
||||
def get_shortish_repr(item) -> str:
|
||||
r = repr(item)
|
||||
if len(r) > 100:
|
||||
r = f'{r[:97]}...'
|
||||
return r
|
||||
|
||||
|
||||
def snoop(output=None, *, variables=()) -> typing.Callable:
|
||||
write = get_write_function(output)
|
||||
@decorator.decorator
|
||||
def decorate(function, *args, **kwargs) -> typing.Callable:
|
||||
target_code_object = function.__code__
|
||||
with Tracer(target_code_object, write, variables=variables):
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2019 Ram Rachum.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
import abc
|
||||
import sys
|
||||
|
||||
|
||||
def _check_methods(C, *methods):
|
||||
mro = C.__mro__
|
||||
for method in methods:
|
||||
for B in mro:
|
||||
if method in B.__dict__:
|
||||
if B.__dict__[method] is None:
|
||||
return NotImplemented
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
|
||||
|
||||
class WritableStream(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def write(self, s):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is WritableStream:
|
||||
return _check_methods(C, 'write')
|
||||
return NotImplemented
|
||||
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2019 Ram Rachum.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
import setuptools
|
||||
|
||||
with open('README.md', 'r') as readme_file:
|
||||
long_description = readme_file.read()
|
||||
|
||||
setuptools.setup(
|
||||
name='PySnooper',
|
||||
version='0.0.1',
|
||||
author='Ram Rachum',
|
||||
author_email='ram@rachum.com',
|
||||
description="A poor man's debugger for Python.",
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/cool-RR/PySnooper',
|
||||
packages=setuptools.find_packages(),
|
||||
install_requires=('decorator>=4.3.0',),
|
||||
tests_require=(
|
||||
'pytest>=4.4.1',
|
||||
'python_toolbox>=0.9.3',
|
||||
),
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
],
|
||||
|
||||
)
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright 2019 Ram Rachum.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
import io
|
||||
import re
|
||||
import abc
|
||||
|
||||
from python_toolbox import caching
|
||||
from python_toolbox import sys_tools
|
||||
|
||||
import pysnooper
|
||||
|
||||
|
||||
class Entry(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def check(self, s: str) -> bool:
|
||||
pass
|
||||
|
||||
class VariableEntry(Entry):
|
||||
line_pattern = re.compile(
|
||||
r"""^ ==> (?P<name>[^ ]*) = (?P<value>.*)$"""
|
||||
)
|
||||
def __init__(self, name=None, value=None, *,
|
||||
name_regex=None, value_regex=None):
|
||||
if name is not None:
|
||||
assert name_regex is None
|
||||
if value is not None:
|
||||
assert value_regex is None
|
||||
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.name_regex = (None if name_regex is None else
|
||||
re.compile(name_regex))
|
||||
self.value_regex = (None if value_regex is None else
|
||||
re.compile(value_regex))
|
||||
|
||||
def _check_name(self, name: str) -> bool:
|
||||
if self.name is not None:
|
||||
return name == self.name
|
||||
elif self.name_regex is not None:
|
||||
return self.name_regex.fullmatch(name)
|
||||
else:
|
||||
return True
|
||||
|
||||
def _check_value(self, value: str) -> bool:
|
||||
if self.value is not None:
|
||||
return value == self.value
|
||||
elif self.value_regex is not None:
|
||||
return self.value_regex.fullmatch(value)
|
||||
else:
|
||||
return True
|
||||
|
||||
def check(self, s: str) -> bool:
|
||||
match: re.Match = self.line_pattern.fullmatch(s)
|
||||
if not match:
|
||||
return False
|
||||
name, value = match.groups()
|
||||
return self._check_name(name) and self._check_value(value)
|
||||
|
||||
|
||||
class EventEntry(Entry):
|
||||
def __init__(self, source=None, *, source_regex=None):
|
||||
if source is not None:
|
||||
assert source_regex is None
|
||||
|
||||
self.source = source
|
||||
self.source_regex = (None if source_regex is None else
|
||||
re.compile(source_regex))
|
||||
|
||||
line_pattern = re.compile(
|
||||
(r"""^[0-9:.]{15} (?P<event_name>[a-z]*) +"""
|
||||
r"""(?P<line_number>[0-9]*) +(?P<source>.*)$""")
|
||||
)
|
||||
|
||||
@caching.CachedProperty
|
||||
def event_name(self):
|
||||
return re.match('^[A-Z][a-z]*', type(self).__name__).group(0).lower()
|
||||
|
||||
def _check_source(self, source: str) -> bool:
|
||||
if self.source is not None:
|
||||
return source == self.source
|
||||
elif self.source_regex is not None:
|
||||
return self.source_regex.fullmatch(source)
|
||||
else:
|
||||
return True
|
||||
|
||||
def check(self, s: str) -> bool:
|
||||
match: re.Match = self.line_pattern.fullmatch(s)
|
||||
if not match:
|
||||
return False
|
||||
event_name, _, source = match.groups()
|
||||
return event_name == self.event_name and self._check_source(source)
|
||||
|
||||
|
||||
|
||||
class CallEntry(EventEntry):
|
||||
pass
|
||||
|
||||
class LineEntry(EventEntry):
|
||||
pass
|
||||
|
||||
class ReturnEntry(EventEntry):
|
||||
pass
|
||||
|
||||
class ExceptionEntry(EventEntry):
|
||||
pass
|
||||
|
||||
class OpcodeEntry(EventEntry):
|
||||
pass
|
||||
|
||||
|
||||
def check_output(output, expected_entries):
|
||||
lines = tuple(filter(None, output.split('\n')))
|
||||
if len(lines) != len(expected_entries):
|
||||
return False
|
||||
return all(expected_entry.check(line) for
|
||||
expected_entry, line in zip(expected_entries, lines))
|
||||
|
||||
|
||||
def test_pysnooper():
|
||||
string_io = io.StringIO()
|
||||
@pysnooper.snoop(string_io)
|
||||
def my_function(foo):
|
||||
x = 7
|
||||
y = 8
|
||||
return y + x
|
||||
result = my_function('baba')
|
||||
assert result == 15
|
||||
output = string_io.getvalue()
|
||||
assert check_output(
|
||||
output,
|
||||
(
|
||||
VariableEntry('foo', "'baba'"),
|
||||
CallEntry(),
|
||||
LineEntry('x = 7'),
|
||||
VariableEntry('x', '7'),
|
||||
LineEntry('y = 8'),
|
||||
VariableEntry('y', '8'),
|
||||
LineEntry('return y + x'),
|
||||
ReturnEntry('return y + x'),
|
||||
)
|
||||
)
|
||||
|
||||
def test_variables():
|
||||
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.x = 2
|
||||
|
||||
def square(self):
|
||||
self.x **= 2
|
||||
|
||||
@pysnooper.snoop(variables=('foo.x', 're'))
|
||||
def my_function():
|
||||
foo = Foo()
|
||||
for i in range(2):
|
||||
foo.square()
|
||||
|
||||
with sys_tools.OutputCapturer(stdout=False,
|
||||
stderr=True) as output_capturer:
|
||||
result = my_function()
|
||||
assert result is None
|
||||
output = output_capturer.string_io.getvalue()
|
||||
assert check_output(
|
||||
output,
|
||||
(
|
||||
VariableEntry('Foo'),
|
||||
VariableEntry('re'),
|
||||
CallEntry(),
|
||||
LineEntry('foo = Foo()'),
|
||||
VariableEntry('foo'),
|
||||
VariableEntry('foo.x', '2'),
|
||||
LineEntry(),
|
||||
VariableEntry('i', '0'),
|
||||
LineEntry(),
|
||||
VariableEntry('foo.x', '4'),
|
||||
LineEntry(),
|
||||
VariableEntry('i', '1'),
|
||||
LineEntry(),
|
||||
VariableEntry('foo.x', '16'),
|
||||
LineEntry(),
|
||||
ReturnEntry(),
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue