Full Code of cool-RR/PySnooper for AI

master 4bff1205c8c6 cached
42 files
216.9 KB
51.0k tokens
393 symbols
1 requests
Download .txt
Showing preview only (229K chars total). Download the full file or copy to clipboard to get everything.
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: <your-pypi-password-here-encrypted-using-the-travis-cli>
  #    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('^<ipython-input-([0-9]+)-.*>$')
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, '<variable>', '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, '<variable>', '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(<arguments>):
            <setup>
            try:
                yield <value>
            finally:
                <cleanup>

    This makes this:

        with some_generator(<arguments>) as <variable>:
            <body>

    equivalent to this:

        <setup>
        try:
            <variable> = <value>
            <body>
        finally:
            <cleanup>

    """
    @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(<module>.open(<arguments>)) as f:
            <block>

    is equivalent to this:

        f = <module>.open(<arguments>)
        try:
            <block>
        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"<class .*\.A.?>"),
                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"<class .*\.A.?>", 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_
Download .txt
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
Download .txt
SYMBOL INDEX (393 symbols across 22 files)

FILE: misc/generate_authors.py
  function drop_recurrences (line 28) | def drop_recurrences(iterable):
  function iterate_authors_by_chronological_order (line 36) | def iterate_authors_by_chronological_order(branch):
  function print_authors (line 51) | def print_authors(branch):

FILE: pysnooper/pycompat.py
  class ABC (line 17) | class ABC(object):
  class PathLike (line 28) | class PathLike(ABC):
    method __fspath__ (line 32) | def __fspath__(self):
    method __subclasshook__ (line 37) | def __subclasshook__(cls, subclass):
  function time_isoformat (line 75) | def time_isoformat(time, timespec='microseconds'):
  function timedelta_format (line 86) | def timedelta_format(timedelta):
  function timedelta_parse (line 90) | def timedelta_parse(s):

FILE: pysnooper/tracer.py
  function get_local_reprs (line 32) | def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None, no...
  class UnavailableSource (line 48) | class UnavailableSource(object):
    method __getitem__ (line 49) | def __getitem__(self, i):
  function get_path_and_source_from_frame (line 56) | def get_path_and_source_from_frame(frame):
  function get_write_function (line 134) | def get_write_function(output, overwrite):
  class FileWriter (line 159) | class FileWriter(object):
    method __init__ (line 160) | def __init__(self, path, overwrite):
    method write (line 164) | def write(self, s):
  class Tracer (line 174) | class Tracer:
    method __init__ (line 236) | def __init__(self, output=None, watch=(), watch_explode=(), depth=1,
    method __call__ (line 294) | def __call__(self, function_or_class):
    method _wrap_class (line 303) | def _wrap_class(self, cls):
    method _wrap_function (line 314) | def _wrap_function(self, function):
    method write (line 346) | def write(self, s):
    method __enter__ (line 350) | def __enter__(self):
    method __exit__ (line 366) | def __exit__(self, exc_type, exc_value, exc_traceback):
    method _is_internal_frame (line 394) | def _is_internal_frame(self, frame):
    method set_thread_info_padding (line 397) | def set_thread_info_padding(self, thread_info):
    method trace (line 403) | def trace(self, frame, event, arg):

FILE: pysnooper/utils.py
  function _check_methods (line 11) | def _check_methods(C, *methods):
  class WritableStream (line 24) | class WritableStream(ABC):
    method write (line 26) | def write(self, s):
    method __subclasshook__ (line 30) | def __subclasshook__(cls, C):
  function shitcode (line 45) | def shitcode(s):
  function get_repr_function (line 51) | def get_repr_function(item, custom_repr):
  function normalize_repr (line 63) | def normalize_repr(item_repr):
  function get_shortish_repr (line 68) | def get_shortish_repr(item, custom_repr=(), max_length=None, normalize=F...
  function truncate (line 82) | def truncate(string, max_length):
  function format_exception (line 91) | def format_exception(exc_type, exc_value):
  function ensure_tuple (line 106) | def ensure_tuple(x):

FILE: pysnooper/variables.py
  function needs_parentheses (line 13) | def needs_parentheses(source):
  class BaseVariable (line 20) | class BaseVariable(pycompat.ABC):
    method __init__ (line 21) | def __init__(self, source, exclude=()):
    method items (line 30) | def items(self, frame, normalize=False):
    method _items (line 38) | def _items(self, key, normalize=False):
    method _fingerprint (line 42) | def _fingerprint(self):
    method __hash__ (line 45) | def __hash__(self):
    method __eq__ (line 48) | def __eq__(self, other):
  class CommonVariable (line 53) | class CommonVariable(BaseVariable):
    method _items (line 54) | def _items(self, main_value, normalize=False):
    method _safe_keys (line 69) | def _safe_keys(self, main_value):
    method _keys (line 76) | def _keys(self, main_value):
    method _format_key (line 79) | def _format_key(self, key):
    method _get_value (line 82) | def _get_value(self, main_value, key):
  class Attrs (line 86) | class Attrs(CommonVariable):
    method _keys (line 87) | def _keys(self, main_value):
    method _format_key (line 93) | def _format_key(self, key):
    method _get_value (line 96) | def _get_value(self, main_value, key):
  class Keys (line 100) | class Keys(CommonVariable):
    method _keys (line 101) | def _keys(self, main_value):
    method _format_key (line 104) | def _format_key(self, key):
    method _get_value (line 107) | def _get_value(self, main_value, key):
  class Indices (line 111) | class Indices(Keys):
    method _keys (line 114) | def _keys(self, main_value):
    method __getitem__ (line 117) | def __getitem__(self, item):
  class Exploding (line 124) | class Exploding(BaseVariable):
    method _items (line 125) | def _items(self, main_value, normalize=False):

FILE: setup.py
  function read_file (line 7) | def read_file(filename):

FILE: tests/mini_toolbox/__init__.py
  function BlankContextManager (line 14) | def BlankContextManager():
  function create_temp_folder (line 18) | def create_temp_folder(prefix=tempfile.template, suffix='',
  class NotInDict (line 58) | class NotInDict:
  class TempValueSetter (line 62) | class TempValueSetter(object):
    method __init__ (line 70) | def __init__(self, variable, value, assert_no_fiddling=True):
    method __enter__ (line 143) | def __enter__(self):
    method __exit__ (line 166) | def __exit__(self, exc_type, exc_value, exc_traceback):
  class OutputCapturer (line 176) | class OutputCapturer(object):
    method __init__ (line 191) | def __init__(self, stdout=True, stderr=True):
    method __enter__ (line 206) | def __enter__(self):
    method __exit__ (line 212) | def __exit__(self, exc_type, exc_value, exc_traceback):
  class TempSysPathAdder (line 221) | class TempSysPathAdder(object):
    method __init__ (line 234) | def __init__(self, addition):
    method __enter__ (line 238) | def __enter__(self):
    method __exit__ (line 245) | def __exit__(self, *args, **kwargs):

FILE: tests/mini_toolbox/contextlib.py
  class ContextDecorator (line 14) | class ContextDecorator(object):
    method refresh_cm (line 17) | def refresh_cm(self):
    method _recreate_cm (line 34) | def _recreate_cm(self):
    method __call__ (line 46) | def __call__(self, func):
  class _GeneratorContextManager (line 54) | class _GeneratorContextManager(ContextDecorator):
    method __init__ (line 57) | def __init__(self, func, args, kwds):
    method _recreate_cm (line 71) | def _recreate_cm(self):
    method __enter__ (line 77) | def __enter__(self):
    method __exit__ (line 83) | def __exit__(self, type, value, traceback):
  function contextmanager (line 126) | def contextmanager(func):
  class closing (line 160) | class closing(object):
    method __init__ (line 177) | def __init__(self, thing):
    method __enter__ (line 179) | def __enter__(self):
    method __exit__ (line 181) | def __exit__(self, *exc_info):
  class _RedirectStream (line 185) | class _RedirectStream(object):
    method __init__ (line 189) | def __init__(self, new_target):
    method __enter__ (line 194) | def __enter__(self):
    method __exit__ (line 199) | def __exit__(self, exctype, excinst, exctb):
  class redirect_stdout (line 203) | class redirect_stdout(_RedirectStream):
  class redirect_stderr (line 219) | class redirect_stderr(_RedirectStream):
  class suppress (line 225) | class suppress(object):
    method __init__ (line 236) | def __init__(self, *exceptions):
    method __enter__ (line 239) | def __enter__(self):
    method __exit__ (line 242) | def __exit__(self, exctype, excinst, exctb):
  function _make_context_fixer (line 258) | def _make_context_fixer(frame_exc):
  function _reraise_with_existing_context (line 274) | def _reraise_with_existing_context(exc_details):
  function _make_context_fixer (line 285) | def _make_context_fixer(frame_exc):
  function _reraise_with_existing_context (line 290) | def _reraise_with_existing_context(exc_details):
  function _get_type (line 302) | def _get_type(obj):
  class ExitStack (line 309) | class ExitStack(object):
    method __init__ (line 321) | def __init__(self):
    method pop_all (line 324) | def pop_all(self):
    method _push_cm_exit (line 331) | def _push_cm_exit(self, cm, cm_exit):
    method push (line 338) | def push(self, exit):
    method callback (line 358) | def callback(self, callback, *args, **kwds):
    method enter_context (line 371) | def enter_context(self, cm):
    method close (line 384) | def close(self):
    method __enter__ (line 388) | def __enter__(self):
    method __exit__ (line 391) | def __exit__(self, *exc_details):
  class ContextStack (line 421) | class ContextStack(ExitStack):
    method __init__ (line 424) | def __init__(self):
    method register_exit (line 429) | def register_exit(self, callback):
    method register (line 432) | def register(self, callback, *args, **kwds):
    method preserve (line 435) | def preserve(self):

FILE: tests/mini_toolbox/pathlib.py
  function _py2_fsencode (line 61) | def _py2_fsencode(parts):
  function _try_except_fileexistserror (line 68) | def _try_except_fileexistserror(try_func, except_func, else_func=None):
  function _try_except_filenotfounderror (line 90) | def _try_except_filenotfounderror(try_func, except_func):
  function _try_except_permissionerror_iter (line 106) | def _try_except_permissionerror_iter(try_iter, except_iter):
  function _win32_get_unique_path_id (line 126) | def _win32_get_unique_path_id(path):
  function _is_wildcard_pattern (line 188) | def _is_wildcard_pattern(pat):
  class _Flavour (line 194) | class _Flavour(object):
    method __init__ (line 199) | def __init__(self):
    method parse_parts (line 202) | def parse_parts(self, parts):
    method join_parsed_parts (line 242) | def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2):
  class _WindowsFlavour (line 260) | class _WindowsFlavour(_Flavour):
    method splitroot (line 286) | def splitroot(self, part, sep=sep):
    method casefold (line 325) | def casefold(self, s):
    method casefold_parts (line 328) | def casefold_parts(self, parts):
    method resolve (line 331) | def resolve(self, path, strict=False):
    method _split_extended_path (line 356) | def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
    method _ext_to_normal (line 366) | def _ext_to_normal(self, s):
    method is_reserved (line 370) | def is_reserved(self, parts):
    method make_uri (line 382) | def make_uri(self, path):
    method gethomedir (line 395) | def gethomedir(self, username):
  class _PosixFlavour (line 427) | class _PosixFlavour(_Flavour):
    method splitroot (line 435) | def splitroot(self, part, sep=sep):
    method casefold (line 451) | def casefold(self, s):
    method casefold_parts (line 454) | def casefold_parts(self, parts):
    method resolve (line 457) | def resolve(self, path, strict=False):
    method is_reserved (line 504) | def is_reserved(self, parts):
    method make_uri (line 507) | def make_uri(self, path):
    method gethomedir (line 513) | def gethomedir(self, username):
  class _Accessor (line 533) | class _Accessor:
  class _NormalAccessor (line 539) | class _NormalAccessor(_Accessor):
    method _wrap_strfunc (line 541) | def _wrap_strfunc(strfunc):
    method _wrap_binary_strfunc (line 547) | def _wrap_binary_strfunc(strfunc):
    method lchmod (line 568) | def lchmod(self, pathobj, mode):
    method symlink (line 586) | def symlink(a, b, target_is_directory):
    method symlink (line 592) | def symlink(a, b, target_is_directory):
    method readlink (line 598) | def readlink(self, path):
  function _make_selector (line 609) | def _make_selector(pattern_parts):
  class _Selector (line 628) | class _Selector:
    method __init__ (line 633) | def __init__(self, child_parts):
    method select_from (line 642) | def select_from(self, parent_path):
  class _TerminatingSelector (line 654) | class _TerminatingSelector:
    method _select_from (line 656) | def _select_from(self, parent_path, is_dir, exists, scandir):
  class _PreciseSelector (line 660) | class _PreciseSelector(_Selector):
    method __init__ (line 662) | def __init__(self, name, child_parts):
    method _select_from (line 666) | def _select_from(self, parent_path, is_dir, exists, scandir):
  class _WildcardSelector (line 682) | class _WildcardSelector(_Selector):
    method __init__ (line 684) | def __init__(self, pat, child_parts):
    method _select_from (line 688) | def _select_from(self, parent_path, is_dir, exists, scandir):
  class _RecursiveWildcardSelector (line 710) | class _RecursiveWildcardSelector(_Selector):
    method __init__ (line 712) | def __init__(self, pat, child_parts):
    method _iterate_directories (line 715) | def _iterate_directories(self, parent_path, is_dir, scandir):
    method _select_from (line 733) | def _select_from(self, parent_path, is_dir, exists, scandir):
  class _PathParents (line 760) | class _PathParents(Sequence):
    method __init__ (line 766) | def __init__(self, path):
    method __len__ (line 773) | def __len__(self):
    method __getitem__ (line 779) | def __getitem__(self, idx):
    method __repr__ (line 785) | def __repr__(self):
  class PurePath (line 789) | class PurePath(object):
    method __new__ (line 802) | def __new__(cls, *args):
    method __reduce__ (line 812) | def __reduce__(self):
    method _parse_args (line 818) | def _parse_args(cls, args):
    method _from_parts (line 847) | def _from_parts(cls, args, init=True):
    method _from_parsed_parts (line 860) | def _from_parsed_parts(cls, drv, root, parts, init=True):
    method _format_parsed_parts (line 870) | def _format_parsed_parts(cls, drv, root, parts):
    method _init (line 876) | def _init(self):
    method _make_child (line 880) | def _make_child(self, args):
    method __str__ (line 886) | def __str__(self):
    method __fspath__ (line 896) | def __fspath__(self):
    method as_posix (line 899) | def as_posix(self):
    method __bytes__ (line 905) | def __bytes__(self):
    method __repr__ (line 912) | def __repr__(self):
    method as_uri (line 915) | def as_uri(self):
    method _cparts (line 922) | def _cparts(self):
    method __eq__ (line 930) | def __eq__(self, other):
    method __ne__ (line 937) | def __ne__(self, other):
    method __hash__ (line 940) | def __hash__(self):
    method __lt__ (line 947) | def __lt__(self, other):
    method __le__ (line 953) | def __le__(self, other):
    method __gt__ (line 959) | def __gt__(self, other):
    method __ge__ (line 965) | def __ge__(self, other):
    method anchor (line 978) | def anchor(self):
    method name (line 984) | def name(self):
    method suffix (line 992) | def suffix(self):
    method suffixes (line 1002) | def suffixes(self):
    method stem (line 1011) | def stem(self):
    method with_name (line 1020) | def with_name(self, name):
    method with_suffix (line 1031) | def with_suffix(self, suffix):
    method relative_to (line 1052) | def relative_to(self, *other):
    method parts (line 1085) | def parts(self):
    method joinpath (line 1096) | def joinpath(self, *args):
    method __truediv__ (line 1104) | def __truediv__(self, key):
    method __rtruediv__ (line 1107) | def __rtruediv__(self, key):
    method parent (line 1115) | def parent(self):
    method parents (line 1125) | def parents(self):
    method is_absolute (line 1129) | def is_absolute(self):
    method is_reserved (line 1136) | def is_reserved(self):
    method match (line 1141) | def match(self, path_pattern):
  class PurePosixPath (line 1173) | class PurePosixPath(PurePath):
  class PureWindowsPath (line 1178) | class PureWindowsPath(PurePath):
  class Path (line 1186) | class Path(PurePath):
    method __new__ (line 1192) | def __new__(cls, *args, **kwargs):
    method _init (line 1202) | def _init(self,
    method _make_child_relpath (line 1212) | def _make_child_relpath(self, part):
    method __enter__ (line 1218) | def __enter__(self):
    method __exit__ (line 1223) | def __exit__(self, t, v, tb):
    method _raise_closed (line 1226) | def _raise_closed(self):
    method _opener (line 1229) | def _opener(self, name, flags, mode=0o666):
    method _raw_open (line 1233) | def _raw_open(self, flags, mode=0o777):
    method cwd (line 1245) | def cwd(cls):
    method home (line 1252) | def home(cls):
    method samefile (line 1258) | def samefile(self, other_path):
    method iterdir (line 1276) | def iterdir(self):
    method glob (line 1290) | def glob(self, pattern):
    method rglob (line 1304) | def rglob(self, pattern):
    method absolute (line 1316) | def absolute(self):
    method resolve (line 1334) | def resolve(self, strict=False):
    method stat (line 1354) | def stat(self):
    method owner (line 1361) | def owner(self):
    method group (line 1368) | def group(self):
    method open (line 1375) | def open(self, mode='r', buffering=-1, encoding=None,
    method read_bytes (line 1391) | def read_bytes(self):
    method read_text (line 1398) | def read_text(self, encoding=None, errors=None):
    method write_bytes (line 1405) | def write_bytes(self, data):
    method write_text (line 1416) | def write_text(self, data, encoding=None, errors=None):
    method touch (line 1427) | def touch(self, mode=0o666, exist_ok=True):
    method mkdir (line 1450) | def mkdir(self, mode=0o777, parents=False, exist_ok=False):
    method chmod (line 1472) | def chmod(self, mode):
    method lchmod (line 1480) | def lchmod(self, mode):
    method unlink (line 1489) | def unlink(self):
    method rmdir (line 1498) | def rmdir(self):
    method lstat (line 1506) | def lstat(self):
    method rename (line 1515) | def rename(self, target):
    method replace (line 1523) | def replace(self, target):
    method symlink_to (line 1535) | def symlink_to(self, target, target_is_directory=False):
    method exists (line 1547) | def exists(self):
    method is_dir (line 1559) | def is_dir(self):
    method is_file (line 1572) | def is_file(self):
    method is_symlink (line 1586) | def is_symlink(self):
    method is_block_device (line 1598) | def is_block_device(self):
    method is_char_device (line 1611) | def is_char_device(self):
    method is_fifo (line 1624) | def is_fifo(self):
    method is_socket (line 1637) | def is_socket(self):
    method expanduser (line 1650) | def expanduser(self):
  class PosixPath (line 1662) | class PosixPath(Path, PurePosixPath):
  class WindowsPath (line 1666) | class WindowsPath(Path, PureWindowsPath):
    method owner (line 1669) | def owner(self):
    method group (line 1672) | def group(self):

FILE: tests/samples/exception.py
  function foo (line 4) | def foo():
  function bar (line 8) | def bar():
  function main (line 17) | def main():

FILE: tests/samples/indentation.py
  function main (line 5) | def main():
  function f2 (line 9) | def f2():
  function f3 (line 13) | def f3():
  function f4 (line 18) | def f4():
  function f5 (line 22) | def f5():

FILE: tests/samples/recursion.py
  function factorial (line 5) | def factorial(x):
  function mul (line 11) | def mul(a, b):
  function main (line 15) | def main():

FILE: tests/test_chinese.py
  function test_chinese (line 26) | def test_chinese():

FILE: tests/test_mini_toolbox.py
  function test_output_capturer_doesnt_swallow_exceptions (line 9) | def test_output_capturer_doesnt_swallow_exceptions():

FILE: tests/test_multiple_files/multiple_files/bar.py
  function bar_function (line 4) | def bar_function(y):

FILE: tests/test_multiple_files/multiple_files/foo.py
  function foo_function (line 9) | def foo_function():

FILE: tests/test_multiple_files/test_multiple_files.py
  function test_multiple_files (line 25) | def test_multiple_files():

FILE: tests/test_not_implemented.py
  function test_rejecting_coroutine_functions (line 26) | def test_rejecting_coroutine_functions():
  function test_rejecting_async_generator_functions (line 44) | def test_rejecting_async_generator_functions():

FILE: tests/test_pysnooper.py
  function test_string_io (line 26) | def test_string_io():
  function test_relative_time (line 56) | def test_relative_time():
  function test_thread_info (line 188) | def test_thread_info():
  function test_multi_thread_info (line 219) | def test_multi_thread_info():
  function test_callable (line 304) | def test_callable(normalize):
  function test_watch (line 339) | def test_watch(normalize):
  function test_watch_explode (line 393) | def test_watch_explode(normalize):
  function test_variables_classes (line 446) | def test_variables_classes(normalize):
  function test_single_watch_no_comma (line 498) | def test_single_watch_no_comma(normalize):
  function test_long_variable (line 541) | def test_long_variable(normalize):
  function test_long_variable_with_custom_max_variable_length (line 570) | def test_long_variable_with_custom_max_variable_length(normalize):
  function test_long_variable_with_infinite_max_variable_length (line 599) | def test_long_variable_with_infinite_max_variable_length(normalize):
  function test_repr_exception (line 628) | def test_repr_exception(normalize):
  function test_depth (line 659) | def test_depth(normalize):
  function test_method_and_prefix (line 722) | def test_method_and_prefix(normalize):
  function test_file_output (line 764) | def test_file_output(normalize):
  function test_confusing_decorator_lines (line 798) | def test_confusing_decorator_lines(normalize):
  function test_lambda (line 844) | def test_lambda(normalize):
  function test_unavailable_source (line 865) | def test_unavailable_source():
  function test_no_overwrite_by_default (line 899) | def test_no_overwrite_by_default():
  function test_overwrite (line 933) | def test_overwrite():
  function test_error_in_overwrite_argument (line 978) | def test_error_in_overwrite_argument():
  function test_needs_parentheses (line 988) | def test_needs_parentheses():
  function test_with_block (line 1006) | def test_with_block(normalize):
  function test_with_block_depth (line 1140) | def test_with_block_depth(normalize):
  function test_cellvars (line 1202) | def test_cellvars(normalize):
  function test_var_order (line 1271) | def test_var_order(normalize):
  function test_truncate (line 1321) | def test_truncate():
  function test_indentation (line 1333) | def test_indentation():
  function test_exception (line 1339) | def test_exception():
  function test_generator (line 1344) | def test_generator():
  function test_custom_repr (line 1428) | def test_custom_repr(normalize):
  function test_custom_repr_single (line 1476) | def test_custom_repr_single(normalize):
  function test_disable (line 1505) | def test_disable():
  function test_class (line 1525) | def test_class(normalize):
  function test_class_with_decorated_method (line 1566) | def test_class_with_decorated_method(normalize):
  function test_class_with_decorated_method_and_snoop_applied_to_method (line 1615) | def test_class_with_decorated_method_and_snoop_applied_to_method(normali...
  function test_class_with_property (line 1675) | def test_class_with_property(normalize):
  function test_snooping_on_class_does_not_cause_base_class_to_be_snooped (line 1756) | def test_snooping_on_class_does_not_cause_base_class_to_be_snooped(norma...
  function test_normalize (line 1793) | def test_normalize():
  function test_normalize_prefix (line 1829) | def test_normalize_prefix():
  function test_normalize_thread_info (line 1866) | def test_normalize_thread_info():
  function test_exception (line 1884) | def test_exception():
  function test_exception_group (line 1913) | def test_exception_group():
  function test_nested_exception_group (line 1943) | def test_nested_exception_group():
  function test_exception_on_entry (line 1970) | def test_exception_on_entry():
  function test_valid_zipfile (line 1979) | def test_valid_zipfile():
  function test_invalid_zipfile (line 2031) | def test_invalid_zipfile():
  function test_valid_damaged_zipfile (line 2082) | def test_valid_damaged_zipfile():

FILE: tests/test_utils/test_ensure_tuple.py
  function test_ensure_tuple (line 7) | def test_ensure_tuple():

FILE: tests/test_utils/test_regex.py
  function test_ansible_filename_pattern (line 7) | def test_ansible_filename_pattern():

FILE: tests/utils.py
  function get_function_arguments (line 21) | def get_function_arguments(function, exclude=()):
  class _BaseEntry (line 33) | class _BaseEntry(pysnooper.pycompat.ABC):
    method __init__ (line 34) | def __init__(self, prefix='', min_python_version=None, max_python_vers...
    method check (line 40) | def check(self, s):
    method is_compatible_with_current_python_version (line 43) | def is_compatible_with_current_python_version(self):
    method __repr__ (line 52) | def __repr__(self):
  class _BaseValueEntry (line 67) | class _BaseValueEntry(_BaseEntry):
    method __init__ (line 68) | def __init__(self, prefix='', min_python_version=None,
    method _check_preamble (line 79) | def _check_preamble(self, preamble):
    method _check_content (line 83) | def _check_content(self, preamble):
    method check (line 86) | def check(self, s):
  class ElapsedTimeEntry (line 95) | class ElapsedTimeEntry(_BaseEntry):
    method __init__ (line 96) | def __init__(self, elapsed_time_value=None, tolerance=0.2, prefix='',
    method check (line 109) | def check(self, s):
  class CallEndedByExceptionEntry (line 122) | class CallEndedByExceptionEntry(_BaseEntry):
    method __init__ (line 124) | def __init__(self, prefix=''):
    method check (line 127) | def check(self, s):
  class VariableEntry (line 135) | class VariableEntry(_BaseValueEntry):
    method __init__ (line 136) | def __init__(self, name=None, value=None, stage=None, prefix='',
    method _check_preamble (line 160) | def _check_preamble(self, preamble):
    method _check_content (line 171) | def _check_content(self, content):
    method _check_name (line 178) | def _check_name(self, name):
    method _check_value (line 186) | def _check_value(self, value):
    method _check_stage (line 194) | def _check_stage(self, stage):
  class _BaseSimpleValueEntry (line 202) | class _BaseSimpleValueEntry(_BaseValueEntry):
    method __init__ (line 203) | def __init__(self, value=None, value_regex=None, prefix='',
    method _check_preamble (line 215) | def _check_preamble(self, preamble):
    method _check_content (line 218) | def _check_content(self, content):
    method _check_value (line 221) | def _check_value(self, value):
  class ReturnValueEntry (line 229) | class ReturnValueEntry(_BaseSimpleValueEntry):
  class ExceptionValueEntry (line 234) | class ExceptionValueEntry(_BaseSimpleValueEntry):
  class SourcePathEntry (line 239) | class SourcePathEntry(_BaseValueEntry):
    method __init__ (line 240) | def __init__(self, source_path=None, source_path_regex=None, prefix=''):
    method _check_preamble (line 253) | def _check_preamble(self, preamble):
    method _check_content (line 256) | def _check_content(self, source_path):
  class _BaseEventEntry (line 265) | class _BaseEventEntry(_BaseEntry):
    method __init__ (line 266) | def __init__(self, source=None, source_regex=None, thread_info=None,
    method event_name (line 291) | def event_name(self):
    method _check_source (line 294) | def _check_source(self, source):
    method _check_thread_info (line 302) | def _check_thread_info(self, thread_info):
    method check (line 310) | def check(self, s):
  class CallEntry (line 320) | class CallEntry(_BaseEventEntry):
  class LineEntry (line 324) | class LineEntry(_BaseEventEntry):
  class ReturnEntry (line 328) | class ReturnEntry(_BaseEventEntry):
  class ExceptionEntry (line 332) | class ExceptionEntry(_BaseEventEntry):
  class OpcodeEntry (line 336) | class OpcodeEntry(_BaseEventEntry):
  class OutputFailure (line 340) | class OutputFailure(Exception):
  function verify_normalize (line 344) | def verify_normalize(lines, prefix):
  function assert_output (line 362) | def assert_output(output, expected_entries, prefix=None, normalize=False):
  function assert_sample_output (line 402) | def assert_sample_output(module):
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (232K chars).
[
  {
    "path": ".gitignore",
    "chars": 116,
    "preview": "*.py[co]\n__pycache__/\n\n.tox/\n.pytest_cache/\n.mypy_cache/\n\ndist/\nbuild/\n*.egg-info/\n\n*.bak\n\n*.wpu\n\n.coverage\nhtmlcov\n"
  },
  {
    "path": ".travis.yml",
    "chars": 817,
    "preview": "dist: xenial\nlanguage: python\n\npython:\n- 2.7\n- 3.5\n- 3.6\n- 3.7\n- 3.8\n- 3.9\n- 3.10-dev\n- pypy2.7-6.0\n- pypy3.5\n\ninstall:\n"
  },
  {
    "path": "ADVANCED_USAGE.md",
    "chars": 2712,
    "preview": "# Advanced Usage #\n\nUse `watch_explode` to expand values to see all their attributes or items of lists/dictionaries:\n\n``"
  },
  {
    "path": "AUTHORS",
    "chars": 406,
    "preview": "Ram Rachum\nOleg Butuzov\nEdward Betts\nwilfredinni\nPeter Bittner\nAlireza Ayinmehr\nChristian Zietz\nBinwei Hu\nLoukas Leontop"
  },
  {
    "path": "LICENSE",
    "chars": 1085,
    "preview": "MIT License\n\nCopyright (c) 2019 Ram Rachum and collaborators\n\nPermission is hereby granted, free of charge, to any perso"
  },
  {
    "path": "MANIFEST.in",
    "chars": 144,
    "preview": "include README.md\ninclude LICENSE\ninclude requirements.in\ninclude requirements.txt\nrecursive-include tests *.txt *.py\npr"
  },
  {
    "path": "README.md",
    "chars": 4299,
    "preview": "# PySnooper - Never use print for debugging again\n\n**PySnooper** is a poor man's debugger. If you've used Bash, it's lik"
  },
  {
    "path": "make_release.sh",
    "chars": 113,
    "preview": "#!/usr/bin/env bash\nrm -rf dist/* build/* && python setup.py sdist bdist_wheel --universal && twine upload dist/*"
  },
  {
    "path": "misc/IDE files/PySnooper.wpr",
    "chars": 1483,
    "preview": "#!wing\n#!version=11.0\n##################################################################\n# Wing project file            "
  },
  {
    "path": "misc/generate_authors.py",
    "chars": 1477,
    "preview": "#!/usr/bin/env python\n# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license"
  },
  {
    "path": "pyproject.toml",
    "chars": 81,
    "preview": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n"
  },
  {
    "path": "pysnooper/__init__.py",
    "chars": 812,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n'''\nPySnooper - Neve"
  },
  {
    "path": "pysnooper/pycompat.py",
    "chars": 2677,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n'''Python 2/3 compat"
  },
  {
    "path": "pysnooper/tracer.py",
    "chars": 23326,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport functools\nim"
  },
  {
    "path": "pysnooper/utils.py",
    "chars": 2864,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport abc\nimport r"
  },
  {
    "path": "pysnooper/variables.py",
    "chars": 3656,
    "preview": "import itertools\nimport abc\ntry:\n    from collections.abc import Mapping, Sequence\nexcept ImportError:\n    from collecti"
  },
  {
    "path": "requirements.in",
    "chars": 37,
    "preview": "# That's right baby! No dependencies!"
  },
  {
    "path": "requirements.txt",
    "chars": 132,
    "preview": "#\n# This file is autogenerated by pip-compile\n# To update, run:\n#\n#    pip-compile --output-file requirements.txt requir"
  },
  {
    "path": "setup.cfg",
    "chars": 1194,
    "preview": "[metadata]\nname = PySnooper\nversion = attr: pysnooper.__version__\nauthor = Ram Rachum\nauthor_email = ram@rachum.com\ndesc"
  },
  {
    "path": "setup.py",
    "chars": 1875,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\nimport setuptools\nim"
  },
  {
    "path": "tests/__init__.py",
    "chars": 61,
    "preview": "import pytest\n\npytest.register_assert_rewrite('tests.utils')\n"
  },
  {
    "path": "tests/mini_toolbox/__init__.py",
    "chars": 8316,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport tempfile\nimp"
  },
  {
    "path": "tests/mini_toolbox/contextlib.py",
    "chars": 14828,
    "preview": "\"\"\"contextlib2 - backports and enhancements to the contextlib module\"\"\"\n\nimport sys\nimport warnings\nfrom collections imp"
  },
  {
    "path": "tests/mini_toolbox/pathlib.py",
    "chars": 54849,
    "preview": "# Copyright (c) 2014-2017 Matthias C. M. Troffaes\n# Copyright (c) 2012-2014 Antoine Pitrou and contributors\n# Distribute"
  },
  {
    "path": "tests/samples/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/samples/exception.py",
    "chars": 1296,
    "preview": "import pysnooper\n\n\ndef foo():\n    raise TypeError('bad')\n\n\ndef bar():\n    try:\n        foo()\n    except Exception:\n     "
  },
  {
    "path": "tests/samples/indentation.py",
    "chars": 1022,
    "preview": "import pysnooper\n\n\n@pysnooper.snoop(depth=2, color=False)\ndef main():\n    f2()\n\n\ndef f2():\n    f3()\n\n\ndef f3():\n    f4()"
  },
  {
    "path": "tests/samples/recursion.py",
    "chars": 2345,
    "preview": "import pysnooper\n\n\n@pysnooper.snoop(depth=2, color=False)\ndef factorial(x):\n    if x <= 1:\n        return 1\n    return m"
  },
  {
    "path": "tests/test_chinese.py",
    "chars": 1497,
    "preview": "# -*- coding: utf-8 -*-\n# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT licen"
  },
  {
    "path": "tests/test_mini_toolbox.py",
    "chars": 304,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport pytest\n\nfrom"
  },
  {
    "path": "tests/test_multiple_files/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_multiple_files/multiple_files/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_multiple_files/multiple_files/bar.py",
    "chars": 149,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\ndef bar_function(y)"
  },
  {
    "path": "tests/test_multiple_files/multiple_files/foo.py",
    "chars": 245,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport pysnooper\n\nf"
  },
  {
    "path": "tests/test_multiple_files/test_multiple_files.py",
    "chars": 1572,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport io\nimport te"
  },
  {
    "path": "tests/test_not_implemented.py",
    "chars": 1583,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport io\nimport te"
  },
  {
    "path": "tests/test_pysnooper.py",
    "chars": 64835,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport io\nimport te"
  },
  {
    "path": "tests/test_utils/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_utils/test_ensure_tuple.py",
    "chars": 476,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport pysnooper\nfr"
  },
  {
    "path": "tests/test_utils/test_regex.py",
    "chars": 3890,
    "preview": "# Copyright 2022 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\n\nimport pysnooper\nfr"
  },
  {
    "path": "tests/utils.py",
    "chars": 14305,
    "preview": "# Copyright 2019 Ram Rachum and collaborators.\n# This program is distributed under the MIT license.\nimport os\nimport re\n"
  },
  {
    "path": "tox.ini",
    "chars": 1217,
    "preview": "# tox (https://tox.readthedocs.io/) is a tool for running tests\n# Run tests in multiple virtualenvs.\n\n[tox]\nenvlist =\n  "
  }
]

About this extraction

This page contains the full source code of the cool-RR/PySnooper GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (216.9 KB), approximately 51.0k tokens, and a symbol index with 393 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!