Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial provider tests #1794

Merged
merged 27 commits into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ac8ac0
Initial provider tests
medariox Dec 14, 2016
e55a467
Added VRC and wrapt (dependency) libs, added basic search test, initi…
medariox Dec 15, 2016
20b53a6
Rebased, fixed tests, added tests for zooqle and thepiratebay
medariox Apr 10, 2017
2e427af
Update test_search
medariox Apr 10, 2017
edc3efb
Updated vcrpy to v1.10.5
medariox Apr 10, 2017
97400b3
Added docstring
medariox Apr 10, 2017
d924f9a
Merge branch 'develop' into feature/provider-tests
medariox Oct 12, 2017
702c612
rm extratorrent
medariox Oct 12, 2017
a6c4f83
Add pubdate to tests
medariox Nov 9, 2017
34dffcc
Fix post-processing ignoring files in subdirectories
medariox Nov 10, 2017
a2f15e0
Improve logging for post_processor
medariox Nov 11, 2017
c0c2a85
Add anidex & fix pubdate tests
medariox Nov 12, 2017
dc9a784
Add vcrpy to requirements.txt
medariox Nov 12, 2017
7d3db4a
Merge branch 'develop' into feature/provider-tests
medariox Nov 12, 2017
81e9f85
Added torrentz2
medariox Nov 12, 2017
496f9f1
Fix elitetorrent & add tests
medariox Nov 14, 2017
0d43743
Added tests for horriblesubs
medariox Nov 14, 2017
0118a89
Added limetorrents test & removed dos
medariox Nov 14, 2017
b95a80e
Rewrite newptc and add tests, some other fixes to providers
medariox Nov 16, 2017
b9ba81c
Fix newpct for guessit
medariox Nov 16, 2017
a30aa38
Added shanaproject
medariox Nov 16, 2017
41b96da
Set newpct as public
medariox Nov 16, 2017
d92a700
Added tokyotoshokan
medariox Nov 16, 2017
8a7063f
flake
medariox Nov 16, 2017
aa98491
Fix torrent9 search and add tests
medariox Nov 17, 2017
29db9d0
Added rarbg tests
medariox Nov 17, 2017
5f3b863
Added nyaa tests
medariox Nov 17, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions ext/vcr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import logging
from .config import VCR

# Set default logging handler to avoid "No handler found" warnings.
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass


logging.getLogger(__name__).addHandler(NullHandler())


default_vcr = VCR()
use_cassette = default_vcr.use_cassette
7 changes: 7 additions & 0 deletions ext/vcr/_handle_coroutine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import asyncio


@asyncio.coroutine
def handle_coroutine(vcr, fn):
with vcr as cassette:
return (yield from fn(cassette)) # noqa: E999
319 changes: 319 additions & 0 deletions ext/vcr/cassette.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
import sys
import inspect
import logging

import wrapt

from .compat import contextlib, collections
from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method
from .patch import CassettePatcherBuilder
from .serializers import yamlserializer
from .persisters.filesystem import FilesystemPersister
from .util import partition_dict

try:
from asyncio import iscoroutinefunction
from ._handle_coroutine import handle_coroutine
except ImportError:
def iscoroutinefunction(*args, **kwargs):
return False

def handle_coroutine(*args, **kwags):
raise NotImplementedError('Not implemented on Python 2')


log = logging.getLogger(__name__)


class CassetteContextDecorator(object):
"""Context manager/decorator that handles installing the cassette and
removing cassettes.

This class defers the creation of a new cassette instance until
the point at which it is installed by context manager or
decorator. The fact that a new cassette is used with each
application prevents the state of any cassette from interfering
with another.

Instances of this class are NOT reentrant as context managers.
However, functions that are decorated by
``CassetteContextDecorator`` instances ARE reentrant. See the
implementation of ``__call__`` on this class for more details.
There is also a guard against attempts to reenter instances of
this class as a context manager in ``__exit__``.
"""

_non_cassette_arguments = ('path_transformer', 'func_path_generator')

@classmethod
def from_args(cls, cassette_class, **kwargs):
return cls(cassette_class, lambda: dict(kwargs))

def __init__(self, cls, args_getter):
self.cls = cls
self._args_getter = args_getter
self.__finish = None

def _patch_generator(self, cassette):
with contextlib.ExitStack() as exit_stack:
for patcher in CassettePatcherBuilder(cassette).build():
exit_stack.enter_context(patcher)
log_format = '{action} context for cassette at {path}.'
log.debug(log_format.format(
action="Entering", path=cassette._path
))
yield cassette
log.debug(log_format.format(
action="Exiting", path=cassette._path
))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save()

def __enter__(self):
# This assertion is here to prevent the dangerous behavior
# that would result from forgetting about a __finish before
# completing it.
# How might this condition be met? Here is an example:
# context_decorator = Cassette.use('whatever')
# with context_decorator:
# with context_decorator:
# pass
assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments,
self._args_getter()
)
if other_kwargs.get('path_transformer'):
transformer = other_kwargs['path_transformer']
cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
self.__finish = self._patch_generator(self.cls.load(**cassette_kwargs))
return next(self.__finish)

def __exit__(self, *args):
next(self.__finish, None)
self.__finish = None

@wrapt.decorator
def __call__(self, function, instance, args, kwargs):
# This awkward cloning thing is done to ensure that decorated
# functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator(function)
return type(self)(self.cls, args_getter)._execute_function(
function, args, kwargs
)

def _execute_function(self, function, args, kwargs):
def handle_function(cassette):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)

if iscoroutinefunction(function):
return handle_coroutine(vcr=self, fn=handle_function)
if inspect.isgeneratorfunction(function):
return self._handle_generator(fn=handle_function)

return self._handle_function(fn=handle_function)

def _handle_generator(self, fn):
"""Wraps a generator so that we're inside the cassette context for the
duration of the generator.
"""
with self as cassette:
coroutine = fn(cassette)
# We don't need to catch StopIteration. The caller (Tornado's
# gen.coroutine, for example) will handle that.
to_yield = next(coroutine)
while True:
try:
to_send = yield to_yield
except Exception:
to_yield = coroutine.throw(*sys.exc_info())
else:
to_yield = coroutine.send(to_send)

def _handle_function(self, fn):
with self as cassette:
return fn(cassette)

@staticmethod
def get_function_name(function):
return function.__name__

def _build_args_getter_for_decorator(self, function):
def new_args_getter():
kwargs = self._args_getter()
if 'path' not in kwargs:
name_generator = (kwargs.get('func_path_generator') or
self.get_function_name)
path = name_generator(function)
kwargs['path'] = path
return kwargs
return new_args_getter


class Cassette(object):
"""A container for recorded requests and responses"""

@classmethod
def load(cls, **kwargs):
"""Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(**kwargs)
new_cassette._load()
return new_cassette

@classmethod
def use_arg_getter(cls, arg_getter):
return CassetteContextDecorator(cls, arg_getter)

@classmethod
def use(cls, **kwargs):
return CassetteContextDecorator.from_args(cls, **kwargs)

def __init__(self, path, serializer=yamlserializer, persister=FilesystemPersister, record_mode='once',
match_on=(uri, method), before_record_request=None,
before_record_response=None, custom_patches=(),
inject=False):
self._persister = persister
self._path = path
self._serializer = serializer
self._match_on = match_on
self._before_record_request = before_record_request or (lambda x: x)
self._before_record_response = before_record_response or (lambda x: x)
self.inject = inject
self.record_mode = record_mode
self.custom_patches = custom_patches

# self.data is the list of (req, resp) tuples
self.data = []
self.play_counts = collections.Counter()
self.dirty = False
self.rewound = False

@property
def play_count(self):
return sum(self.play_counts.values())

@property
def all_played(self):
"""Returns True if all responses have been played, False otherwise."""
return self.play_count == len(self)

@property
def requests(self):
return [request for (request, response) in self.data]

@property
def responses(self):
return [response for (request, response) in self.data]

@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'

def append(self, request, response):
"""Add a request, response pair to this cassette"""
request = self._before_record_request(request)
if not request:
return
response = self._before_record_response(response)
if response is None:
return
self.data.append((request, response))
self.dirty = True

def filter_request(self, request):
return self._before_record_request(request)

def _responses(self, request):
"""
internal API, returns an iterator with all responses matching
the request.
"""
request = self._before_record_request(request)
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
yield index, response

def can_play_response_for(self, request):
request = self._before_record_request(request)
return request and request in self and \
self.record_mode != 'all' and \
self.rewound

def play_response(self, request):
"""
Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)

def responses_of(self, request):
"""
Find the responses corresponding to a request.
This function isn't actually used by VCR internally, but is
provided as an external API.
"""
responses = [response for index, response in self._responses(request)]

if responses:
return responses
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for"
% (self._path, request)
)

def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}

def _save(self, force=False):
if force or self.dirty:
self._persister.save_cassette(
self._path,
self._as_dict(),
serializer=self._serializer,
)
self.dirty = False

def _load(self):
try:
requests, responses = self._persister.load_cassette(
self._path,
serializer=self._serializer,
)
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except ValueError:
pass

def __str__(self):
return "<Cassette containing {0} recorded response(s)>".format(
len(self)
)

def __len__(self):
"""Return the number of request,response pairs stored in here"""
return len(self.data)

def __contains__(self, request):
"""Return whether or not a request has been stored"""
for index, response in self._responses(request):
if self.play_counts[index] == 0:
return True
return False
18 changes: 18 additions & 0 deletions ext/vcr/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
try:
from unittest import mock
except ImportError:
import mock

try:
import contextlib
except ImportError:
import contextlib2 as contextlib
else:
if not hasattr(contextlib, 'ExitStack'):
import contextlib2 as contextlib

import collections
if not hasattr(collections, 'Counter'):
import backport_collections as collections

__all__ = ['mock', 'contextlib', 'collections']
Loading