-
Notifications
You must be signed in to change notification settings - Fork 955
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit cb2d96d
Showing
10 changed files
with
561 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
*.pyc | ||
*.pyo | ||
__pycache__ | ||
|
||
.pytest_cache | ||
|
||
*.wpu |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2019 Ram Rachum. | ||
# This program is distributed under the MIT license. | ||
|
||
from .pysnooper import snoop |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
], | ||
|
||
) |
Empty file.
Oops, something went wrong.