1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Ivan Malison
80ece7750f v1.6.1 2015-07-15 00:25:56 -07:00
Ivan Malison
8a86d75dc5 Merge remote-tracking branch 'upstream/master' into improved_body_matcher 2015-07-15 00:10:37 -07:00
Ivan Malison
33a4fb98c6 Update unit tests for body matcher. Simplified logic. 2015-07-15 00:01:31 -07:00
Diaoul
a046697567 Add a read_body helper function 2015-07-15 01:16:10 +02:00
Diaoul
c0286dfd97 Add body matcher unit tests 2015-07-11 23:22:42 +02:00
Diaoul
cc9af1d5fb Use CaseInsensitiveDict in body matcher 2015-07-11 23:18:45 +02:00
Ivan 'Goat' Malison
5f8407a8a1 Merge pull request #170 from graingert/manual-conditional-requirements-for-old-pip
Support conditional requirements in old versions of pip
2015-07-07 16:09:46 -07:00
Thomas Grainger
c789c82c1d Support conditional requirements in old versions of pip 2015-07-07 11:28:49 +01:00
Ivan 'Goat' Malison
16b5b77bcd Merge pull request #168 from graingert/patch-1
Fix RST parse errors generated by pandoc
2015-07-05 12:43:43 -07:00
Thomas Grainger
0a093786ed Fix RST parse errors generated by pandoc 2015-07-05 12:14:01 +01:00
Diaoul
3986caf182 Use Content-Type based approach for body matcher
When converting objects to body, dicts and sets order can change
resulting in a different but same body. This fixes the issue by
comparing the enclosed data in the body rather than the body itself
while still allowing raw body matching with the raw_body matcher.
2015-07-04 19:21:14 +02:00
Ivan 'Goat' Malison
cc6c26646c Merge pull request #165 from abhinav/master
[Tornado] Fix unsupported features exception not being raised.
2015-07-03 16:04:49 -07:00
Abhinav Gupta
3846a4ccef [Tornado] Fix unsupported features exception not being raised.
Add tests for that exception being raisd correctly and for
CannotOverwriteCassetteException.
2015-07-03 12:34:57 -07:00
Ivan Malison
aae4ae255b README spacing fix. 2015-07-03 10:26:52 -07:00
8 changed files with 323 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ VCR.py
:alt: vcr.py
vcr.py
This is a Python version of `Ruby's VCR
library <https://github.com/vcr/vcr>`__.
@@ -144,7 +145,9 @@ The following options are available :
- port (the port of the server receiving the request)
- path (the path of the request)
- query (the query string of the request)
- body (the entire request body)
- raw\_body (the entire request body as is)
- body (the entire request body unmarshalled by content-type
i.e. xmlrpc, json, form-urlencoded, falling back on raw\_body)
- headers (the headers of the request)
Backwards compatible matchers:
@@ -603,7 +606,11 @@ new API in version 1.0.x
Changelog
---------
- 1.6.0 [#120] Tornado support thanks (thanks @abhinav), [#147] packaging fixes
- 1.6.1 [#169] Support conditional requirements in old versions of
pip, Fix RST parse errors generated by pandoc, [Tornado] Fix
unsupported features exception not being raised, [#166]
content-aware body matcher.
- 1.6.0 [#120] Tornado support (thanks @abhinav), [#147] packaging fixes
(thanks @graingert), [#158] allow filtering post params in requests
(thanks @MrJohz), [#140] add xmlrpclib support (thanks @Diaoul).
- 1.5.2 Fix crash when cassette path contains cassette library

View File

@@ -3,6 +3,7 @@
import sys
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import pkg_resources
long_description = open('README.rst', 'r').read()
@@ -20,9 +21,24 @@ class PyTest(TestCommand):
sys.exit(errno)
install_requires=['PyYAML', 'wrapt', 'six>=1.5']
extras_require = {
':python_version in "2.4, 2.5, 2.6"':
['contextlib2', 'backport_collections', 'mock'],
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
}
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)
setup(
name='vcrpy',
version='1.6.0',
version='1.6.1',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
@@ -32,12 +48,8 @@ setup(
author_email='me@kevinmccarthy.org',
url='https://github.com/kevin1024/vcrpy',
packages=find_packages(exclude=("tests*",)),
install_requires=['PyYAML', 'wrapt', 'six>=1.5'],
extras_require = {
':python_version in "2.4, 2.5, 2.6"':
['contextlib2', 'backport_collections', 'mock'],
':python_version in "2.7, 3.1, 3.2"': ['contextlib2', 'mock'],
},
install_requires=install_requires,
extras_require=extras_require,
license='MIT',
tests_require=['pytest', 'mock', 'pytest-localserver'],
cmdclass={'test': PyTest},

View File

@@ -5,6 +5,7 @@ import json
import pytest
import vcr
from vcr.errors import CannotOverwriteExistingCassetteException
from assertions import assert_cassette_empty, assert_is_json
@@ -203,3 +204,74 @@ def test_https_with_cert_validation_disabled(get_client, tmpdir):
with vcr.use_cassette(cass_path) as cass:
yield get(get_client(), 'https://httpbin.org', validate_cert=False)
assert 1 == cass.play_count
@pytest.mark.gen_test
def test_unsupported_features_raises_in_future(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being
supported is raised inside the future.'''
def callback(chunk):
assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
future = get(
get_client(), 'http://httpbin.org', streaming_callback=callback
)
with pytest.raises(Exception) as excinfo:
yield future
assert "not yet supported by VCR" in str(excinfo)
@pytest.mark.gen_test
def test_unsupported_features_raise_error_disabled(get_client, tmpdir):
'''Ensure that the exception for an AsyncHTTPClient feature not being
supported is not raised if raise_error=False.'''
def callback(chunk):
assert False, "Did not expect to be called."
with vcr.use_cassette(str(tmpdir.join('invalid.yaml'))):
response = yield get(
get_client(),
'http://httpbin.org',
streaming_callback=callback,
raise_error=False,
)
assert "not yet supported by VCR" in str(response.error)
@pytest.mark.gen_test
def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is raised inside
the future.'''
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
yield get(get_client(), 'http://httpbin.org/get')
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
future = get(get_client(), 'http://httpbin.org/headers')
with pytest.raises(CannotOverwriteExistingCassetteException):
yield future
@pytest.mark.gen_test
def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir):
'''Ensure that CannotOverwriteExistingCassetteException is not raised if
raise_error=False in the fetch() call.'''
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
yield get(
get_client(), 'http://httpbin.org/get', raise_error=False
)
with vcr.use_cassette(str(tmpdir.join('overwrite.yaml'))):
response = yield get(
get_client(), 'http://httpbin.org/headers', raise_error=False
)
assert isinstance(response.error, CannotOverwriteExistingCassetteException)

View File

@@ -1,5 +1,7 @@
import itertools
import pytest
from vcr import matchers
from vcr import request
@@ -35,6 +37,107 @@ def test_uri_matcher():
assert matched
req1_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>")
req2_body = (b"<?xml version='1.0'?><methodCall><methodName>test</methodName>"
b"<params><param><value><array><data><value><struct>"
b"<member><name>b</name><value><string>2</string></value></member>"
b"<member><name>a</name><value><string>1</string></value></member>"
b"</struct></value></data></array></value></param></params></methodCall>")
@pytest.mark.parametrize("r1, r2", [
(
request.Request('POST', 'http://host.com/', '123', {}),
request.Request('POST', 'http://another-host.com/',
'123', {'Some-Header': 'value'})
),
(
request.Request('POST', 'http://host.com/', 'a=1&b=2',
{'Content-Type': 'application/x-www-form-urlencoded'}),
request.Request('POST', 'http://host.com/', 'b=2&a=1',
{'Content-Type': 'application/x-www-form-urlencoded'})
),
(
request.Request('POST', 'http://host.com/', '123', {}),
request.Request('POST', 'http://another-host.com/', '123', {'Some-Header': 'value'})
),
(
request.Request(
'POST', 'http://host.com/', 'a=1&b=2',
{'Content-Type': 'application/x-www-form-urlencoded'}
),
request.Request(
'POST', 'http://host.com/', 'b=2&a=1',
{'Content-Type': 'application/x-www-form-urlencoded'}
)
),
(
request.Request(
'POST', 'http://host.com/', '{"a": 1, "b": 2}',
{'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/', '{"b": 2, "a": 1}',
{'content-type': 'application/json'}
)
),
(
request.Request(
'POST', 'http://host.com/', req1_body,
{'User-Agent': 'xmlrpclib', 'Content-Type': 'text/xml'}
),
request.Request(
'POST', 'http://host.com/', req2_body,
{'user-agent': 'somexmlrpc', 'content-type': 'text/xml'}
)
),
(
request.Request(
'POST', 'http://host.com/',
'{"a": 1, "b": 2}', {'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/',
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
)
)
])
def test_body_matcher_does_match(r1, r2):
assert matchers.body(r1, r2)
@pytest.mark.parametrize("r1, r2", [
(
request.Request('POST', 'http://host.com/', '{"a": 1, "b": 2}', {}),
request.Request('POST', 'http://host.com/', '{"b": 2, "a": 1}', {}),
),
(
request.Request(
'POST', 'http://host.com/',
'{"a": 1, "b": 3}', {'Content-Type': 'application/json'}
),
request.Request(
'POST', 'http://host.com/',
'{"b": 2, "a": 1}', {'content-type': 'application/json'}
)
),
(
request.Request(
'POST', 'http://host.com/', req1_body, {'Content-Type': 'text/xml'}
),
request.Request(
'POST', 'http://host.com/', req2_body, {'content-type': 'text/xml'}
)
)
])
def test_body_match_does_not_match(r1, r2):
assert not matchers.body(r1, r2)
def test_query_matcher():
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})

View File

@@ -47,6 +47,7 @@ class VCR(object):
'path': matchers.path,
'query': matchers.query,
'headers': matchers.headers,
'raw_body': matchers.raw_body,
'body': matchers.body,
}
self.record_mode = record_mode

View File

@@ -1,3 +1,6 @@
import json
from six.moves import urllib, xmlrpc_client
from .util import CaseInsensitiveDict, read_body
import logging
log = logging.getLogger(__name__)
@@ -30,10 +33,43 @@ def query(r1, r2):
return r1.query == r2.query
def raw_body(r1, r2):
return read_body(r1) == read_body(r2)
def _header_checker(value, header='Content-Type'):
def checker(headers):
return value in headers.get(header, '').lower()
return checker
_xml_header_checker = _header_checker('text/xml')
_xmlrpc_header_checker = _header_checker('xmlrpc', header='User-Agent')
_checker_transformer_pairs = (
(_header_checker('application/x-www-form-urlencoded'), urllib.parse.parse_qs),
(_header_checker('application/json'), json.loads),
(lambda request: _xml_header_checker(request) and _xmlrpc_header_checker(request), xmlrpc_client.loads),
)
def _identity(x):
return x
def _get_transformer(request):
headers = CaseInsensitiveDict(request.headers)
for checker, transformer in _checker_transformer_pairs:
if checker(headers): return transformer
else:
return _identity
def body(r1, r2):
if hasattr(r1.body, 'read') and hasattr(r2.body, 'read'):
return r1.body.read() == r2.body.read()
return r1.body == r2.body
transformer = _get_transformer(r1)
r2_transformer = _get_transformer(r2)
if transformer != r2_transformer:
transformer = _identity
return transformer(read_body(r1)) == transformer(read_body(r2))
def headers(r1, r2):

View File

@@ -65,6 +65,7 @@ class _VCRAsyncClient(object):
"request outside a VCR.py context." % repr(request)
),
)
return callback(response)
vcr_request = Request(
request.method,
@@ -90,7 +91,7 @@ class _VCRAsyncClient(object):
headers=headers,
buffer=BytesIO(vcr_response['body']['string']),
)
callback(response)
return callback(response)
else:
if self.cassette.write_protected and self.cassette.filter_request(
vcr_request
@@ -106,7 +107,7 @@ class _VCRAsyncClient(object):
self.cassette.record_mode)
),
)
callback(response)
return callback(response)
def new_callback(response):
headers = [
@@ -123,7 +124,7 @@ class _VCRAsyncClient(object):
'body': {'string': response.body},
}
self.cassette.append(vcr_request, vcr_response)
callback(response)
return callback(response)
from vcr.patch import force_reset
with force_reset():

View File

@@ -1,3 +1,74 @@
import collections
# Shamelessly stolen from https://github.com/kennethreitz/requests/blob/master/requests/structures.py
class CaseInsensitiveDict(collections.MutableMapping):
"""
A case-insensitive ``dict``-like object.
Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``.
All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``,
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
will contain case-sensitive keys. However, querying and contains
testing is case insensitive::
cid = CaseInsensitiveDict()
cid['Accept'] = 'application/json'
cid['aCCEPT'] == 'application/json' # True
list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
value of a ``'Content-Encoding'`` response header, regardless
of how the header name was originally stored.
If the constructor, ``.update``, or equality comparison
operations are given keys that have equal ``.lower()``s, the
behavior is undefined.
"""
def __init__(self, data=None, **kwargs):
self._store = dict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return (
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
def partition_dict(predicate, dictionary):
true_dict = {}
false_dict = {}
@@ -14,3 +85,8 @@ def compose(*functions):
res = function(res)
return res
return composed
def read_body(request):
if hasattr(request.body, 'read'):
return request.body.read()
return request.body