Repository: cool-RR/PySnooper Branch: master Commit: 4bff1205c8c6 Files: 42 Total size: 216.9 KB Directory structure: gitextract__n6r0460/ ├── .gitignore ├── .travis.yml ├── ADVANCED_USAGE.md ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.md ├── make_release.sh ├── misc/ │ ├── IDE files/ │ │ └── PySnooper.wpr │ └── generate_authors.py ├── pyproject.toml ├── pysnooper/ │ ├── __init__.py │ ├── pycompat.py │ ├── tracer.py │ ├── utils.py │ └── variables.py ├── requirements.in ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── mini_toolbox/ │ │ ├── __init__.py │ │ ├── contextlib.py │ │ └── pathlib.py │ ├── samples/ │ │ ├── __init__.py │ │ ├── exception.py │ │ ├── indentation.py │ │ └── recursion.py │ ├── test_chinese.py │ ├── test_mini_toolbox.py │ ├── test_multiple_files/ │ │ ├── __init__.py │ │ ├── multiple_files/ │ │ │ ├── __init__.py │ │ │ ├── bar.py │ │ │ └── foo.py │ │ └── test_multiple_files.py │ ├── test_not_implemented.py │ ├── test_pysnooper.py │ ├── test_utils/ │ │ ├── __init__.py │ │ ├── test_ensure_tuple.py │ │ └── test_regex.py │ └── utils.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.py[co] __pycache__/ .tox/ .pytest_cache/ .mypy_cache/ dist/ build/ *.egg-info/ *.bak *.wpu .coverage htmlcov ================================================ FILE: .travis.yml ================================================ dist: xenial language: python python: - 2.7 - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - 3.10-dev - pypy2.7-6.0 - pypy3.5 install: - pip install tox-travis script: - tox stages: - lint - test #- deploy matrix: allow_failures: - env: TOXENV=flake8 - env: TOXENV=pylint - env: TOXENV=bandit - python: 3.10-dev jobs: include: #- { stage: lint, python: 3.7, env: TOXENV=flake8 } #- { stage: lint, python: 3.7, env: TOXENV=pylint } #- { stage: lint, python: 3.7, env: TOXENV=bandit } - { stage: lint, python: 3.7, env: TOXENV=readme } #- stage: deploy # install: skip # script: skip # deploy: # provider: pypi # distributions: sdist bdist_wheel # user: cool-RR # password: # secure: # on: # tags: true ================================================ FILE: ADVANCED_USAGE.md ================================================ # Advanced Usage # Use `watch_explode` to expand values to see all their attributes or items of lists/dictionaries: ```python @pysnooper.snoop(watch_explode=('foo', 'self')) ``` `watch_explode` will automatically guess how to expand the expression passed to it based on its class. You can be more specific by using one of the following classes: ```python import pysnooper @pysnooper.snoop(watch=( pysnooper.Attrs('x'), # attributes pysnooper.Keys('y'), # mapping (e.g. dict) items pysnooper.Indices('z'), # sequence (e.g. list/tuple) items )) ``` Exclude specific keys/attributes/indices with the `exclude` parameter, e.g. `Attrs('x', exclude=('_foo', '_bar'))`. Add a slice after `Indices` to only see the values within that slice, e.g. `Indices('z')[-3:]`. ```console $ export PYSNOOPER_DISABLED=1 # This makes PySnooper not do any snooping ``` This will output lines like: ``` Modified var:.. foo[2] = 'whatever' New var:....... self.baz = 8 ``` Start all snoop lines with a prefix, to grep for them easily: ```python @pysnooper.snoop(prefix='ZZZ ') ``` Remove all machine-related data (paths, timestamps, memory addresses) to compare with other traces easily: ```python @pysnooper.snoop(normalize=True) ``` On multi-threaded apps identify which thread are snooped in output: ```python @pysnooper.snoop(thread_info=True) ``` PySnooper supports decorating generators. If you decorate a class with `snoop`, it'll automatically apply the decorator to all the methods. (Not including properties and other special cases.) You can also customize the repr of an object: ```python def large(l): return isinstance(l, list) and len(l) > 5 def print_list_size(l): return 'list(size={})'.format(len(l)) def print_ndarray(a): return 'ndarray(shape={}, dtype={})'.format(a.shape, a.dtype) @pysnooper.snoop(custom_repr=((large, print_list_size), (numpy.ndarray, print_ndarray))) def sum_to_x(x): l = list(range(x)) a = numpy.zeros((10,10)) return sum(l) sum_to_x(10000) ``` You will get `l = list(size=10000)` for the list, and `a = ndarray(shape=(10, 10), dtype=float64)` for the ndarray. The `custom_repr` are matched in order, if one condition matches, no further conditions will be checked. Variables and exceptions get truncated to 100 characters by default. You can customize that: ```python @pysnooper.snoop(max_variable_length=200) ``` You can also use `max_variable_length=None` to never truncate them. Use `relative_time=True` to show timestamps relative to start time rather than wall time. The output is colored for easy viewing by default, except on Windows. Disable colors like so: ```python @pysnooper.snoop(color=False) ```` ================================================ FILE: AUTHORS ================================================ Ram Rachum Oleg Butuzov Edward Betts wilfredinni Peter Bittner Alireza Ayinmehr Christian Zietz Binwei Hu Loukas Leontopoulos Shlomi Fish Alex Hall pohmelie Nikita Melentev Mike Bayer Andreas van Cranenburgh Hervé Beraud Diego Volpatto Alexander Bersenev Xiang Gao pikez Jonathan Reichelt Gjertsen Guoqiang Ding Itamar.Raviv iory Mark Blakeney Yael Mintz Lumír 'Frenzy' Balhar Lukas Klenk sizhky Andrej730 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Ram Rachum and collaborators 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. ================================================ FILE: MANIFEST.in ================================================ include README.md include LICENSE include requirements.in include requirements.txt recursive-include tests *.txt *.py prune tests/.pytest_cache ================================================ FILE: README.md ================================================ # PySnooper - Never use print for debugging again **PySnooper** is a poor man's debugger. If you've used Bash, it's like `set -x` for Python, except it's fancier. Your story: 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 want to know which lines are running and which aren't, and what the values of the local variables are. Most people would use `print` lines, in strategic locations, some of them showing the values of 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 returning a list of bits. Let's snoop on it by adding the `@pysnooper.snoop()` decorator: ```python 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: ![](https://i.imgur.com/TrF3VVj.jpg) Or if you don't want to trace an entire function, you can wrap the relevant part in a `with` block: ```python import pysnooper import random def foo(): lst = [] for i in range(10): lst.append(random.randrange(1, 1000)) with pysnooper.snoop(): lower = min(lst) upper = max(lst) mid = (lower + upper) / 2 print(lower, mid, upper) foo() ``` which outputs something like: ``` New var:....... i = 9 New var:....... lst = [681, 267, 74, 832, 284, 678, ...] 09:37:35.881721 line 10 lower = min(lst) New var:....... lower = 74 09:37:35.882137 line 11 upper = max(lst) New var:....... upper = 832 09:37:35.882304 line 12 mid = (lower + upper) / 2 74 453.0 832 New var:....... mid = 453.0 09:37:35.882486 line 13 print(lower, mid, upper) Elapsed time: 00:00:00.000344 ``` ## Features If stderr is not easily accessible for you, you can redirect the output to a file: ```python @pysnooper.snoop('/my/log/file.log') ``` You can also pass a stream or a callable instead, and they'll be used. See values of some expressions that aren't local variables: ```python @pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]')) ``` Show snoop lines for functions that your function calls: ```python @pysnooper.snoop(depth=2) ``` **See [Advanced Usage](https://github.com/cool-RR/PySnooper/blob/master/ADVANCED_USAGE.md) for more options.** <------ ## Installation with Pip The best way to install **PySnooper** is with Pip: ```console $ pip install pysnooper ``` ## Other installation options Conda with conda-forge channel: ```console $ conda install -c conda-forge pysnooper ``` Arch Linux: ```console $ yay -S python-pysnooper ``` Fedora Linux: ```console $ dnf install python3-pysnooper ``` ## Citing PySnooper If you use PySnooper in academic work, please use this citation format: ```bibtex @software{rachum2019pysnooper, title={PySnooper: Never use print for debugging again}, author={Rachum, Ram and Hall, Alex and Yanokura, Iori and others}, year={2019}, month={jun}, publisher={PyCon Israel}, doi={10.5281/zenodo.10462459}, url={https://github.com/cool-RR/PySnooper} } ``` ## License Copyright (c) 2019 Ram Rachum and collaborators, released under the MIT license. ## Media Coverage [Hacker News thread](https://news.ycombinator.com/item?id=19717786) and [/r/Python Reddit thread](https://www.reddit.com/r/Python/comments/bg0ida/pysnooper_never_use_print_for_debugging_again/) (22 April 2019) ================================================ FILE: make_release.sh ================================================ #!/usr/bin/env bash rm -rf dist/* build/* && python setup.py sdist bdist_wheel --universal && twine upload dist/* ================================================ FILE: misc/IDE files/PySnooper.wpr ================================================ #!wing #!version=11.0 ################################################################## # Wing project file # ################################################################## [project attributes] proj.directory-list = [{'dirloc': loc('../..'), 'excludes': ['dist', '.tox', 'htmlcov', 'build', '.ipynb_checkpoints', 'PySnooper.egg-info'], 'filter': '*', 'include_hidden': False, 'recursive': True, 'watch_for_changes': True}] proj.file-type = 'shared' proj.home-dir = loc('../..') proj.launch-config = {loc('../../../../../../Program Files/Python37/Scripts/pasteurize-script.py'): ('project', ('"c:\\Users\\Administrator\\Documents\\Python Projects\\PySnooper\\pysnooper" "c:\\Users\\Administrator\\Documents\\Python Projects\\PySnooper\\tests"', '')), loc('../../../../../Dropbox/Scripts and shortcuts/_simplify3d_add_m600.py'): ('project', ('"C:\\Users\\Administrator\\Dropbox\\Desktop\\foo.gcode"', ''))} testing.auto-test-file-specs = (('regex', 'pysnooper/tests.*/test[^./]*.py.?$'),) testing.test-framework = {None: ':internal pytest'} ================================================ FILE: misc/generate_authors.py ================================================ #!/usr/bin/env python # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. ''' Generate an AUTHORS file for your Git repo. This will list the authors by chronological order, from their first contribution. You probably want to run it this way: ./generate_authors > AUTHORS ''' import subprocess import sys # This is used for people who show up more than once: deny_list = frozenset(( 'Lumir Balhar', )) def drop_recurrences(iterable): s = set() for item in iterable: if item not in s: s.add(item) yield item def iterate_authors_by_chronological_order(branch): log_call = subprocess.run( ( 'git', 'log', branch, '--encoding=utf-8', '--full-history', '--reverse', '--format=format:%at;%an;%ae' ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) log_lines = log_call.stdout.decode('utf-8').split('\n') authors = tuple(line.strip().split(";")[1] for line in log_lines) authors = (author for author in authors if author not in deny_list) return drop_recurrences(authors) def print_authors(branch): for author in iterate_authors_by_chronological_order(branch): sys.stdout.buffer.write(author.encode()) sys.stdout.buffer.write(b'\n') if __name__ == '__main__': try: branch = sys.argv[1] except IndexError: branch = 'master' print_authors(branch) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" ================================================ FILE: pysnooper/__init__.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. ''' PySnooper - Never use print for debugging again Usage: import pysnooper @pysnooper.snoop() def your_function(x): ... A log will be written to stderr showing the lines executed and variables changed in the decorated function. For more information, see https://github.com/cool-RR/PySnooper ''' from .tracer import Tracer as snoop from .variables import Attrs, Exploding, Indices, Keys import collections __VersionInfo = collections.namedtuple('VersionInfo', ('major', 'minor', 'micro')) __version__ = '1.2.3' __version_info__ = __VersionInfo(*(map(int, __version__.split('.')))) del collections, __VersionInfo # Avoid polluting the namespace ================================================ FILE: pysnooper/pycompat.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. '''Python 2/3 compatibility''' import abc import os import inspect import sys import datetime as datetime_module PY3 = (sys.version_info[0] == 3) PY2 = not PY3 if hasattr(abc, 'ABC'): ABC = abc.ABC else: class ABC(object): """Helper class that provides a standard way to create an ABC using inheritance. """ __metaclass__ = abc.ABCMeta __slots__ = () if hasattr(os, 'PathLike'): PathLike = os.PathLike else: class PathLike(ABC): """Abstract base class for implementing the file system path protocol.""" @abc.abstractmethod def __fspath__(self): """Return the file system path representation of the object.""" raise NotImplementedError @classmethod def __subclasshook__(cls, subclass): return ( hasattr(subclass, '__fspath__') or # Make a concession for older `pathlib` versions:g (hasattr(subclass, 'open') and 'path' in subclass.__name__.lower()) ) try: iscoroutinefunction = inspect.iscoroutinefunction except AttributeError: iscoroutinefunction = lambda whatever: False # Lolz try: isasyncgenfunction = inspect.isasyncgenfunction except AttributeError: isasyncgenfunction = lambda whatever: False # Lolz if PY3: string_types = (str,) text_type = str binary_type = bytes else: string_types = (basestring,) text_type = unicode binary_type = str try: from collections import abc as collections_abc except ImportError: # Python 2.7 import collections as collections_abc if sys.version_info[:2] >= (3, 6): time_isoformat = datetime_module.time.isoformat else: def time_isoformat(time, timespec='microseconds'): assert isinstance(time, datetime_module.time) if timespec != 'microseconds': raise NotImplementedError result = '{:02d}:{:02d}:{:02d}.{:06d}'.format( time.hour, time.minute, time.second, time.microsecond ) assert len(result) == 15 return result def timedelta_format(timedelta): time = (datetime_module.datetime.min + timedelta).time() return time_isoformat(time, timespec='microseconds') def timedelta_parse(s): hours, minutes, seconds, microseconds = map( int, s.replace('.', ':').split(':') ) return datetime_module.timedelta(hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds) ================================================ FILE: pysnooper/tracer.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import functools import inspect import opcode import os import sys import re import collections import datetime as datetime_module import itertools import threading import traceback from .variables import CommonVariable, Exploding, BaseVariable from . import utils, pycompat if pycompat.PY2: from io import open ipython_filename_pattern = re.compile('^$') ansible_filename_pattern = re.compile(r'^(.+\.zip)[/|\\](ansible[/|\\]modules[/|\\].+\.py)$') ipykernel_filename_pattern = re.compile(r'^/var/folders/.*/ipykernel_[0-9]+/[0-9]+.py$') RETURN_OPCODES = { 'RETURN_GENERATOR', 'RETURN_VALUE', 'RETURN_CONST', 'INSTRUMENTED_RETURN_GENERATOR', 'INSTRUMENTED_RETURN_VALUE', 'INSTRUMENTED_RETURN_CONST', 'YIELD_VALUE', 'INSTRUMENTED_YIELD_VALUE' } def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None, normalize=False): code = frame.f_code vars_order = (code.co_varnames + code.co_cellvars + code.co_freevars + tuple(frame.f_locals.keys())) result_items = [(key, utils.get_shortish_repr(value, custom_repr, max_length, normalize)) for key, value in frame.f_locals.items()] result_items.sort(key=lambda key_value: vars_order.index(key_value[0])) result = collections.OrderedDict(result_items) for variable in watch: result.update(sorted(variable.items(frame, normalize))) return result class UnavailableSource(object): def __getitem__(self, i): return u'SOURCE IS UNAVAILABLE' source_and_path_cache = {} def get_path_and_source_from_frame(frame): globs = frame.f_globals or {} module_name = globs.get('__name__') file_name = frame.f_code.co_filename cache_key = (module_name, file_name) try: return source_and_path_cache[cache_key] except KeyError: pass loader = globs.get('__loader__') 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: ipython_filename_match = ipython_filename_pattern.match(file_name) ansible_filename_match = ansible_filename_pattern.match(file_name) ipykernel_filename_match = ipykernel_filename_pattern.match(file_name) if ipykernel_filename_match: try: import linecache _, _, source, _ = linecache.cache.get(file_name) source = [line.rstrip() for line in source] # remove '\n' at the end except Exception: pass elif 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 elif ansible_filename_match: try: import zipfile archive_file = zipfile.ZipFile(ansible_filename_match.group(1), 'r') source = archive_file.read(ansible_filename_match.group(2).replace('\\', '/')).splitlines() except Exception: pass else: try: with open(file_name, 'rb') as fp: source = fp.read().splitlines() except utils.file_reading_errors: pass if not source: # We used to check `if source is None` but I found a rare bug where it # was empty, but not `None`, so now we check `if not source`. source = UnavailableSource() # 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 = 'utf-8' 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 = [pycompat.text_type(sline, encoding, 'replace') for sline in source] result = (file_name, source) source_and_path_cache[cache_key] = result return result def get_write_function(output, overwrite): is_path = isinstance(output, (pycompat.PathLike, str)) if overwrite and not is_path: raise Exception('`overwrite=True` can only be used when writing ' 'content to file.') if output is None: def write(s): stderr = sys.stderr try: stderr.write(s) except UnicodeEncodeError: # God damn Python 2 stderr.write(utils.shitcode(s)) elif is_path: return FileWriter(output, overwrite).write elif callable(output): write = output else: assert isinstance(output, utils.WritableStream) def write(s): output.write(s) return write class FileWriter(object): def __init__(self, path, overwrite): self.path = pycompat.text_type(path) self.overwrite = overwrite def write(self, s): with open(self.path, 'w' if self.overwrite else 'a', encoding='utf-8') as output_file: output_file.write(s) self.overwrite = False thread_global = threading.local() DISABLED = bool(os.getenv('PYSNOOPER_DISABLED', '')) class Tracer: ''' Snoop on the function, writing everything it's doing to stderr. This is useful for debugging. When you decorate a function with `@pysnooper.snoop()` or wrap a block of code in `with pysnooper.snoop():`, you'll get a log of every line that ran in the function and a play-by-play of every local variable that changed. If stderr is not easily accessible for you, you can redirect the output to a file:: @pysnooper.snoop('/my/log/file.log') See values of some expressions that aren't local variables:: @pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]')) Expand values to see all their attributes or items of lists/dictionaries: @pysnooper.snoop(watch_explode=('foo', 'self')) (see Advanced Usage in the README for more control) Show snoop lines for functions that your function calls:: @pysnooper.snoop(depth=2) 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:: @pysnooper.snoop(thread_info=True) Customize how values are represented as strings:: @pysnooper.snoop(custom_repr=((type1, custom_repr_func1), (condition2, custom_repr_func2), ...)) Variables and exceptions get truncated to 100 characters by default. You can customize that: @pysnooper.snoop(max_variable_length=200) You can also use `max_variable_length=None` to never truncate them. Show timestamps relative to start time rather than wall time:: @pysnooper.snoop(relative_time=True) The output is colored for easy viewing by default, except on Windows (but can be enabled by setting `color=True`). Disable colors like so: @pysnooper.snoop(color=False) ''' def __init__(self, output=None, watch=(), watch_explode=(), depth=1, prefix='', overwrite=False, thread_info=False, custom_repr=(), max_variable_length=100, normalize=False, relative_time=False, color=sys.platform in ('linux', 'linux2', 'cygwin', 'darwin')): self._write = get_write_function(output, overwrite) self.watch = [ v if isinstance(v, BaseVariable) else CommonVariable(v) for v in utils.ensure_tuple(watch) ] + [ v if isinstance(v, BaseVariable) else Exploding(v) for v in utils.ensure_tuple(watch_explode) ] self.frame_to_local_reprs = {} self.start_times = {} self.depth = depth self.prefix = prefix self.thread_info = thread_info self.thread_info_padding = 0 assert self.depth >= 1 self.target_codes = set() self.target_frames = set() self.thread_local = threading.local() if len(custom_repr) == 2 and not all(isinstance(x, pycompat.collections_abc.Iterable) for x in custom_repr): custom_repr = (custom_repr,) self.custom_repr = custom_repr self.last_source_path = None self.max_variable_length = max_variable_length self.normalize = normalize self.relative_time = relative_time self.color = color and (output is None) if self.color: self._FOREGROUND_BLUE = '\x1b[34m' self._FOREGROUND_CYAN = '\x1b[36m' self._FOREGROUND_GREEN = '\x1b[32m' self._FOREGROUND_MAGENTA = '\x1b[35m' self._FOREGROUND_RED = '\x1b[31m' self._FOREGROUND_RESET = '\x1b[39m' self._FOREGROUND_YELLOW = '\x1b[33m' self._STYLE_BRIGHT = '\x1b[1m' self._STYLE_DIM = '\x1b[2m' self._STYLE_NORMAL = '\x1b[22m' self._STYLE_RESET_ALL = '\x1b[0m' else: self._FOREGROUND_BLUE = '' self._FOREGROUND_CYAN = '' self._FOREGROUND_GREEN = '' self._FOREGROUND_MAGENTA = '' self._FOREGROUND_RED = '' self._FOREGROUND_RESET = '' self._FOREGROUND_YELLOW = '' self._STYLE_BRIGHT = '' self._STYLE_DIM = '' self._STYLE_NORMAL = '' self._STYLE_RESET_ALL = '' def __call__(self, function_or_class): if DISABLED: return function_or_class if inspect.isclass(function_or_class): return self._wrap_class(function_or_class) else: return self._wrap_function(function_or_class) def _wrap_class(self, cls): for attr_name, attr in cls.__dict__.items(): # Coroutines are functions, but snooping them is not supported # at the moment if pycompat.iscoroutinefunction(attr): continue if inspect.isfunction(attr): setattr(cls, attr_name, self._wrap_function(attr)) return cls def _wrap_function(self, function): self.target_codes.add(function.__code__) @functools.wraps(function) def simple_wrapper(*args, **kwargs): with self: return function(*args, **kwargs) @functools.wraps(function) def generator_wrapper(*args, **kwargs): gen = function(*args, **kwargs) method, incoming = gen.send, None while True: with self: try: outgoing = method(incoming) except StopIteration: return try: method, incoming = gen.send, (yield outgoing) except Exception as e: method, incoming = gen.throw, e if pycompat.iscoroutinefunction(function): raise NotImplementedError if pycompat.isasyncgenfunction(function): raise NotImplementedError elif inspect.isgeneratorfunction(function): return generator_wrapper else: return simple_wrapper def write(self, s): s = u'{self.prefix}{s}\n'.format(**locals()) self._write(s) def __enter__(self): if DISABLED: return thread_global.__dict__.setdefault('depth', -1) calling_frame = inspect.currentframe().f_back if not self._is_internal_frame(calling_frame): calling_frame.f_trace = self.trace self.target_frames.add(calling_frame) stack = self.thread_local.__dict__.setdefault( 'original_trace_functions', [] ) stack.append(sys.gettrace()) self.start_times[calling_frame] = datetime_module.datetime.now() sys.settrace(self.trace) def __exit__(self, exc_type, exc_value, exc_traceback): if DISABLED: return stack = self.thread_local.original_trace_functions sys.settrace(stack.pop()) calling_frame = inspect.currentframe().f_back self.target_frames.discard(calling_frame) self.frame_to_local_reprs.pop(calling_frame, None) ### Writing elapsed time: ############################################# # # _FOREGROUND_YELLOW = self._FOREGROUND_YELLOW _STYLE_DIM = self._STYLE_DIM _STYLE_NORMAL = self._STYLE_NORMAL _STYLE_RESET_ALL = self._STYLE_RESET_ALL start_time = self.start_times.pop(calling_frame) duration = datetime_module.datetime.now() - start_time elapsed_time_string = pycompat.timedelta_format(duration) indent = ' ' * 4 * (thread_global.depth + 1) self.write( '{indent}{_FOREGROUND_YELLOW}{_STYLE_DIM}' 'Elapsed time: {_STYLE_NORMAL}{elapsed_time_string}' '{_STYLE_RESET_ALL}'.format(**locals()) ) # # ### Finished writing elapsed time. #################################### 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: ####################### # # # 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. if not (frame.f_code in self.target_codes or frame in self.target_frames): 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 None elif self._is_internal_frame(frame): return None else: _frame_candidate = frame for i in range(1, self.depth): _frame_candidate = _frame_candidate.f_back if _frame_candidate is None: return None elif _frame_candidate.f_code in self.target_codes or _frame_candidate in self.target_frames: break else: return None # # ### Finished checking whether we should trace this line. ############## if event == 'call': thread_global.depth += 1 indent = ' ' * 4 * thread_global.depth _FOREGROUND_BLUE = self._FOREGROUND_BLUE _FOREGROUND_CYAN = self._FOREGROUND_CYAN _FOREGROUND_GREEN = self._FOREGROUND_GREEN _FOREGROUND_MAGENTA = self._FOREGROUND_MAGENTA _FOREGROUND_RED = self._FOREGROUND_RED _FOREGROUND_RESET = self._FOREGROUND_RESET _FOREGROUND_YELLOW = self._FOREGROUND_YELLOW _STYLE_BRIGHT = self._STYLE_BRIGHT _STYLE_DIM = self._STYLE_DIM _STYLE_NORMAL = self._STYLE_NORMAL _STYLE_RESET_ALL = self._STYLE_RESET_ALL ### Making timestamp: ################################################# # # if self.normalize: timestamp = ' ' * 15 elif self.relative_time: try: start_time = self.start_times[frame] except KeyError: start_time = self.start_times[frame] = \ datetime_module.datetime.now() duration = datetime_module.datetime.now() - start_time timestamp = pycompat.timedelta_format(duration) else: timestamp = pycompat.time_isoformat( datetime_module.datetime.now().time(), timespec='microseconds' ) # # ### Finished making timestamp. ######################################## line_no = frame.f_lineno source_path, source = get_path_and_source_from_frame(frame) source_path = source_path if not self.normalize else os.path.basename(source_path) if self.last_source_path != source_path: self.write(u'{_FOREGROUND_YELLOW}{_STYLE_DIM}{indent}Source path:... ' u'{_STYLE_NORMAL}{source_path}' u'{_STYLE_RESET_ALL}'.format(**locals())) self.last_source_path = source_path source_line = source[line_no - 1] thread_info = "" if self.thread_info: if self.normalize: raise NotImplementedError("normalize is not supported with " "thread_info") current_thread = threading.current_thread() thread_info = "{ident}-{name} ".format( ident=current_thread.ident, name=current_thread.name) thread_info = self.set_thread_info_padding(thread_info) ### Reporting newish and modified variables: ########################## # # old_local_reprs = self.frame_to_local_reprs.get(frame, {}) self.frame_to_local_reprs[frame] = local_reprs = \ get_local_reprs(frame, watch=self.watch, custom_repr=self.custom_repr, max_length=self.max_variable_length, normalize=self.normalize, ) newish_string = ('Starting var:.. ' if event == 'call' else 'New var:....... ') for name, value_repr in local_reprs.items(): if name not in old_local_reprs: self.write('{indent}{_FOREGROUND_GREEN}{_STYLE_DIM}' '{newish_string}{_STYLE_NORMAL}{name} = ' '{value_repr}{_STYLE_RESET_ALL}'.format(**locals())) elif old_local_reprs[name] != value_repr: self.write('{indent}{_FOREGROUND_GREEN}{_STYLE_DIM}' 'Modified var:.. {_STYLE_NORMAL}{name} = ' '{value_repr}{_STYLE_RESET_ALL}'.format(**locals())) # # ### Finished newish and modified variables. ########################### ### 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 = source[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. ############## # If a call ends due to an exception, we still get a 'return' event # with arg = None. This seems to be the only way to tell the difference # https://stackoverflow.com/a/12800909/2482744 code_byte = frame.f_code.co_code[frame.f_lasti] if not isinstance(code_byte, int): code_byte = ord(code_byte) ended_by_exception = ( event == 'return' and arg is None and opcode.opname[code_byte] not in RETURN_OPCODES ) if ended_by_exception: self.write('{_FOREGROUND_RED}{indent}Call ended by exception{_STYLE_RESET_ALL}'. format(**locals())) else: self.write(u'{indent}{_STYLE_DIM}{timestamp} {thread_info}{event:9} ' u'{line_no:4}{_STYLE_RESET_ALL} {source_line}'.format(**locals())) if event == 'return': self.frame_to_local_reprs.pop(frame, None) self.start_times.pop(frame, None) thread_global.depth -= 1 if not ended_by_exception: return_value_repr = utils.get_shortish_repr(arg, custom_repr=self.custom_repr, max_length=self.max_variable_length, normalize=self.normalize, ) self.write('{indent}{_FOREGROUND_CYAN}{_STYLE_DIM}' 'Return value:.. {_STYLE_NORMAL}{return_value_repr}' '{_STYLE_RESET_ALL}'. format(**locals())) if event == 'exception': exception = utils.format_exception(*arg[:2]) if self.max_variable_length: exception = utils.truncate(exception, self.max_variable_length) self.write('{indent}{_FOREGROUND_RED}Exception:..... ' '{_STYLE_BRIGHT}{exception}' '{_STYLE_RESET_ALL}'.format(**locals())) return self.trace ================================================ FILE: pysnooper/utils.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import abc import re import traceback import sys from .pycompat import ABC, string_types, collections_abc 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(ABC): @abc.abstractmethod def write(self, s): pass @classmethod def __subclasshook__(cls, C): if cls is WritableStream: return _check_methods(C, 'write') return NotImplemented file_reading_errors = ( IOError, OSError, ValueError # IronPython weirdness. ) def shitcode(s): return ''.join( (c if (0 < ord(c) < 256) else '?') for c in s ) def get_repr_function(item, custom_repr): for condition, action in custom_repr: if isinstance(condition, type): condition = lambda x, y=condition: isinstance(x, y) if condition(item): return action return repr DEFAULT_REPR_RE = re.compile(r' at 0x[a-f0-9A-F]{4,}') def normalize_repr(item_repr): """Remove memory address (0x...) from a default python repr""" return DEFAULT_REPR_RE.sub('', item_repr) def get_shortish_repr(item, custom_repr=(), max_length=None, normalize=False): repr_function = get_repr_function(item, custom_repr) try: r = repr_function(item) except Exception: r = 'REPR FAILED' r = r.replace('\r', '').replace('\n', '') if normalize: r = normalize_repr(r) if max_length: r = truncate(r, max_length) return r def truncate(string, max_length): if (max_length is None) or (len(string) <= max_length): return string else: left = (max_length - 3) // 2 right = max_length - 3 - left return u'{}...{}'.format(string[:left], string[-right:]) def format_exception(exc_type, exc_value): try: is_group = isinstance(exc_value, BaseExceptionGroup) except NameError: is_group = False if is_group: sub_types = ', '.join(type(e).__name__ for e in exc_value.exceptions) message = exc_value.args[0] if exc_value.args else '' return u"{}: '{}' ({} sub-exceptions: {})".format( exc_type.__name__, message, len(exc_value.exceptions), sub_types, ) return u'\n'.join(traceback.format_exception_only(exc_type, exc_value)).strip() def ensure_tuple(x): if isinstance(x, collections_abc.Iterable) and \ not isinstance(x, string_types): return tuple(x) else: return (x,) ================================================ FILE: pysnooper/variables.py ================================================ import itertools import abc try: from collections.abc import Mapping, Sequence except ImportError: from collections import Mapping, Sequence from copy import deepcopy from . import utils from . import pycompat def needs_parentheses(source): def code(s): return compile(s, '', 'eval').co_code return code('{}.x'.format(source)) != code('({}).x'.format(source)) class BaseVariable(pycompat.ABC): def __init__(self, source, exclude=()): self.source = source self.exclude = utils.ensure_tuple(exclude) self.code = compile(source, '', 'eval') if needs_parentheses(source): self.unambiguous_source = '({})'.format(source) else: self.unambiguous_source = source def items(self, frame, normalize=False): try: main_value = eval(self.code, frame.f_globals or {}, frame.f_locals) except Exception: return () return self._items(main_value, normalize) @abc.abstractmethod def _items(self, key, normalize=False): raise NotImplementedError @property def _fingerprint(self): return (type(self), self.source, self.exclude) def __hash__(self): return hash(self._fingerprint) def __eq__(self, other): return (isinstance(other, BaseVariable) and self._fingerprint == other._fingerprint) class CommonVariable(BaseVariable): def _items(self, main_value, normalize=False): result = [(self.source, utils.get_shortish_repr(main_value, normalize=normalize))] for key in self._safe_keys(main_value): try: if key in self.exclude: continue value = self._get_value(main_value, key) except Exception: continue result.append(( '{}{}'.format(self.unambiguous_source, self._format_key(key)), utils.get_shortish_repr(value) )) return result def _safe_keys(self, main_value): try: for key in self._keys(main_value): yield key except Exception: pass def _keys(self, main_value): return () def _format_key(self, key): raise NotImplementedError def _get_value(self, main_value, key): raise NotImplementedError class Attrs(CommonVariable): def _keys(self, main_value): return itertools.chain( getattr(main_value, '__dict__', ()), getattr(main_value, '__slots__', ()) ) def _format_key(self, key): return '.' + key def _get_value(self, main_value, key): return getattr(main_value, key) class Keys(CommonVariable): def _keys(self, main_value): return main_value.keys() def _format_key(self, key): return '[{}]'.format(utils.get_shortish_repr(key)) def _get_value(self, main_value, key): return main_value[key] class Indices(Keys): _slice = slice(None) def _keys(self, main_value): return range(len(main_value))[self._slice] def __getitem__(self, item): assert isinstance(item, slice) result = deepcopy(self) result._slice = item return result class Exploding(BaseVariable): def _items(self, main_value, normalize=False): if isinstance(main_value, Mapping): cls = Keys elif isinstance(main_value, Sequence): cls = Indices else: cls = Attrs return cls(self.source, self.exclude)._items(main_value, normalize) ================================================ FILE: requirements.in ================================================ # That's right baby! No dependencies! ================================================ FILE: requirements.txt ================================================ # # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file requirements.txt requirements.in # ================================================ FILE: setup.cfg ================================================ [metadata] name = PySnooper version = attr: pysnooper.__version__ author = Ram Rachum author_email = ram@rachum.com description = A poor man's debugger for Python. url = https://github.com/cool-RR/PySnooper long_description = file: README.md long_description_content_type = text/markdown classifiers = Environment :: Console Intended Audience :: Developers Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy License :: OSI Approved :: MIT License Operating System :: OS Independent Topic :: Software Development :: Debuggers [options] packages = find: install_requires = file: requirements.in [options.packages.find] exclude = tests* [options.extras_require] tests = pytest ================================================ FILE: setup.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import setuptools import re def read_file(filename): with open(filename) as file: return file.read() version = re.search("__version__ = '([0-9.]*)'", read_file('pysnooper/__init__.py')).group(1) setuptools.setup( name='PySnooper', version=version, author='Ram Rachum', author_email='ram@rachum.com', description="A poor man's debugger for Python.", long_description=read_file('README.md'), long_description_content_type='text/markdown', url='https://github.com/cool-RR/PySnooper', packages=setuptools.find_packages(exclude=['tests*']), install_requires=read_file('requirements.in'), extras_require={ 'tests': { 'pytest', }, }, classifiers=[ 'Environment :: Console', 'Intended Audience :: Developers', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: 3.15', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Debuggers', ], ) ================================================ FILE: tests/__init__.py ================================================ import pytest pytest.register_assert_rewrite('tests.utils') ================================================ FILE: tests/mini_toolbox/__init__.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import tempfile import shutil import io import sys from . import pathlib from . import contextlib @contextlib.contextmanager def BlankContextManager(): yield @contextlib.contextmanager def create_temp_folder(prefix=tempfile.template, suffix='', parent_folder=None, chmod=None): ''' Context manager that creates a temporary folder and deletes it after usage. After the suite finishes, the temporary folder and all its files and subfolders will be deleted. Example: with create_temp_folder() as temp_folder: # We have a temporary folder! assert temp_folder.is_dir() # We can create files in it: (temp_folder / 'my_file').open('w') # The suite is finished, now it's all cleaned: assert not temp_folder.exists() Use the `prefix` and `suffix` string arguments to dictate a prefix and/or a suffix to the temporary folder's name in the filesystem. If you'd like to set the permissions of the temporary folder, pass them to the optional `chmod` argument, like this: create_temp_folder(chmod=0o550) ''' temp_folder = pathlib.Path(tempfile.mkdtemp(prefix=prefix, suffix=suffix, dir=parent_folder)) try: if chmod is not None: temp_folder.chmod(chmod) yield temp_folder finally: shutil.rmtree(str(temp_folder)) class NotInDict: '''Object signifying that the key was not found in the dict.''' class TempValueSetter(object): ''' Context manager for temporarily setting a value to a variable. The value is set to the variable before the suite starts, and gets reset back to the old value after the suite finishes. ''' def __init__(self, variable, value, assert_no_fiddling=True): ''' Construct the `TempValueSetter`. `variable` may be either an `(object, attribute_string)`, a `(dict, key)` pair, or a `(getter, setter)` pair. `value` is the temporary value to set to the variable. ''' self.assert_no_fiddling = assert_no_fiddling ####################################################################### # We let the user input either an `(object, attribute_string)`, a # `(dict, key)` pair, or a `(getter, setter)` pair. So now it's our job # to inspect `variable` and figure out which one of these options the # user chose, and then obtain from that a `(getter, setter)` pair that # we could use. bad_input_exception = Exception( '`variable` must be either an `(object, attribute_string)` pair, ' 'a `(dict, key)` pair, or a `(getter, setter)` pair.' ) try: first, second = variable except Exception: raise bad_input_exception if hasattr(first, '__getitem__') and hasattr(first, 'get') and \ hasattr(first, '__setitem__') and hasattr(first, '__delitem__'): # `first` is a dictoid; so we were probably handed a `(dict, key)` # pair. self.getter = lambda: first.get(second, NotInDict) self.setter = lambda value: (first.__setitem__(second, value) if value is not NotInDict else first.__delitem__(second)) ### Finished handling the `(dict, key)` case. ### elif callable(second): # `second` is a callable; so we were probably handed a `(getter, # setter)` pair. if not callable(first): raise bad_input_exception self.getter, self.setter = first, second ### Finished handling the `(getter, setter)` case. ### else: # All that's left is the `(object, attribute_string)` case. if not isinstance(second, str): raise bad_input_exception parent, attribute_name = first, second self.getter = lambda: getattr(parent, attribute_name) self.setter = lambda value: setattr(parent, attribute_name, value) ### Finished handling the `(object, attribute_string)` case. ### # # ### Finished obtaining a `(getter, setter)` pair from `variable`. ##### self.getter = self.getter '''Getter for getting the current value of the variable.''' self.setter = self.setter '''Setter for Setting the the variable's value.''' self.value = value '''The value to temporarily set to the variable.''' self.active = False def __enter__(self): self.active = True self.old_value = self.getter() '''The old value of the variable, before entering the suite.''' self.setter(self.value) # In `__exit__` we'll want to check if anyone changed the value of the # variable in the suite, which is unallowed. But we can't compare to # `.value`, because sometimes when you set a value to a variable, some # mechanism modifies that value for various reasons, resulting in a # supposedly equivalent, but not identical, value. For example this # happens when you set the current working directory on Mac OS. # # So here we record the value right after setting, and after any # possible processing the system did to it: self._value_right_after_setting = self.getter() return self def __exit__(self, exc_type, exc_value, exc_traceback): if self.assert_no_fiddling: # Asserting no-one inside the suite changed our variable: assert self.getter() == self._value_right_after_setting self.setter(self.old_value) self.active = False class OutputCapturer(object): ''' Context manager for catching all system output generated during suite. Example: with OutputCapturer() as output_capturer: print('woo!') assert output_capturer.output == 'woo!\n' The boolean arguments `stdout` and `stderr` determine, respectively, whether the standard-output and the standard-error streams will be captured. ''' def __init__(self, stdout=True, stderr=True): self.string_io = io.StringIO() if stdout: self._stdout_temp_setter = \ TempValueSetter((sys, 'stdout'), self.string_io) else: # not stdout self._stdout_temp_setter = BlankContextManager() if stderr: self._stderr_temp_setter = \ TempValueSetter((sys, 'stderr'), self.string_io) else: # not stderr self._stderr_temp_setter = BlankContextManager() def __enter__(self): '''Manage the `OutputCapturer`'s context.''' self._stdout_temp_setter.__enter__() self._stderr_temp_setter.__enter__() return self def __exit__(self, exc_type, exc_value, exc_traceback): # Not doing exception swallowing anywhere here. self._stderr_temp_setter.__exit__(exc_type, exc_value, exc_traceback) self._stdout_temp_setter.__exit__(exc_type, exc_value, exc_traceback) output = property(lambda self: self.string_io.getvalue(), doc='''The string of output that was captured.''') class TempSysPathAdder(object): ''' Context manager for temporarily adding paths to `sys.path`. Removes the path(s) after suite. Example: with TempSysPathAdder('path/to/fubar/package'): import fubar fubar.do_stuff() ''' def __init__(self, addition): self.addition = [str(addition)] def __enter__(self): self.entries_not_in_sys_path = [entry for entry in self.addition if entry not in sys.path] sys.path += self.entries_not_in_sys_path return self def __exit__(self, *args, **kwargs): for entry in self.entries_not_in_sys_path: # We don't allow anyone to remove it except for us: assert entry in sys.path sys.path.remove(entry) ================================================ FILE: tests/mini_toolbox/contextlib.py ================================================ """contextlib2 - backports and enhancements to the contextlib module""" import sys import warnings from collections import deque from functools import wraps __all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", "redirect_stdout", "redirect_stderr", "suppress"] # Backwards compatibility __all__ += ["ContextStack"] class ContextDecorator(object): "A base class or mixin that enables context managers to work as decorators." def refresh_cm(self): """Returns the context manager used to actually wrap the call to the decorated function. The default implementation just returns *self*. Overriding this method allows otherwise one-shot context managers like _GeneratorContextManager to support use as decorators via implicit recreation. DEPRECATED: refresh_cm was never added to the standard library's ContextDecorator API """ warnings.warn("refresh_cm was never added to the standard library", DeprecationWarning) return self._recreate_cm() def _recreate_cm(self): """Return a recreated instance of self. Allows an otherwise one-shot context manager like _GeneratorContextManager to support use as a decorator via implicit recreation. This is a private interface just for _GeneratorContextManager. See issue #11647 for details. """ return self def __call__(self, func): @wraps(func) def inner(*args, **kwds): with self._recreate_cm(): return func(*args, **kwds) return inner class _GeneratorContextManager(ContextDecorator): """Helper for @contextmanager decorator.""" def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) self.func, self.args, self.kwds = func, args, kwds # Issue 19330: ensure context manager instances have good docstrings doc = getattr(func, "__doc__", None) if doc is None: doc = type(self).__doc__ self.__doc__ = doc # Unfortunately, this still doesn't provide good help output when # inspecting the created context manager instances, since pydoc # currently bypasses the instance docstring and shows the docstring # for the class instead. # See http://bugs.python.org/issue19404 for more details. def _recreate_cm(self): # _GCM instances are one-shot context managers, so the # CM must be recreated each time a decorated function is # called return self.__class__(self.func, self.args, self.kwds) def __enter__(self): try: return next(self.gen) except StopIteration: raise RuntimeError("generator didn't yield") def __exit__(self, type, value, traceback): if type is None: try: next(self.gen) except StopIteration: return else: raise RuntimeError("generator didn't stop") else: if value is None: # Need to force instantiation so we can reliably # tell if we get the same exception back value = type() try: self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration # raised inside the "with" statement from being suppressed. return exc is not value except RuntimeError as exc: # Don't re-raise the passed in exception if exc is value: return False # Likewise, avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError # (see PEP 479). if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: return False raise except: # only re-raise if it's *not* the exception that was # passed to throw(), because __exit__() must not raise # an exception unless __exit__() itself failed. But throw() # has to raise the exception to signal propagation, so this # fixes the impedance mismatch between the throw() protocol # and the __exit__() protocol. # if sys.exc_info()[1] is not value: raise def contextmanager(func): """@contextmanager decorator. Typical usage: @contextmanager def some_generator(): try: yield finally: This makes this: with some_generator() as : equivalent to this: try: = finally: """ @wraps(func) def helper(*args, **kwds): return _GeneratorContextManager(func, args, kwds) return helper class closing(object): """Context to automatically close something at the end of a block. Code like this: with closing(.open()) as f: is equivalent to this: f = .open() try: finally: f.close() """ def __init__(self, thing): self.thing = thing def __enter__(self): return self.thing def __exit__(self, *exc_info): self.thing.close() class _RedirectStream(object): _stream = None def __init__(self, new_target): self._new_target = new_target # We use a list of old targets to make this CM re-entrant self._old_targets = [] def __enter__(self): self._old_targets.append(getattr(sys, self._stream)) setattr(sys, self._stream, self._new_target) return self._new_target def __exit__(self, exctype, excinst, exctb): setattr(sys, self._stream, self._old_targets.pop()) class redirect_stdout(_RedirectStream): """Context manager for temporarily redirecting stdout to another file. # How to send help() to stderr with redirect_stdout(sys.stderr): help(dir) # How to write help() to a file with open('help.txt', 'w') as f: with redirect_stdout(f): help(pow) """ _stream = "stdout" class redirect_stderr(_RedirectStream): """Context manager for temporarily redirecting stderr to another file.""" _stream = "stderr" class suppress(object): """Context manager to suppress specified exceptions After the exception is suppressed, execution proceeds with the next statement following the with statement. with suppress(FileNotFoundError): os.remove(somefile) # Execution still resumes here if the file was already removed """ def __init__(self, *exceptions): self._exceptions = exceptions def __enter__(self): pass def __exit__(self, exctype, excinst, exctb): # Unlike isinstance and issubclass, CPython exception handling # currently only looks at the concrete type hierarchy (ignoring # the instance and subclass checking hooks). While Guido considers # that a bug rather than a feature, it's a fairly hard one to fix # due to various internal implementation details. suppress provides # the simpler issubclass based semantics, rather than trying to # exactly reproduce the limitations of the CPython interpreter. # # See http://bugs.python.org/issue12029 for more details return exctype is not None and issubclass(exctype, self._exceptions) # Context manipulation is Python 3 only _HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 if _HAVE_EXCEPTION_CHAINING: def _make_context_fixer(frame_exc): def _fix_exception_context(new_exc, old_exc): # Context may not be correct, so find the end of the chain while 1: exc_context = new_exc.__context__ if exc_context is old_exc: # Context is already set correctly (see issue 20317) return if exc_context is None or exc_context is frame_exc: break new_exc = exc_context # Change the end of the chain to point to the exception # we expect it to reference new_exc.__context__ = old_exc return _fix_exception_context def _reraise_with_existing_context(exc_details): try: # bare "raise exc_details[1]" replaces our carefully # set-up context fixed_ctx = exc_details[1].__context__ raise exc_details[1] except BaseException: exc_details[1].__context__ = fixed_ctx raise else: # No exception context in Python 2 def _make_context_fixer(frame_exc): return lambda new_exc, old_exc: None # Use 3 argument raise in Python 2, # but use exec to avoid SyntaxError in Python 3 def _reraise_with_existing_context(exc_details): exc_type, exc_value, exc_tb = exc_details exec ("raise exc_type, exc_value, exc_tb") # Handle old-style classes if they exist try: from types import InstanceType except ImportError: # Python 3 doesn't have old-style classes _get_type = type else: # Need to handle old-style context managers on Python 2 def _get_type(obj): obj_type = type(obj) if obj_type is InstanceType: return obj.__class__ # Old-style class return obj_type # New-style class # Inspired by discussions on http://bugs.python.org/issue13585 class ExitStack(object): """Context manager for dynamic management of a stack of exit callbacks For example: with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception """ def __init__(self): self._exit_callbacks = deque() def pop_all(self): """Preserve the context stack by transferring it to a new instance""" new_stack = type(self)() new_stack._exit_callbacks = self._exit_callbacks self._exit_callbacks = deque() return new_stack def _push_cm_exit(self, cm, cm_exit): """Helper to correctly register callbacks to __exit__ methods""" def _exit_wrapper(*exc_details): return cm_exit(cm, *exc_details) _exit_wrapper.__self__ = cm self.push(_exit_wrapper) def push(self, exit): """Registers a callback with the standard __exit__ method signature Can suppress exceptions the same way __exit__ methods can. Also accepts any object with an __exit__ method (registering a call to the method instead of the object itself) """ # We use an unbound method rather than a bound method to follow # the standard lookup behaviour for special methods _cb_type = _get_type(exit) try: exit_method = _cb_type.__exit__ except AttributeError: # Not a context manager, so assume its a callable self._exit_callbacks.append(exit) else: self._push_cm_exit(exit, exit_method) return exit # Allow use as a decorator def callback(self, callback, *args, **kwds): """Registers an arbitrary callback and arguments. Cannot suppress exceptions. """ def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) # We changed the signature, so using @wraps is not appropriate, but # setting __wrapped__ may still help with introspection _exit_wrapper.__wrapped__ = callback self.push(_exit_wrapper) return callback # Allow use as a decorator def enter_context(self, cm): """Enters the supplied context manager If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ # We look up the special methods on the type to match the with statement _cm_type = _get_type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) self._push_cm_exit(cm, _exit) return result def close(self): """Immediately unwind the context stack""" self.__exit__(None, None, None) def __enter__(self): return self def __exit__(self, *exc_details): received_exc = exc_details[0] is not None # We manipulate the exception state so it behaves as though # we were actually nesting multiple with statements frame_exc = sys.exc_info()[1] _fix_exception_context = _make_context_fixer(frame_exc) # Callbacks are invoked in LIFO order to match the behaviour of # nested context managers suppressed_exc = False pending_raise = False while self._exit_callbacks: cb = self._exit_callbacks.pop() try: if cb(*exc_details): suppressed_exc = True pending_raise = False exc_details = (None, None, None) except: new_exc_details = sys.exc_info() # simulate the stack of exceptions by setting the context _fix_exception_context(new_exc_details[1], exc_details[1]) pending_raise = True exc_details = new_exc_details if pending_raise: _reraise_with_existing_context(exc_details) return received_exc and suppressed_exc # Preserve backwards compatibility class ContextStack(ExitStack): """Backwards compatibility alias for ExitStack""" def __init__(self): warnings.warn("ContextStack has been renamed to ExitStack", DeprecationWarning) super(ContextStack, self).__init__() def register_exit(self, callback): return self.push(callback) def register(self, callback, *args, **kwds): return self.callback(callback, *args, **kwds) def preserve(self): return self.pop_all() ================================================ FILE: tests/mini_toolbox/pathlib.py ================================================ # Copyright (c) 2014-2017 Matthias C. M. Troffaes # Copyright (c) 2012-2014 Antoine Pitrou and contributors # Distributed under the terms of the MIT License. import ctypes import fnmatch import functools import io import ntpath import os import posixpath import re from pysnooper import pycompat import sys try: from collections.abc import Sequence except ImportError: from collections import Sequence from errno import EINVAL, ENOENT, ENOTDIR, EEXIST, EPERM, EACCES from operator import attrgetter from stat import ( S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO) try: from urllib import quote as urlquote_from_bytes except ImportError: from urllib.parse import quote_from_bytes as urlquote_from_bytes try: intern = intern except NameError: intern = sys.intern supports_symlinks = True if os.name == 'nt': import nt if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2): from nt import _getfinalpathname else: supports_symlinks = False _getfinalpathname = None else: nt = None try: from os import scandir as os_scandir except ImportError: from scandir import scandir as os_scandir __all__ = [ "PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath", ] # # Internals # def _py2_fsencode(parts): # py2 => minimal unicode support assert pycompat.PY2 return [part.encode('ascii') if isinstance(part, pycompat.text_type) else part for part in parts] def _try_except_fileexistserror(try_func, except_func, else_func=None): if sys.version_info >= (3, 3): try: try_func() except FileExistsError as exc: except_func(exc) else: if else_func is not None: else_func() else: try: try_func() except EnvironmentError as exc: if exc.errno != EEXIST: raise else: except_func(exc) else: if else_func is not None: else_func() def _try_except_filenotfounderror(try_func, except_func): if sys.version_info >= (3, 3): try: try_func() except FileNotFoundError as exc: except_func(exc) else: try: try_func() except EnvironmentError as exc: if exc.errno != ENOENT: raise else: except_func(exc) def _try_except_permissionerror_iter(try_iter, except_iter): if sys.version_info >= (3, 3): try: for x in try_iter(): yield x except PermissionError as exc: for x in except_iter(exc): yield x else: try: for x in try_iter(): yield x except EnvironmentError as exc: if exc.errno not in (EPERM, EACCES): raise else: for x in except_iter(exc): yield x def _win32_get_unique_path_id(path): # get file information, needed for samefile on older Python versions # see http://timgolden.me.uk/python/win32_how_do_i/ # see_if_two_files_are_the_same_file.html from ctypes import POINTER, Structure, WinError from ctypes.wintypes import DWORD, HANDLE, BOOL class FILETIME(Structure): _fields_ = [("datetime_lo", DWORD), ("datetime_hi", DWORD), ] class BY_HANDLE_FILE_INFORMATION(Structure): _fields_ = [("attributes", DWORD), ("created_at", FILETIME), ("accessed_at", FILETIME), ("written_at", FILETIME), ("volume", DWORD), ("file_hi", DWORD), ("file_lo", DWORD), ("n_links", DWORD), ("index_hi", DWORD), ("index_lo", DWORD), ] CreateFile = ctypes.windll.kernel32.CreateFileW CreateFile.argtypes = [ctypes.c_wchar_p, DWORD, DWORD, ctypes.c_void_p, DWORD, DWORD, HANDLE] CreateFile.restype = HANDLE GetFileInformationByHandle = ( ctypes.windll.kernel32.GetFileInformationByHandle) GetFileInformationByHandle.argtypes = [ HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)] GetFileInformationByHandle.restype = BOOL CloseHandle = ctypes.windll.kernel32.CloseHandle CloseHandle.argtypes = [HANDLE] CloseHandle.restype = BOOL GENERIC_READ = 0x80000000 FILE_SHARE_READ = 0x00000001 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 OPEN_EXISTING = 3 if os.path.isdir(path): flags = FILE_FLAG_BACKUP_SEMANTICS else: flags = 0 hfile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, None, OPEN_EXISTING, flags, None) if hfile == 0xffffffff: if sys.version_info >= (3, 3): raise FileNotFoundError(path) else: exc = OSError("file not found: path") exc.errno = ENOENT raise exc info = BY_HANDLE_FILE_INFORMATION() success = GetFileInformationByHandle(hfile, info) CloseHandle(hfile) if success == 0: raise WinError() return info.volume, info.index_hi, info.index_lo def _is_wildcard_pattern(pat): # Whether this pattern needs actual matching using fnmatch, or can # be looked up directly as a file. return "*" in pat or "?" in pat or "[" in pat class _Flavour(object): """A flavour implements a particular (platform-specific) set of path semantics.""" def __init__(self): self.join = self.sep.join def parse_parts(self, parts): if pycompat.PY2: parts = _py2_fsencode(parts) parsed = [] sep = self.sep altsep = self.altsep drv = root = '' it = reversed(parts) for part in it: if not part: continue if altsep: part = part.replace(altsep, sep) drv, root, rel = self.splitroot(part) if sep in rel: for x in reversed(rel.split(sep)): if x and x != '.': parsed.append(intern(x)) else: if rel and rel != '.': parsed.append(intern(rel)) if drv or root: if not drv: # If no drive is present, try to find one in the previous # parts. This makes the result of parsing e.g. # ("C:", "/", "a") reasonably intuitive. for part in it: if not part: continue if altsep: part = part.replace(altsep, sep) drv = self.splitroot(part)[0] if drv: break break if drv or root: parsed.append(drv + root) parsed.reverse() return drv, root, parsed def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2): """ Join the two paths represented by the respective (drive, root, parts) tuples. Return a new (drive, root, parts) tuple. """ if root2: if not drv2 and drv: return drv, root2, [drv + root2] + parts2[1:] elif drv2: if drv2 == drv or self.casefold(drv2) == self.casefold(drv): # Same drive => second path is relative to the first return drv, root, parts + parts2[1:] else: # Second path is non-anchored (common case) return drv, root, parts + parts2 return drv2, root2, parts2 class _WindowsFlavour(_Flavour): # Reference for Windows paths can be found at # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx sep = '\\' altsep = '/' has_drv = True pathmod = ntpath is_supported = (os.name == 'nt') drive_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') ext_namespace_prefix = '\\\\?\\' reserved_names = ( set(['CON', 'PRN', 'AUX', 'NUL']) | set(['COM%d' % i for i in range(1, 10)]) | set(['LPT%d' % i for i in range(1, 10)]) ) # Interesting findings about extended paths: # - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported # but '\\?\c:/a' is not # - extended paths are always absolute; "relative" extended paths will # fail. def splitroot(self, part, sep=sep): first = part[0:1] second = part[1:2] if (second == sep and first == sep): # XXX extended paths should also disable the collapsing of "." # components (according to MSDN docs). prefix, part = self._split_extended_path(part) first = part[0:1] second = part[1:2] else: prefix = '' third = part[2:3] if (second == sep and first == sep and third != sep): # is a UNC path: # vvvvvvvvvvvvvvvvvvvvv root # \\machine\mountpoint\directory\etc\... # directory ^^^^^^^^^^^^^^ index = part.find(sep, 2) if index != -1: index2 = part.find(sep, index + 1) # a UNC path can't have two slashes in a row # (after the initial two) if index2 != index + 1: if index2 == -1: index2 = len(part) if prefix: return prefix + part[1:index2], sep, part[index2 + 1:] else: return part[:index2], sep, part[index2 + 1:] drv = root = '' if second == ':' and first in self.drive_letters: drv = part[:2] part = part[2:] first = third if first == sep: root = first part = part.lstrip(sep) return prefix + drv, root, part def casefold(self, s): return s.lower() def casefold_parts(self, parts): return [p.lower() for p in parts] def resolve(self, path, strict=False): s = str(path) if not s: return os.getcwd() previous_s = None if _getfinalpathname is not None: if strict: return self._ext_to_normal(_getfinalpathname(s)) else: # End of the path after the first one not found tail_parts = [] while True: try: s = self._ext_to_normal(_getfinalpathname(s)) except FileNotFoundError: previous_s = s s, tail = os.path.split(s) tail_parts.append(tail) if previous_s == s: return path else: return os.path.join(s, *reversed(tail_parts)) # Means fallback on absolute return None def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix): prefix = '' if s.startswith(ext_prefix): prefix = s[:4] s = s[4:] if s.startswith('UNC\\'): prefix += s[:3] s = '\\' + s[3:] return prefix, s def _ext_to_normal(self, s): # Turn back an extended path into a normal DOS-like path return self._split_extended_path(s)[1] def is_reserved(self, parts): # NOTE: the rules for reserved names seem somewhat complicated # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). # We err on the side of caution and return True for paths which are # not considered reserved by Windows. if not parts: return False if parts[0].startswith('\\\\'): # UNC paths are never reserved return False return parts[-1].partition('.')[0].upper() in self.reserved_names def make_uri(self, path): # Under Windows, file URIs use the UTF-8 encoding. drive = path.drive if len(drive) == 2 and drive[1] == ':': # It's a path on a local drive => 'file:///c:/a/b' rest = path.as_posix()[2:].lstrip('/') return 'file:///%s/%s' % ( drive, urlquote_from_bytes(rest.encode('utf-8'))) else: # It's a path on a network drive => 'file://host/share/a/b' return 'file:' + urlquote_from_bytes( path.as_posix().encode('utf-8')) def gethomedir(self, username): if 'HOME' in os.environ: userhome = os.environ['HOME'] elif 'USERPROFILE' in os.environ: userhome = os.environ['USERPROFILE'] elif 'HOMEPATH' in os.environ: try: drv = os.environ['HOMEDRIVE'] except KeyError: drv = '' userhome = drv + os.environ['HOMEPATH'] else: raise RuntimeError("Can't determine home directory") if username: # Try to guess user home directory. By default all users # directories are located in the same place and are named by # corresponding usernames. If current user home directory points # to nonstandard place, this guess is likely wrong. if os.environ['USERNAME'] != username: drv, root, parts = self.parse_parts((userhome,)) if parts[-1] != os.environ['USERNAME']: raise RuntimeError("Can't determine home directory " "for %r" % username) parts[-1] = username if drv or root: userhome = drv + root + self.join(parts[1:]) else: userhome = self.join(parts) return userhome class _PosixFlavour(_Flavour): sep = '/' altsep = '' has_drv = False pathmod = posixpath is_supported = (os.name != 'nt') def splitroot(self, part, sep=sep): if part and part[0] == sep: stripped_part = part.lstrip(sep) # According to POSIX path resolution: # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/ # xbd_chap04.html#tag_04_11 # "A pathname that begins with two successive slashes may be # interpreted in an implementation-defined manner, although more # than two leading slashes shall be treated as a single slash". if len(part) - len(stripped_part) == 2: return '', sep * 2, stripped_part else: return '', sep, stripped_part else: return '', '', part def casefold(self, s): return s def casefold_parts(self, parts): return parts def resolve(self, path, strict=False): sep = self.sep accessor = path._accessor seen = {} def _resolve(path, rest): if rest.startswith(sep): path = '' for name in rest.split(sep): if not name or name == '.': # current dir continue if name == '..': # parent dir path, _, _ = path.rpartition(sep) continue newpath = path + sep + name if newpath in seen: # Already seen this path path = seen[newpath] if path is not None: # use cached value continue # The symlink is not resolved, so we must have a symlink # loop. raise RuntimeError("Symlink loop from %r" % newpath) # Resolve the symbolic link try: target = accessor.readlink(newpath) except OSError as e: if e.errno != EINVAL and strict: raise # Not a symlink, or non-strict mode. We just leave the path # untouched. path = newpath else: seen[newpath] = None # not resolved symlink path = _resolve(path, target) seen[newpath] = path # resolved symlink return path # NOTE: according to POSIX, getcwd() cannot contain path components # which are symlinks. base = '' if path.is_absolute() else os.getcwd() return _resolve(base, str(path)) or sep def is_reserved(self, parts): return False def make_uri(self, path): # We represent the path using the local filesystem encoding, # for portability to other applications. bpath = bytes(path) return 'file://' + urlquote_from_bytes(bpath) def gethomedir(self, username): if not username: try: return os.environ['HOME'] except KeyError: import pwd return pwd.getpwuid(os.getuid()).pw_dir else: import pwd try: return pwd.getpwnam(username).pw_dir except KeyError: raise RuntimeError("Can't determine home directory " "for %r" % username) _windows_flavour = _WindowsFlavour() _posix_flavour = _PosixFlavour() class _Accessor: """An accessor implements a particular (system-specific or not) way of accessing paths on the filesystem.""" class _NormalAccessor(_Accessor): def _wrap_strfunc(strfunc): @functools.wraps(strfunc) def wrapped(pathobj, *args): return strfunc(str(pathobj), *args) return staticmethod(wrapped) def _wrap_binary_strfunc(strfunc): @functools.wraps(strfunc) def wrapped(pathobjA, pathobjB, *args): return strfunc(str(pathobjA), str(pathobjB), *args) return staticmethod(wrapped) stat = _wrap_strfunc(os.stat) lstat = _wrap_strfunc(os.lstat) open = _wrap_strfunc(os.open) listdir = _wrap_strfunc(os.listdir) scandir = _wrap_strfunc(os_scandir) chmod = _wrap_strfunc(os.chmod) if hasattr(os, "lchmod"): lchmod = _wrap_strfunc(os.lchmod) else: def lchmod(self, pathobj, mode): raise NotImplementedError("lchmod() not available on this system") mkdir = _wrap_strfunc(os.mkdir) unlink = _wrap_strfunc(os.unlink) rmdir = _wrap_strfunc(os.rmdir) rename = _wrap_binary_strfunc(os.rename) if sys.version_info >= (3, 3): replace = _wrap_binary_strfunc(os.replace) if nt: if supports_symlinks: symlink = _wrap_binary_strfunc(os.symlink) else: def symlink(a, b, target_is_directory): raise NotImplementedError( "symlink() not available on this system") else: # Under POSIX, os.symlink() takes two args @staticmethod def symlink(a, b, target_is_directory): return os.symlink(str(a), str(b)) utime = _wrap_strfunc(os.utime) # Helper for resolve() def readlink(self, path): return os.readlink(path) _normal_accessor = _NormalAccessor() # # Globbing helpers # def _make_selector(pattern_parts): pat = pattern_parts[0] child_parts = pattern_parts[1:] if pat == '**': cls = _RecursiveWildcardSelector elif '**' in pat: raise ValueError( "Invalid pattern: '**' can only be an entire path component") elif _is_wildcard_pattern(pat): cls = _WildcardSelector else: cls = _PreciseSelector return cls(pat, child_parts) if hasattr(functools, "lru_cache"): _make_selector = functools.lru_cache()(_make_selector) class _Selector: """A selector matches a specific glob pattern part against the children of a given path.""" def __init__(self, child_parts): self.child_parts = child_parts if child_parts: self.successor = _make_selector(child_parts) self.dironly = True else: self.successor = _TerminatingSelector() self.dironly = False def select_from(self, parent_path): """Iterate over all child paths of `parent_path` matched by this selector. This can contain parent_path itself.""" path_cls = type(parent_path) is_dir = path_cls.is_dir exists = path_cls.exists scandir = parent_path._accessor.scandir if not is_dir(parent_path): return iter([]) return self._select_from(parent_path, is_dir, exists, scandir) class _TerminatingSelector: def _select_from(self, parent_path, is_dir, exists, scandir): yield parent_path class _PreciseSelector(_Selector): def __init__(self, name, child_parts): self.name = name _Selector.__init__(self, child_parts) def _select_from(self, parent_path, is_dir, exists, scandir): def try_iter(): path = parent_path._make_child_relpath(self.name) if (is_dir if self.dironly else exists)(path): for p in self.successor._select_from( path, is_dir, exists, scandir): yield p def except_iter(exc): return yield for x in _try_except_permissionerror_iter(try_iter, except_iter): yield x class _WildcardSelector(_Selector): def __init__(self, pat, child_parts): self.pat = re.compile(fnmatch.translate(pat)) _Selector.__init__(self, child_parts) def _select_from(self, parent_path, is_dir, exists, scandir): def try_iter(): cf = parent_path._flavour.casefold entries = list(scandir(parent_path)) for entry in entries: if not self.dironly or entry.is_dir(): name = entry.name casefolded = cf(name) if self.pat.match(casefolded): path = parent_path._make_child_relpath(name) for p in self.successor._select_from( path, is_dir, exists, scandir): yield p def except_iter(exc): return yield for x in _try_except_permissionerror_iter(try_iter, except_iter): yield x class _RecursiveWildcardSelector(_Selector): def __init__(self, pat, child_parts): _Selector.__init__(self, child_parts) def _iterate_directories(self, parent_path, is_dir, scandir): yield parent_path def try_iter(): entries = list(scandir(parent_path)) for entry in entries: if entry.is_dir() and not entry.is_symlink(): path = parent_path._make_child_relpath(entry.name) for p in self._iterate_directories(path, is_dir, scandir): yield p def except_iter(exc): return yield for x in _try_except_permissionerror_iter(try_iter, except_iter): yield x def _select_from(self, parent_path, is_dir, exists, scandir): def try_iter(): yielded = set() try: successor_select = self.successor._select_from for starting_point in self._iterate_directories( parent_path, is_dir, scandir): for p in successor_select( starting_point, is_dir, exists, scandir): if p not in yielded: yield p yielded.add(p) finally: yielded.clear() def except_iter(exc): return yield for x in _try_except_permissionerror_iter(try_iter, except_iter): yield x # # Public API # class _PathParents(Sequence): """This object provides sequence-like access to the logical ancestors of a path. Don't try to construct it yourself.""" __slots__ = ('_pathcls', '_drv', '_root', '_parts') def __init__(self, path): # We don't store the instance to avoid reference cycles self._pathcls = type(path) self._drv = path._drv self._root = path._root self._parts = path._parts def __len__(self): if self._drv or self._root: return len(self._parts) - 1 else: return len(self._parts) def __getitem__(self, idx): if idx < 0 or idx >= len(self): raise IndexError(idx) return self._pathcls._from_parsed_parts(self._drv, self._root, self._parts[:-idx - 1]) def __repr__(self): return "<{0}.parents>".format(self._pathcls.__name__) class PurePath(object): """PurePath represents a filesystem path and offers operations which don't imply any actual filesystem I/O. Depending on your system, instantiating a PurePath will return either a PurePosixPath or a PureWindowsPath object. You can also instantiate either of these classes directly, regardless of your system. """ __slots__ = ( '_drv', '_root', '_parts', '_str', '_hash', '_pparts', '_cached_cparts', ) def __new__(cls, *args): """Construct a PurePath from one or several strings and or existing PurePath objects. The strings and path objects are combined so as to yield a canonicalized path, which is incorporated into the new PurePath object. """ if cls is PurePath: cls = PureWindowsPath if os.name == 'nt' else PurePosixPath return cls._from_parts(args) def __reduce__(self): # Using the parts tuple helps share interned path parts # when pickling related paths. return (self.__class__, tuple(self._parts)) @classmethod def _parse_args(cls, args): # This is useful when you don't want to create an instance, just # canonicalize some constructor arguments. parts = [] for a in args: if isinstance(a, PurePath): parts += a._parts else: if sys.version_info >= (3, 6): a = os.fspath(a) else: # duck typing for older Python versions if hasattr(a, "__fspath__"): a = a.__fspath__() if isinstance(a, str): # Force-cast str subclasses to str (issue #21127) parts.append(str(a)) # also handle unicode for PY2 (pycompat.text_type = unicode) elif pycompat.PY2 and isinstance(a, pycompat.text_type): # cast to str using filesystem encoding parts.append(a.encode(sys.getfilesystemencoding())) else: raise TypeError( "argument should be a str object or an os.PathLike " "object returning str, not %r" % type(a)) return cls._flavour.parse_parts(parts) @classmethod def _from_parts(cls, args, init=True): # We need to call _parse_args on the instance, so as to get the # right flavour. self = object.__new__(cls) drv, root, parts = self._parse_args(args) self._drv = drv self._root = root self._parts = parts if init: self._init() return self @classmethod def _from_parsed_parts(cls, drv, root, parts, init=True): self = object.__new__(cls) self._drv = drv self._root = root self._parts = parts if init: self._init() return self @classmethod def _format_parsed_parts(cls, drv, root, parts): if drv or root: return drv + root + cls._flavour.join(parts[1:]) else: return cls._flavour.join(parts) def _init(self): # Overridden in concrete Path pass def _make_child(self, args): drv, root, parts = self._parse_args(args) drv, root, parts = self._flavour.join_parsed_parts( self._drv, self._root, self._parts, drv, root, parts) return self._from_parsed_parts(drv, root, parts) def __str__(self): """Return the string representation of the path, suitable for passing to system calls.""" try: return self._str except AttributeError: self._str = self._format_parsed_parts(self._drv, self._root, self._parts) or '.' return self._str def __fspath__(self): return str(self) def as_posix(self): """Return the string representation of the path with forward (/) slashes.""" f = self._flavour return str(self).replace(f.sep, '/') def __bytes__(self): """Return the bytes representation of the path. This is only recommended to use under Unix.""" if sys.version_info < (3, 2): raise NotImplementedError("needs Python 3.2 or later") return os.fsencode(str(self)) def __repr__(self): return "{0}({1!r})".format(self.__class__.__name__, self.as_posix()) def as_uri(self): """Return the path as a 'file' URI.""" if not self.is_absolute(): raise ValueError("relative path can't be expressed as a file URI") return self._flavour.make_uri(self) @property def _cparts(self): # Cached casefolded parts, for hashing and comparison try: return self._cached_cparts except AttributeError: self._cached_cparts = self._flavour.casefold_parts(self._parts) return self._cached_cparts def __eq__(self, other): if not isinstance(other, PurePath): return NotImplemented return ( self._cparts == other._cparts and self._flavour is other._flavour) def __ne__(self, other): return not self == other def __hash__(self): try: return self._hash except AttributeError: self._hash = hash(tuple(self._cparts)) return self._hash def __lt__(self, other): if (not isinstance(other, PurePath) or self._flavour is not other._flavour): return NotImplemented return self._cparts < other._cparts def __le__(self, other): if (not isinstance(other, PurePath) or self._flavour is not other._flavour): return NotImplemented return self._cparts <= other._cparts def __gt__(self, other): if (not isinstance(other, PurePath) or self._flavour is not other._flavour): return NotImplemented return self._cparts > other._cparts def __ge__(self, other): if (not isinstance(other, PurePath) or self._flavour is not other._flavour): return NotImplemented return self._cparts >= other._cparts drive = property(attrgetter('_drv'), doc="""The drive prefix (letter or UNC path), if any.""") root = property(attrgetter('_root'), doc="""The root of the path, if any.""") @property def anchor(self): """The concatenation of the drive and root, or ''.""" anchor = self._drv + self._root return anchor @property def name(self): """The final path component, if any.""" parts = self._parts if len(parts) == (1 if (self._drv or self._root) else 0): return '' return parts[-1] @property def suffix(self): """The final component's last suffix, if any.""" name = self.name i = name.rfind('.') if 0 < i < len(name) - 1: return name[i:] else: return '' @property def suffixes(self): """A list of the final component's suffixes, if any.""" name = self.name if name.endswith('.'): return [] name = name.lstrip('.') return ['.' + suffix for suffix in name.split('.')[1:]] @property def stem(self): """The final path component, minus its last suffix.""" name = self.name i = name.rfind('.') if 0 < i < len(name) - 1: return name[:i] else: return name def with_name(self, name): """Return a new path with the file name changed.""" if not self.name: raise ValueError("%r has an empty name" % (self,)) drv, root, parts = self._flavour.parse_parts((name,)) if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep] or drv or root or len(parts) != 1): raise ValueError("Invalid name %r" % (name)) return self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]) def with_suffix(self, suffix): """Return a new path with the file suffix changed (or added, if none). """ # XXX if suffix is None, should the current suffix be removed? f = self._flavour if f.sep in suffix or f.altsep and f.altsep in suffix: raise ValueError("Invalid suffix %r" % (suffix)) if suffix and not suffix.startswith('.') or suffix == '.': raise ValueError("Invalid suffix %r" % (suffix)) name = self.name if not name: raise ValueError("%r has an empty name" % (self,)) old_suffix = self.suffix if not old_suffix: name = name + suffix else: name = name[:-len(old_suffix)] + suffix return self._from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]) def relative_to(self, *other): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not a subpath of the other path), raise ValueError. """ # For the purpose of this method, drive and root are considered # separate parts, i.e.: # Path('c:/').relative_to('c:') gives Path('/') # Path('c:/').relative_to('/') raise ValueError if not other: raise TypeError("need at least one argument") parts = self._parts drv = self._drv root = self._root if root: abs_parts = [drv, root] + parts[1:] else: abs_parts = parts to_drv, to_root, to_parts = self._parse_args(other) if to_root: to_abs_parts = [to_drv, to_root] + to_parts[1:] else: to_abs_parts = to_parts n = len(to_abs_parts) cf = self._flavour.casefold_parts if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): formatted = self._format_parsed_parts(to_drv, to_root, to_parts) raise ValueError("{0!r} does not start with {1!r}" .format(str(self), str(formatted))) return self._from_parsed_parts('', root if n == 1 else '', abs_parts[n:]) @property def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" # We cache the tuple to avoid building a new one each time .parts # is accessed. XXX is this necessary? try: return self._pparts except AttributeError: self._pparts = tuple(self._parts) return self._pparts def joinpath(self, *args): """Combine this path with one or several arguments, and return a new path representing either a subpath (if all arguments are relative paths) or a totally different path (if one of the arguments is anchored). """ return self._make_child(args) def __truediv__(self, key): return self._make_child((key,)) def __rtruediv__(self, key): return self._from_parts([key] + self._parts) if pycompat.PY2: __div__ = __truediv__ __rdiv__ = __rtruediv__ @property def parent(self): """The logical parent of the path.""" drv = self._drv root = self._root parts = self._parts if len(parts) == 1 and (drv or root): return self return self._from_parsed_parts(drv, root, parts[:-1]) @property def parents(self): """A sequence of this path's logical parents.""" return _PathParents(self) def is_absolute(self): """True if the path is absolute (has both a root and, if applicable, a drive).""" if not self._root: return False return not self._flavour.has_drv or bool(self._drv) def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" return self._flavour.is_reserved(self._parts) def match(self, path_pattern): """ Return True if this path matches the given pattern. """ cf = self._flavour.casefold path_pattern = cf(path_pattern) drv, root, pat_parts = self._flavour.parse_parts((path_pattern,)) if not pat_parts: raise ValueError("empty pattern") if drv and drv != cf(self._drv): return False if root and root != cf(self._root): return False parts = self._cparts if drv or root: if len(pat_parts) != len(parts): return False pat_parts = pat_parts[1:] elif len(pat_parts) > len(parts): return False for part, pat in zip(reversed(parts), reversed(pat_parts)): if not fnmatch.fnmatchcase(part, pat): return False return True # Can't subclass os.PathLike from PurePath and keep the constructor # optimizations in PurePath._parse_args(). if sys.version_info >= (3, 6): os.PathLike.register(PurePath) class PurePosixPath(PurePath): _flavour = _posix_flavour __slots__ = () class PureWindowsPath(PurePath): _flavour = _windows_flavour __slots__ = () # Filesystem-accessing classes class Path(PurePath): __slots__ = ( '_accessor', '_closed', ) def __new__(cls, *args, **kwargs): if cls is Path: cls = WindowsPath if os.name == 'nt' else PosixPath self = cls._from_parts(args, init=False) if not self._flavour.is_supported: raise NotImplementedError("cannot instantiate %r on your system" % (cls.__name__,)) self._init() return self def _init(self, # Private non-constructor arguments template=None, ): self._closed = False if template is not None: self._accessor = template._accessor else: self._accessor = _normal_accessor def _make_child_relpath(self, part): # This is an optimization used for dir walking. `part` must be # a single part relative to this path. parts = self._parts + [part] return self._from_parsed_parts(self._drv, self._root, parts) def __enter__(self): if self._closed: self._raise_closed() return self def __exit__(self, t, v, tb): self._closed = True def _raise_closed(self): raise ValueError("I/O operation on closed path") def _opener(self, name, flags, mode=0o666): # A stub for the opener argument to built-in open() return self._accessor.open(self, flags, mode) def _raw_open(self, flags, mode=0o777): """ Open the file pointed by this path and return a file descriptor, as os.open() does. """ if self._closed: self._raise_closed() return self._accessor.open(self, flags, mode) # Public API @classmethod def cwd(cls): """Return a new path pointing to the current working directory (as returned by os.getcwd()). """ return cls(os.getcwd()) @classmethod def home(cls): """Return a new path pointing to the user's home directory (as returned by os.path.expanduser('~')). """ return cls(cls()._flavour.gethomedir(None)) def samefile(self, other_path): """Return whether other_path is the same or not as this file (as returned by os.path.samefile()). """ if hasattr(os.path, "samestat"): st = self.stat() try: other_st = other_path.stat() except AttributeError: other_st = os.stat(other_path) return os.path.samestat(st, other_st) else: filename1 = pycompat.text_type(self) filename2 = pycompat.text_type(other_path) st1 = _win32_get_unique_path_id(filename1) st2 = _win32_get_unique_path_id(filename2) return st1 == st2 def iterdir(self): """Iterate over the files in this directory. Does not yield any result for the special paths '.' and '..'. """ if self._closed: self._raise_closed() for name in self._accessor.listdir(self): if name in ('.', '..'): # Yielding a path object for these makes little sense continue yield self._make_child_relpath(name) if self._closed: self._raise_closed() def glob(self, pattern): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given pattern. """ if not pattern: raise ValueError("Unacceptable pattern: {0!r}".format(pattern)) pattern = self._flavour.casefold(pattern) drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) if drv or root: raise NotImplementedError("Non-relative patterns are unsupported") selector = _make_selector(tuple(pattern_parts)) for p in selector.select_from(self): yield p def rglob(self, pattern): """Recursively yield all existing files (of any kind, including directories) matching the given pattern, anywhere in this subtree. """ pattern = self._flavour.casefold(pattern) drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) if drv or root: raise NotImplementedError("Non-relative patterns are unsupported") selector = _make_selector(("**",) + tuple(pattern_parts)) for p in selector.select_from(self): yield p def absolute(self): """Return an absolute version of this path. This function works even if the path doesn't point to anything. No normalization is done, i.e. all '.' and '..' will be kept along. Use resolve() to get the canonical path to a file. """ # XXX untested yet! if self._closed: self._raise_closed() if self.is_absolute(): return self # FIXME this must defer to the specific flavour (and, under Windows, # use nt._getfullpathname()) obj = self._from_parts([os.getcwd()] + self._parts, init=False) obj._init(template=self) return obj def resolve(self, strict=False): """ Make the path absolute, resolving all symlinks on the way and also normalizing it (for example turning slashes into backslashes under Windows). """ if self._closed: self._raise_closed() s = self._flavour.resolve(self, strict=strict) if s is None: # No symlink resolution => for consistency, raise an error if # the path doesn't exist or is forbidden self.stat() s = str(self.absolute()) # Now we have no symlinks in the path, it's safe to normalize it. normed = self._flavour.pathmod.normpath(s) obj = self._from_parts((normed,), init=False) obj._init(template=self) return obj def stat(self): """ Return the result of the stat() system call on this path, like os.stat() does. """ return self._accessor.stat(self) def owner(self): """ Return the login name of the file owner. """ import pwd return pwd.getpwuid(self.stat().st_uid).pw_name def group(self): """ Return the group name of the file gid. """ import grp return grp.getgrgid(self.stat().st_gid).gr_name def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): """ Open the file pointed by this path and return a file object, as the built-in open() function does. """ if self._closed: self._raise_closed() if sys.version_info >= (3, 3): return io.open( str(self), mode, buffering, encoding, errors, newline, opener=self._opener) else: return io.open(str(self), mode, buffering, encoding, errors, newline) def read_bytes(self): """ Open the file in bytes mode, read it, and close the file. """ with self.open(mode='rb') as f: return f.read() def read_text(self, encoding=None, errors=None): """ Open the file in text mode, read it, and close the file. """ with self.open(mode='r', encoding=encoding, errors=errors) as f: return f.read() def write_bytes(self, data): """ Open the file in bytes mode, write to it, and close the file. """ if not isinstance(data, pycompat.binary_type): raise TypeError( 'data must be %s, not %s' % (pycompat.binary_type.__name__, data.__class__.__name__)) with self.open(mode='wb') as f: return f.write(data) def write_text(self, data, encoding=None, errors=None): """ Open the file in text mode, write to it, and close the file. """ if not isinstance(data, pycompat.text_type): raise TypeError( 'data must be %s, not %s' % (pycompat.text_type.__name__, data.__class__.__name__)) with self.open(mode='w', encoding=encoding, errors=errors) as f: return f.write(data) def touch(self, mode=0o666, exist_ok=True): """ Create this file with the given access mode, if it doesn't exist. """ if self._closed: self._raise_closed() if exist_ok: # First try to bump modification time # Implementation note: GNU touch uses the UTIME_NOW option of # the utimensat() / futimens() functions. try: self._accessor.utime(self, None) except OSError: # Avoid exception chaining pass else: return flags = os.O_CREAT | os.O_WRONLY if not exist_ok: flags |= os.O_EXCL fd = self._raw_open(flags, mode) os.close(fd) def mkdir(self, mode=0o777, parents=False, exist_ok=False): """ Create a new directory at this given path. """ if self._closed: self._raise_closed() def _try_func(): self._accessor.mkdir(self, mode) def _exc_func(exc): if not parents or self.parent == self: raise exc self.parent.mkdir(parents=True, exist_ok=True) self.mkdir(mode, parents=False, exist_ok=exist_ok) try: _try_except_filenotfounderror(_try_func, _exc_func) except OSError: if not exist_ok or not self.is_dir(): raise def chmod(self, mode): """ Change the permissions of the path, like os.chmod(). """ if self._closed: self._raise_closed() self._accessor.chmod(self, mode) def lchmod(self, mode): """ Like chmod(), except if the path points to a symlink, the symlink's permissions are changed, rather than its target's. """ if self._closed: self._raise_closed() self._accessor.lchmod(self, mode) def unlink(self): """ Remove this file or link. If the path is a directory, use rmdir() instead. """ if self._closed: self._raise_closed() self._accessor.unlink(self) def rmdir(self): """ Remove this directory. The directory must be empty. """ if self._closed: self._raise_closed() self._accessor.rmdir(self) def lstat(self): """ Like stat(), except if the path points to a symlink, the symlink's status information is returned, rather than its target's. """ if self._closed: self._raise_closed() return self._accessor.lstat(self) def rename(self, target): """ Rename this path to the given path. """ if self._closed: self._raise_closed() self._accessor.rename(self, target) def replace(self, target): """ Rename this path to the given path, clobbering the existing destination if it exists. """ if sys.version_info < (3, 3): raise NotImplementedError("replace() is only available " "with Python 3.3 and later") if self._closed: self._raise_closed() self._accessor.replace(self, target) def symlink_to(self, target, target_is_directory=False): """ Make this path a symlink pointing to the given path. Note the order of arguments (self, target) is the reverse of os.symlink's. """ if self._closed: self._raise_closed() self._accessor.symlink(target, self, target_is_directory) # Convenience functions for querying the stat results def exists(self): """ Whether this path exists. """ try: self.stat() except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise return False return True def is_dir(self): """ Whether this path is a directory. """ try: return S_ISDIR(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def is_file(self): """ Whether this path is a regular file (also True for symlinks pointing to regular files). """ try: return S_ISREG(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def is_symlink(self): """ Whether this path is a symbolic link. """ try: return S_ISLNK(self.lstat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist return False def is_block_device(self): """ Whether this path is a block device. """ try: return S_ISBLK(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def is_char_device(self): """ Whether this path is a character device. """ try: return S_ISCHR(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def is_fifo(self): """ Whether this path is a FIFO. """ try: return S_ISFIFO(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def is_socket(self): """ Whether this path is a socket. """ try: return S_ISSOCK(self.stat().st_mode) except OSError as e: if e.errno not in (ENOENT, ENOTDIR): raise # Path doesn't exist or is a broken symlink # (see https://bitbucket.org/pitrou/pathlib/issue/12/) return False def expanduser(self): """ Return a new path with expanded ~ and ~user constructs (as returned by os.path.expanduser) """ if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): homedir = self._flavour.gethomedir(self._parts[0][1:]) return self._from_parts([homedir] + self._parts[1:]) return self class PosixPath(Path, PurePosixPath): __slots__ = () class WindowsPath(Path, PureWindowsPath): __slots__ = () def owner(self): raise NotImplementedError("Path.owner() is unsupported on this system") def group(self): raise NotImplementedError("Path.group() is unsupported on this system") ================================================ FILE: tests/samples/__init__.py ================================================ ================================================ FILE: tests/samples/exception.py ================================================ import pysnooper def foo(): raise TypeError('bad') def bar(): try: foo() except Exception: str(1) raise @pysnooper.snoop(depth=3, color=False) def main(): try: bar() except: pass expected_output = ''' Source path:... Whatever 12:18:08.017782 call 17 def main(): 12:18:08.018142 line 18 try: 12:18:08.018181 line 19 bar() 12:18:08.018223 call 8 def bar(): 12:18:08.018260 line 9 try: 12:18:08.018293 line 10 foo() 12:18:08.018329 call 4 def foo(): 12:18:08.018364 line 5 raise TypeError('bad') 12:18:08.018396 exception 5 raise TypeError('bad') TypeError: bad Call ended by exception 12:18:08.018494 exception 10 foo() TypeError: bad 12:26:33.942623 line 11 except Exception: 12:26:33.942674 line 12 str(1) 12:18:08.018655 line 13 raise Call ended by exception 12:18:08.018718 exception 19 bar() TypeError: bad 12:18:08.018761 line 20 except: 12:18:08.018787 line 21 pass 12:18:08.018813 return 21 pass Return value:.. None Elapsed time: 00:00:00.000885 ''' ================================================ FILE: tests/samples/indentation.py ================================================ import pysnooper @pysnooper.snoop(depth=2, color=False) def main(): f2() def f2(): f3() def f3(): f4() @pysnooper.snoop(depth=2, color=False) def f4(): f5() def f5(): pass expected_output = ''' Source path:... Whatever 21:10:42.298924 call 5 def main(): 21:10:42.299158 line 6 f2() 21:10:42.299205 call 9 def f2(): 21:10:42.299246 line 10 f3() Source path:... Whatever 21:10:42.299305 call 18 def f4(): 21:10:42.299348 line 19 f5() 21:10:42.299386 call 22 def f5(): 21:10:42.299424 line 23 pass 21:10:42.299460 return 23 pass Return value:.. None 21:10:42.299509 return 19 f5() Return value:.. None Elapsed time: 00:00:00.000134 21:10:42.299577 return 10 f3() Return value:.. None 21:10:42.299627 return 6 f2() Return value:.. None Elapsed time: 00:00:00.000885 ''' ================================================ FILE: tests/samples/recursion.py ================================================ import pysnooper @pysnooper.snoop(depth=2, color=False) def factorial(x): if x <= 1: return 1 return mul(x, factorial(x - 1)) def mul(a, b): return a * b def main(): factorial(4) expected_output = ''' Source path:... Whatever Starting var:.. x = 4 09:31:32.691599 call 5 def factorial(x): 09:31:32.691722 line 6 if x <= 1: 09:31:32.691746 line 8 return mul(x, factorial(x - 1)) Starting var:.. x = 3 09:31:32.691781 call 5 def factorial(x): 09:31:32.691806 line 6 if x <= 1: 09:31:32.691823 line 8 return mul(x, factorial(x - 1)) Starting var:.. x = 2 09:31:32.691852 call 5 def factorial(x): 09:31:32.691875 line 6 if x <= 1: 09:31:32.691892 line 8 return mul(x, factorial(x - 1)) Starting var:.. x = 1 09:31:32.691918 call 5 def factorial(x): 09:31:32.691941 line 6 if x <= 1: 09:31:32.691961 line 7 return 1 09:31:32.691978 return 7 return 1 Return value:.. 1 Elapsed time: 00:00:00.000092 Starting var:.. a = 2 Starting var:.. b = 1 09:31:32.692025 call 11 def mul(a, b): 09:31:32.692055 line 12 return a * b 09:31:32.692075 return 12 return a * b Return value:.. 2 09:31:32.692102 return 8 return mul(x, factorial(x - 1)) Return value:.. 2 Elapsed time: 00:00:00.000283 Starting var:.. a = 3 Starting var:.. b = 2 09:31:32.692147 call 11 def mul(a, b): 09:31:32.692174 line 12 return a * b 09:31:32.692193 return 12 return a * b Return value:.. 6 09:31:32.692216 return 8 return mul(x, factorial(x - 1)) Return value:.. 6 Elapsed time: 00:00:00.000468 Starting var:.. a = 4 Starting var:.. b = 6 09:31:32.692259 call 11 def mul(a, b): 09:31:32.692285 line 12 return a * b 09:31:32.692304 return 12 return a * b Return value:.. 24 09:31:32.692326 return 8 return mul(x, factorial(x - 1)) Return value:.. 24 Elapsed time: 00:00:00.000760 ''' ================================================ FILE: tests/test_chinese.py ================================================ # -*- coding: utf-8 -*- # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import io import textwrap import threading import types import sys from pysnooper.utils import truncate import pytest import pysnooper from pysnooper import pycompat from pysnooper.variables import needs_parentheses from .utils import (assert_output, assert_sample_output, VariableEntry, CallEntry, LineEntry, ReturnEntry, OpcodeEntry, ReturnValueEntry, ExceptionEntry, ExceptionValueEntry, SourcePathEntry, CallEndedByExceptionEntry, ElapsedTimeEntry) from . import mini_toolbox def test_chinese(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: path = folder / 'foo.log' @pysnooper.snoop(path, color=False) def foo(): a = 1 x = '失败' return 7 foo() with path.open(encoding='utf-8') as file: output = file.read() assert_output( output, ( SourcePathEntry(), CallEntry(), LineEntry(), VariableEntry('a'), LineEntry(u"x = '失败'"), VariableEntry(u'x', (u"'失败'" if pycompat.PY3 else None)), LineEntry(), ReturnEntry(), ReturnValueEntry('7'), ElapsedTimeEntry(), ), ) ================================================ FILE: tests/test_mini_toolbox.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import pytest from . import mini_toolbox def test_output_capturer_doesnt_swallow_exceptions(): with pytest.raises(ZeroDivisionError): with mini_toolbox.OutputCapturer(): 1 / 0 ================================================ FILE: tests/test_multiple_files/__init__.py ================================================ ================================================ FILE: tests/test_multiple_files/multiple_files/__init__.py ================================================ ================================================ FILE: tests/test_multiple_files/multiple_files/bar.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. def bar_function(y): x = 7 * y return x ================================================ FILE: tests/test_multiple_files/multiple_files/foo.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import pysnooper from .bar import bar_function @pysnooper.snoop(depth=2, color=False) def foo_function(): z = bar_function(3) return z ================================================ FILE: tests/test_multiple_files/test_multiple_files.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import io import textwrap import threading import types import os import sys from pysnooper.utils import truncate import pytest import pysnooper from pysnooper.variables import needs_parentheses from ..utils import (assert_output, assert_sample_output, VariableEntry, CallEntry, LineEntry, ReturnEntry, OpcodeEntry, ReturnValueEntry, ExceptionEntry, ExceptionValueEntry, SourcePathEntry, CallEndedByExceptionEntry, ElapsedTimeEntry) from .. import mini_toolbox from .multiple_files import foo def test_multiple_files(): with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = foo.foo_function() assert result == 21 output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(source_path_regex=r'.*foo\.py$'), CallEntry(), LineEntry(), SourcePathEntry(source_path_regex=r'.*bar\.py$'), VariableEntry(), CallEntry(), LineEntry(), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry(), SourcePathEntry(source_path_regex=r'.*foo\.py$'), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry(), ElapsedTimeEntry(), ) ) ================================================ FILE: tests/test_not_implemented.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import io import textwrap import threading import collections import types import os import sys from pysnooper.utils import truncate import pytest import pysnooper from pysnooper.variables import needs_parentheses from pysnooper import pycompat from .utils import (assert_output, assert_sample_output, VariableEntry, CallEntry, LineEntry, ReturnEntry, OpcodeEntry, ReturnValueEntry, ExceptionEntry, ExceptionValueEntry, SourcePathEntry, CallEndedByExceptionEntry, ElapsedTimeEntry) from . import mini_toolbox def test_rejecting_coroutine_functions(): if sys.version_info[:2] <= (3, 4): pytest.skip() code = textwrap.dedent(''' async def foo(x): return 'lol' ''') namespace = {} exec(code, namespace) foo = namespace['foo'] assert pycompat.iscoroutinefunction(foo) assert not pycompat.isasyncgenfunction(foo) with pytest.raises(NotImplementedError): pysnooper.snoop(color=False)(foo) def test_rejecting_async_generator_functions(): if sys.version_info[:2] <= (3, 6): pytest.skip() code = textwrap.dedent(''' async def foo(x): yield 'lol' ''') namespace = {} exec(code, namespace) foo = namespace['foo'] assert not pycompat.iscoroutinefunction(foo) assert pycompat.isasyncgenfunction(foo) with pytest.raises(NotImplementedError): pysnooper.snoop(color=False)(foo) ================================================ FILE: tests/test_pysnooper.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import io import textwrap import threading import time import types import os import sys import zipfile from pysnooper.utils import truncate import pytest import pysnooper from pysnooper.variables import needs_parentheses from .utils import (assert_output, assert_sample_output, VariableEntry, CallEntry, LineEntry, ReturnEntry, OpcodeEntry, ReturnValueEntry, ExceptionEntry, ExceptionValueEntry, SourcePathEntry, CallEndedByExceptionEntry, ElapsedTimeEntry) from . import mini_toolbox def test_string_io(): string_io = io.StringIO() @pysnooper.snoop(string_io, color=False) def my_function(foo): x = 7 y = 8 return y + x result = my_function('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), ) ) def test_relative_time(): snoop = pysnooper.snoop(relative_time=True, color=False) def foo(x): if x == 0: bar1(x) qux() return with snoop: # There should be line entries for these three lines, # no line entries for anything else in this function, # but calls to all bar functions should be traced foo(x - 1) bar2(x) qux() int(4) bar3(9) return x @snoop def bar1(_x): qux() @snoop def bar2(_x): qux() @snoop def bar3(_x): qux() def qux(): time.sleep(0.1) return 9 # not traced, mustn't show up with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = foo(2) assert result == 2 output = output_capturer.string_io.getvalue() assert_output( output, ( # In first with SourcePathEntry(), VariableEntry('x', '2'), VariableEntry('bar1'), VariableEntry('bar2'), VariableEntry('bar3'), VariableEntry('foo'), VariableEntry('qux'), VariableEntry('snoop'), LineEntry('foo(x - 1)'), # In with in recursive call VariableEntry('x', '1'), VariableEntry('bar1'), VariableEntry('bar2'), VariableEntry('bar3'), VariableEntry('foo'), VariableEntry('qux'), VariableEntry('snoop'), LineEntry('foo(x - 1)'), # Call to bar1 from if block outside with VariableEntry('_x', '0'), VariableEntry('qux'), CallEntry('def bar1(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(0.1), # In with in recursive call LineEntry('bar2(x)'), # Call to bar2 from within with VariableEntry('_x', '1'), VariableEntry('qux'), CallEntry('def bar2(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(0.1), # In with in recursive call LineEntry('qux()'), LineEntry(source_regex="with snoop:", min_python_version=(3, 10)), ElapsedTimeEntry(0.4), # Call to bar3 from after with VariableEntry('_x', '9'), VariableEntry('qux'), CallEntry('def bar3(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(0.1), # -- Similar to previous few sections, # -- but from first call to foo # In with in first call LineEntry('bar2(x)'), # Call to bar2 from within with VariableEntry('_x', '2'), VariableEntry('qux'), CallEntry('def bar2(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(0.1), # In with in first call LineEntry('qux()'), LineEntry(source_regex="with snoop:", min_python_version=(3, 10)), ElapsedTimeEntry(0.7), # Call to bar3 from after with VariableEntry('_x', '9'), VariableEntry('qux'), CallEntry('def bar3(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(0.1), ), ) def test_thread_info(): @pysnooper.snoop(thread_info=True, color=False) def my_function(foo): x = 7 y = 8 return y + x with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function('baba') assert result == 15 output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), ) ) def test_multi_thread_info(): @pysnooper.snoop(thread_info=True, color=False) def my_function(foo): x = 7 y = 8 return y + x def parse_call_content(line): return line.split('{event:9} '.format(event='call'))[-1] with mini_toolbox.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 parse_call_content(main_thread) == parse_call_content(calls[1]) assert parse_call_content(main_thread) == parse_call_content(calls[2]) thread_info_regex = '([0-9]+-{name}+[ ]+)' assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), 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'), ElapsedTimeEntry(), 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'), ElapsedTimeEntry(), ) ) @pytest.mark.parametrize("normalize", (True, False)) def test_callable(normalize): string_io = io.StringIO() def write(msg): string_io.write(msg) @pysnooper.snoop(write, normalize=normalize, color=False) def my_function(foo): x = 7 y = 8 return y + x result = my_function('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_watch(normalize): class Foo(object): def __init__(self): self.x = 2 def square(self): self.x **= 2 @pysnooper.snoop(watch=( 'foo.x', 'io.__name__', 'len(foo.__dict__["x"] * "abc")', ), normalize=normalize, color=False) def my_function(): foo = Foo() for i in range(2): foo.square() with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result is None output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('Foo'), VariableEntry('io.__name__', "'io'"), CallEntry('def my_function():'), LineEntry('foo = Foo()'), VariableEntry('foo'), VariableEntry('foo.x', '2'), VariableEntry('len(foo.__dict__["x"] * "abc")', '6'), LineEntry(), VariableEntry('i', '0'), LineEntry(), VariableEntry('foo.x', '4'), VariableEntry('len(foo.__dict__["x"] * "abc")', '12'), LineEntry(), VariableEntry('i', '1'), LineEntry(), VariableEntry('foo.x', '16'), VariableEntry('len(foo.__dict__["x"] * "abc")', '48'), LineEntry(), ReturnEntry(), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_watch_explode(normalize): class Foo: def __init__(self, x, y): self.x = x self.y = y @pysnooper.snoop(watch_explode=('_d', '_point', 'lst + []'), normalize=normalize, color=False) def my_function(): _d = {'a': 1, 'b': 2, 'c': 'ignore'} _point = Foo(x=3, y=4) lst = [7, 8, 9] lst.append(10) with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result is None output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('Foo'), CallEntry('def my_function():'), LineEntry(), VariableEntry('_d'), VariableEntry("_d['a']", '1'), VariableEntry("_d['b']", '2'), VariableEntry("_d['c']", "'ignore'"), LineEntry(), VariableEntry('_point'), VariableEntry('_point.x', '3'), VariableEntry('_point.y', '4'), LineEntry(), VariableEntry('lst'), VariableEntry('(lst + [])[0]', '7'), VariableEntry('(lst + [])[1]', '8'), VariableEntry('(lst + [])[2]', '9'), VariableEntry('lst + []'), LineEntry(), VariableEntry('lst'), VariableEntry('(lst + [])[3]', '10'), VariableEntry('lst + []'), ReturnEntry(), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_variables_classes(normalize): class WithSlots(object): __slots__ = ('x', 'y') def __init__(self): self.x = 3 self.y = 4 @pysnooper.snoop(watch=( pysnooper.Keys('_d', exclude='c'), pysnooper.Attrs('_d'), # doesn't have attributes pysnooper.Attrs('_s'), pysnooper.Indices('_lst')[-3:], ), normalize=normalize, color=False) def my_function(): _d = {'a': 1, 'b': 2, 'c': 'ignore'} _s = WithSlots() _lst = list(range(1000)) with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result is None output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('WithSlots'), CallEntry('def my_function():'), LineEntry(), VariableEntry('_d'), VariableEntry("_d['a']", '1'), VariableEntry("_d['b']", '2'), LineEntry(), VariableEntry('_s'), VariableEntry('_s.x', '3'), VariableEntry('_s.y', '4'), LineEntry(), VariableEntry('_lst'), VariableEntry('_lst[997]', '997'), VariableEntry('_lst[998]', '998'), VariableEntry('_lst[999]', '999'), ReturnEntry(), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_single_watch_no_comma(normalize): class Foo(object): def __init__(self): self.x = 2 def square(self): self.x **= 2 @pysnooper.snoop(watch='foo', normalize=normalize, color=False) def my_function(): foo = Foo() for i in range(2): foo.square() with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result is None output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('Foo'), CallEntry('def my_function():'), LineEntry('foo = Foo()'), VariableEntry('foo'), LineEntry(), VariableEntry('i', '0'), LineEntry(), LineEntry(), VariableEntry('i', '1'), LineEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_long_variable(normalize): @pysnooper.snoop(normalize=normalize, color=False) def my_function(): foo = list(range(1000)) return foo with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result == list(range(1000)) output = output_capturer.string_io.getvalue() regex = r'^(?=.{100}$)\[0, 1, 2, .*\.\.\..*, 997, 998, 999\]$' assert_output( output, ( SourcePathEntry(), CallEntry('def my_function():'), LineEntry('foo = list(range(1000))'), VariableEntry('foo', value_regex=regex), LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_long_variable_with_custom_max_variable_length(normalize): @pysnooper.snoop(max_variable_length=200, normalize=normalize, color=False) def my_function(): foo = list(range(1000)) return foo with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result == list(range(1000)) output = output_capturer.string_io.getvalue() regex = r'^(?=.{200}$)\[0, 1, 2, .*\.\.\..*, 997, 998, 999\]$' assert_output( output, ( SourcePathEntry(), CallEntry('def my_function():'), LineEntry('foo = list(range(1000))'), VariableEntry('foo', value_regex=regex), LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_long_variable_with_infinite_max_variable_length(normalize): @pysnooper.snoop(max_variable_length=None, normalize=normalize, color=False) def my_function(): foo = list(range(1000)) return foo with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result == list(range(1000)) output = output_capturer.string_io.getvalue() regex = r'^(?=.{1000,100000}$)\[0, 1, 2, [^.]+ 997, 998, 999\]$' assert_output( output, ( SourcePathEntry(), CallEntry('def my_function():'), LineEntry('foo = list(range(1000))'), VariableEntry('foo', value_regex=regex), LineEntry(), ReturnEntry(), ReturnValueEntry(value_regex=regex), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_repr_exception(normalize): class Bad(object): def __repr__(self): 1 / 0 @pysnooper.snoop(normalize=normalize, color=False) def my_function(): bad = Bad() with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = my_function() assert result is None output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('Bad'), CallEntry('def my_function():'), LineEntry('bad = Bad()'), VariableEntry('bad', value='REPR FAILED'), ReturnEntry(), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_depth(normalize): string_io = io.StringIO() def f4(x4): result4 = x4 * 2 return result4 def f3(x3): result3 = f4(x3) return result3 def f2(x2): result2 = f3(x2) return result2 @pysnooper.snoop(string_io, depth=3, normalize=normalize, color=False) def f1(x1): result1 = f2(x1) return result1 result = f1(10) assert result == 20 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry(), VariableEntry(), CallEntry('def f1(x1):'), LineEntry(), VariableEntry(), VariableEntry(), CallEntry('def f2(x2):'), LineEntry(), VariableEntry(), VariableEntry(), CallEntry('def f3(x3):'), LineEntry(), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('20'), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('20'), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('20'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_method_and_prefix(normalize): class Baz(object): def __init__(self): self.x = 2 @pysnooper.snoop(watch=('self.x',), prefix='ZZZ', normalize=normalize, color=False) def square(self): foo = 7 self.x **= 2 return self baz = Baz() with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = baz.square() assert result is baz assert result.x == 4 output = output_capturer.string_io.getvalue() assert_output( output, ( SourcePathEntry(prefix='ZZZ'), VariableEntry('self', prefix='ZZZ'), VariableEntry('self.x', '2', prefix='ZZZ'), CallEntry('def square(self):', prefix='ZZZ'), LineEntry('foo = 7', prefix='ZZZ'), VariableEntry('foo', '7', prefix='ZZZ'), LineEntry('self.x **= 2', prefix='ZZZ'), VariableEntry('self.x', '4', prefix='ZZZ'), LineEntry(prefix='ZZZ'), ReturnEntry(prefix='ZZZ'), ReturnValueEntry(prefix='ZZZ'), ElapsedTimeEntry(prefix='ZZZ'), ), prefix='ZZZ', normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_file_output(normalize): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: path = folder / 'foo.log' @pysnooper.snoop(path, normalize=normalize, color=False) def my_function(_foo): x = 7 y = 8 return y + x result = my_function('baba') assert result == 15 with path.open() as output_file: output = output_file.read() assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_confusing_decorator_lines(normalize): string_io = io.StringIO() def empty_decorator(function): return function @empty_decorator @pysnooper.snoop(string_io, normalize=normalize, depth=2, color=False) @empty_decorator @empty_decorator def my_function(foo): x = lambda bar: 7 y = 8 return y + x(foo) result = my_function('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('foo', value_regex="u?'baba'"), CallEntry('def my_function(foo):'), LineEntry(), VariableEntry(), LineEntry(), VariableEntry(), LineEntry(), # inside lambda VariableEntry('bar', value_regex="u?'baba'"), CallEntry('x = lambda bar: 7'), LineEntry(), ReturnEntry(), ReturnValueEntry('7'), # back in my_function ReturnEntry(), ReturnValueEntry('15'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_lambda(normalize): string_io = io.StringIO() my_function = pysnooper.snoop(string_io, normalize=normalize, color=False)(lambda x: x ** 2) result = my_function(7) assert result == 49 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('x', '7'), CallEntry(source_regex='^my_function = pysnooper.*'), LineEntry(source_regex='^my_function = pysnooper.*'), ReturnEntry(source_regex='^my_function = pysnooper.*'), ReturnValueEntry('49'), ElapsedTimeEntry(), ), normalize=normalize, ) def test_unavailable_source(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder, \ mini_toolbox.TempSysPathAdder(str(folder)): module_name = 'iaerojajsijf' python_file_path = folder / ('%s.py' % (module_name,)) content = textwrap.dedent(u''' import pysnooper @pysnooper.snoop(color=False) def f(x): return x ''') with python_file_path.open('w') as python_file: python_file.write(content) module = __import__(module_name) python_file_path.unlink() with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = getattr(module, 'f')(7) assert result == 7 output = output_capturer.output assert_output( output, ( SourcePathEntry(), VariableEntry(stage='starting'), CallEntry('SOURCE IS UNAVAILABLE'), LineEntry('SOURCE IS UNAVAILABLE'), ReturnEntry('SOURCE IS UNAVAILABLE'), ReturnValueEntry('7'), ElapsedTimeEntry(), ) ) def test_no_overwrite_by_default(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: path = folder / 'foo.log' with path.open('w') as output_file: output_file.write(u'lala') @pysnooper.snoop(str(path), color=False) def my_function(foo): x = 7 y = 8 return y + x result = my_function('baba') assert result == 15 with path.open() as output_file: output = output_file.read() assert output.startswith('lala') shortened_output = output[4:] assert_output( shortened_output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), ) ) def test_overwrite(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: path = folder / 'foo.log' with path.open('w') as output_file: output_file.write(u'lala') @pysnooper.snoop(str(path), overwrite=True, color=False) def my_function(foo): x = 7 y = 8 return y + x result = my_function('baba') result = my_function('baba') assert result == 15 with path.open() as output_file: output = output_file.read() assert 'lala' not in output assert_output( output, ( SourcePathEntry(), 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'), ElapsedTimeEntry(), 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'), ElapsedTimeEntry(), ) ) def test_error_in_overwrite_argument(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder: with pytest.raises(Exception, match='can only be used when writing'): @pysnooper.snoop(overwrite=True, color=False) def my_function(foo): x = 7 y = 8 return y + x def test_needs_parentheses(): assert not needs_parentheses('x') assert not needs_parentheses('x.y') assert not needs_parentheses('x.y.z') assert not needs_parentheses('x.y.z[0]') assert not needs_parentheses('x.y.z[0]()') assert not needs_parentheses('x.y.z[0]()(3, 4 * 5)') assert not needs_parentheses('foo(x)') assert not needs_parentheses('foo(x+y)') assert not needs_parentheses('(x+y)') assert not needs_parentheses('[x+1 for x in ()]') assert needs_parentheses('x + y') assert needs_parentheses('x * y') assert needs_parentheses('x and y') assert needs_parentheses('x if z else y') @pytest.mark.parametrize("normalize", (True, False)) def test_with_block(normalize): # Testing that a single Tracer can handle many mixed uses snoop = pysnooper.snoop(normalize=normalize, color=False) def foo(x): if x == 0: bar1(x) qux() return with snoop: # There should be line entries for these three lines, # no line entries for anything else in this function, # but calls to all bar functions should be traced foo(x - 1) bar2(x) qux() int(4) bar3(9) return x @snoop def bar1(_x): qux() @snoop def bar2(_x): qux() @snoop def bar3(_x): qux() def qux(): return 9 # not traced, mustn't show up with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = foo(2) assert result == 2 output = output_capturer.string_io.getvalue() assert_output( output, ( # In first with SourcePathEntry(), VariableEntry('x', '2'), VariableEntry('bar1'), VariableEntry('bar2'), VariableEntry('bar3'), VariableEntry('foo'), VariableEntry('qux'), VariableEntry('snoop'), LineEntry('foo(x - 1)'), # In with in recursive call VariableEntry('x', '1'), VariableEntry('bar1'), VariableEntry('bar2'), VariableEntry('bar3'), VariableEntry('foo'), VariableEntry('qux'), VariableEntry('snoop'), LineEntry('foo(x - 1)'), # Call to bar1 from if block outside with VariableEntry('_x', '0'), VariableEntry('qux'), CallEntry('def bar1(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(), # In with in recursive call LineEntry('bar2(x)'), # Call to bar2 from within with VariableEntry('_x', '1'), VariableEntry('qux'), CallEntry('def bar2(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(), # In with in recursive call LineEntry('qux()'), LineEntry(source_regex="with snoop:", min_python_version=(3, 10)), ElapsedTimeEntry(), # Call to bar3 from after with VariableEntry('_x', '9'), VariableEntry('qux'), CallEntry('def bar3(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(), # -- Similar to previous few sections, # -- but from first call to foo # In with in first call LineEntry('bar2(x)'), # Call to bar2 from within with VariableEntry('_x', '2'), VariableEntry('qux'), CallEntry('def bar2(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(), # In with in first call LineEntry('qux()'), LineEntry(source_regex="with snoop:", min_python_version=(3, 10)), ElapsedTimeEntry(), # Call to bar3 from after with VariableEntry('_x', '9'), VariableEntry('qux'), CallEntry('def bar3(_x):'), LineEntry('qux()'), ReturnEntry('qux()'), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_with_block_depth(normalize): string_io = io.StringIO() def f4(x4): result4 = x4 * 2 return result4 def f3(x3): result3 = f4(x3) return result3 def f2(x2): result2 = f3(x2) return result2 def f1(x1): str(3) with pysnooper.snoop(string_io, depth=3, normalize=normalize, color=False): result1 = f2(x1) return result1 result = f1(10) assert result == 20 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry(), VariableEntry(), VariableEntry(), VariableEntry(), LineEntry('result1 = f2(x1)'), VariableEntry(), VariableEntry(), CallEntry('def f2(x2):'), LineEntry(), VariableEntry(), VariableEntry(), CallEntry('def f3(x3):'), LineEntry(), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('20'), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('20'), VariableEntry(min_python_version=(3, 10)), LineEntry(source_regex="with pysnooper.snoop.*", min_python_version=(3, 10)), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_cellvars(normalize): string_io = io.StringIO() def f2(a): def f3(a): x = 0 x += 1 def f4(a): y = x return 42 return f4(a) return f3(a) def f1(a): with pysnooper.snoop(string_io, depth=4, normalize=normalize, color=False): result1 = f2(a) return result1 result = f1(42) assert result == 42 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry(), VariableEntry(), VariableEntry(), VariableEntry(), LineEntry('result1 = f2(a)'), VariableEntry(), CallEntry('def f2(a):'), LineEntry(), VariableEntry(), LineEntry(), VariableEntry("a"), CallEntry('def f3(a):'), LineEntry(), VariableEntry("x"), LineEntry(), VariableEntry("x"), LineEntry(), VariableEntry(), LineEntry(), VariableEntry(), VariableEntry("x"), CallEntry('def f4(a):'), LineEntry(), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry(), ReturnEntry(), ReturnValueEntry(), ReturnEntry(), ReturnValueEntry(), VariableEntry(min_python_version=(3, 10)), LineEntry(source_regex="with pysnooper.snoop.*", min_python_version=(3, 10)), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_var_order(normalize): string_io = io.StringIO() def f(one, two, three, four): five = None six = None seven = None five, six, seven = 5, 6, 7 with pysnooper.snoop(string_io, depth=2, normalize=normalize, color=False): result = f(1, 2, 3, 4) output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry(), VariableEntry(), VariableEntry(), LineEntry('result = f(1, 2, 3, 4)'), VariableEntry("one", "1"), VariableEntry("two", "2"), VariableEntry("three", "3"), VariableEntry("four", "4"), CallEntry('def f(one, two, three, four):'), LineEntry(), VariableEntry("five"), LineEntry(), VariableEntry("six"), LineEntry(), VariableEntry("seven"), LineEntry(), VariableEntry("five", "5"), VariableEntry("six", "6"), VariableEntry("seven", "7"), ReturnEntry(), ReturnValueEntry(), VariableEntry("result", "None", min_python_version=(3, 10)), LineEntry(source_regex="with pysnooper.snoop.*", min_python_version=(3, 10)), ElapsedTimeEntry(), ), normalize=normalize, ) def test_truncate(): max_length = 20 for i in range(max_length * 2): string = i * 'a' truncated = truncate(string, max_length) if len(string) <= max_length: assert string == truncated else: assert truncated == 'aaaaaaaa...aaaaaaaaa' assert len(truncated) == max_length def test_indentation(): from .samples import indentation, recursion assert_sample_output(indentation) assert_sample_output(recursion) def test_exception(): from .samples import exception assert_sample_output(exception) def test_generator(): string_io = io.StringIO() original_tracer = sys.gettrace() original_tracer_active = lambda: (sys.gettrace() is original_tracer) @pysnooper.snoop(string_io, color=False) def f(x1): assert not original_tracer_active() x2 = (yield x1) assert not original_tracer_active() x3 = 'foo' assert not original_tracer_active() x4 = (yield 2) assert not original_tracer_active() return assert original_tracer_active() generator = f(0) assert original_tracer_active() first_item = next(generator) assert original_tracer_active() assert first_item == 0 second_item = generator.send('blabla') assert original_tracer_active() assert second_item == 2 with pytest.raises(StopIteration) as exc_info: generator.send('looloo') assert original_tracer_active() output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('x1', '0'), VariableEntry(), CallEntry(), LineEntry(), VariableEntry(), VariableEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('0'), ElapsedTimeEntry(), # Pause and resume: VariableEntry('x1', '0'), VariableEntry(), VariableEntry(), VariableEntry(), CallEntry(), VariableEntry('x2', "'blabla'"), LineEntry(), LineEntry(), VariableEntry('x3', "'foo'"), LineEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry('2'), ElapsedTimeEntry(), # Pause and resume: VariableEntry('x1', '0'), VariableEntry(), VariableEntry(), VariableEntry(), VariableEntry(), VariableEntry(), CallEntry(), VariableEntry('x4', "'looloo'"), LineEntry(), LineEntry(), ReturnEntry(), ReturnValueEntry(None), ElapsedTimeEntry(), ) ) @pytest.mark.parametrize("normalize", (True, False)) def test_custom_repr(normalize): string_io = io.StringIO() def large(l): return isinstance(l, list) and len(l) > 5 def print_list_size(l): return 'list(size={})'.format(len(l)) def print_dict(d): return 'dict(keys={})'.format(sorted(list(d.keys()))) def evil_condition(x): return large(x) or isinstance(x, dict) @pysnooper.snoop(string_io, custom_repr=( (large, print_list_size), (dict, print_dict), (evil_condition, lambda x: 'I am evil')), normalize=normalize, color=False) def sum_to_x(x): l = list(range(x)) a = {'1': 1, '2': 2} return sum(l) result = sum_to_x(10000) output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('x', '10000'), CallEntry(), LineEntry(), VariableEntry('l', 'list(size=10000)'), LineEntry(), VariableEntry('a', "dict(keys=['1', '2'])"), LineEntry(), ReturnEntry(), ReturnValueEntry('49995000'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_custom_repr_single(normalize): string_io = io.StringIO() @pysnooper.snoop(string_io, custom_repr=(list, lambda l: 'foofoo!'), normalize=normalize, color=False) def sum_to_x(x): l = list(range(x)) return 7 result = sum_to_x(10000) output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('x', '10000'), CallEntry(), LineEntry(), VariableEntry('l', 'foofoo!'), LineEntry(), ReturnEntry(), ReturnValueEntry('7'), ElapsedTimeEntry(), ), normalize=normalize, ) def test_disable(): string_io = io.StringIO() def my_function(foo): x = 7 y = 8 return x + y with mini_toolbox.TempValueSetter((pysnooper.tracer, 'DISABLED'), True): tracer = pysnooper.snoop(string_io, color=False) with tracer: result = my_function('baba') my_decorated_function = tracer(my_function) my_decorated_function('booboo') output = string_io.getvalue() assert not output @pytest.mark.parametrize("normalize", (True, False)) def test_class(normalize): string_io = io.StringIO() @pysnooper.snoop(string_io, normalize=normalize, color=False) class MyClass(object): def __init__(self): self.x = 7 def my_method(self, foo): y = 8 return y + self.x instance = MyClass() result = instance.my_method('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('self', value_regex="u?.+MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), ElapsedTimeEntry(), VariableEntry('self', value_regex="u?.+MyClass object"), VariableEntry('foo', value_regex="u?'baba'"), CallEntry('def my_method(self, foo):'), LineEntry('y = 8'), VariableEntry('y', '8'), LineEntry('return y + self.x'), ReturnEntry('return y + self.x'), ReturnValueEntry('15'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_class_with_decorated_method(normalize): string_io = io.StringIO() def decorator(function): def wrapper(*args, **kwargs): result = function(*args, **kwargs) return result return wrapper @pysnooper.snoop(string_io, normalize=normalize, color=False) class MyClass(object): def __init__(self): self.x = 7 @decorator def my_method(self, foo): y = 8 return y + self.x instance = MyClass() result = instance.my_method('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('self', value_regex="u?.+MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), ElapsedTimeEntry(), VariableEntry('args', value_regex=r"\(<.+>, 'baba'\)"), VariableEntry('kwargs', value_regex=r"\{\}"), VariableEntry('function', value_regex="u?.+my_method"), CallEntry('def wrapper(*args, **kwargs):'), LineEntry('result = function(*args, **kwargs)'), VariableEntry('result', '15'), LineEntry('return result'), ReturnEntry('return result'), ReturnValueEntry('15'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_class_with_decorated_method_and_snoop_applied_to_method(normalize): string_io = io.StringIO() def decorator(function): def wrapper(*args, **kwargs): result = function(*args, **kwargs) return result return wrapper @pysnooper.snoop(string_io, normalize=normalize, color=False) class MyClass(object): def __init__(self): self.x = 7 @decorator @pysnooper.snoop(string_io, normalize=normalize, color=False) def my_method(self, foo): y = 8 return y + self.x instance = MyClass() result = instance.my_method('baba') assert result == 15 output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def __init__(self):'), LineEntry('self.x = 7'), ReturnEntry('self.x = 7'), ReturnValueEntry('None'), ElapsedTimeEntry(), VariableEntry('args', value_regex=r"u?\(<.+>, 'baba'\)"), VariableEntry('kwargs', value_regex=r"u?\{\}"), VariableEntry('function', value_regex="u?.*my_method"), CallEntry('def wrapper(*args, **kwargs):'), LineEntry('result = function(*args, **kwargs)'), SourcePathEntry(), VariableEntry('self', value_regex="u?.*MyClass object"), VariableEntry('foo', value_regex="u?'baba'"), CallEntry('def my_method(self, foo):'), LineEntry('y = 8'), VariableEntry('y', '8'), LineEntry('return y + self.x'), ReturnEntry('return y + self.x'), ReturnValueEntry('15'), ElapsedTimeEntry(), VariableEntry('result', '15'), LineEntry('return result'), ReturnEntry('return result'), ReturnValueEntry('15'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_class_with_property(normalize): string_io = io.StringIO() @pysnooper.snoop(string_io, normalize=normalize, color=False) class MyClass(object): def __init__(self): self._x = 0 def plain_method(self): pass @property def x(self): self.plain_method() return self._x @x.setter def x(self, value): self.plain_method() self._x = value @x.deleter def x(self): self.plain_method() del self._x instance = MyClass() # Do simple property operations, make sure we didn't mess up the normal behavior result = instance.x assert result == instance._x instance.x = 1 assert instance._x == 1 del instance.x with pytest.raises(AttributeError): instance._x # The property methods will not be traced, but their calls to plain_method will be. output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def __init__(self):'), LineEntry('self._x = 0'), ReturnEntry('self._x = 0'), ReturnValueEntry('None'), ElapsedTimeEntry(), # Called from getter VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), ElapsedTimeEntry(), # Called from setter VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), ElapsedTimeEntry(), # Called from deleter VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def plain_method(self):'), LineEntry('pass'), ReturnEntry('pass'), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) @pytest.mark.parametrize("normalize", (True, False)) def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(normalize): string_io = io.StringIO() class UnsnoopedBaseClass(object): def __init__(self): self.method_on_base_class_was_called = False def method_on_base_class(self): self.method_on_base_class_was_called = True @pysnooper.snoop(string_io, normalize=normalize, color=False) class MyClass(UnsnoopedBaseClass): def method_on_child_class(self): self.method_on_base_class() instance = MyClass() assert not instance.method_on_base_class_was_called instance.method_on_child_class() assert instance.method_on_base_class_was_called output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), VariableEntry('self', value_regex="u?.*MyClass object"), CallEntry('def method_on_child_class(self):'), LineEntry('self.method_on_base_class()'), ReturnEntry('self.method_on_base_class()'), ReturnValueEntry('None'), ElapsedTimeEntry(), ), normalize=normalize, ) def test_normalize(): string_io = io.StringIO() class A: def __init__(self, a): self.a = a @pysnooper.snoop(string_io, normalize=True, color=False) def add(): a = A(19) b = A(22) res = a.a + b.a return res add() output = string_io.getvalue() assert_output( output, ( SourcePathEntry('test_pysnooper.py'), VariableEntry('A', value_regex=r""), CallEntry('def add():'), LineEntry('a = A(19)'), VariableEntry('a', value_regex=r"<.*\.A (?:object|instance)>"), LineEntry('b = A(22)'), VariableEntry('b', value_regex=r"<.*\.A (?:object|instance)>"), LineEntry('res = a.a + b.a'), VariableEntry('res', value="41"), LineEntry('return res'), ReturnEntry('return res'), ReturnValueEntry('41'), ElapsedTimeEntry(), ) ) def test_normalize_prefix(): string_io = io.StringIO() _prefix = 'ZZZZ' class A: def __init__(self, a): self.a = a @pysnooper.snoop(string_io, normalize=True, prefix=_prefix, color=False) def add(): a = A(19) b = A(22) res = a.a + b.a return res add() output = string_io.getvalue() assert_output( output, ( SourcePathEntry('test_pysnooper.py', prefix=_prefix), VariableEntry('A', value_regex=r"", prefix=_prefix), CallEntry('def add():', prefix=_prefix), LineEntry('a = A(19)', prefix=_prefix), VariableEntry('a', value_regex=r"<.*\.A (?:object|instance)>", prefix=_prefix), LineEntry('b = A(22)', prefix=_prefix), VariableEntry('b', value_regex=r"<.*\.A (?:object|instance)>", prefix=_prefix), LineEntry('res = a.a + b.a', prefix=_prefix), VariableEntry('res', value="41", prefix=_prefix), LineEntry('return res', prefix=_prefix), ReturnEntry('return res', prefix=_prefix), ReturnValueEntry('41', prefix=_prefix), ElapsedTimeEntry(prefix=_prefix), ) ) def test_normalize_thread_info(): string_io = io.StringIO() class A: def __init__(self, a): self.a = a @pysnooper.snoop(string_io, normalize=True, thread_info=True, color=False) def add(): a = A(19) b = A(22) res = a.a + b.a return res with pytest.raises(NotImplementedError): add() def test_exception(): string_io = io.StringIO() @pysnooper.snoop(string_io, color=False) def f(): x = 8 raise MemoryError with pytest.raises(MemoryError): f() output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), CallEntry(), LineEntry(), VariableEntry(), LineEntry(), ExceptionEntry(), ExceptionValueEntry('MemoryError'), CallEndedByExceptionEntry(), ElapsedTimeEntry(), ) ) @pytest.mark.skipif(sys.version_info < (3, 11), reason='ExceptionGroup requires Python 3.11+') def test_exception_group(): string_io = io.StringIO() @pysnooper.snoop(string_io, color=False) def f(): raise ExceptionGroup('task errors', [ValueError('bad'), TypeError('wrong'), RuntimeError('fail')]) with pytest.raises(ExceptionGroup): f() output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), CallEntry(), LineEntry(), ExceptionEntry(), ExceptionValueEntry( value_regex=r"ExceptionGroup: 'task errors' " r"\(3 sub-exceptions: ValueError, TypeError, " r"RuntimeError\)" ), CallEndedByExceptionEntry(), ElapsedTimeEntry(), ) ) @pytest.mark.skipif(sys.version_info < (3, 11), reason='ExceptionGroup requires Python 3.11+') def test_nested_exception_group(): string_io = io.StringIO() @pysnooper.snoop(string_io, color=False) def f(): raise ExceptionGroup('outer', [ExceptionGroup('inner', [ValueError('deep')]), TypeError('shallow')]) with pytest.raises(ExceptionGroup): f() output = string_io.getvalue() assert_output( output, ( SourcePathEntry(), CallEntry(), LineEntry(), ExceptionEntry(), ExceptionValueEntry( value_regex=r"ExceptionGroup: 'outer' " r"\(2 sub-exceptions: ExceptionGroup, TypeError\)" ), CallEndedByExceptionEntry(), ElapsedTimeEntry(), ) ) def test_exception_on_entry(): @pysnooper.snoop(color=False) def f(x): pass with pytest.raises(TypeError): f() def test_valid_zipfile(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder, \ mini_toolbox.TempSysPathAdder(str(folder)): module_name = 'my_valid_zip_module' zip_name = 'valid.zip' zip_base_path = mini_toolbox.pathlib.Path('ansible/modules') python_file_path = folder / zip_name / zip_base_path / ('%s.py' % (module_name)) os.makedirs(str(folder / zip_name / zip_base_path)) try: sys.path.insert(0, str(folder / zip_name / zip_base_path)) content = textwrap.dedent(u''' import pysnooper @pysnooper.snoop(color=False) def f(x): return x ''') python_file_path.write_text(content) module = __import__(module_name) with zipfile.ZipFile(str(folder / 'foo_bar.zip'), 'w') as myZipFile: myZipFile.write(str(folder / zip_name / zip_base_path / ('%s.py' % (module_name))), \ '%s/%s.py' % (zip_base_path, module_name,), \ zipfile.ZIP_DEFLATED) python_file_path.unlink() folder.joinpath(zip_name).rename(folder.joinpath('%s.delete' % (zip_name))) folder.joinpath('foo_bar.zip').rename(folder.joinpath(zip_name)) with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = getattr(module, 'f')(7) assert result == 7 output = output_capturer.output assert_output( output, ( SourcePathEntry(), VariableEntry(stage='starting'), CallEntry('def f(x):'), LineEntry('return x'), ReturnEntry('return x'), ReturnValueEntry('7'), ElapsedTimeEntry(), ) ) finally: sys.path.remove(str(folder / zip_name / zip_base_path)) def test_invalid_zipfile(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder, \ mini_toolbox.TempSysPathAdder(str(folder)): module_name = 'my_invalid_zip_module' zip_name = 'invalid.zip' zip_base_path = mini_toolbox.pathlib.Path('invalid/modules/path') python_file_path = folder / zip_name / zip_base_path / ('%s.py' % (module_name)) os.makedirs(str(folder / zip_name / zip_base_path)) try: sys.path.insert(0, str(folder / zip_name / zip_base_path)) content = textwrap.dedent(u''' import pysnooper @pysnooper.snoop(color=False) def f(x): return x ''') python_file_path.write_text(content) module = __import__(module_name) with zipfile.ZipFile(str(folder / 'foo_bar.zip'), 'w') as myZipFile: myZipFile.write(str(folder / zip_name / zip_base_path / ('%s.py' % (module_name))), \ str(zip_base_path / ('%s.py' % (module_name,))), \ zipfile.ZIP_DEFLATED) python_file_path.unlink() folder.joinpath(zip_name).rename(folder.joinpath('%s.delete' % (zip_name))) folder.joinpath('foo_bar.zip').rename(folder.joinpath(zip_name)) with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = getattr(module, 'f')(7) assert result == 7 output = output_capturer.output assert_output( output, ( SourcePathEntry(), VariableEntry(stage='starting'), CallEntry('SOURCE IS UNAVAILABLE'), LineEntry('SOURCE IS UNAVAILABLE'), ReturnEntry('SOURCE IS UNAVAILABLE'), ReturnValueEntry('7'), ElapsedTimeEntry(), ) ) finally: sys.path.remove(str(folder / zip_name / zip_base_path)) def test_valid_damaged_zipfile(): with mini_toolbox.create_temp_folder(prefix='pysnooper') as folder, \ mini_toolbox.TempSysPathAdder(str(folder)): module_name = 'my_damaged_module' zip_name = 'damaged.zip' zip_base_path = mini_toolbox.pathlib.Path('ansible/modules') python_file_path = folder / zip_name / zip_base_path / ('%s.py' % (module_name)) os.makedirs(str(folder / zip_name / zip_base_path)) try: sys.path.insert(0, str(folder / zip_name / zip_base_path)) content = textwrap.dedent(u''' import pysnooper @pysnooper.snoop(color=False) def f(x): return x ''') python_file_path.write_text(content) module = __import__(module_name) python_file_path.unlink() folder.joinpath(zip_name).rename(folder.joinpath('%s.delete' % (zip_name))) folder.joinpath(zip_name).write_text(u'I am not a zip file') with mini_toolbox.OutputCapturer(stdout=False, stderr=True) as output_capturer: result = getattr(module, 'f')(7) assert result == 7 output = output_capturer.output assert_output( output, ( SourcePathEntry(), VariableEntry(stage='starting'), CallEntry('SOURCE IS UNAVAILABLE'), LineEntry('SOURCE IS UNAVAILABLE'), ReturnEntry('SOURCE IS UNAVAILABLE'), ReturnValueEntry('7'), ElapsedTimeEntry(), ) ) finally: sys.path.remove(str(folder / zip_name / zip_base_path)) ================================================ FILE: tests/test_utils/__init__.py ================================================ ================================================ FILE: tests/test_utils/test_ensure_tuple.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import pysnooper from pysnooper.utils import ensure_tuple def test_ensure_tuple(): x1 = ('foo', ('foo',), ['foo'], {'foo'}) assert set(map(ensure_tuple, x1)) == {('foo',)} x2 = (pysnooper.Keys('foo'), (pysnooper.Keys('foo'),), [pysnooper.Keys('foo')], {pysnooper.Keys('foo')}) assert set(map(ensure_tuple, x2)) == {(pysnooper.Keys('foo'),)} ================================================ FILE: tests/test_utils/test_regex.py ================================================ # Copyright 2022 Ram Rachum and collaborators. # This program is distributed under the MIT license. import pysnooper from pysnooper.tracer import ansible_filename_pattern def test_ansible_filename_pattern(): archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ansible/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_with.zip_name.zip' source_code_file = 'ansible/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = '/my/new/path/payload.zip' source_code_file = 'ansible/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ansible/modules/in/new/path/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ansible/modules/my_module_is_called_.py.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = 'C:\\Users\\vagrant\\AppData\\Local\\Temp\\pysnooperw5c2lg35\\valid.zip' source_code_file = 'ansible\\modules\\my_valid_zip_module.py' file_name = '%s\\%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name).group(1) == archive_file assert ansible_filename_pattern.match(file_name).group(2) == source_code_file archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ANSIBLE/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ansible/modules/my_module.PY' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.Zip' source_code_file = 'ansible/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = 'ansible/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None archive_file = '/tmp/ansible_my_module_payload_xyz1234/ansible_my_module_payload.zip' source_code_file = '' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None archive_file = '' source_code_file = 'ansible/modules/my_module.py' file_name = '%s/%s' % (archive_file, source_code_file) assert ansible_filename_pattern.match(file_name) is None ================================================ FILE: tests/utils.py ================================================ # Copyright 2019 Ram Rachum and collaborators. # This program is distributed under the MIT license. import os import re import abc import inspect import sys from pysnooper.utils import DEFAULT_REPR_RE try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest from . import mini_toolbox import pysnooper.pycompat def get_function_arguments(function, exclude=()): try: getfullargspec = inspect.getfullargspec except AttributeError: result = inspect.getargspec(function).args else: result = getfullargspec(function).args for exclude_item in exclude: result.remove(exclude_item) return result class _BaseEntry(pysnooper.pycompat.ABC): def __init__(self, prefix='', min_python_version=None, max_python_version=None): self.prefix = prefix self.min_python_version = min_python_version self.max_python_version = max_python_version @abc.abstractmethod def check(self, s): pass def is_compatible_with_current_python_version(self): compatible = True if self.min_python_version and self.min_python_version > sys.version_info: compatible = False if self.max_python_version and self.max_python_version < sys.version_info: compatible = False return compatible def __repr__(self): init_arguments = get_function_arguments(self.__init__, exclude=('self',)) attributes = { key: repr(getattr(self, key)) for key in init_arguments if getattr(self, key) is not None } return '%s(%s)' % ( type(self).__name__, ', '.join('{key}={value}'.format(**locals()) for key, value in attributes.items()) ) class _BaseValueEntry(_BaseEntry): def __init__(self, prefix='', min_python_version=None, max_python_version=None): _BaseEntry.__init__(self, prefix=prefix, min_python_version=min_python_version, max_python_version=max_python_version) self.line_pattern = re.compile( r"""^%s(?P(?: {4})*)(?P[^:]*):""" r"""\.{2,7} (?P.*)$""" % (re.escape(self.prefix),) ) @abc.abstractmethod def _check_preamble(self, preamble): pass @abc.abstractmethod def _check_content(self, preamble): pass def check(self, s): match = self.line_pattern.match(s) if not match: return False _, preamble, content = match.groups() return (self._check_preamble(preamble) and self._check_content(content)) class ElapsedTimeEntry(_BaseEntry): def __init__(self, elapsed_time_value=None, tolerance=0.2, prefix='', min_python_version=None, max_python_version=None): _BaseEntry.__init__(self, prefix=prefix, min_python_version=min_python_version, max_python_version=max_python_version) self.line_pattern = re.compile( r"""^%s(?P(?: {4})*)Elapsed time: (?P