1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 17:15:35 +00:00

Compare commits

..

24 Commits

Author SHA1 Message Date
Ivan Malison
d0aa5fddb7 1.7.1 2015-08-12 12:21:29 -07:00
Kevin McCarthy
e54aeadc68 Merge pull request #184 from abhinav/master
For Tornado AsyncHTTPClient, replace the methods instead of the class.
2015-08-12 09:16:22 -10:00
Abhinav Gupta
c4a33d1cff For Tornado AsyncHTTPClient, replace the methods instead of the class.
This makes it so patching works even if the user has a reference to, or an
instance of the original unpatched AsyncHTTPClient class.

Fixes #183.
2015-08-12 10:51:08 -07:00
Ivan Malison
8b59d73f25 v1.7.0 2015-08-02 13:36:57 -07:00
Ivan 'Goat' Malison
eb394b90d9 Merge pull request #181 from coagulant/fix-readme-custom-response
Fix example for custom response filtering in docs
2015-08-01 06:06:34 -07:00
Ilya Baryshev
14931dd47a Fix example for custom response filtering in docs 2015-08-01 11:10:51 +03:00
Ivan Malison
89cdda86d1 fix generator test. 2015-07-30 14:22:16 -07:00
Ivan 'Goat' Malison
ad48d71897 Merge pull request #180 from abhinav/master
Fix exception catching in coroutines.
2015-07-30 14:21:44 -07:00
Abhinav Gupta
946ce17a97 Fix exception catching in coroutines. 2015-07-30 14:13:58 -07:00
Ivan Malison
4d438dac75 Fix tornado python3 tests. 2015-07-30 04:19:17 -07:00
Ivan Malison
a234ad6b12 Fix all the tests in python 3 2015-07-30 03:39:04 -07:00
Ivan Malison
1d000ac652 Fix all the failing tests 2015-07-30 02:08:42 -07:00
Ivan Malison
21c176ee1e Make cassette active for duration of coroutine/generator
Closes #177.
2015-07-30 01:47:29 -07:00
Ivan 'Goat' Malison
4fb5bef8e1 Merge pull request #179 from graingert/support-ancient-PyPA-tools
Support distribute, Fixes #178
2015-07-29 23:22:16 -07:00
Thomas Grainger
9717596e2c Support distribute, Fixes #178 2015-07-29 22:19:45 +01:00
Ivan 'Goat' Malison
1660cc3a9f Merge pull request #176 from charlax/patch-1
Make setup example pep8 compliant
2015-07-27 10:37:42 -07:00
Charles-Axel Dein
4beb023204 Make setup example pep8 compliant
Pretty minor doc change.
2015-07-27 18:22:51 +02:00
Ivan 'Goat' Malison
72eb5345d6 Merge pull request #175 from gward/issue163-v2
Fix for #163, take 2.
2015-07-26 02:40:27 -07:00
Greg Ward
fe7d193d1a Add several more test cases for issue #163. 2015-07-16 14:49:48 -04:00
Greg Ward
09b7ccf561 Ensure that request bodies are always bytes, not text (fixes #163).
It shouldn't matter whether the request body comes from a file or a
string, or whether it is passed to the Request constructor or assigned
later. It should always be stored internally as bytes.
2015-07-16 14:36:26 -04:00
Kevin McCarthy
a4a80b431b Merge pull request #173 from graingert/patch-2
Fix before_record_reponse doc
2015-07-16 07:28:25 -10:00
Thomas Grainger
025a3b422d Fix before_record_reponse doc 2015-07-16 15:13:19 +01:00
Kevin McCarthy
bb05b2fcf7 Merge pull request #172 from abhinav/patch-1
Add Tornado to list of supported libraries
2015-07-15 10:31:51 -10:00
Abhinav Gupta
f77ef81877 Add Tornado to list of supported libraries 2015-07-15 12:43:36 -07:00
13 changed files with 378 additions and 117 deletions

View File

@@ -50,6 +50,7 @@ The following http libraries are supported:
- requests (both 1.x and 2.x versions) - requests (both 1.x and 2.x versions)
- httplib2 - httplib2
- boto - boto
- Tornado's AsyncHTTPClient
Usage Usage
----- -----
@@ -108,10 +109,10 @@ If you don't like VCR's defaults, you can set options by instantiating a
import vcr import vcr
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
serializer = 'json', serializer='json',
cassette_library_dir = 'fixtures/cassettes', cassette_library_dir='fixtures/cassettes',
record_mode = 'once', record_mode='once',
match_on = ['uri', 'method'], match_on=['uri', 'method'],
) )
with my_vcr.use_cassette('test.json'): with my_vcr.use_cassette('test.json'):
@@ -416,12 +417,13 @@ that of ``before_record``:
.. code:: python .. code:: python
def scrub_string(string, replacement=''): def scrub_string(string, replacement=''):
def before_record_reponse(response): def before_record_response(response):
return response['body']['string'] = response['body']['string'].replace(string, replacement) response['body']['string'] = response['body']['string'].replace(string, replacement)
return scrub_string return response
return before_record_response
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
before_record=scrub_string(settings.USERNAME, 'username'), before_record_response=scrub_string(settings.USERNAME, 'username'),
) )
with my_vcr.use_cassette('test.yml'): with my_vcr.use_cassette('test.yml'):
# your http code here # your http code here
@@ -606,6 +608,12 @@ new API in version 1.0.x
Changelog Changelog
--------- ---------
- 1.7.1 [#183] Patch ``fetch_impl`` instead of the entire HTTPClient
class for Tornado (thanks @abhinav).
- 1.7.0 [#177] Properly support coroutine/generator decoration. [#178]
Support distribute (thanks @graingert). [#163] Make compatibility
between python2 and python3 recorded cassettes more robust (thanks
@gward).
- 1.6.1 [#169] Support conditional requirements in old versions of - 1.6.1 [#169] Support conditional requirements in old versions of
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
unsupported features exception not being raised, [#166] unsupported features exception not being raised, [#166]

View File

@@ -1,12 +1,15 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import logging
from setuptools import setup, find_packages from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
import pkg_resources import pkg_resources
long_description = open('README.rst', 'r').read() long_description = open('README.rst', 'r').read()
class PyTest(TestCommand): class PyTest(TestCommand):
def finalize_options(self): def finalize_options(self):
@@ -21,7 +24,7 @@ class PyTest(TestCommand):
sys.exit(errno) sys.exit(errno)
install_requires=['PyYAML', 'wrapt', 'six>=1.5'] install_requires = ['PyYAML', 'wrapt', 'six>=1.5']
extras_require = { extras_require = {
@@ -31,14 +34,24 @@ extras_require = {
} }
if 'bdist_wheel' not in sys.argv: try:
if 'bdist_wheel' not in sys.argv:
for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
install_requires.extend(value)
except Exception:
logging.getLogger(__name__).exception(
'Something went wrong calculating platform specific dependencies, so '
"you're getting them all!"
)
for key, value in extras_require.items(): for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]): if key.startswith(':'):
install_requires.extend(value) install_requires.extend(value)
setup( setup(
name='vcrpy', name='vcrpy',
version='1.6.1', version='1.7.1',
description=( description=(
"Automatically mock your HTTP interactions to simplify and " "Automatically mock your HTTP interactions to simplify and "
"speed up testing" "speed up testing"

View File

@@ -275,3 +275,54 @@ def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
) )
assert isinstance(response.error, CannotOverwriteExistingCassetteException) assert isinstance(response.error, CannotOverwriteExistingCassetteException)
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_with_decorator_use_cassette(get_client):
response = yield get_client().fetch(
http.HTTPRequest('http://www.google.com/', method='GET')
)
assert response.body.decode('utf-8') == "not actually google"
@pytest.mark.gen_test
@vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix('.yaml'))
def test_tornado_exception_can_be_caught(get_client):
try:
yield get(get_client(), 'http://httpbin.org/status/500')
except http.HTTPError as e:
assert e.code == 500
try:
yield get(get_client(), 'http://httpbin.org/status/404')
except http.HTTPError as e:
assert e.code == 404
@pytest.mark.gen_test
def test_existing_references_get_patched(tmpdir):
from tornado.httpclient import AsyncHTTPClient
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
client = AsyncHTTPClient()
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1
@pytest.mark.gen_test
def test_existing_instances_get_patched(get_client, tmpdir):
'''Ensure that existing instances of AsyncHTTPClient get patched upon
entering VCR context.'''
client = get_client()
with vcr.use_cassette(str(tmpdir.join('data.yaml'))):
yield get(client, 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('data.yaml'))) as cass:
yield get(client, 'http://httpbin.org/get')
assert cass.play_count == 1

View File

@@ -0,0 +1,62 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/500
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 500, message: INTERNAL SERVER ERROR}
- request:
body: null
headers: {}
method: GET
uri: http://httpbin.org/status/404
response:
body: {string: !!python/unicode ''}
headers:
- !!python/tuple
- Content-Length
- ['0']
- !!python/tuple
- Server
- [nginx]
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- Access-Control-Allow-Credentials
- ['true']
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 17:32:39 GMT']
- !!python/tuple
- Access-Control-Allow-Origin
- ['*']
- !!python/tuple
- Content-Type
- [text/html; charset=utf-8]
status: {code: 404, message: NOT FOUND}
version: 1

View File

@@ -0,0 +1,53 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: http://www.google.com/
response:
body: {string: !!python/unicode 'not actually google'}
headers:
- !!python/tuple
- Expires
- ['-1']
- !!python/tuple
- Connection
- [close]
- !!python/tuple
- P3p
- ['CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
for more info."']
- !!python/tuple
- Alternate-Protocol
- ['80:quic,p=0']
- !!python/tuple
- Accept-Ranges
- [none]
- !!python/tuple
- X-Xss-Protection
- [1; mode=block]
- !!python/tuple
- Vary
- [Accept-Encoding]
- !!python/tuple
- Date
- ['Thu, 30 Jul 2015 08:41:40 GMT']
- !!python/tuple
- Cache-Control
- ['private, max-age=0']
- !!python/tuple
- Content-Type
- [text/html; charset=ISO-8859-1]
- !!python/tuple
- Set-Cookie
- ['PREF=ID=1111111111111111:FF=0:TM=1438245700:LM=1438245700:V=1:S=GAzVO0ALebSpC_cJ;
expires=Sat, 29-Jul-2017 08:41:40 GMT; path=/; domain=.google.com', 'NID=69=Br7oRAwgmKoK__HC6FEnuxglTFDmFxqP6Md63lKhzW1w6WkDbp3U90CDxnUKvDP6wJH8yxY5Lk5ZnFf66Q1B0d4OsYoKgq0vjfBAYXuCIAWtOuGZEOsFXanXs7pt2Mjx;
expires=Fri, 29-Jan-2016 08:41:40 GMT; path=/; domain=.google.com; HttpOnly']
- !!python/tuple
- X-Frame-Options
- [SAMEORIGIN]
- !!python/tuple
- Server
- [gws]
status: {code: 200, message: OK}
version: 1

View File

@@ -253,3 +253,37 @@ def test_func_path_generator():
def function_name(cassette): def function_name(cassette):
assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name') assert cassette._path == os.path.join(os.path.dirname(__file__), 'function_name')
function_name() function_name()
def test_use_as_decorator_on_coroutine():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 1
assert value == 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
value = yield 2
assert value == 2
coroutine = test_function()
value = next(coroutine)
while True:
try:
value = coroutine.send(value)
except StopIteration:
break
def test_use_as_decorator_on_generator():
original_http_connetion = httplib.HTTPConnection
@Cassette.use(inject=True)
def test_function(cassette):
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 1
assert httplib.HTTPConnection.cassette is cassette
assert httplib.HTTPConnection is not original_http_connetion
yield 2
assert list(test_function()) == [1, 2]

View File

@@ -1,3 +1,4 @@
# -*- encoding: utf-8 -*-
import pytest import pytest
from vcr.compat import mock from vcr.compat import mock
@@ -27,6 +28,55 @@ def test_deserialize_new_json_cassette():
deserialize(f.read(), jsonserializer) deserialize(f.read(), jsonserializer)
REQBODY_TEMPLATE = u'''\
interactions:
- request:
body: {req_body}
headers:
Content-Type: [application/x-www-form-urlencoded]
Host: [httpbin.org]
method: POST
uri: http://httpbin.org/post
response:
body: {{string: ""}}
headers:
content-length: ['0']
content-type: [application/json]
status: {{code: 200, message: OK}}
'''
# A cassette generated under Python 2 stores the request body as a string,
# but the same cassette generated under Python 3 stores it as "!!binary".
# Make sure we accept both forms, regardless of whether we're running under
# Python 2 or 3.
@pytest.mark.parametrize("req_body, expect", [
# Cassette written under Python 2 (pure ASCII body)
('x=5&y=2', b'x=5&y=2'),
# Cassette written under Python 3 (pure ASCII body)
('!!binary |\n eD01Jnk9Mg==', b'x=5&y=2'),
# Request body has non-ASCII chars (x=föo&y=2), encoded in UTF-8.
('!!python/str "x=f\\xF6o&y=2"', b'x=f\xc3\xb6o&y=2'),
('!!binary |\n eD1mw7ZvJnk9Mg==', b'x=f\xc3\xb6o&y=2'),
# Same request body, this time encoded in UTF-16. In this case, we
# write the same YAML file under both Python 2 and 3, so there's only
# one test case here.
('!!binary |\n //54AD0AZgD2AG8AJgB5AD0AMgA=',
b'\xff\xfex\x00=\x00f\x00\xf6\x00o\x00&\x00y\x00=\x002\x00'),
# Same again, this time encoded in ISO-8859-1.
('!!binary |\n eD1m9m8meT0y', b'x=f\xf6o&y=2'),
])
def test_deserialize_py2py3_yaml_cassette(tmpdir, req_body, expect):
cfile = tmpdir.join('test_cassette.yaml')
cfile.write(REQBODY_TEMPLATE.format(req_body=req_body))
with open(str(cfile)) as f:
(requests, responses) = deserialize(f.read(), yamlserializer)
assert requests[0].body == expect
@mock.patch.object(jsonserializer.json, 'dumps', @mock.patch.object(jsonserializer.json, 'dumps',
side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization', side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
0, 10, 'blew up')) 0, 10, 'blew up'))

View File

@@ -1,11 +1,9 @@
"""The container for recorded requests and responses""" import sys
import functools import inspect
import logging import logging
import wrapt import wrapt
# Internal imports
from .compat import contextlib, collections from .compat import contextlib, collections
from .errors import UnhandledHTTPRequestError from .errors import UnhandledHTTPRequestError
from .matchers import requests_match, uri, method from .matchers import requests_match, uri, method
@@ -50,14 +48,6 @@ class CassetteContextDecorator(object):
# somewhere else. # somewhere else.
cassette._save() cassette._save()
@classmethod
def key_predicate(cls, key, value):
return key in cls._non_cassette_arguments
@classmethod
def _split_keys(cls, kwargs):
return partition_dict(cls.key_predicate, kwargs)
def __enter__(self): def __enter__(self):
# This assertion is here to prevent the dangerous behavior # This assertion is here to prevent the dangerous behavior
# that would result from forgetting about a __finish before # that would result from forgetting about a __finish before
@@ -68,7 +58,10 @@ class CassetteContextDecorator(object):
# with context_decorator: # with context_decorator:
# pass # pass
assert self.__finish is None, "Cassette already open." assert self.__finish is None, "Cassette already open."
other_kwargs, cassette_kwargs = self._split_keys(self._args_getter()) other_kwargs, cassette_kwargs = partition_dict(
lambda key, _: key in self._non_cassette_arguments,
self._args_getter()
)
if 'path_transformer' in other_kwargs: if 'path_transformer' in other_kwargs:
transformer = other_kwargs['path_transformer'] transformer = other_kwargs['path_transformer']
cassette_kwargs['path'] = transformer(cassette_kwargs['path']) cassette_kwargs['path'] = transformer(cassette_kwargs['path'])
@@ -84,27 +77,53 @@ class CassetteContextDecorator(object):
# This awkward cloning thing is done to ensure that decorated # This awkward cloning thing is done to ensure that decorated
# functions are reentrant. This is required for thread # functions are reentrant. This is required for thread
# safety and the correct operation of recursive functions. # safety and the correct operation of recursive functions.
args_getter = self._build_args_getter_for_decorator( args_getter = self._build_args_getter_for_decorator(function)
function, self._args_getter return type(self)(self.cls, args_getter)._execute_function(function, args, kwargs)
)
clone = type(self)(self.cls, args_getter) def _execute_function(self, function, args, kwargs):
with clone as cassette: if inspect.isgeneratorfunction(function):
if cassette.inject: handler = self._handle_coroutine
return function(cassette, *args, **kwargs) else:
else: handler = self._handle_function
return function(*args, **kwargs) return handler(function, args, kwargs)
def _handle_coroutine(self, function, args, kwargs):
"""Wraps a coroutine so that we're inside the cassette context for the
duration of the coroutine.
"""
with self as cassette:
coroutine = self.__handle_function(cassette, function, args, kwargs)
# 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, cassette, function, args, kwargs):
if cassette.inject:
return function(cassette, *args, **kwargs)
else:
return function(*args, **kwargs)
def _handle_function(self, function, args, kwargs):
with self as cassette:
self.__handle_function(cassette, function, args, kwargs)
@staticmethod @staticmethod
def get_function_name(function): def get_function_name(function):
return function.__name__ return function.__name__
@classmethod def _build_args_getter_for_decorator(self, function):
def _build_args_getter_for_decorator(cls, function, args_getter):
def new_args_getter(): def new_args_getter():
kwargs = args_getter() kwargs = self._args_getter()
if 'path' not in kwargs: if 'path' not in kwargs:
name_generator = (kwargs.get('func_path_generator') or name_generator = (kwargs.get('func_path_generator') or
cls.get_function_name) self.get_function_name)
path = name_generator(function) path = name_generator(function)
kwargs['path'] = path kwargs['path'] = path
return kwargs return kwargs

View File

@@ -107,7 +107,7 @@ class VCR(object):
matcher_names = kwargs.get('match_on', self.match_on) matcher_names = kwargs.get('match_on', self.match_on)
path_transformer = kwargs.get( path_transformer = kwargs.get(
'path_transformer', 'path_transformer',
self.path_transformer self.path_transformer or self.ensure_suffix('.yaml')
) )
func_path_generator = kwargs.get( func_path_generator = kwargs.get(
'func_path_generator', 'func_path_generator',

View File

@@ -43,11 +43,18 @@ def _header_checker(value, header='Content-Type'):
return checker return checker
def _transform_json(body):
# Request body is always a byte string, but json.loads() wants a text
# string. RFC 7159 says the default encoding is UTF-8 (although UTF-16
# and UTF-32 are also allowed: hmmmmm).
return json.loads(body.decode('utf-8'))
_xml_header_checker = _header_checker('text/xml') _xml_header_checker = _header_checker('text/xml')
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent') _xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent')
_checker_transformer_pairs = ( _checker_transformer_pairs = (
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs), (_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs),
(_header_checker('application/json'), json.loads), (_header_checker('application/json'), _transform_json),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads), (lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
) )

View File

@@ -54,13 +54,12 @@ else:
# Try to save the original types for Tornado # Try to save the original types for Tornado
try: try:
import tornado.httpclient
import tornado.simple_httpclient import tornado.simple_httpclient
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_AsyncHTTPClient = tornado.httpclient.AsyncHTTPClient _SimpleAsyncHTTPClient_fetch_impl = \
_SimpleAsyncHTTPClient = tornado.simple_httpclient.SimpleAsyncHTTPClient tornado.simple_httpclient.SimpleAsyncHTTPClient.fetch_impl
try: try:
@@ -68,7 +67,8 @@ try:
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
_CurlAsyncHTTPClient = tornado.curl_httpclient.CurlAsyncHTTPClient _CurlAsyncHTTPClient_fetch_impl = \
tornado.curl_httpclient.CurlAsyncHTTPClient.fetch_impl
class CassettePatcherBuilder(object): class CassettePatcherBuilder(object):
@@ -228,23 +228,27 @@ class CassettePatcherBuilder(object):
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _tornado(self): def _tornado(self):
try: try:
import tornado.httpclient as http
import tornado.simple_httpclient as simple import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
from .stubs.tornado_stubs import VCRAsyncHTTPClient from .stubs.tornado_stubs import vcr_fetch_impl
from .stubs.tornado_stubs import VCRSimpleAsyncHTTPClient
yield http, 'AsyncHTTPClient', VCRAsyncHTTPClient new_fetch_impl = vcr_fetch_impl(
yield simple, 'SimpleAsyncHTTPClient', VCRSimpleAsyncHTTPClient self._cassette, _SimpleAsyncHTTPClient_fetch_impl
)
yield simple.SimpleAsyncHTTPClient, 'fetch_impl', new_fetch_impl
try: try:
import tornado.curl_httpclient as curl import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
from .stubs.tornado_stubs import VCRCurlAsyncHTTPClient from .stubs.tornado_stubs import vcr_fetch_impl
yield curl, 'CurlAsyncHTTPClient', VCRCurlAsyncHTTPClient
new_fetch_impl = vcr_fetch_impl(
self._cassette, _CurlAsyncHTTPClient_fetch_impl
)
yield curl.CurlAsyncHTTPClient, 'fetch_impl', new_fetch_impl
def _urllib3_patchers(self, cpool, stubs): def _urllib3_patchers(self, cpool, stubs):
http_connection_remover = ConnectionRemover( http_connection_remover = ConnectionRemover(
@@ -362,19 +366,25 @@ def reset_patchers():
_CertValidatingHTTPSConnection) _CertValidatingHTTPSConnection)
try: try:
import tornado.httpclient as http
import tornado.simple_httpclient as simple import tornado.simple_httpclient as simple
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object(http, 'AsyncHTTPClient', _AsyncHTTPClient) yield mock.patch.object(
yield mock.patch.object(simple, 'SimpleAsyncHTTPClient', _SimpleAsyncHTTPClient) simple.SimpleAsyncHTTPClient,
'fetch_impl',
_SimpleAsyncHTTPClient_fetch_impl,
)
try: try:
import tornado.curl_httpclient as curl import tornado.curl_httpclient as curl
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
else: else:
yield mock.patch.object(curl, 'CurlAsyncHTTPClient', _CurlAsyncHTTPClient) yield mock.patch.object(
curl.CurlAsyncHTTPClient,
'fetch_impl',
_CurlAsyncHTTPClient_fetch_impl,
)
@contextlib.contextmanager @contextlib.contextmanager

View File

@@ -1,4 +1,4 @@
from six import BytesIO, binary_type from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl from six.moves.urllib.parse import urlparse, parse_qsl
@@ -29,11 +29,9 @@ class Request(object):
self.uri = uri self.uri = uri
self._was_file = hasattr(body, 'read') self._was_file = hasattr(body, 'read')
if self._was_file: if self._was_file:
self._body = body.read() self.body = body.read()
if not isinstance(self._body, binary_type):
self._body = self._body.encode('utf-8')
else: else:
self._body = body self.body = body
self.headers = {} self.headers = {}
for key in headers: for key in headers:
self.add_header(key, headers[key]) self.add_header(key, headers[key])
@@ -44,6 +42,8 @@ class Request(object):
@body.setter @body.setter
def body(self, value): def body(self, value):
if isinstance(value, text_type):
value = value.encode('utf-8')
self._body = value self._body = value
def add_header(self, key, value): def add_header(self, key, value):

View File

@@ -1,48 +1,20 @@
'''Stubs for tornado HTTP clients''' '''Stubs for tornado HTTP clients'''
from __future__ import absolute_import from __future__ import absolute_import
import functools
from six import BytesIO from six import BytesIO
from tornado import httputil from tornado import httputil
from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPResponse from tornado.httpclient import HTTPResponse
from tornado.simple_httpclient import SimpleAsyncHTTPClient
from vcr.errors import CannotOverwriteExistingCassetteException from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.request import Request from vcr.request import Request
class _VCRAsyncClient(object): def vcr_fetch_impl(cassette, real_fetch_impl):
cassette = None
def __new__(cls, *args, **kwargs): @functools.wraps(real_fetch_impl)
from vcr.patch import force_reset def new_fetch_impl(self, request, callback):
with force_reset():
return super(_VCRAsyncClient, cls).__new__(cls, *args, **kwargs)
def initialize(self, *args, **kwargs):
from vcr.patch import force_reset
with force_reset():
self.real_client = self._baseclass(*args, **kwargs)
@property
def io_loop(self):
return self.real_client.io_loop
@property
def _closed(self):
return self.real_client._closed
@property
def defaults(self):
return self.real_client.defaults
def close(self):
from vcr.patch import force_reset
with force_reset():
self.real_client.close()
def fetch_impl(self, request, callback):
headers = dict(request.headers) headers = dict(request.headers)
if request.user_agent: if request.user_agent:
headers.setdefault('User-Agent', request.user_agent) headers.setdefault('User-Agent', request.user_agent)
@@ -74,8 +46,8 @@ class _VCRAsyncClient(object):
headers, headers,
) )
if self.cassette.can_play_response_for(vcr_request): if cassette.can_play_response_for(vcr_request):
vcr_response = self.cassette.play_response(vcr_request) vcr_response = cassette.play_response(vcr_request)
headers = httputil.HTTPHeaders() headers = httputil.HTTPHeaders()
recorded_headers = vcr_response['headers'] recorded_headers = vcr_response['headers']
@@ -93,7 +65,7 @@ class _VCRAsyncClient(object):
) )
return callback(response) return callback(response)
else: else:
if self.cassette.write_protected and self.cassette.filter_request( if cassette.write_protected and cassette.filter_request(
vcr_request vcr_request
): ):
response = HTTPResponse( response = HTTPResponse(
@@ -103,8 +75,7 @@ class _VCRAsyncClient(object):
"No match for the request (%r) was found. " "No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in " "Can't overwrite existing cassette (%r) in "
"your current record mode (%r)." "your current record mode (%r)."
% (vcr_request, self.cassette._path, % (vcr_request, cassette._path, cassette.record_mode)
self.cassette.record_mode)
), ),
) )
return callback(response) return callback(response)
@@ -123,26 +94,9 @@ class _VCRAsyncClient(object):
'headers': headers, 'headers': headers,
'body': {'string': response.body}, 'body': {'string': response.body},
} }
self.cassette.append(vcr_request, vcr_response) cassette.append(vcr_request, vcr_response)
return callback(response) return callback(response)
from vcr.patch import force_reset real_fetch_impl(self, request, new_callback)
with force_reset():
self.real_client.fetch_impl(request, new_callback)
return new_fetch_impl
class VCRAsyncHTTPClient(_VCRAsyncClient, AsyncHTTPClient):
_baseclass = AsyncHTTPClient
class VCRSimpleAsyncHTTPClient(_VCRAsyncClient, SimpleAsyncHTTPClient):
_baseclass = SimpleAsyncHTTPClient
try:
from tornado.curl_httpclient import CurlAsyncHTTPClient
except ImportError: # pragma: no cover
VCRCurlAsyncHTTPClient = None
else:
class VCRCurlAsyncHTTPClient(_VCRAsyncClient, CurlAsyncHTTPClient):
_baseclass = CurlAsyncHTTPClient