mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e049ba7a1 | ||
|
|
916e7839e5 | ||
|
|
99692a92d2 | ||
|
|
a9a68ba44b | ||
|
|
e9f35db405 | ||
|
|
7193407a07 | ||
|
|
c3427ae3a2 | ||
|
|
3a46a6f210 | ||
|
|
163181844b | ||
|
|
2c6f072d11 | ||
|
|
361ed82a10 | ||
|
|
0871c3b87c | ||
|
|
d484dee50f | ||
|
|
b046ee4bb1 | ||
|
|
3dea853482 | ||
|
|
113c95f971 | ||
|
|
a2c947dc48 | ||
|
|
757ad9c836 | ||
|
|
18e5898ec4 | ||
|
|
83211a1887 | ||
|
|
dc249b0965 | ||
|
|
121ed79172 | ||
|
|
b1cdd50e9b | ||
|
|
1018867838 | ||
|
|
b6e96020c1 | ||
|
|
8947f0fc5c | ||
|
|
2bf23b2cdf | ||
|
|
58fcb2b453 | ||
|
|
0c19acd74f | ||
|
|
4868a63876 | ||
|
|
e1e08c7a2c | ||
|
|
5edc58f10c | ||
|
|
2193008150 | ||
|
|
958aac3af3 | ||
|
|
9a564586a4 | ||
|
|
643a4c91ee | ||
|
|
472cc3bffe | ||
|
|
8db46002a3 | ||
|
|
a08c90c5d6 | ||
|
|
8e01426056 | ||
|
|
9a4f5f23a4 | ||
|
|
366e2b75bb | ||
|
|
0cfe63ef6e | ||
|
|
cb05f4163c | ||
|
|
20057a6815 | ||
|
|
0d313502b8 | ||
|
|
d9c2b4b25d | ||
|
|
640681138a | ||
|
|
a02bbbab2b | ||
|
|
f719f90e63 | ||
|
|
3c410b5f9d | ||
|
|
7a5795a547 | ||
|
|
1bd3fbd2c6 | ||
|
|
cd715f37c6 | ||
|
|
9a1147196a | ||
|
|
a23c5d8508 | ||
|
|
868a974900 | ||
|
|
c56de472cd | ||
|
|
c6590f2caf | ||
|
|
70abc5058c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ dist/
|
||||
.coverage
|
||||
*.egg-info/
|
||||
pytestdebug.log
|
||||
|
||||
fixtures/
|
||||
13
.travis.yml
13
.travis.yml
@@ -5,7 +5,9 @@ env:
|
||||
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
|
||||
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
|
||||
matrix:
|
||||
- WITH_LIB="requests2.x"
|
||||
- WITH_LIB="requests2.2"
|
||||
- WITH_LIB="requests2.3"
|
||||
- WITH_LIB="requests2.4"
|
||||
- WITH_LIB="requests1.x"
|
||||
- WITH_LIB="httplib2"
|
||||
- WITH_LIB="boto"
|
||||
@@ -15,15 +17,22 @@ matrix:
|
||||
exclude:
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.3
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.4
|
||||
- env: WITH_LIB="requests1.x"
|
||||
python: 3.4
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- pypy
|
||||
install:
|
||||
- pip install PyYAML pytest pytest-localserver --use-mirrors
|
||||
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
|
||||
- if [ $WITH_LIB = "requests2.x" ] ; then pip install requests; fi
|
||||
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; fi
|
||||
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
|
||||
- if [ $WITH_LIB = "requests2.4" ] ; then pip install requests==2.4.0; fi
|
||||
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
|
||||
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
|
||||
script: python setup.py test
|
||||
|
||||
77
README.md
77
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||

|
||||
|
||||
This is a Python version of [Ruby's VCR library](https://github.com/myronmarston/vcr).
|
||||
This is a Python version of [Ruby's VCR library](https://github.com/vcr/vcr).
|
||||
|
||||
[](http://travis-ci.org/kevin1024/vcrpy)
|
||||
[](https://waffle.io/kevin1024/vcrpy)
|
||||
@@ -51,7 +51,7 @@ You can also use VCR.py as a decorator. The same request above would look like
|
||||
this:
|
||||
|
||||
```python
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml')
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
@@ -176,13 +176,13 @@ with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||
The `Cassette` object exposes the following properties which I consider part of
|
||||
the API. The fields are as follows:
|
||||
|
||||
* `requests`: A list of vcr.Request objects containing the requests made while
|
||||
this cassette was being used, ordered by the order that the request was made.
|
||||
* `requests`: A list of vcr.Request objects corresponding to the http requests
|
||||
that were made during the recording of the cassette. The requests appear in the
|
||||
order that they were originally processed.
|
||||
* `responses`: A list of the responses made.
|
||||
* `play_count`: The number of times this cassette has had a response played
|
||||
back
|
||||
* `all_played`: A boolean indicates whether all the responses have been
|
||||
played back
|
||||
* `play_count`: The number of times this cassette has played back a response.
|
||||
* `all_played`: A boolean indicating whether all the responses have been
|
||||
played back.
|
||||
* `responses_of(request)`: Access the responses that match a given request
|
||||
|
||||
The `Request` object has the following properties:
|
||||
@@ -215,7 +215,7 @@ Finally, register your class with VCR to use your new serializer.
|
||||
```python
|
||||
import vcr
|
||||
|
||||
BogoSerializer(object):
|
||||
class BogoSerializer(object):
|
||||
"""
|
||||
Must implement serialize() and deserialize() methods
|
||||
"""
|
||||
@@ -293,12 +293,12 @@ with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
|
||||
requests.get('http://api.com/getdata?api_key=secretstring')
|
||||
```
|
||||
|
||||
### Custom request filtering
|
||||
### Custom Request filtering
|
||||
|
||||
If neither of these covers your use case, you can register a callback that will
|
||||
manipulate the HTTP request before adding it to the cassette. Use the
|
||||
`before_record` configuration option to so this. Here is an
|
||||
example that will never record requests to the /login endpoint.
|
||||
If neither of these covers your request filtering needs, you can register a callback
|
||||
that will manipulate the HTTP request before adding it to the cassette. Use the
|
||||
`before_record` configuration option to so this. Here is an example that will
|
||||
never record requests to the /login endpoint.
|
||||
|
||||
```python
|
||||
def before_record_cb(request):
|
||||
@@ -312,6 +312,40 @@ with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
You can also mutate the response using this callback. For example, you could
|
||||
remove all query parameters from any requests to the `'/login'` path.
|
||||
|
||||
```python
|
||||
def scrub_login_request(request):
|
||||
if request.path == '/login':
|
||||
request.uri, _ = urllib.splitquery(response.uri)
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_login_request,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
### Custom Response Filtering
|
||||
|
||||
VCR.py also suports response filtering with the `before_record_response` keyword
|
||||
argument. It's usage is similar to that of `before_record`:
|
||||
|
||||
```python
|
||||
def scrub_string(string, replacement=''):
|
||||
def before_record_reponse(response):
|
||||
return response['body']['string] = response['body']['string].replace(string, replacement)
|
||||
return scrub_string
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record=scrub_string(settings.USERNAME, 'username'),
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
## Ignore requests
|
||||
|
||||
If you would like to completely ignore certain requests, you can do it in a
|
||||
@@ -335,7 +369,7 @@ to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
|
||||
|
||||
## Ruby VCR compatibility
|
||||
|
||||
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes
|
||||
VCR.py does not aim to match the format of the Ruby VCR YAML files. Cassettes
|
||||
generated by Ruby's VCR are not compatible with VCR.py.
|
||||
|
||||
## Running VCR's test suite
|
||||
@@ -356,7 +390,7 @@ installed.
|
||||
Also, in order for the boto tests to run, you will need an AWS key. Refer to
|
||||
the [boto
|
||||
documentation](http://boto.readthedocs.org/en/latest/getting_started.html) for
|
||||
how to set this up. I have marked the boto tests as optional in Travis so you
|
||||
how to set this up. I have marked the boto tests as optional in Travis so you
|
||||
don't have to worry about them failing if you submit a pull request.
|
||||
|
||||
|
||||
@@ -423,6 +457,17 @@ API in version 1.0.x
|
||||
|
||||
|
||||
## Changelog
|
||||
* 1.1.2 Add urllib==1.7.1 support. Make json serialize error handling correct
|
||||
Improve logging of match failures.
|
||||
* 1.1.1 Use function signature preserving `wrapt.decorator` to write the
|
||||
decorator version of use_cassette in order to ensure compatibility with
|
||||
py.test fixtures and python 2. Move all request filtering into the
|
||||
`before_record_callable`.
|
||||
* 1.1.0 Add `before_record_response`. Fix several bugs related to the context
|
||||
management of cassettes.
|
||||
* 1.0.3: Fix an issue with requests 2.4 and make sure case sensitivity is
|
||||
consistent across python versions
|
||||
* 1.0.2: Fix an issue with requests 2.3
|
||||
* 1.0.1: Fix a bug with the new ignore requests feature and the once
|
||||
record mode
|
||||
* 1.0.0: _BACKWARDS INCOMPATIBLE_: Please see the 'upgrade' section in the
|
||||
|
||||
4
setup.py
4
setup.py
@@ -20,7 +20,7 @@ class PyTest(TestCommand):
|
||||
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.0.1',
|
||||
version='1.1.2',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
@@ -41,7 +41,7 @@ setup(
|
||||
'vcr.compat': 'vcr/compat',
|
||||
'vcr.persisters': 'vcr/persisters',
|
||||
},
|
||||
install_requires=['PyYAML', 'contextdecorator', 'six'],
|
||||
install_requires=['PyYAML', 'mock', 'six', 'contextlib2', 'wrapt'],
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'''Basic tests about cassettes'''
|
||||
'''Basic tests for cassettes'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
|
||||
@@ -54,15 +54,20 @@ def test_filter_querystring(tmpdir):
|
||||
urlopen(url)
|
||||
assert 'foo' not in cass.requests[0].url
|
||||
|
||||
|
||||
def test_filter_callback(tmpdir):
|
||||
url = 'http://httpbin.org/get'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
def before_record_cb(request):
|
||||
if request.path != '/get':
|
||||
return request
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
)
|
||||
# Test the legacy keyword.
|
||||
my_vcr = vcr.VCR(before_record=before_record_cb)
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
urlopen(url)
|
||||
assert len(cass) == 0
|
||||
|
||||
my_vcr = vcr.VCR(before_record_request=before_record_cb)
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
urlopen(url)
|
||||
assert len(cass) == 0
|
||||
|
||||
@@ -9,3 +9,14 @@ def test_recorded_request_uri_with_redirected_request(tmpdir):
|
||||
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
|
||||
assert cass.requests[3].uri == 'http://httpbin.org/get'
|
||||
assert len(cass) == 4
|
||||
|
||||
|
||||
def test_records_multiple_header_values(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
|
||||
assert len(cass) == 0
|
||||
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 1
|
||||
assert cass.responses[0]['headers']['foo'] == ['bar', 'baz']
|
||||
|
||||
@@ -24,30 +24,30 @@ def scheme(request):
|
||||
def test_status_code(scheme, tmpdir):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
status_code = requests.get(url).status_code
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
assert status_code == requests.get(url).status_code
|
||||
|
||||
|
||||
def test_headers(scheme, tmpdir):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
headers = requests.get(url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
assert headers == requests.get(url).headers
|
||||
|
||||
|
||||
def test_body(tmpdir, scheme):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
assert content == requests.get(url).content
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@ def test_auth(tmpdir, scheme):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
auth = ('user', 'passwd')
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = requests.get(url, auth=auth)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
two = requests.get(url, auth=auth)
|
||||
assert one.content == two.content
|
||||
assert one.status_code == two.status_code
|
||||
@@ -81,10 +81,10 @@ def test_post(tmpdir, scheme):
|
||||
'''Ensure that we can post and cache the results'''
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
req1 = requests.post(url, data).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
req2 = requests.post(url, data).content
|
||||
|
||||
assert req1 == req2
|
||||
@@ -93,7 +93,7 @@ def test_post(tmpdir, scheme):
|
||||
def test_redirects(tmpdir, scheme):
|
||||
'''Ensure that we can handle redirects'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))):
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
@@ -124,11 +124,11 @@ def test_gzip(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
response = requests.get(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
response = requests.get(url)
|
||||
assert_is_json(response.content)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
assert_is_json(response.content)
|
||||
|
||||
|
||||
@@ -143,5 +143,65 @@ def test_session_and_connection_close(tmpdir, scheme):
|
||||
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
|
||||
session = requests.session()
|
||||
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
|
||||
|
||||
def test_https_with_cert_validation_disabled(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
|
||||
requests.get('https://httpbin.org', verify=False)
|
||||
|
||||
|
||||
def test_session_can_make_requests_after_requests_unpatched(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
|
||||
session = requests.session()
|
||||
session.get('http://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('test_session_after_unpatched.yaml'))):
|
||||
session = requests.session()
|
||||
session.get('http://httpbin.org/get')
|
||||
|
||||
session.get('http://httpbin.org/status/200')
|
||||
|
||||
|
||||
def test_session_created_before_use_cassette_is_patched(tmpdir, scheme):
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
# Record arbitrary, random data to the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
|
||||
session = requests.session()
|
||||
body = session.get(url).content
|
||||
|
||||
# Create a session outside of any cassette context manager
|
||||
session = requests.session()
|
||||
# Make a request to make sure that a connectionpool is instantiated
|
||||
session.get(scheme + '://httpbin.org/get')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('session_created_outside.yaml'))):
|
||||
# These should only be the same if the patching succeeded.
|
||||
assert session.get(url).content == body
|
||||
|
||||
|
||||
def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
|
||||
'''
|
||||
This tests ensures that a session that was created while one cassette was
|
||||
active is patched to the use the responses of a second cassette when it
|
||||
is enabled.
|
||||
'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
|
||||
session = requests.session()
|
||||
first_body = session.get(url).content
|
||||
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
|
||||
second_body = session.get(url).content
|
||||
third_body = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('second_nested.yaml'))):
|
||||
session = requests.session()
|
||||
assert session.get(url).content == second_body
|
||||
with vcr.use_cassette(str(tmpdir.join('first_nested.yaml'))):
|
||||
assert session.get(url).content == first_body
|
||||
assert session.get(url).content == third_body
|
||||
|
||||
# Make sure that the session can now get content normally.
|
||||
session.get('http://www.reddit.com')
|
||||
|
||||
|
||||
42
tests/integration/test_stubs.py
Normal file
42
tests/integration/test_stubs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import vcr
|
||||
import six.moves.http_client as httplib
|
||||
|
||||
def _headers_are_case_insensitive():
|
||||
conn = httplib.HTTPConnection('httpbin.org')
|
||||
conn.request('GET', "/cookies/set?k1=v1")
|
||||
r1 = conn.getresponse()
|
||||
cookie_data1 = r1.getheader('set-cookie')
|
||||
conn = httplib.HTTPConnection('httpbin.org')
|
||||
conn.request('GET', "/cookies/set?k1=v1")
|
||||
r2 = conn.getresponse()
|
||||
cookie_data2 = r2.getheader('Set-Cookie')
|
||||
return cookie_data1 == cookie_data2
|
||||
|
||||
def test_case_insensitivity(tmpdir):
|
||||
testfile = str(tmpdir.join('case_insensitivity.yml'))
|
||||
# check if headers are case insensitive outside of vcrpy
|
||||
outside = _headers_are_case_insensitive()
|
||||
with vcr.use_cassette(testfile):
|
||||
# check if headers are case insensitive inside of vcrpy
|
||||
inside = _headers_are_case_insensitive()
|
||||
# check if headers are case insensitive after vcrpy deserializes headers
|
||||
inside2 = _headers_are_case_insensitive()
|
||||
|
||||
# behavior should be the same both inside and outside
|
||||
assert outside == inside == inside2
|
||||
|
||||
def _multiple_header_value(httpserver):
|
||||
conn = httplib.HTTPConnection('%s:%s' % httpserver.server_address)
|
||||
conn.request('GET', "/")
|
||||
r = conn.getresponse()
|
||||
return r.getheader('foo')
|
||||
|
||||
def test_multiple_headers(tmpdir, httpserver):
|
||||
testfile = str(tmpdir.join('multiple_headers.yaml'))
|
||||
httpserver.serve_content('Hello!', headers=[('foo', 'bar'), ('foo', 'baz')])
|
||||
outside = _multiple_header_value(httpserver)
|
||||
|
||||
with vcr.use_cassette(testfile):
|
||||
inside = _multiple_header_value(httpserver)
|
||||
|
||||
assert outside == inside
|
||||
@@ -1,7 +1,13 @@
|
||||
import copy
|
||||
|
||||
from six.moves import http_client as httplib
|
||||
import contextlib2
|
||||
import mock
|
||||
import pytest
|
||||
import yaml
|
||||
import mock
|
||||
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.patch import force_reset
|
||||
from vcr.errors import UnhandledHTTPRequestError
|
||||
|
||||
|
||||
@@ -68,6 +74,46 @@ def test_cassette_cant_read_same_request_twice():
|
||||
a.play_response('foo')
|
||||
|
||||
|
||||
def make_get_request():
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html")
|
||||
return conn.getresponse()
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', return_value=True)
|
||||
@mock.patch('vcr.cassette.load_cassette', lambda *args, **kwargs: (('foo',), (mock.MagicMock(),)))
|
||||
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
|
||||
@mock.patch('vcr.stubs.VCRHTTPResponse')
|
||||
def test_function_decorated_with_use_cassette_can_be_invoked_multiple_times(*args):
|
||||
decorated_function = Cassette.use('test')(make_get_request)
|
||||
for i in range(2):
|
||||
decorated_function()
|
||||
|
||||
|
||||
def test_arg_getter_functionality():
|
||||
arg_getter = mock.Mock(return_value=('test', {}))
|
||||
context_decorator = Cassette.use_arg_getter(arg_getter)
|
||||
|
||||
with context_decorator as cassette:
|
||||
assert cassette._path == 'test'
|
||||
|
||||
arg_getter.return_value = ('other', {})
|
||||
|
||||
with context_decorator as cassette:
|
||||
assert cassette._path == 'other'
|
||||
|
||||
arg_getter.return_value = ('', {'filter_headers': ('header_name',)})
|
||||
|
||||
@context_decorator
|
||||
def function():
|
||||
pass
|
||||
|
||||
with mock.patch.object(Cassette, 'load') as cassette_load:
|
||||
function()
|
||||
cassette_load.assert_called_once_with(arg_getter.return_value[0],
|
||||
**arg_getter.return_value[1])
|
||||
|
||||
|
||||
def test_cassette_not_all_played():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
@@ -80,3 +126,58 @@ def test_cassette_all_played():
|
||||
a.append('foo', 'bar')
|
||||
a.play_response('foo')
|
||||
assert a.all_played
|
||||
|
||||
|
||||
def test_before_record_response():
|
||||
before_record_response = mock.Mock(return_value='mutated')
|
||||
cassette = Cassette('test', before_record_response=before_record_response)
|
||||
cassette.append('req', 'res')
|
||||
|
||||
before_record_response.assert_called_once_with('res')
|
||||
assert cassette.responses[0] == 'mutated'
|
||||
|
||||
|
||||
def assert_get_response_body_is(value):
|
||||
conn = httplib.HTTPConnection("www.python.org")
|
||||
conn.request("GET", "/index.html")
|
||||
assert conn.getresponse().read().decode('utf8') == value
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
@mock.patch('vcr.cassette.Cassette.can_play_response_for', return_value=True)
|
||||
@mock.patch('vcr.cassette.Cassette._save', return_value=True)
|
||||
def test_nesting_cassette_context_managers(*args):
|
||||
first_response = {'body': {'string': b'first_response'}, 'headers': {},
|
||||
'status': {'message': 'm', 'code': 200}}
|
||||
|
||||
second_response = copy.deepcopy(first_response)
|
||||
second_response['body']['string'] = b'second_response'
|
||||
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
first_cassette = exit_stack.enter_context(Cassette.use('test'))
|
||||
exit_stack.enter_context(mock.patch.object(first_cassette, 'play_response',
|
||||
return_value=first_response))
|
||||
assert_get_response_body_is('first_response')
|
||||
|
||||
# Make sure a second cassette can supercede the first
|
||||
with Cassette.use('test') as second_cassette:
|
||||
with mock.patch.object(second_cassette, 'play_response', return_value=second_response):
|
||||
assert_get_response_body_is('second_response')
|
||||
|
||||
# Now the first cassette should be back in effect
|
||||
assert_get_response_body_is('first_response')
|
||||
|
||||
|
||||
def test_nesting_context_managers_by_checking_references_of_http_connection():
|
||||
original = httplib.HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
first_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
second_cassette_HTTPConnection = httplib.HTTPConnection
|
||||
assert second_cassette_HTTPConnection is not first_cassette_HTTPConnection
|
||||
with Cassette.use('test'):
|
||||
assert httplib.HTTPConnection is not second_cassette_HTTPConnection
|
||||
with force_reset():
|
||||
assert httplib.HTTPConnection is original
|
||||
assert httplib.HTTPConnection is second_cassette_HTTPConnection
|
||||
assert httplib.HTTPConnection is first_cassette_HTTPConnection
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
from vcr.filters import _remove_headers, _remove_query_parameters
|
||||
from vcr.filters import remove_headers, remove_query_parameters
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_remove_headers():
|
||||
headers = {'hello': ['goodbye'], 'secret': ['header']}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
_remove_headers(request, ['secret'])
|
||||
remove_headers(request, ['secret'])
|
||||
assert request.headers == {'hello': 'goodbye'}
|
||||
|
||||
|
||||
def test_remove_headers_empty():
|
||||
headers = {'hello': 'goodbye', 'secret': 'header'}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
_remove_headers(request, [])
|
||||
remove_headers(request, [])
|
||||
assert request.headers == headers
|
||||
|
||||
|
||||
def test_remove_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w'])
|
||||
remove_query_parameters(request, ['w'])
|
||||
assert request.uri == 'http://g.com/?q=cowboys'
|
||||
|
||||
|
||||
def test_remove_all_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w', 'q'])
|
||||
remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
|
||||
def test_remove_nonexistent_query_parameters():
|
||||
uri = 'http://g.com/'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w', 'q'])
|
||||
remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from vcr.serialize import deserialize
|
||||
from vcr.serializers import yamlserializer, jsonserializer
|
||||
|
||||
|
||||
def test_deserialize_old_yaml_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_old_json_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.json', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_yaml_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
|
||||
def test_deserialize_new_json_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
|
||||
@mock.patch.object(jsonserializer.json, 'dumps',
|
||||
side_effect=UnicodeDecodeError('utf-8', b'unicode error in serialization',
|
||||
0, 10, 'blew up'))
|
||||
def test_serialize_constructs_UnicodeDecodeError(mock_dumps):
|
||||
with pytest.raises(UnicodeDecodeError):
|
||||
jsonserializer.serialize({})
|
||||
|
||||
76
tests/unit/test_vcr.py
Normal file
76
tests/unit/test_vcr.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from vcr import VCR, use_cassette
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_vcr_use_cassette():
|
||||
record_mode = mock.Mock()
|
||||
test_vcr = VCR(record_mode=record_mode)
|
||||
with mock.patch('vcr.cassette.Cassette.load') as mock_cassette_load:
|
||||
@test_vcr.use_cassette('test')
|
||||
def function():
|
||||
pass
|
||||
assert mock_cassette_load.call_count == 0
|
||||
function()
|
||||
assert mock_cassette_load.call_args[1]['record_mode'] is record_mode
|
||||
|
||||
# Make sure that calls to function now use cassettes with the
|
||||
# new filter_header_settings
|
||||
test_vcr.record_mode = mock.Mock()
|
||||
function()
|
||||
assert mock_cassette_load.call_args[1]['record_mode'] == test_vcr.record_mode
|
||||
|
||||
# Ensure that explicitly provided arguments still supercede
|
||||
# those on the vcr.
|
||||
new_record_mode = mock.Mock()
|
||||
|
||||
with test_vcr.use_cassette('test', record_mode=new_record_mode) as cassette:
|
||||
assert cassette.record_mode == new_record_mode
|
||||
|
||||
|
||||
def test_vcr_before_record_request_params():
|
||||
base_path = 'http://httpbin.org/'
|
||||
def before_record_cb(request):
|
||||
if request.path != '/get':
|
||||
return request
|
||||
test_vcr = VCR(filter_headers=('cookie',), before_record_request=before_record_cb,
|
||||
ignore_hosts=('www.test.com',), ignore_localhost=True,
|
||||
filter_query_parameters=('foo',))
|
||||
|
||||
with test_vcr.use_cassette('test') as cassette:
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is None
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get2', '', {})) is not None
|
||||
|
||||
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '', {})).query == []
|
||||
assert cassette.filter_request(
|
||||
Request('GET', base_path + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
|
||||
assert cassette.filter_request(Request('GET', base_path + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})).headers == {'other': 'fun'}
|
||||
|
||||
assert cassette.filter_request(Request('GET', 'http://www.test.com' + '?foo=bar', '',
|
||||
{'cookie': 'test', 'other': 'fun'})) is None
|
||||
|
||||
with test_vcr.use_cassette('test', before_record_request=None) as cassette:
|
||||
# Test that before_record can be overwritten with
|
||||
assert cassette.filter_request(Request('GET', base_path + 'get', '', {})) is not None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def random_fixture():
|
||||
return 1
|
||||
|
||||
|
||||
@use_cassette('test')
|
||||
def test_fixtures_with_use_cassette(random_fixture):
|
||||
# Applying a decorator to a test function that requests features can cause
|
||||
# problems if the decorator does not preserve the signature of the original
|
||||
# test function.
|
||||
|
||||
# This test ensures that use_cassette preserves the signature of the original
|
||||
# test function, and thus that use_cassette is compatible with py.test
|
||||
# fixtures. It is admittedly a bit strange because the test would never even
|
||||
# run if the relevant feature were broken.
|
||||
pass
|
||||
174
tox.ini
174
tox.ini
@@ -1,170 +1,24 @@
|
||||
# Tox (http://tox.testrun.org/) is a tool for running tests
|
||||
# in multiple virtualenvs. This configuration file will run the
|
||||
# test suite on all supported python versions. To use it, "pip install tox"
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist =
|
||||
py26,
|
||||
py27,
|
||||
py33,
|
||||
py34,
|
||||
pypy,
|
||||
py26requests,
|
||||
py27requests,
|
||||
py34requests,
|
||||
pypyrequests,
|
||||
py26oldrequests,
|
||||
py27oldrequests,
|
||||
py33oldrequests,
|
||||
py34oldrequests,
|
||||
pypyoldrequests,
|
||||
py26httplib2,
|
||||
py27httplib2,
|
||||
py33httplib2,
|
||||
py34httplib2,
|
||||
pypyhttplib2,
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
py.test {posargs}
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
pypy: pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
|
||||
[testenv:py26oldrequests]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py27oldrequests]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py33oldrequests]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py34oldrequests]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:pypyoldrequests]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py26requests]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py27requests]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py33requests]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py34requests]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:pypyrequests]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
|
||||
[testenv:py26httplib2]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py27httplib2]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py33httplib2]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py34httplib2]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:pypyhttplib2]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
requests1: requests==1.2.3
|
||||
requests24: requests==2.4.0
|
||||
requests23: requests==2.3.0
|
||||
requests22: requests==2.2.1
|
||||
httplib2: httplib2
|
||||
urllib3: urllib3==1.7.1
|
||||
boto: boto
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
from .config import VCR
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
@@ -10,10 +9,9 @@ except ImportError:
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
|
||||
default_vcr = VCR()
|
||||
|
||||
|
||||
def use_cassette(path, **kwargs):
|
||||
return default_vcr.use_cassette(path, **kwargs)
|
||||
use_cassette = default_vcr.use_cassette
|
||||
|
||||
148
vcr/cassette.py
148
vcr/cassette.py
@@ -1,53 +1,98 @@
|
||||
'''The container for recorded requests and responses'''
|
||||
"""The container for recorded requests and responses"""
|
||||
import logging
|
||||
|
||||
import contextlib2
|
||||
import wrapt
|
||||
try:
|
||||
from collections import Counter
|
||||
except ImportError:
|
||||
from .compat.counter import Counter
|
||||
|
||||
from contextdecorator import ContextDecorator
|
||||
|
||||
# Internal imports
|
||||
from .patch import install, reset
|
||||
from .patch import CassettePatcherBuilder
|
||||
from .persist import load_cassette, save_cassette
|
||||
from .filters import filter_request
|
||||
from .serializers import yamlserializer
|
||||
from .matchers import requests_match, uri, method
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
|
||||
|
||||
class Cassette(ContextDecorator):
|
||||
'''A container for recorded requests and responses'''
|
||||
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.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, cassette_class, path, **kwargs):
|
||||
return cls(cassette_class, lambda: (path, kwargs))
|
||||
|
||||
def __init__(self, cls, args_getter):
|
||||
self.cls = cls
|
||||
self._args_getter = args_getter
|
||||
self.__finish = None
|
||||
|
||||
def _patch_generator(self, cassette):
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
for patcher in CassettePatcherBuilder(cassette).build():
|
||||
exit_stack.enter_context(patcher)
|
||||
log.debug('Entered context for cassette at {0}.'.format(cassette._path))
|
||||
yield cassette
|
||||
log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
|
||||
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be
|
||||
# somewhere else.
|
||||
cassette._save()
|
||||
|
||||
def __enter__(self):
|
||||
assert self.__finish is None, "Cassette already open."
|
||||
path, kwargs = self._args_getter()
|
||||
self.__finish = self._patch_generator(self.cls.load(path, **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):
|
||||
with self:
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
class Cassette(object):
|
||||
"""A container for recorded requests and responses"""
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, **kwargs):
|
||||
'''Load in the cassette stored at the provided path'''
|
||||
"""Instantiate and load the cassette stored at the specified path."""
|
||||
new_cassette = cls(path, **kwargs)
|
||||
new_cassette._load()
|
||||
return new_cassette
|
||||
|
||||
def __init__(self,
|
||||
path,
|
||||
serializer=yamlserializer,
|
||||
record_mode='once',
|
||||
match_on=[uri, method],
|
||||
filter_headers=[],
|
||||
filter_query_parameters=[],
|
||||
before_record=None,
|
||||
ignore_hosts=[],
|
||||
ignore_localhost=[],
|
||||
):
|
||||
@classmethod
|
||||
def use_arg_getter(cls, arg_getter):
|
||||
return CassetteContextDecorator(cls, arg_getter)
|
||||
|
||||
@classmethod
|
||||
def use(cls, *args, **kwargs):
|
||||
return CassetteContextDecorator.from_args(cls, *args, **kwargs)
|
||||
|
||||
def __init__(self, path, serializer=yamlserializer, record_mode='once',
|
||||
match_on=(uri, method), filter_headers=(),
|
||||
filter_query_parameters=(), before_record_request=None,
|
||||
before_record_response=None, ignore_hosts=(),
|
||||
ignore_localhost=()):
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self._match_on = match_on
|
||||
self._filter_headers = filter_headers
|
||||
self._filter_query_parameters = filter_query_parameters
|
||||
self._before_record = before_record
|
||||
self._ignore_hosts = ignore_hosts
|
||||
if ignore_localhost:
|
||||
self._ignore_hosts = list(set(
|
||||
self._ignore_hosts + ['localhost', '0.0.0.0', '127.0.0.1']
|
||||
))
|
||||
self._before_record_request = before_record_request or (lambda x: x)
|
||||
self._before_record_response = before_record_response or (lambda x: x)
|
||||
|
||||
# self.data is the list of (req, resp) tuples
|
||||
self.data = []
|
||||
@@ -62,9 +107,7 @@ class Cassette(ContextDecorator):
|
||||
|
||||
@property
|
||||
def all_played(self):
|
||||
"""
|
||||
Returns True if all responses have been played, False otherwise.
|
||||
"""
|
||||
"""Returns True if all responses have been played, False otherwise."""
|
||||
return self.play_count == len(self)
|
||||
|
||||
@property
|
||||
@@ -80,46 +123,40 @@ class Cassette(ContextDecorator):
|
||||
return self.rewound and self.record_mode == 'once' or \
|
||||
self.record_mode == 'none'
|
||||
|
||||
def _filter_request(self, request):
|
||||
return filter_request(
|
||||
request=request,
|
||||
filter_headers=self._filter_headers,
|
||||
filter_query_parameters=self._filter_query_parameters,
|
||||
before_record=self._before_record,
|
||||
ignore_hosts=self._ignore_hosts
|
||||
)
|
||||
|
||||
def append(self, request, response):
|
||||
'''Add a request, response pair to this cassette'''
|
||||
request = self._filter_request(request)
|
||||
"""Add a request, response pair to this cassette"""
|
||||
request = self._before_record_request(request)
|
||||
if not request:
|
||||
return
|
||||
if self._before_record_response:
|
||||
response = self._before_record_response(response)
|
||||
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._filter_request(request)
|
||||
if not request:
|
||||
return
|
||||
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._filter_request(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
|
||||
@@ -131,11 +168,11 @@ class Cassette(ContextDecorator):
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -177,20 +214,11 @@ class Cassette(ContextDecorator):
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
'''Return the number of request,response pairs stored in here'''
|
||||
"""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'''
|
||||
"""Return whether or not a request has been stored"""
|
||||
for response in self._responses(request):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __enter__(self):
|
||||
'''Patch the fetching libraries we know about'''
|
||||
install(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, typ, value, traceback):
|
||||
self._save()
|
||||
reset()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import print_function
|
||||
from operator import itemgetter
|
||||
from heapq import nlargest
|
||||
from itertools import repeat, ifilter
|
||||
@@ -25,7 +26,7 @@ class Counter(dict):
|
||||
>>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping
|
||||
>>> c = Counter(a=4, b=2) # a new counter from keyword args
|
||||
|
||||
'''
|
||||
'''
|
||||
self.update(iterable, **kwds)
|
||||
|
||||
def __missing__(self, key):
|
||||
@@ -38,7 +39,7 @@ class Counter(dict):
|
||||
>>> Counter('abracadabra').most_common(3)
|
||||
[('a', 5), ('r', 2), ('b', 2)]
|
||||
|
||||
'''
|
||||
'''
|
||||
if n is None:
|
||||
return sorted(self.iteritems(), key=itemgetter(1), reverse=True)
|
||||
return nlargest(n, self.iteritems(), key=itemgetter(1))
|
||||
@@ -77,7 +78,7 @@ class Counter(dict):
|
||||
>>> c['h'] # four 'h' in which, witch, and watch
|
||||
4
|
||||
|
||||
'''
|
||||
'''
|
||||
if iterable is not None:
|
||||
if hasattr(iterable, 'iteritems'):
|
||||
if self:
|
||||
@@ -189,5 +190,5 @@ class Counter(dict):
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
print doctest.testmod()
|
||||
print(doctest.testmod())
|
||||
|
||||
|
||||
142
vcr/config.py
142
vcr/config.py
@@ -1,28 +1,22 @@
|
||||
import collections
|
||||
import copy
|
||||
import functools
|
||||
import os
|
||||
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from . import matchers
|
||||
from . import filters
|
||||
|
||||
|
||||
class VCR(object):
|
||||
def __init__(self,
|
||||
serializer='yaml',
|
||||
cassette_library_dir=None,
|
||||
record_mode="once",
|
||||
filter_headers=[],
|
||||
filter_query_parameters=[],
|
||||
before_record=None,
|
||||
match_on=[
|
||||
'method',
|
||||
'scheme',
|
||||
'host',
|
||||
'port',
|
||||
'path',
|
||||
'query',
|
||||
],
|
||||
ignore_hosts=[],
|
||||
ignore_localhost=False,
|
||||
):
|
||||
|
||||
def __init__(self, serializer='yaml', cassette_library_dir=None,
|
||||
record_mode="once", filter_headers=(),
|
||||
filter_query_parameters=(), before_record_request=None,
|
||||
before_record_response=None, ignore_hosts=(),
|
||||
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
|
||||
ignore_localhost=False, before_record=None):
|
||||
self.serializer = serializer
|
||||
self.match_on = match_on
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
@@ -45,7 +39,8 @@ class VCR(object):
|
||||
self.record_mode = record_mode
|
||||
self.filter_headers = filter_headers
|
||||
self.filter_query_parameters = filter_query_parameters
|
||||
self.before_record = before_record
|
||||
self.before_record_request = before_record_request or before_record
|
||||
self.before_record_response = before_record_response
|
||||
self.ignore_hosts = ignore_hosts
|
||||
self.ignore_localhost = ignore_localhost
|
||||
|
||||
@@ -66,44 +61,107 @@ class VCR(object):
|
||||
matchers.append(self.matchers[m])
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Matcher {0} doesn't exist or isn't registered".format(
|
||||
m)
|
||||
"Matcher {0} doesn't exist or isn't registered".format(m)
|
||||
)
|
||||
return matchers
|
||||
|
||||
def use_cassette(self, path, **kwargs):
|
||||
def use_cassette(self, path, with_current_defaults=False, **kwargs):
|
||||
if with_current_defaults:
|
||||
return Cassette.use(path, self.get_path_and_merged_config(path, **kwargs))
|
||||
args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
|
||||
return Cassette.use_arg_getter(args_getter)
|
||||
|
||||
def get_path_and_merged_config(self, path, **kwargs):
|
||||
serializer_name = kwargs.get('serializer', self.serializer)
|
||||
matcher_names = kwargs.get('match_on', self.match_on)
|
||||
cassette_library_dir = kwargs.get(
|
||||
'cassette_library_dir',
|
||||
self.cassette_library_dir
|
||||
)
|
||||
|
||||
if cassette_library_dir:
|
||||
path = os.path.join(cassette_library_dir, path)
|
||||
|
||||
merged_config = {
|
||||
"serializer": self._get_serializer(serializer_name),
|
||||
"match_on": self._get_matchers(matcher_names),
|
||||
"record_mode": kwargs.get('record_mode', self.record_mode),
|
||||
"filter_headers": kwargs.get(
|
||||
'filter_headers', self.filter_headers
|
||||
),
|
||||
"filter_query_parameters": kwargs.get(
|
||||
'filter_query_parameters', self.filter_query_parameters
|
||||
),
|
||||
"before_record": kwargs.get(
|
||||
"before_record", self.before_record
|
||||
),
|
||||
"ignore_hosts": kwargs.get(
|
||||
'ignore_hosts', self.ignore_hosts
|
||||
),
|
||||
"ignore_localhost": kwargs.get(
|
||||
'ignore_localhost', self.ignore_localhost
|
||||
),
|
||||
'serializer': self._get_serializer(serializer_name),
|
||||
'match_on': self._get_matchers(matcher_names),
|
||||
'record_mode': kwargs.get('record_mode', self.record_mode),
|
||||
'before_record_request': self._build_before_record_request(kwargs),
|
||||
'before_record_response': self._build_before_record_response(kwargs)
|
||||
}
|
||||
return path, merged_config
|
||||
|
||||
return Cassette.load(path, **merged_config)
|
||||
def _build_before_record_response(self, options):
|
||||
before_record_response = options.get(
|
||||
'before_record_response', self.before_record_response
|
||||
)
|
||||
filter_functions = []
|
||||
if before_record_response and not isinstance(before_record_response,
|
||||
collections.Iterable):
|
||||
before_record_response = (before_record_response,)
|
||||
for function in before_record_response:
|
||||
filter_functions.append(function)
|
||||
def before_record_response(response):
|
||||
for function in filter_functions:
|
||||
if response is None:
|
||||
break
|
||||
response = function(response)
|
||||
return response
|
||||
return before_record_response
|
||||
|
||||
def _build_before_record_request(self, options):
|
||||
filter_functions = []
|
||||
filter_headers = options.get(
|
||||
'filter_headers', self.filter_headers
|
||||
)
|
||||
filter_query_parameters = options.get(
|
||||
'filter_query_parameters', self.filter_query_parameters
|
||||
)
|
||||
before_record_request = options.get(
|
||||
"before_record_request", options.get("before_record", self.before_record_request)
|
||||
)
|
||||
ignore_hosts = options.get(
|
||||
'ignore_hosts', self.ignore_hosts
|
||||
)
|
||||
ignore_localhost = options.get(
|
||||
'ignore_localhost', self.ignore_localhost
|
||||
)
|
||||
if filter_headers:
|
||||
filter_functions.append(functools.partial(filters.remove_headers,
|
||||
headers_to_remove=filter_headers))
|
||||
if filter_query_parameters:
|
||||
filter_functions.append(functools.partial(filters.remove_query_parameters,
|
||||
query_parameters_to_remove=filter_query_parameters))
|
||||
|
||||
hosts_to_ignore = list(ignore_hosts)
|
||||
if ignore_localhost:
|
||||
hosts_to_ignore.extend(('localhost', '0.0.0.0', '127.0.0.1'))
|
||||
|
||||
if hosts_to_ignore:
|
||||
hosts_to_ignore = set(hosts_to_ignore)
|
||||
filter_functions.append(self._build_ignore_hosts(hosts_to_ignore))
|
||||
|
||||
if before_record_request:
|
||||
if not isinstance(before_record_request, collections.Iterable):
|
||||
before_record_request = (before_record_request,)
|
||||
for function in before_record_request:
|
||||
filter_functions.append(function)
|
||||
def before_record_request(request):
|
||||
request = copy.copy(request)
|
||||
for function in filter_functions:
|
||||
if request is None:
|
||||
break
|
||||
request = function(request)
|
||||
return request
|
||||
|
||||
return before_record_request
|
||||
|
||||
@staticmethod
|
||||
def _build_ignore_hosts(hosts_to_ignore):
|
||||
def filter_ignored_hosts(request):
|
||||
if hasattr(request, 'host') and request.host in hosts_to_ignore:
|
||||
return
|
||||
return request
|
||||
return filter_ignored_hosts
|
||||
|
||||
def register_serializer(self, name, serializer):
|
||||
self.serializers[name] = serializer
|
||||
|
||||
@@ -2,7 +2,7 @@ from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
import copy
|
||||
|
||||
|
||||
def _remove_headers(request, headers_to_remove):
|
||||
def remove_headers(request, headers_to_remove):
|
||||
headers = copy.copy(request.headers)
|
||||
headers_to_remove = [h.lower() for h in headers_to_remove]
|
||||
keys = [k for k in headers if k.lower() in headers_to_remove]
|
||||
@@ -13,7 +13,7 @@ def _remove_headers(request, headers_to_remove):
|
||||
return request
|
||||
|
||||
|
||||
def _remove_query_parameters(request, query_parameters_to_remove):
|
||||
def remove_query_parameters(request, query_parameters_to_remove):
|
||||
query = request.query
|
||||
new_query = [(k, v) for (k, v) in query
|
||||
if k not in query_parameters_to_remove]
|
||||
@@ -22,22 +22,3 @@ def _remove_query_parameters(request, query_parameters_to_remove):
|
||||
uri_parts[4] = urlencode(new_query)
|
||||
request.uri = urlunparse(uri_parts)
|
||||
return request
|
||||
|
||||
|
||||
def filter_request(
|
||||
request,
|
||||
filter_headers,
|
||||
filter_query_parameters,
|
||||
before_record,
|
||||
ignore_hosts
|
||||
):
|
||||
request = copy.copy(request) # don't mutate request object
|
||||
if hasattr(request, 'headers') and filter_headers:
|
||||
request = _remove_headers(request, filter_headers)
|
||||
if hasattr(request, 'host') and request.host in ignore_hosts:
|
||||
return None
|
||||
if filter_query_parameters:
|
||||
request = _remove_query_parameters(request, filter_query_parameters)
|
||||
if before_record:
|
||||
request = before_record(request)
|
||||
return request
|
||||
|
||||
@@ -38,16 +38,16 @@ def headers(r1, r2):
|
||||
return r1.headers == r2.headers
|
||||
|
||||
|
||||
def _log_matches(matches):
|
||||
def _log_matches(r1, r2, matches):
|
||||
differences = [m for m in matches if not m[0]]
|
||||
if differences:
|
||||
log.debug(
|
||||
'Requests differ according to the following matchers: ' +
|
||||
str(differences)
|
||||
"Requests {0} and {1} differ according to "
|
||||
"the following matchers: {2}".format(r1, r2, differences)
|
||||
)
|
||||
|
||||
|
||||
def requests_match(r1, r2, matchers):
|
||||
matches = [(m(r1, r2), m) for m in matchers]
|
||||
_log_matches(matches)
|
||||
_log_matches(r1, r2, matches)
|
||||
return all([m[0] for m in matches])
|
||||
|
||||
340
vcr/patch.py
340
vcr/patch.py
@@ -1,4 +1,9 @@
|
||||
'''Utilities for patching in cassettes'''
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
import contextlib2
|
||||
import mock
|
||||
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
from six.moves import http_client as httplib
|
||||
@@ -8,138 +13,293 @@ from six.moves import http_client as httplib
|
||||
_HTTPConnection = httplib.HTTPConnection
|
||||
_HTTPSConnection = httplib.HTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for requests
|
||||
try:
|
||||
# Try to save the original types for requests
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
# Try to save the original types for urllib3
|
||||
try:
|
||||
# Try to save the original types for urllib3
|
||||
import urllib3
|
||||
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_VerifiedHTTPSConnection = urllib3.connectionpool.VerifiedHTTPSConnection
|
||||
|
||||
|
||||
# Try to save the original types for httplib2
|
||||
try:
|
||||
# Try to save the original types for httplib2
|
||||
import httplib2
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
|
||||
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
||||
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
# Try to save the original types for boto
|
||||
try:
|
||||
# Try to save the original types for boto
|
||||
import boto.https_connection
|
||||
_CertValidatingHTTPSConnection = \
|
||||
boto.https_connection.CertValidatingHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
_CertValidatingHTTPSConnection = boto.https_connection.CertValidatingHTTPSConnection
|
||||
|
||||
|
||||
def install(cassette):
|
||||
"""
|
||||
Patch all the HTTPConnections references we can find!
|
||||
This replaces the actual HTTPConnection with a VCRHTTPConnection
|
||||
object which knows how to save to / read from cassettes
|
||||
"""
|
||||
httplib.HTTPConnection = VCRHTTPConnection
|
||||
httplib.HTTPSConnection = VCRHTTPSConnection
|
||||
httplib.HTTPConnection.cassette = cassette
|
||||
httplib.HTTPSConnection.cassette = cassette
|
||||
class CassettePatcherBuilder(object):
|
||||
|
||||
# patch requests v1.x
|
||||
def _build_patchers_from_mock_triples_decorator(function):
|
||||
@functools.wraps(function)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
return self._build_patchers_from_mock_triples(
|
||||
function(self, *args, **kwargs)
|
||||
)
|
||||
return wrapped
|
||||
|
||||
def __init__(self, cassette):
|
||||
self._cassette = cassette
|
||||
self._class_to_cassette_subclass = {}
|
||||
|
||||
def build(self):
|
||||
return itertools.chain(self._httplib(), self._requests(),
|
||||
self._urllib3(), self._httplib2(),
|
||||
self._boto())
|
||||
|
||||
def _build_patchers_from_mock_triples(self, mock_triples):
|
||||
for args in mock_triples:
|
||||
patcher = self._build_patcher(*args)
|
||||
if patcher:
|
||||
yield patcher
|
||||
|
||||
def _build_patcher(self, obj, patched_attribute, replacement_class):
|
||||
if not hasattr(obj, patched_attribute):
|
||||
return
|
||||
|
||||
return mock.patch.object(obj, patched_attribute,
|
||||
self._recursively_apply_get_cassette_subclass(
|
||||
replacement_class))
|
||||
|
||||
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
||||
if isinstance(replacement_dict_or_obj, dict):
|
||||
for key, replacement_obj in replacement_dict_or_obj.items():
|
||||
replacement_obj = self._recursively_apply_get_cassette_subclass(
|
||||
replacement_obj)
|
||||
replacement_dict_or_obj[key] = replacement_obj
|
||||
return replacement_dict_or_obj
|
||||
if hasattr(replacement_dict_or_obj, 'cassette'):
|
||||
replacement_dict_or_obj = self._get_cassette_subclass(
|
||||
replacement_dict_or_obj)
|
||||
return replacement_dict_or_obj
|
||||
|
||||
def _get_cassette_subclass(self, klass):
|
||||
if klass.cassette is not None:
|
||||
return klass
|
||||
if klass not in self._class_to_cassette_subclass:
|
||||
subclass = self._build_cassette_subclass(klass)
|
||||
self._class_to_cassette_subclass[klass] = subclass
|
||||
return self._class_to_cassette_subclass[klass]
|
||||
|
||||
def _build_cassette_subclass(self, base_class):
|
||||
bases = (base_class,)
|
||||
if not issubclass(base_class, object): # Check for old style class
|
||||
bases += (object,)
|
||||
return type('{0}{1}'.format(base_class.__name__, self._cassette._path),
|
||||
bases, dict(cassette=self._cassette))
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib(self):
|
||||
yield httplib, 'HTTPConnection', VCRHTTPConnection
|
||||
yield httplib, 'HTTPSConnection', VCRHTTPSConnection
|
||||
|
||||
def _requests(self):
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
return ()
|
||||
from .stubs.requests_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(VCRRequestsHTTPConnection)
|
||||
)
|
||||
https_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(VCRRequestsHTTPSConnection)
|
||||
)
|
||||
mock_triples = (
|
||||
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
|
||||
(cpool, 'VerifiedHTTPSConnection', VCRRequestsHTTPSConnection),
|
||||
(cpool, 'HTTPConnection', VCRRequestsHTTPConnection),
|
||||
(cpool, 'HTTPSConnection', VCRRequestsHTTPSConnection),
|
||||
(cpool.HTTPConnectionPool, 'ConnectionCls', VCRRequestsHTTPConnection),
|
||||
(cpool.HTTPSConnectionPool, 'ConnectionCls', VCRRequestsHTTPSConnection),
|
||||
)
|
||||
# These handle making sure that sessions only use the
|
||||
# connections of the appropriate type.
|
||||
mock_triples += ((cpool.HTTPConnectionPool, '_get_conn',
|
||||
self._patched_get_conn(cpool.HTTPConnectionPool,
|
||||
lambda : cpool.HTTPConnection)),
|
||||
(cpool.HTTPSConnectionPool, '_get_conn',
|
||||
self._patched_get_conn(cpool.HTTPSConnectionPool,
|
||||
lambda : cpool.HTTPSConnection)),
|
||||
(cpool.HTTPConnectionPool, '_new_conn',
|
||||
self._patched_new_conn(cpool.HTTPConnectionPool,
|
||||
http_connection_remover)),
|
||||
(cpool.HTTPSConnectionPool, '_new_conn',
|
||||
self._patched_new_conn(cpool.HTTPSConnectionPool,
|
||||
https_connection_remover)))
|
||||
|
||||
return itertools.chain(self._build_patchers_from_mock_triples(mock_triples),
|
||||
(http_connection_remover, https_connection_remover))
|
||||
|
||||
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
|
||||
get_conn = connection_pool_class._get_conn
|
||||
@functools.wraps(get_conn)
|
||||
def patched_get_conn(pool, timeout=None):
|
||||
connection = get_conn(pool, timeout)
|
||||
connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \
|
||||
else connection_class_getter()
|
||||
while not isinstance(connection, connection_class):
|
||||
connection = get_conn(pool, timeout)
|
||||
return connection
|
||||
return patched_get_conn
|
||||
|
||||
def _patched_new_conn(self, connection_pool_class, connection_remover):
|
||||
new_conn = connection_pool_class._new_conn
|
||||
@functools.wraps(new_conn)
|
||||
def patched_new_conn(pool):
|
||||
new_connection = new_conn(pool)
|
||||
connection_remover.add_connection_to_pool_entry(pool, new_connection)
|
||||
return new_connection
|
||||
return patched_new_conn
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _urllib3(self):
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
|
||||
|
||||
yield cpool, 'VerifiedHTTPSConnection', VCRVerifiedHTTPSConnection
|
||||
yield cpool, 'HTTPConnection', VCRHTTPConnection
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib2(self):
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
|
||||
yield cpool, 'HTTPConnectionWithTimeout', VCRHTTPConnectionWithTimeout
|
||||
yield cpool, 'HTTPSConnectionWithTimeout', VCRHTTPSConnectionWithTimeout
|
||||
yield cpool, 'SCHEME_TO_CONNECTION', {'http': VCRHTTPConnectionWithTimeout,
|
||||
'https': VCRHTTPSConnectionWithTimeout}
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _boto(self):
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
|
||||
|
||||
|
||||
class ConnectionRemover(object):
|
||||
|
||||
def __init__(self, connection_class):
|
||||
self._connection_class = connection_class
|
||||
self._connection_pool_to_connections = {}
|
||||
|
||||
def add_connection_to_pool_entry(self, pool, connection):
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections.setdefault(pool, set()).add(connection)
|
||||
|
||||
def remove_connection_to_pool_entry(self, pool, connection):
|
||||
if isinstance(connection, self._connection_class):
|
||||
self._connection_pool_to_connections[self._connection_class].remove(connection)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
for pool, connections in self._connection_pool_to_connections.items():
|
||||
readd_connections = []
|
||||
while not pool.pool.empty() and connections:
|
||||
connection = pool.pool.get()
|
||||
if isinstance(connection, self._connection_class):
|
||||
connections.remove(connection)
|
||||
else:
|
||||
readd_connections.append(connection)
|
||||
for connection in readd_connections:
|
||||
pool._put_conn(connection)
|
||||
|
||||
|
||||
def reset_patchers():
|
||||
yield mock.patch.object(httplib, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(httplib, 'HTTPSConnection', _HTTPSConnection)
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection.cassette = cassette
|
||||
cpool.HTTPConnection = VCRHTTPConnection
|
||||
cpool.HTTPConnection.cassette = cassette
|
||||
# patch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = VCRHTTPConnection
|
||||
cpool.HTTPConnectionPool.cassette = cassette
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = VCRHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch urllib3
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection = VCRVerifiedHTTPSConnection
|
||||
cpool.VerifiedHTTPSConnection.cassette = cassette
|
||||
cpool.HTTPConnection = VCRHTTPConnection
|
||||
cpool.HTTPConnection.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch httplib2
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
cpool.HTTPConnectionWithTimeout = VCRHTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = VCRHTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = {
|
||||
'http': VCRHTTPConnectionWithTimeout,
|
||||
'https': VCRHTTPSConnectionWithTimeout
|
||||
}
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch boto
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
cpool.CertValidatingHTTPSConnection = VCRCertValidatingHTTPSConnection
|
||||
cpool.CertValidatingHTTPSConnection.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def reset():
|
||||
'''Undo all the patching'''
|
||||
httplib.HTTPConnection = _HTTPConnection
|
||||
httplib.HTTPSConnection = _HTTPSConnection
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
else:
|
||||
# unpatch requests v1.x
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _cpoolHTTPConnection
|
||||
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _cpoolHTTPConnection)
|
||||
# unpatch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
|
||||
cpool.HTTPSConnection = _cpoolHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls',
|
||||
_cpoolHTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls',
|
||||
_cpoolHTTPSConnection)
|
||||
|
||||
if hasattr(cpool, 'HTTPSConnection'):
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _cpoolHTTPSConnection)
|
||||
|
||||
try:
|
||||
import urllib3.connectionpool as cpool
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _HTTPConnection
|
||||
cpool.HTTPSConnection = _HTTPSConnection
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
|
||||
if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
|
||||
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
|
||||
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
|
||||
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
cpool.HTTPConnectionWithTimeout = _HTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = _HTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = _SCHEME_TO_CONNECTION
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'HTTPConnectionWithTimeout', _HTTPConnectionWithTimeout)
|
||||
yield mock.patch.object(cpool, 'HTTPSConnectionWithTimeout', _HTTPSConnectionWithTimeout)
|
||||
yield mock.patch.object(cpool, 'SCHEME_TO_CONNECTION', _SCHEME_TO_CONNECTION)
|
||||
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
cpool.CertValidatingHTTPSConnection = _CertValidatingHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
yield mock.patch.object(cpool, 'CertValidatingHTTPSConnection',
|
||||
_CertValidatingHTTPSConnection)
|
||||
|
||||
|
||||
@contextlib2.contextmanager
|
||||
def force_reset():
|
||||
with contextlib2.ExitStack() as exit_stack:
|
||||
for patcher in reset_patchers():
|
||||
exit_stack.enter_context(patcher)
|
||||
yield
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
|
||||
@@ -11,10 +11,14 @@ def deserialize(cassette_string):
|
||||
def serialize(cassette_dict):
|
||||
try:
|
||||
return json.dumps(cassette_dict, indent=4)
|
||||
except UnicodeDecodeError:
|
||||
except UnicodeDecodeError as original:
|
||||
raise UnicodeDecodeError(
|
||||
"Error serializing cassette to JSON. ",
|
||||
"Does this HTTP interaction contain binary data? ",
|
||||
"If so, use a different serializer (like the yaml serializer) ",
|
||||
"for this request"
|
||||
original.encoding,
|
||||
b"Error serializing cassette to JSON",
|
||||
original.start,
|
||||
original.end,
|
||||
original.args[-1] +
|
||||
("Does this HTTP interaction contain binary data? "
|
||||
"If so, use a different serializer (like the yaml serializer) "
|
||||
"for this request?")
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ def parse_headers(header_list):
|
||||
|
||||
def serialize_headers(response):
|
||||
out = {}
|
||||
for key, values in compat.get_headers(response):
|
||||
for key, values in compat.get_headers(response.msg):
|
||||
out.setdefault(key, [])
|
||||
out[key].extend(values)
|
||||
return out
|
||||
@@ -108,14 +108,18 @@ class VCRHTTPResponse(HTTPResponse):
|
||||
|
||||
def getheaders(self):
|
||||
message = parse_headers(self.recorded_response['headers'])
|
||||
return compat.get_header_items(message)
|
||||
return list(compat.get_header_items(message))
|
||||
|
||||
def getheader(self, header, default=None):
|
||||
headers = dict(((k, v) for k, v in self.getheaders()))
|
||||
return headers.get(header, default)
|
||||
values = [v for (k, v) in self.getheaders() if k.lower() == header.lower()]
|
||||
|
||||
if values:
|
||||
return ', '.join(values)
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
class VCRConnection:
|
||||
class VCRConnection(object):
|
||||
# A reference to the cassette that's currently being patched in
|
||||
cassette = None
|
||||
|
||||
@@ -201,7 +205,7 @@ class VCRConnection:
|
||||
pass
|
||||
|
||||
def getresponse(self, _=False):
|
||||
'''Retrieve a the response'''
|
||||
'''Retrieve the response'''
|
||||
# Check to see if the cassette has a response for this request. If so,
|
||||
# then return it
|
||||
if self.cassette.can_play_response_for(self._vcr_request):
|
||||
@@ -213,11 +217,15 @@ class VCRConnection:
|
||||
response = self.cassette.play_response(self._vcr_request)
|
||||
return VCRHTTPResponse(response)
|
||||
else:
|
||||
if self.cassette.write_protected and self.cassette._filter_request(self._vcr_request):
|
||||
if self.cassette.write_protected and self.cassette.filter_request(
|
||||
self._vcr_request
|
||||
):
|
||||
raise CannotOverwriteExistingCassetteException(
|
||||
"No match for the request (%r) was found. "
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r)."
|
||||
% (self.cassette._path, self.cassette.record_mode)
|
||||
% (self._vcr_request, self.cassette._path,
|
||||
self.cassette.record_mode)
|
||||
)
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
@@ -285,13 +293,15 @@ class VCRConnection:
|
||||
self.real_connection.sock = value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.PY3:
|
||||
kwargs.pop('strict', None) # apparently this is gone in py3
|
||||
|
||||
# need to temporarily reset here because the real connection
|
||||
# inherits from the thing that we are mocking out. Take out
|
||||
# the reset if you want to see what I mean :)
|
||||
from vcr.patch import install, reset
|
||||
reset()
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
install(self.cassette)
|
||||
from vcr.patch import force_reset
|
||||
with force_reset():
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
|
||||
|
||||
class VCRHTTPConnection(VCRConnection):
|
||||
|
||||
@@ -21,18 +21,17 @@ def get_header(message, name):
|
||||
|
||||
|
||||
def get_header_items(message):
|
||||
if six.PY3:
|
||||
return dict(message._headers).items()
|
||||
else:
|
||||
return message.dict.items()
|
||||
for (key, values) in get_headers(message):
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
def get_headers(response):
|
||||
for key in response.msg.keys():
|
||||
def get_headers(message):
|
||||
for key in set(message.keys()):
|
||||
if six.PY3:
|
||||
yield key, response.msg.get_all(key)
|
||||
yield key, message.get_all(key)
|
||||
else:
|
||||
yield key, response.msg.getheaders(key)
|
||||
yield key, message.getheaders(key)
|
||||
|
||||
|
||||
def get_httpmessage(headers):
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'''Stubs for requests'''
|
||||
|
||||
from requests.packages.urllib3.connectionpool import VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
from requests.packages.urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
# urllib3 defines its own HTTPConnection classes, which requests goes ahead and assumes
|
||||
# you're using. It includes some polyfills for newer features missing in older pythons.
|
||||
|
||||
class VCRVerifiedHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
class VCRRequestsHTTPConnection(VCRHTTPConnection, HTTPConnection):
|
||||
_baseclass = HTTPConnection
|
||||
|
||||
class VCRRequestsHTTPSConnection(VCRHTTPSConnection, VerifiedHTTPSConnection):
|
||||
_baseclass = VerifiedHTTPSConnection
|
||||
|
||||
Reference in New Issue
Block a user