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

Compare commits

..

21 Commits

Author SHA1 Message Date
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
11 changed files with 309 additions and 44 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,10 @@ new API in version 1.0.x
Changelog Changelog
--------- ---------
- 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,6 +1,8 @@
#!/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
@@ -31,14 +33,23 @@ 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.0',
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,26 @@ 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

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

@@ -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):