mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bb3c6beee | ||
|
|
df3ad5f35c | ||
|
|
e8a6a7a49f | ||
|
|
881138cb8d | ||
|
|
639dba6f7a | ||
|
|
b9bdc6401d | ||
|
|
3ca5529d26 | ||
|
|
e3f2bc8369 | ||
|
|
fc4e985ee9 | ||
|
|
9038bc9066 | ||
|
|
0def349420 | ||
|
|
0dd7b05990 | ||
|
|
630088599f | ||
|
|
870ab276c4 | ||
|
|
779f3b0474 | ||
|
|
b948ed4857 | ||
|
|
c43e618635 | ||
|
|
5bd40a447a | ||
|
|
4b4be7f661 | ||
|
|
6602a449b1 | ||
|
|
7cd7264034 | ||
|
|
e9c690b9e7 | ||
|
|
bba5df2fbb | ||
|
|
39c3b15e02 | ||
|
|
c87e6d6f6a | ||
|
|
5ab77e22db | ||
|
|
ec6f27bbad | ||
|
|
8930c97ff7 | ||
|
|
e6b43a0374 | ||
|
|
63ec95be06 | ||
|
|
84c45b2742 | ||
|
|
87a25e9ab0 | ||
|
|
2473bdb77a | ||
|
|
32831d4151 | ||
|
|
4991d6f1c8 | ||
|
|
14ef1e87f7 |
@@ -12,6 +12,9 @@ env:
|
||||
- WITH_LIB="requests1.x"
|
||||
- WITH_LIB="httplib2"
|
||||
- WITH_LIB="boto"
|
||||
- WITH_LIB="urllib31.7"
|
||||
- WITH_LIB="urllib31.9"
|
||||
- WITH_LIB="urllib31.10"
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: WITH_LIB="boto"
|
||||
@@ -37,4 +40,7 @@ install:
|
||||
- if [ $WITH_LIB = "requests2.5" ] ; then pip install requests==2.5.0; fi
|
||||
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
|
||||
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
|
||||
- if [ $WITH_LIB = "urllib31.7" ] ; then pip install certifi urllib3==1.7.1; fi
|
||||
- if [ $WITH_LIB = "urllib31.9" ] ; then pip install certifi urllib3==1.9.1; fi
|
||||
- if [ $WITH_LIB = "urllib31.10" ] ; then pip install certifi urllib3==1.10.2; fi
|
||||
script: python setup.py test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2012-2014 Kevin McCarthy
|
||||
Copyright (c) 2012-2015 Kevin McCarthy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
37
README.md
37
README.md
@@ -24,6 +24,7 @@ VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and [pypy](http://pypy.org).
|
||||
The following http libraries are supported:
|
||||
|
||||
* urllib2
|
||||
* urllib3
|
||||
* http.client (python3)
|
||||
* requests (both 1.x and 2.x versions)
|
||||
* httplib2
|
||||
@@ -293,9 +294,18 @@ with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
|
||||
requests.get('http://api.com/getdata?api_key=secretstring')
|
||||
```
|
||||
|
||||
### Filter information from HTTP post data
|
||||
Use the `filter_post_data_parameters` configuration option with a list of post data
|
||||
parameters to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_post_data_parameters=['client_secret']):
|
||||
requests.post('http://api.com/postdata', data={'api_key': 'secretstring'})
|
||||
```
|
||||
|
||||
### Custom Request filtering
|
||||
|
||||
If neither of these covers your request filtering needs, you can register a callback
|
||||
If none 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.
|
||||
@@ -336,7 +346,7 @@ 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 response['body']['string'] = response['body']['string'].replace(string, replacement)
|
||||
return scrub_string
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
@@ -362,6 +372,23 @@ back from a cassette. VCR will completely ignore those requests as if it
|
||||
didn't notice them at all, and they will continue to hit the server as if VCR
|
||||
were not there.
|
||||
|
||||
## Custom Patches
|
||||
|
||||
If you use a custom `HTTPConnection` class, or otherwise make http
|
||||
requests in a way that requires additional patching, you can use the
|
||||
`custom_patches` keyword argument of the `VCR` and `Cassette` objects
|
||||
to patch those objects whenever a cassette's context is entered. To
|
||||
patch a custom version of `HTTPConnection` you can do something like
|
||||
this:
|
||||
|
||||
```
|
||||
import where_the_custom_https_connection_lives
|
||||
from vcr.stubs import VCRHTTPSConnection
|
||||
my_vcr = config.VCR(custom_patches=((where_the_custom_https_connection_lives, 'CustomHTTPSConnection', VCRHTTPSConnection),))
|
||||
|
||||
@my_vcr.use_cassette(...)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need
|
||||
@@ -457,6 +484,12 @@ API in version 1.0.x
|
||||
|
||||
|
||||
## Changelog
|
||||
* 1.4.0 Filter post data parameters (thanks @eadmundo), support for
|
||||
posting files through requests, inject_cassette kwarg to access
|
||||
cassette from `use_cassette` decorated function,
|
||||
`with_current_defaults` actually works (thanks @samstav).
|
||||
* 1.3.0 Fix/add support for urllib3 (thanks @aisch), fix default
|
||||
port for https (thanks @abhinav).
|
||||
* 1.2.0 Add custom_patches argument to VCR/Cassette objects to allow
|
||||
users to stub custom classes when cassettes become active.
|
||||
* 1.1.4 Add force reset around calls to actual connection from stubs, to ensure
|
||||
|
||||
25
setup.py
25
setup.py
@@ -1,7 +1,7 @@
|
||||
##!/usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from setuptools import setup
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class PyTest(TestCommand):
|
||||
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.2.0',
|
||||
version='1.4.0',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
@@ -28,20 +28,9 @@ setup(
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=[
|
||||
'vcr',
|
||||
'vcr.stubs',
|
||||
'vcr.compat',
|
||||
'vcr.persisters',
|
||||
'vcr.serializers',
|
||||
],
|
||||
package_dir={
|
||||
'vcr': 'vcr',
|
||||
'vcr.stubs': 'vcr/stubs',
|
||||
'vcr.compat': 'vcr/compat',
|
||||
'vcr.persisters': 'vcr/persisters',
|
||||
},
|
||||
install_requires=['PyYAML', 'mock', 'six', 'contextlib2', 'wrapt'],
|
||||
packages=find_packages(exclude=("tests*",)),
|
||||
install_requires=['PyYAML', 'mock', 'six', 'contextlib2',
|
||||
'wrapt', 'backport_collections'],
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
@@ -54,5 +43,5 @@ setup(
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Basic tests for cassettes'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Basic tests about save behavior'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
|
||||
@@ -55,6 +56,16 @@ def test_filter_querystring(tmpdir):
|
||||
assert 'foo' not in cass.requests[0].url
|
||||
|
||||
|
||||
def test_filter_post_data(tmpdir):
|
||||
url = 'http://httpbin.org/post'
|
||||
data = urlencode({'id': 'secret', 'foo': 'bar'}).encode('utf-8')
|
||||
cass_file = str(tmpdir.join('filter_pd.yaml'))
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']):
|
||||
urlopen(url, data)
|
||||
with vcr.use_cassette(cass_file, filter_post_data_parameters=['id']) as cass:
|
||||
assert b'id=secret' not in cass.requests[0].body
|
||||
|
||||
|
||||
def test_filter_callback(tmpdir):
|
||||
url = 'http://httpbin.org/get'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with httplib2'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
from six.moves.urllib_parse import urlencode
|
||||
@@ -54,7 +54,7 @@ def test_response_headers(scheme, tmpdir):
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert headers == resp.items()
|
||||
assert set(headers) == set(resp.items())
|
||||
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Test requests' interaction with vcr'''
|
||||
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import vcr
|
||||
from assertions import (
|
||||
assert_cassette_empty,
|
||||
assert_cassette_has_one_response,
|
||||
assert_is_json
|
||||
)
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
|
||||
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""
|
||||
Fixture that returns both http and https
|
||||
"""
|
||||
'''Fixture that returns both http and https.'''
|
||||
return request.param
|
||||
|
||||
|
||||
@@ -205,3 +199,21 @@ def test_nested_cassettes_with_session_created_before_nesting(scheme, tmpdir):
|
||||
# Make sure that the session can now get content normally.
|
||||
session.get('http://www.reddit.com')
|
||||
|
||||
|
||||
def test_post_file(tmpdir, scheme):
|
||||
'''Ensure that we handle posting a file.'''
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml'))) as cass:
|
||||
# Don't use 2.7+ only style ',' separated with here because we support python 2.6
|
||||
with open('tox.ini') as f:
|
||||
original_response = requests.post(url, f).content
|
||||
|
||||
# This also tests that we do the right thing with matching the body when they are files.
|
||||
with vcr.use_cassette(str(tmpdir.join('post_file.yaml')),
|
||||
match_on=('method', 'scheme', 'host', 'port', 'path', 'query', 'body')) as cass:
|
||||
with open('tox.ini', 'rb') as f:
|
||||
tox_content = f.read()
|
||||
assert cass.requests[0].body.read() == tox_content
|
||||
with open('tox.ini', 'rb') as f:
|
||||
new_response = requests.post(url, f).content
|
||||
assert original_response == new_response
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''Integration tests with urllib2'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
@@ -11,7 +8,7 @@ from six.moves.urllib_parse import urlencode
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_empty, assert_cassette_has_one_response
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
|
||||
148
tests/integration/test_urllib3.py
Normal file
148
tests/integration/test_urllib3.py
Normal file
@@ -0,0 +1,148 @@
|
||||
'''Integration tests with urllib3'''
|
||||
|
||||
# coding=utf-8
|
||||
|
||||
import pytest
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_is_json
|
||||
certifi = pytest.importorskip("certifi")
|
||||
urllib3 = pytest.importorskip("urllib3")
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""
|
||||
Fixture that returns both http and https
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def verify_pool_mgr():
|
||||
return urllib3.PoolManager(
|
||||
cert_reqs='CERT_REQUIRED', # Force certificate check.
|
||||
ca_certs=certifi.where()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def pool_mgr():
|
||||
return urllib3.PoolManager()
|
||||
|
||||
|
||||
def test_status_code(scheme, tmpdir, verify_pool_mgr):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
status_code = verify_pool_mgr.request('GET', url).status
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))):
|
||||
assert status_code == verify_pool_mgr.request('GET', url).status
|
||||
|
||||
|
||||
def test_headers(scheme, tmpdir, verify_pool_mgr):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
headers = verify_pool_mgr.request('GET', url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))):
|
||||
assert headers == verify_pool_mgr.request('GET', url).headers
|
||||
|
||||
|
||||
def test_body(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
content = verify_pool_mgr.request('GET', url).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))):
|
||||
assert content == verify_pool_mgr.request('GET', url).data
|
||||
|
||||
|
||||
def test_auth(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can handle basic auth'''
|
||||
auth = ('user', 'passwd')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
one = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))):
|
||||
two = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
assert one.data == two.data
|
||||
assert one.status == two.status
|
||||
|
||||
|
||||
def test_auth_failed(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can save failed auth statuses'''
|
||||
auth = ('user', 'wrongwrongwrong')
|
||||
headers = urllib3.util.make_headers(basic_auth='{0}:{1}'.format(*auth))
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth-failed.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
one = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
two = verify_pool_mgr.request('GET', url, headers=headers)
|
||||
assert one.data == two.data
|
||||
assert one.status == two.status == 401
|
||||
|
||||
|
||||
def test_post(tmpdir, scheme, verify_pool_mgr):
|
||||
'''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('verify_pool_mgr.yaml'))):
|
||||
req1 = verify_pool_mgr.request('POST', url, data).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
|
||||
req2 = verify_pool_mgr.request('POST', url, data).data
|
||||
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that we can handle redirects'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))):
|
||||
content = verify_pool_mgr.request('GET', url).data
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('verify_pool_mgr.yaml'))) as cass:
|
||||
assert content == verify_pool_mgr.request('GET', url).data
|
||||
# Ensure that we've now cached *two* responses. One for the redirect
|
||||
# and one for the final fetch
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 2
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir, scheme, verify_pool_mgr):
|
||||
'''Ensure that requests between schemes are treated separately'''
|
||||
# First fetch a url under http, and then again under https and then
|
||||
# ensure that we haven't served anything out of cache, and we have two
|
||||
# requests / response pairs in the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
|
||||
verify_pool_mgr.request('GET', 'https://httpbin.org/')
|
||||
verify_pool_mgr.request('GET', 'http://httpbin.org/')
|
||||
assert cass.play_count == 0
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_gzip(tmpdir, scheme, verify_pool_mgr):
|
||||
'''
|
||||
Ensure that requests (actually urllib3) is able to automatically decompress
|
||||
the response body
|
||||
'''
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
response = verify_pool_mgr.request('GET', url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
response = verify_pool_mgr.request('GET', url)
|
||||
assert_is_json(response.data)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))):
|
||||
assert_is_json(response.data)
|
||||
|
||||
|
||||
def test_https_with_cert_validation_disabled(tmpdir, pool_mgr):
|
||||
with vcr.use_cassette(str(tmpdir.join('cert_validation_disabled.yaml'))):
|
||||
pool_mgr.request('GET', 'https://httpbin.org')
|
||||
@@ -110,7 +110,7 @@ def test_arg_getter_functionality():
|
||||
def function():
|
||||
pass
|
||||
|
||||
with mock.patch.object(Cassette, 'load') as cassette_load:
|
||||
with mock.patch.object(Cassette, 'load', return_value=mock.MagicMock(inject=False)) as cassette_load:
|
||||
function()
|
||||
cassette_load.assert_called_once_with(arg_getter.return_value[0],
|
||||
**arg_getter.return_value[1])
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from vcr.filters import remove_headers, remove_query_parameters
|
||||
from vcr.filters import (
|
||||
remove_headers,
|
||||
remove_query_parameters,
|
||||
remove_post_data_parameters
|
||||
)
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
@@ -35,3 +39,31 @@ def test_remove_nonexistent_query_parameters():
|
||||
request = Request('GET', uri, '', {})
|
||||
remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
|
||||
def test_remove_post_data_parameters():
|
||||
body = b'id=secret&foo=bar'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'foo=bar'
|
||||
|
||||
|
||||
def test_preserve_multiple_post_data_parameters():
|
||||
body = b'id=secret&foo=bar&foo=baz'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b'foo=bar&foo=baz'
|
||||
|
||||
|
||||
def test_remove_all_post_data_parameters():
|
||||
body = b'id=secret&foo=bar'
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id', 'foo'])
|
||||
assert request.body == b''
|
||||
|
||||
|
||||
def test_remove_nonexistent_post_data_parameters():
|
||||
body = b''
|
||||
request = Request('POST', 'http://google.com', body, {})
|
||||
remove_post_data_parameters(request, ['id'])
|
||||
assert request.body == b''
|
||||
|
||||
@@ -21,8 +21,8 @@ def test_headers():
|
||||
('http://go.com/', 80),
|
||||
('http://go.com:80/', 80),
|
||||
('http://go.com:3000/', 3000),
|
||||
('https://go.com/', 433),
|
||||
('https://go.com:433/', 433),
|
||||
('https://go.com/', 443),
|
||||
('https://go.com:443/', 443),
|
||||
('https://go.com:3000/', 3000),
|
||||
])
|
||||
def test_port(uri, expected_port):
|
||||
|
||||
@@ -9,7 +9,7 @@ from vcr.stubs import VCRHTTPSConnection
|
||||
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:
|
||||
with mock.patch('vcr.cassette.Cassette.load', return_value=mock.MagicMock(inject=False)) as mock_cassette_load:
|
||||
@test_vcr.use_cassette('test')
|
||||
def function():
|
||||
pass
|
||||
@@ -70,10 +70,11 @@ def test_fixtures_with_use_cassette(random_fixture):
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
|
||||
@@ -90,3 +91,40 @@ def test_custom_patchers():
|
||||
assert issubclass(Test.attribute, VCRHTTPSConnection)
|
||||
assert VCRHTTPSConnection is not Test.attribute
|
||||
assert Test.attribute is Test.attribute2
|
||||
|
||||
|
||||
def test_inject_cassette():
|
||||
vcr = VCR(inject_cassette=True)
|
||||
@vcr.use_cassette('test', record_mode='once')
|
||||
def with_cassette_injected(cassette):
|
||||
assert cassette.record_mode == 'once'
|
||||
|
||||
@vcr.use_cassette('test', record_mode='once', inject_cassette=False)
|
||||
def without_cassette_injected():
|
||||
pass
|
||||
|
||||
with_cassette_injected()
|
||||
without_cassette_injected()
|
||||
|
||||
|
||||
def test_with_current_defaults():
|
||||
vcr = VCR(inject_cassette=True, record_mode='once')
|
||||
@vcr.use_cassette('test', with_current_defaults=False)
|
||||
def changing_defaults(cassette, checks):
|
||||
checks(cassette)
|
||||
@vcr.use_cassette('test', with_current_defaults=True)
|
||||
def current_defaults(cassette, checks):
|
||||
checks(cassette)
|
||||
|
||||
def assert_record_mode_once(cassette):
|
||||
assert cassette.record_mode == 'once'
|
||||
|
||||
def assert_record_mode_all(cassette):
|
||||
assert cassette.record_mode == 'all'
|
||||
|
||||
changing_defaults(assert_record_mode_once)
|
||||
current_defaults(assert_record_mode_once)
|
||||
|
||||
vcr.record_mode = 'all'
|
||||
changing_defaults(assert_record_mode_all)
|
||||
current_defaults(assert_record_mode_once)
|
||||
|
||||
6
tox.ini
6
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests25,requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
|
||||
envlist = {py26,py27,py33,py34,pypy}-{requests25,requests24,requests23,requests22,requests1,httplib2,urllib317,urllib319,urllib3110,boto}
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
@@ -21,5 +21,7 @@ deps =
|
||||
requests23: requests==2.3.0
|
||||
requests22: requests==2.2.1
|
||||
httplib2: httplib2
|
||||
urllib3: urllib3==1.7.1
|
||||
urllib317: urllib3==1.7.1
|
||||
urllib319: urllib3==1.9.1
|
||||
urllib3110: urllib3==1.10.2
|
||||
boto: boto
|
||||
|
||||
@@ -6,7 +6,7 @@ import wrapt
|
||||
try:
|
||||
from collections import Counter
|
||||
except ImportError:
|
||||
from .compat.counter import Counter
|
||||
from backport_collections import Counter
|
||||
|
||||
# Internal imports
|
||||
from .patch import CassettePatcherBuilder
|
||||
@@ -61,8 +61,11 @@ class CassetteContextDecorator(object):
|
||||
|
||||
@wrapt.decorator
|
||||
def __call__(self, function, instance, args, kwargs):
|
||||
with self:
|
||||
return function(*args, **kwargs)
|
||||
with self as cassette:
|
||||
if cassette.inject:
|
||||
return function(cassette, *args, **kwargs)
|
||||
else:
|
||||
return function(*args, **kwargs)
|
||||
|
||||
|
||||
class Cassette(object):
|
||||
@@ -84,23 +87,24 @@ class Cassette(object):
|
||||
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=(), custom_patches=()):
|
||||
match_on=(uri, method), before_record_request=None,
|
||||
before_record_response=None, custom_patches=(),
|
||||
inject=False):
|
||||
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self._match_on = match_on
|
||||
self._before_record_request = before_record_request or (lambda x: x)
|
||||
self._before_record_response = before_record_response or (lambda x: x)
|
||||
self.inject = inject
|
||||
self.record_mode = record_mode
|
||||
self.custom_patches = custom_patches
|
||||
|
||||
# self.data is the list of (req, resp) tuples
|
||||
self.data = []
|
||||
self.play_counts = Counter()
|
||||
self.dirty = False
|
||||
self.rewound = False
|
||||
self.record_mode = record_mode
|
||||
self.custom_patches = custom_patches
|
||||
|
||||
@property
|
||||
def play_count(self):
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
from __future__ import print_function
|
||||
from operator import itemgetter
|
||||
from heapq import nlargest
|
||||
from itertools import repeat, ifilter
|
||||
|
||||
# From http://code.activestate.com/recipes/576611-counter-class/
|
||||
# Backported for python 2.6 support
|
||||
|
||||
class Counter(dict):
|
||||
'''Dict subclass for counting hashable objects. Sometimes called a bag
|
||||
or multiset. Elements are stored as dictionary keys and their counts
|
||||
are stored as dictionary values.
|
||||
|
||||
>>> Counter('zyzygy')
|
||||
Counter({'y': 3, 'z': 2, 'g': 1})
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, iterable=None, **kwds):
|
||||
'''Create a new, empty Counter object. And if given, count elements
|
||||
from an input iterable. Or, initialize the count from another mapping
|
||||
of elements to their counts.
|
||||
|
||||
>>> c = Counter() # a new, empty counter
|
||||
>>> c = Counter('gallahad') # a new counter from an iterable
|
||||
>>> 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):
|
||||
return 0
|
||||
|
||||
def most_common(self, n=None):
|
||||
'''List the n most common elements and their counts from the most
|
||||
common to the least. If n is None, then list all element counts.
|
||||
|
||||
>>> 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))
|
||||
|
||||
def elements(self):
|
||||
'''Iterator over elements repeating each as many times as its count.
|
||||
|
||||
>>> c = Counter('ABCABC')
|
||||
>>> sorted(c.elements())
|
||||
['A', 'A', 'B', 'B', 'C', 'C']
|
||||
|
||||
If an element's count has been set to zero or is a negative number,
|
||||
elements() will ignore it.
|
||||
|
||||
'''
|
||||
for elem, count in self.iteritems():
|
||||
for _ in repeat(None, count):
|
||||
yield elem
|
||||
|
||||
# Override dict methods where the meaning changes for Counter objects.
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, v=None):
|
||||
raise NotImplementedError(
|
||||
'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
|
||||
|
||||
def update(self, iterable=None, **kwds):
|
||||
'''Like dict.update() but add counts instead of replacing them.
|
||||
|
||||
Source can be an iterable, a dictionary, or another Counter instance.
|
||||
|
||||
>>> c = Counter('which')
|
||||
>>> c.update('witch') # add elements from another iterable
|
||||
>>> d = Counter('watch')
|
||||
>>> c.update(d) # add elements from another counter
|
||||
>>> c['h'] # four 'h' in which, witch, and watch
|
||||
4
|
||||
|
||||
'''
|
||||
if iterable is not None:
|
||||
if hasattr(iterable, 'iteritems'):
|
||||
if self:
|
||||
self_get = self.get
|
||||
for elem, count in iterable.iteritems():
|
||||
self[elem] = self_get(elem, 0) + count
|
||||
else:
|
||||
dict.update(self, iterable) # fast path when counter is empty
|
||||
else:
|
||||
self_get = self.get
|
||||
for elem in iterable:
|
||||
self[elem] = self_get(elem, 0) + 1
|
||||
if kwds:
|
||||
self.update(kwds)
|
||||
|
||||
def copy(self):
|
||||
'Like dict.copy() but returns a Counter instance instead of a dict.'
|
||||
return Counter(self)
|
||||
|
||||
def __delitem__(self, elem):
|
||||
'Like dict.__delitem__() but does not raise KeyError for missing values.'
|
||||
if elem in self:
|
||||
dict.__delitem__(self, elem)
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % self.__class__.__name__
|
||||
items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
|
||||
return '%s({%s})' % (self.__class__.__name__, items)
|
||||
|
||||
# Multiset-style mathematical operations discussed in:
|
||||
# Knuth TAOCP Volume II section 4.6.3 exercise 19
|
||||
# and at http://en.wikipedia.org/wiki/Multiset
|
||||
#
|
||||
# Outputs guaranteed to only include positive counts.
|
||||
#
|
||||
# To strip negative and zero counts, add-in an empty counter:
|
||||
# c += Counter()
|
||||
|
||||
def __add__(self, other):
|
||||
'''Add counts from two counters.
|
||||
|
||||
>>> Counter('abbb') + Counter('bcc')
|
||||
Counter({'b': 4, 'c': 2, 'a': 1})
|
||||
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = self[elem] + other[elem]
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __sub__(self, other):
|
||||
''' Subtract count, but keep only results with positive counts.
|
||||
|
||||
>>> Counter('abbbc') - Counter('bccd')
|
||||
Counter({'b': 2, 'a': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = self[elem] - other[elem]
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __or__(self, other):
|
||||
'''Union is the maximum of value in either of the input counters.
|
||||
|
||||
>>> Counter('abbb') | Counter('bcc')
|
||||
Counter({'b': 3, 'c': 2, 'a': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
_max = max
|
||||
result = Counter()
|
||||
for elem in set(self) | set(other):
|
||||
newcount = _max(self[elem], other[elem])
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
def __and__(self, other):
|
||||
''' Intersection is the minimum of corresponding counts.
|
||||
|
||||
>>> Counter('abbb') & Counter('bcc')
|
||||
Counter({'b': 1})
|
||||
|
||||
'''
|
||||
if not isinstance(other, Counter):
|
||||
return NotImplemented
|
||||
_min = min
|
||||
result = Counter()
|
||||
if len(self) < len(other):
|
||||
self, other = other, self
|
||||
for elem in ifilter(self.__contains__, other):
|
||||
newcount = _min(self[elem], other[elem])
|
||||
if newcount > 0:
|
||||
result[elem] = newcount
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
print(doctest.testmod())
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
|
||||
# Passes Python2.7's test suite and incorporates all the latest updates.
|
||||
|
||||
try:
|
||||
from thread import get_ident as _get_ident
|
||||
except ImportError:
|
||||
from dummy_thread import get_ident as _get_ident
|
||||
|
||||
try:
|
||||
from _abcoll import KeysView, ValuesView, ItemsView
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class OrderedDict(dict):
|
||||
'Dictionary that remembers insertion order'
|
||||
# An inherited dict maps keys to values.
|
||||
# The inherited dict provides __getitem__, __len__, __contains__, and get.
|
||||
# The remaining methods are order-aware.
|
||||
# Big-O running times for all methods are the same as for regular dictionaries.
|
||||
|
||||
# The internal self.__map dictionary maps keys to links in a doubly linked list.
|
||||
# The circular doubly linked list starts and ends with a sentinel element.
|
||||
# The sentinel element never gets deleted (this simplifies the algorithm).
|
||||
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
'''Initialize an ordered dictionary. Signature is the same as for
|
||||
regular dictionaries, but keyword arguments are not recommended
|
||||
because their insertion order is arbitrary.
|
||||
|
||||
'''
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__root
|
||||
except AttributeError:
|
||||
self.__root = root = [] # sentinel node
|
||||
root[:] = [root, root, None]
|
||||
self.__map = {}
|
||||
self.__update(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
|
||||
'od.__setitem__(i, y) <==> od[i]=y'
|
||||
# Setting a new item creates a new link which goes at the end of the linked
|
||||
# list, and the inherited dictionary is updated with the new key/value pair.
|
||||
if key not in self:
|
||||
root = self.__root
|
||||
last = root[0]
|
||||
last[1] = root[0] = self.__map[key] = [last, root, key]
|
||||
dict_setitem(self, key, value)
|
||||
|
||||
def __delitem__(self, key, dict_delitem=dict.__delitem__):
|
||||
'od.__delitem__(y) <==> del od[y]'
|
||||
# Deleting an existing item uses self.__map to find the link which is
|
||||
# then removed by updating the links in the predecessor and successor nodes.
|
||||
dict_delitem(self, key)
|
||||
link_prev, link_next, key = self.__map.pop(key)
|
||||
link_prev[1] = link_next
|
||||
link_next[0] = link_prev
|
||||
|
||||
def __iter__(self):
|
||||
'od.__iter__() <==> iter(od)'
|
||||
root = self.__root
|
||||
curr = root[1]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[1]
|
||||
|
||||
def __reversed__(self):
|
||||
'od.__reversed__() <==> reversed(od)'
|
||||
root = self.__root
|
||||
curr = root[0]
|
||||
while curr is not root:
|
||||
yield curr[2]
|
||||
curr = curr[0]
|
||||
|
||||
def clear(self):
|
||||
'od.clear() -> None. Remove all items from od.'
|
||||
try:
|
||||
for node in self.__map.itervalues():
|
||||
del node[:]
|
||||
root = self.__root
|
||||
root[:] = [root, root, None]
|
||||
self.__map.clear()
|
||||
except AttributeError:
|
||||
pass
|
||||
dict.clear(self)
|
||||
|
||||
def popitem(self, last=True):
|
||||
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
|
||||
Pairs are returned in LIFO order if last is true or FIFO order if false.
|
||||
|
||||
'''
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
root = self.__root
|
||||
if last:
|
||||
link = root[0]
|
||||
link_prev = link[0]
|
||||
link_prev[1] = root
|
||||
root[0] = link_prev
|
||||
else:
|
||||
link = root[1]
|
||||
link_next = link[1]
|
||||
root[1] = link_next
|
||||
link_next[0] = root
|
||||
key = link[2]
|
||||
del self.__map[key]
|
||||
value = dict.pop(self, key)
|
||||
return key, value
|
||||
|
||||
# -- the following methods do not depend on the internal structure --
|
||||
|
||||
def keys(self):
|
||||
'od.keys() -> list of keys in od'
|
||||
return list(self)
|
||||
|
||||
def values(self):
|
||||
'od.values() -> list of values in od'
|
||||
return [self[key] for key in self]
|
||||
|
||||
def items(self):
|
||||
'od.items() -> list of (key, value) pairs in od'
|
||||
return [(key, self[key]) for key in self]
|
||||
|
||||
def iterkeys(self):
|
||||
'od.iterkeys() -> an iterator over the keys in od'
|
||||
return iter(self)
|
||||
|
||||
def itervalues(self):
|
||||
'od.itervalues -> an iterator over the values in od'
|
||||
for k in self:
|
||||
yield self[k]
|
||||
|
||||
def iteritems(self):
|
||||
'od.iteritems -> an iterator over the (key, value) items in od'
|
||||
for k in self:
|
||||
yield (k, self[k])
|
||||
|
||||
def update(*args, **kwds):
|
||||
'''od.update(E, **F) -> None. Update od from dict/iterable E and F.
|
||||
|
||||
If E is a dict instance, does: for k in E: od[k] = E[k]
|
||||
If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
|
||||
Or if E is an iterable of items, does: for k, v in E: od[k] = v
|
||||
In either case, this is followed by: for k, v in F.items(): od[k] = v
|
||||
|
||||
'''
|
||||
if len(args) > 2:
|
||||
raise TypeError('update() takes at most 2 positional '
|
||||
'arguments (%d given)' % (len(args),))
|
||||
elif not args:
|
||||
raise TypeError('update() takes at least 1 argument (0 given)')
|
||||
self = args[0]
|
||||
# Make progressively weaker assumptions about "other"
|
||||
other = ()
|
||||
if len(args) == 2:
|
||||
other = args[1]
|
||||
if isinstance(other, dict):
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, 'keys'):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
for key, value in other:
|
||||
self[key] = value
|
||||
for key, value in kwds.items():
|
||||
self[key] = value
|
||||
|
||||
__update = update # let subclasses override update without breaking __init__
|
||||
|
||||
__marker = object()
|
||||
|
||||
def pop(self, key, default=__marker):
|
||||
'''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
|
||||
If key is not found, d is returned if given, otherwise KeyError is raised.
|
||||
|
||||
'''
|
||||
if key in self:
|
||||
result = self[key]
|
||||
del self[key]
|
||||
return result
|
||||
if default is self.__marker:
|
||||
raise KeyError(key)
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
|
||||
if key in self:
|
||||
return self[key]
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
def __repr__(self, _repr_running={}):
|
||||
'od.__repr__() <==> repr(od)'
|
||||
call_key = id(self), _get_ident()
|
||||
if call_key in _repr_running:
|
||||
return '...'
|
||||
_repr_running[call_key] = 1
|
||||
try:
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
def __reduce__(self):
|
||||
'Return state information for pickling'
|
||||
items = [[k, self[k]] for k in self]
|
||||
inst_dict = vars(self).copy()
|
||||
for k in vars(OrderedDict()):
|
||||
inst_dict.pop(k, None)
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def copy(self):
|
||||
'od.copy() -> a shallow copy of od'
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
'''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
|
||||
and values equal to v (which defaults to None).
|
||||
|
||||
'''
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
'''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
|
||||
while comparison to a regular mapping is order-insensitive.
|
||||
|
||||
'''
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
# -- the following methods are only used in Python 2.7 --
|
||||
|
||||
def viewkeys(self):
|
||||
"od.viewkeys() -> a set-like object providing a view on od's keys"
|
||||
return KeysView(self)
|
||||
|
||||
def viewvalues(self):
|
||||
"od.viewvalues() -> an object providing a view on od's values"
|
||||
return ValuesView(self)
|
||||
|
||||
def viewitems(self):
|
||||
"od.viewitems() -> a set-like object providing a view on od's items"
|
||||
return ItemsView(self)
|
||||
@@ -12,11 +12,12 @@ from . import filters
|
||||
class VCR(object):
|
||||
|
||||
def __init__(self, serializer='yaml', cassette_library_dir=None,
|
||||
record_mode="once", filter_headers=(), custom_patches=(),
|
||||
filter_query_parameters=(), before_record_request=None,
|
||||
record_mode="once", filter_headers=(), ignore_localhost=False,
|
||||
custom_patches=(), filter_query_parameters=(),
|
||||
filter_post_data_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):
|
||||
match_on=('method', 'scheme', 'host', 'port', 'path', 'query'),
|
||||
before_record=None, inject_cassette=False):
|
||||
self.serializer = serializer
|
||||
self.match_on = match_on
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
@@ -39,10 +40,12 @@ class VCR(object):
|
||||
self.record_mode = record_mode
|
||||
self.filter_headers = filter_headers
|
||||
self.filter_query_parameters = filter_query_parameters
|
||||
self.filter_post_data_parameters = filter_post_data_parameters
|
||||
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
|
||||
self.inject_cassette = inject_cassette
|
||||
self._custom_patches = tuple(custom_patches)
|
||||
|
||||
def _get_serializer(self, serializer_name):
|
||||
@@ -68,11 +71,14 @@ class VCR(object):
|
||||
|
||||
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))
|
||||
# This is made a function that evaluates every time a cassette is made so that
|
||||
# changes that are made to this VCR instance that occur AFTER the use_cassette
|
||||
# decorator is applied still affect subsequent calls to the decorated function.
|
||||
args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
|
||||
path, config = self.get_path_and_merged_config(path, **kwargs)
|
||||
return Cassette.use(path, **config)
|
||||
# This is made a function that evaluates every time a cassette
|
||||
# is made so that changes that are made to this VCR instance
|
||||
# that occur AFTER the `use_cassette` decorator is applied
|
||||
# still affect subsequent calls to the decorated function.
|
||||
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):
|
||||
@@ -90,8 +96,13 @@ class VCR(object):
|
||||
'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),
|
||||
'custom_patches': self._custom_patches + kwargs.get('custom_patches', ())
|
||||
'before_record_response': self._build_before_record_response(
|
||||
kwargs
|
||||
),
|
||||
'custom_patches': self._custom_patches + kwargs.get(
|
||||
'custom_patches', ()
|
||||
),
|
||||
'inject': kwargs.get('inject_cassette', self.inject_cassette)
|
||||
}
|
||||
return path, merged_config
|
||||
|
||||
@@ -121,6 +132,9 @@ class VCR(object):
|
||||
filter_query_parameters = options.get(
|
||||
'filter_query_parameters', self.filter_query_parameters
|
||||
)
|
||||
filter_post_data_parameters = options.get(
|
||||
'filter_post_data_parameters', self.filter_post_data_parameters
|
||||
)
|
||||
before_record_request = options.get(
|
||||
"before_record_request", options.get("before_record", self.before_record_request)
|
||||
)
|
||||
@@ -136,6 +150,9 @@ class VCR(object):
|
||||
if filter_query_parameters:
|
||||
filter_functions.append(functools.partial(filters.remove_query_parameters,
|
||||
query_parameters_to_remove=filter_query_parameters))
|
||||
if filter_post_data_parameters:
|
||||
filter_functions.append(functools.partial(filters.remove_post_data_parameters,
|
||||
post_data_parameters_to_remove=filter_post_data_parameters))
|
||||
|
||||
hosts_to_ignore = list(ignore_hosts)
|
||||
if ignore_localhost:
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from six import BytesIO
|
||||
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from backport_collections import OrderedDict
|
||||
import copy
|
||||
|
||||
|
||||
@@ -22,3 +27,17 @@ def remove_query_parameters(request, query_parameters_to_remove):
|
||||
uri_parts[4] = urlencode(new_query)
|
||||
request.uri = urlunparse(uri_parts)
|
||||
return request
|
||||
|
||||
|
||||
def remove_post_data_parameters(request, post_data_parameters_to_remove):
|
||||
if request.method == 'POST' and not isinstance(request.body, BytesIO):
|
||||
post_data = OrderedDict()
|
||||
for k, sep, v in [p.partition(b'=') for p in request.body.split(b'&')]:
|
||||
if k in post_data:
|
||||
post_data[k].append(v)
|
||||
elif len(k) > 0 and k.decode('utf-8') not in post_data_parameters_to_remove:
|
||||
post_data[k] = [v]
|
||||
request.body = b'&'.join(
|
||||
b'='.join([k, v])
|
||||
for k, vals in post_data.items() for v in vals)
|
||||
return request
|
||||
|
||||
@@ -31,6 +31,8 @@ def query(r1, r2):
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ PARTS = [
|
||||
def build_uri(**parts):
|
||||
port = parts['port']
|
||||
scheme = parts['protocol']
|
||||
default_port = {'https': 433, 'http': 80}[scheme]
|
||||
default_port = {'https': 443, 'http': 80}[scheme]
|
||||
parts['port'] = ':{0}'.format(port) if port != default_port else ''
|
||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||
|
||||
|
||||
101
vcr/patch.py
101
vcr/patch.py
@@ -92,15 +92,21 @@ class CassettePatcherBuilder(object):
|
||||
|
||||
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
|
||||
"""One of the subtleties of this class is that it does not directly
|
||||
replace HTTPSConnection with VCRRequestsHTTPSConnection, but a
|
||||
subclass of this class that has cassette assigned to the
|
||||
appropriate value. This behavior is necessary to properly
|
||||
support nested cassette contexts
|
||||
replace HTTPSConnection with `VCRRequestsHTTPSConnection`, but a
|
||||
subclass of the aforementioned class that has the `cassette`
|
||||
class attribute assigned to `self._cassette`. This behavior is
|
||||
necessary to properly support nested cassette contexts.
|
||||
|
||||
This function exists to ensure that we use the same class
|
||||
object (reference) to patch everything that replaces
|
||||
VCRRequestHTTP[S]Connection, but that we can talk about
|
||||
patching them with the raw references instead.
|
||||
patching them with the raw references instead, and without
|
||||
worrying about exactly where the subclass with the relevant
|
||||
value for `cassette` is first created.
|
||||
|
||||
The function is recursive because it looks in to dictionaries
|
||||
and replaces class values at any depth with the subclass
|
||||
described in the previous paragraph.
|
||||
"""
|
||||
if isinstance(replacement_dict_or_obj, dict):
|
||||
for key, replacement_obj in replacement_dict_or_obj.items():
|
||||
@@ -138,39 +144,8 @@ class CassettePatcherBuilder(object):
|
||||
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, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only
|
||||
(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))
|
||||
from .stubs import requests_stubs
|
||||
return self._urllib3_patchers(cpool, requests_stubs)
|
||||
|
||||
def _patched_get_conn(self, connection_pool_class, connection_class_getter):
|
||||
get_conn = connection_pool_class._get_conn
|
||||
@@ -179,6 +154,12 @@ class CassettePatcherBuilder(object):
|
||||
connection = get_conn(pool, timeout)
|
||||
connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \
|
||||
else connection_class_getter()
|
||||
# We need to make sure that we are actually providing a
|
||||
# patched version of the connection class. This might not
|
||||
# always be the case because the pool keeps previously
|
||||
# used connections (which might actually be of a different
|
||||
# class) around. This while loop will terminate because
|
||||
# eventually the pool will run out of connections.
|
||||
while not isinstance(connection, connection_class):
|
||||
connection = get_conn(pool, timeout)
|
||||
return connection
|
||||
@@ -193,17 +174,13 @@ class CassettePatcherBuilder(object):
|
||||
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
|
||||
return ()
|
||||
from .stubs import urllib3_stubs
|
||||
return self._urllib3_patchers(cpool, urllib3_stubs)
|
||||
|
||||
@_build_patchers_from_mock_triples_decorator
|
||||
def _httplib2(self):
|
||||
@@ -229,6 +206,40 @@ class CassettePatcherBuilder(object):
|
||||
else:
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
yield cpool, 'CertValidatingHTTPSConnection', VCRCertValidatingHTTPSConnection
|
||||
|
||||
def _urllib3_patchers(self, cpool, stubs):
|
||||
http_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||
)
|
||||
https_connection_remover = ConnectionRemover(
|
||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||
)
|
||||
mock_triples = (
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'VerifiedHTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'HTTPConnection', stubs.VCRRequestsHTTPConnection),
|
||||
(cpool, 'HTTPSConnection', stubs.VCRRequestsHTTPSConnection),
|
||||
(cpool, 'is_connection_dropped', mock.Mock(return_value=False)), # Needed on Windows only
|
||||
(cpool.HTTPConnectionPool, 'ConnectionCls', stubs.VCRRequestsHTTPConnection),
|
||||
(cpool.HTTPSConnectionPool, 'ConnectionCls', stubs.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))
|
||||
|
||||
|
||||
class ConnectionRemover(object):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from six import BytesIO, binary_type
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
|
||||
|
||||
@@ -26,11 +27,25 @@ class Request(object):
|
||||
def __init__(self, method, uri, body, headers):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.body = body
|
||||
self._was_file = hasattr(body, 'read')
|
||||
if self._was_file:
|
||||
self._body = body.read()
|
||||
if not isinstance(self._body, binary_type):
|
||||
self._body = self._body.encode('utf-8')
|
||||
else:
|
||||
self._body = body
|
||||
self.headers = {}
|
||||
for key in headers:
|
||||
self.add_header(key, headers[key])
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return BytesIO(self._body) if self._was_file else self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
self._body = value
|
||||
|
||||
def add_header(self, key, value):
|
||||
# see class docstring for an explanation
|
||||
if isinstance(value, (tuple, list)):
|
||||
@@ -51,7 +66,7 @@ class Request(object):
|
||||
parse_uri = urlparse(self.uri)
|
||||
port = parse_uri.port
|
||||
if port is None:
|
||||
port = {'https': 433, 'http': 80}[parse_uri.scheme]
|
||||
port = {'https': 443, 'http': 80}[parse_uri.scheme]
|
||||
return port
|
||||
|
||||
@property
|
||||
|
||||
@@ -128,7 +128,7 @@ class VCRConnection(object):
|
||||
Returns empty string for the default port and ':port' otherwise
|
||||
"""
|
||||
port = self.real_connection.port
|
||||
default_port = {'https': 433, 'http': 80}[self._protocol]
|
||||
default_port = {'https': 443, 'http': 80}[self._protocol]
|
||||
return ':{0}'.format(port) if port != default_port else ''
|
||||
|
||||
def _uri(self, url):
|
||||
@@ -204,7 +204,7 @@ class VCRConnection(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def getresponse(self, _=False):
|
||||
def getresponse(self, _=False, **kwargs):
|
||||
'''Retrieve the response'''
|
||||
# Check to see if the cassette has a response for this request. If so,
|
||||
# then return it
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'''Stubs for urllib3'''
|
||||
|
||||
from urllib3.connectionpool import VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
# urllib3 defines its own HTTPConnection classes. 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