1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-10 01:25:34 +00:00

Compare commits

...

50 Commits

Author SHA1 Message Date
Ivan Malison
e9c690b9e7 Version 1.3.0. 2015-03-23 18:10:26 -07:00
Ivan Malison
bba5df2fbb clarifying comment in patch.py. 2015-03-23 18:00:23 -07:00
Ivan Malison
39c3b15e02 unused imports. 2015-03-23 17:56:46 -07:00
Ivan Malison
c87e6d6f6a Clarifying comments in patch.py. 2015-03-23 17:55:49 -07:00
Ivan Malison
5ab77e22db Use suggested emacs style coding statements (see https://www.python.org/dev/peps/pep-0263/). 2015-03-23 17:55:49 -07:00
Ivan 'Goat' Malison
ec6f27bbad Merge pull request #138 from aisch/patch-and-test-urllib3
update urllib3 patch/stub to be same as used for requests and add tests
2015-03-23 17:47:42 -07:00
aisch
8930c97ff7 rm unused imports 2015-03-23 13:56:48 -07:00
aisch
e6b43a0374 rename urllib3 patch method and rm unused imports from tests 2015-03-23 13:43:30 -07:00
aisch
63ec95be06 update urllib3 patch/stub to be same as used for requests and add tests 2015-03-23 12:12:49 -07:00
Kevin McCarthy
84c45b2742 Merge pull request #136 from abhinav/https-port-fix
Fix default port for HTTPS
2015-02-24 09:12:49 -10:00
Abhinav Gupta
87a25e9ab0 Fix httplib2 integration test. 2015-02-24 00:10:08 -08:00
Abhinav Gupta
2473bdb77a Fix default port for HTTPS. 2015-02-23 23:37:04 -08:00
Ivan 'Goat' Malison
32831d4151 Merge pull request #135 from RomuloOliveira/patch-1
Fix missing quotes on Custom Response Filtering
2015-01-28 12:02:57 -08:00
Rômulo Oliveira
4991d6f1c8 Fix missing quotes on Custom Response Filtering
Missing quotes are bad
2015-01-28 11:34:47 -02:00
Ivan Malison
14ef1e87f7 Add custom_patches section to README.md 2015-01-08 14:02:41 -08:00
Ivan 'Goat' Malison
fb14739cc1 Merge pull request #133 from IvanMalison/custom_patches
Custom patches
2015-01-08 11:08:55 -08:00
Ivan Malison
a7c7e4e279 Bump version to 1.2.0 2015-01-08 10:56:39 -08:00
Ivan Malison
c0a22df7ed Add ability to add custom patches to vcr and cassettes. 2015-01-08 10:54:27 -08:00
Ivan Malison
83aed99058 Bump vesrsion to 1.1.4, add to release notes. 2014-12-26 05:26:24 -05:00
Ivan Malison
e1f65bcbdc Add force reset around calls to actual connection from stubs, to ensure
compatibility with version of httplib/urlib2 in python 2.7.9. Closes #130.
2014-12-26 05:10:20 -05:00
Kevin McCarthy
5301149bd8 Merge pull request #128 from gazpachoking/patch-1
Update changelog to note requests 2.5 support
2014-12-09 08:55:52 -10:00
Chase Sterling
0297fcdde7 Update changelog to note requests 2.5 support 2014-12-09 13:26:46 -05:00
Kevin McCarthy
9480954c33 update release notes 2014-12-08 17:10:35 -10:00
Kevin McCarthy
8432ad32f1 Merge pull request #127 from gazpachoking/1.1.3
Version bump to v1.1.3
2014-12-08 17:09:07 -10:00
Chase Sterling
fabef3d988 Version bump to v1.1.3 2014-12-08 21:43:01 -05:00
Ivan 'Goat' Malison
da45f46b2d Merge pull request #125 from gazpachoking/pool_is_none
Fix crash with requests 2.5 where connectionpool was None
2014-12-08 13:20:36 -08:00
Ivan 'Goat' Malison
562a0ebadc Merge pull request #126 from gazpachoking/116
Play back requests requests on windows. fix #116
2014-12-08 12:29:34 -08:00
Chase Sterling
ef8ba6d51b Add requests 2.5 to testing list in .travis.yml and tox.ini 2014-12-08 14:40:55 -05:00
Chase Sterling
f6aa6eac84 Play back requests requests on windows. fix #116 2014-12-08 14:28:48 -05:00
Chase Sterling
821e148752 Fix crash with requests 2.5 where connectionpool was None 2014-12-07 13:49:23 -05:00
Ivan Malison
7306205b8a Improve test_new_episodes_record_mode_two_times test. 2014-11-21 17:15:15 -08:00
Nithin Reddy
2a128893cc Adds a test to ensure that the cassette created with "new_episodes" has different expected behavior when opened with "once". 2014-11-21 09:47:28 -08:00
Nithin Reddy
5162d183e5 Fixes #123. When attempting to replay the same request twice using record_mode="new_episodes", vcr.py raises UnhandledHTTPRequestError. 2014-11-20 19:07:21 -08:00
Ivan Malison
9d52c3ed42 Remove warning message caused by lack of is_verified property on HTTPSConnection stub. 2014-11-13 16:32:38 -08:00
Ivan 'Goat' Malison
0e37759175 Merge pull request #118 from rtaboada/fix-response-stub-headers-field
Create headers field in VCRHTTPResponse. Fixes #117.
2014-11-03 04:07:12 -08:00
Ivan 'Goat' Malison
78c6258ba3 Merge pull request #119 from telaviv/make_boto_tests_pass_again
test_boto_stubs passes again.
2014-10-31 00:14:45 -07:00
Shawn Krisman
b047336690 test_boto_stubs passes again. 2014-10-30 16:08:17 -07:00
Rodrigo Taboada
c955a5ea88 String in request body should be bytes. 2014-10-24 18:30:32 -02:00
Rodrigo Taboada
5423d99f5a Tests for VCRHTTPResponse headers field. 2014-10-24 17:40:51 -02:00
Rodrigo Taboada
a71c15f398 Create headers field in VCRHTTPResponse. Fixes #117. 2014-10-24 16:37:12 -02:00
Ivan Malison
6e049ba7a1 version bump to v1.1.2 2014-10-08 12:11:53 -07:00
Ivan Malison
916e7839e5 Actually use pytest.raises in test. 2014-10-07 13:45:09 -07:00
Ivan Malison
99692a92d2 Handle unicode error in json serialize properly. 2014-10-07 13:21:47 -07:00
Ivan Malison
a9a68ba44b Random tweaks. 2014-10-05 18:37:01 -07:00
Ivan Malison
e9f35db405 Remove .travis.yml changes. 2014-10-05 16:42:46 -07:00
Ivan Malison
7193407a07 Remove ipdb because it causes python below 2.6 to blow up. 2014-10-03 01:40:02 -07:00
Ivan Malison
c3427ae3a2 Fix pip install of tox in travis. 2014-10-02 15:48:29 -07:00
Ivan Malison
3a46a6f210 travis through tox. 2014-10-02 15:26:22 -07:00
Ivan Malison
163181844b Refactor tox.ini using new 1.8 features. 2014-10-02 14:57:53 -07:00
Ivan Malison
2c6f072d11 better logging when matches aren't working. 2014-09-25 04:49:00 -07:00
26 changed files with 507 additions and 296 deletions

View File

@@ -8,9 +8,13 @@ env:
- WITH_LIB="requests2.2" - WITH_LIB="requests2.2"
- WITH_LIB="requests2.3" - WITH_LIB="requests2.3"
- WITH_LIB="requests2.4" - WITH_LIB="requests2.4"
- WITH_LIB="requests2.5"
- WITH_LIB="requests1.x" - WITH_LIB="requests1.x"
- WITH_LIB="httplib2" - WITH_LIB="httplib2"
- WITH_LIB="boto" - WITH_LIB="boto"
- WITH_LIB="urllib31.7"
- WITH_LIB="urllib31.9"
- WITH_LIB="urllib31.10"
matrix: matrix:
allow_failures: allow_failures:
- env: WITH_LIB="boto" - env: WITH_LIB="boto"
@@ -33,6 +37,10 @@ install:
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; 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.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 = "requests2.4" ] ; then pip install requests==2.4.0; fi
- 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 = "httplib2" ] ; then pip install httplib2; fi
- if [ $WITH_LIB = "boto" ] ; then pip install boto; 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 script: python setup.py test

View File

@@ -336,7 +336,7 @@ argument. It's usage is similar to that of `before_record`:
```python ```python
def scrub_string(string, replacement=''): def scrub_string(string, replacement=''):
def before_record_reponse(response): 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 return scrub_string
my_vcr = vcr.VCR( my_vcr = vcr.VCR(
@@ -362,6 +362,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 didn't notice them at all, and they will continue to hit the server as if VCR
were not there. 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 ## Installation
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need
@@ -457,6 +474,17 @@ API in version 1.0.x
## Changelog ## Changelog
* 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
compatibility with the version of httplib/urlib2 in python 2.7.9.
* 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test (thanks
@telaviv), fix new_episodes record mode (thanks @jashugan), fix Windows
connectionpool stub bug (thanks @gazpachoking), add support for requests 2.5
* 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 * 1.1.1 Use function signature preserving `wrapt.decorator` to write the
decorator version of use_cassette in order to ensure compatibility with decorator version of use_cassette in order to ensure compatibility with
py.test fixtures and python 2. Move all request filtering into the py.test fixtures and python 2. Move all request filtering into the

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python ##!/usr/bin/env python
import sys import sys
from setuptools import setup from setuptools import setup
@@ -20,7 +20,7 @@ class PyTest(TestCommand):
setup( setup(
name='vcrpy', name='vcrpy',
version='1.1.1', version='1.3.0',
description=( description=(
"Automatically mock your HTTP interactions to simplify and " "Automatically mock your HTTP interactions to simplify and "
"speed up testing" "speed up testing"

View File

@@ -1,5 +1,5 @@
'''Basic tests about cassettes''' # -*- coding: utf-8 -*-
# coding=utf-8 '''Basic tests for cassettes'''
# External imports # External imports
import os import os

View File

@@ -14,7 +14,7 @@ def test_boto_stubs(tmpdir):
from boto.https_connection import CertValidatingHTTPSConnection from boto.https_connection import CertValidatingHTTPSConnection
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
# Prove that the class was patched by the stub and that we can instantiate it. # Prove that the class was patched by the stub and that we can instantiate it.
assert CertValidatingHTTPSConnection is VCRCertValidatingHTTPSConnection assert issubclass(CertValidatingHTTPSConnection, VCRCertValidatingHTTPSConnection)
CertValidatingHTTPSConnection('hostname.does.not.matter') CertValidatingHTTPSConnection('hostname.does.not.matter')
def test_boto_without_vcr(): def test_boto_without_vcr():

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
'''Basic tests about save behavior''' '''Basic tests about save behavior'''
# coding=utf-8
# External imports # External imports
import os import os

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
'''Integration tests with httplib2''' '''Integration tests with httplib2'''
# coding=utf-8
# External imports # External imports
from six.moves.urllib_parse import urlencode 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: with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
resp, _ = httplib2.Http().request(url) resp, _ = httplib2.Http().request(url)
assert headers == resp.items() assert set(headers) == set(resp.items())
def test_multiple_requests(scheme, tmpdir): def test_multiple_requests(scheme, tmpdir):

View File

@@ -72,6 +72,31 @@ def test_new_episodes_record_mode(tmpdir):
assert len(cass.responses) == 2 assert len(cass.responses) == 2
def test_new_episodes_record_mode_two_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
url = 'http://httpbin.org/bytes/1024'
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create.
original_first_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# make the same request again
assert urlopen(url).read() == original_first_response
# in the "new_episodes" record mode, we can add the same request
# to the cassette without repercussions
original_second_response = urlopen(url).read()
with vcr.use_cassette(testfile, record_mode="once"):
# make the same request again
assert urlopen(url).read() == original_first_response
assert urlopen(url).read() == original_second_response
# now that we are back in once mode, this should raise
# an error.
with pytest.raises(Exception):
urlopen(url).read()
def test_all_record_mode(tmpdir): def test_all_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml')) testfile = str(tmpdir.join('recordmode.yml'))

View File

@@ -1,23 +1,17 @@
# -*- coding: utf-8 -*-
'''Test requests' interaction with vcr''' '''Test requests' interaction with vcr'''
# coding=utf-8
import os
import pytest import pytest
import vcr import vcr
from assertions import ( from assertions import assert_cassette_empty, assert_is_json
assert_cassette_empty,
assert_cassette_has_one_response,
assert_is_json
)
requests = pytest.importorskip("requests") requests = pytest.importorskip("requests")
@pytest.fixture(params=["https", "http"]) @pytest.fixture(params=["https", "http"])
def scheme(request): def scheme(request):
""" '''Fixture that returns both http and https.'''
Fixture that returns both http and https
"""
return request.param return request.param

View File

@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
'''Integration tests with urllib2''' '''Integration tests with urllib2'''
# coding=utf-8
# External imports
import os
import pytest import pytest
from six.moves.urllib.request import urlopen from six.moves.urllib.request import urlopen
@@ -11,7 +8,7 @@ from six.moves.urllib_parse import urlencode
# Internal imports # Internal imports
import vcr 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"]) @pytest.fixture(params=["https", "http"])

View 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')

View File

@@ -7,8 +7,10 @@ import pytest
import yaml import yaml
from vcr.cassette import Cassette from vcr.cassette import Cassette
from vcr.patch import force_reset
from vcr.errors import UnhandledHTTPRequestError from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.stubs import VCRHTTPSConnection
def test_cassette_load(tmpdir): def test_cassette_load(tmpdir):
@@ -181,3 +183,21 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
assert httplib.HTTPConnection is original assert httplib.HTTPConnection is original
assert httplib.HTTPConnection is second_cassette_HTTPConnection assert httplib.HTTPConnection is second_cassette_HTTPConnection
assert httplib.HTTPConnection is first_cassette_HTTPConnection assert httplib.HTTPConnection is first_cassette_HTTPConnection
def test_custom_patchers():
class Test(object):
attribute = None
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
old_attribute = Test.attribute
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is not old_attribute
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is old_attribute

View File

@@ -21,8 +21,8 @@ def test_headers():
('http://go.com/', 80), ('http://go.com/', 80),
('http://go.com:80/', 80), ('http://go.com:80/', 80),
('http://go.com:3000/', 3000), ('http://go.com:3000/', 3000),
('https://go.com/', 433), ('https://go.com/', 443),
('https://go.com:433/', 433), ('https://go.com:443/', 443),
('https://go.com:3000/', 3000), ('https://go.com:3000/', 3000),
]) ])
def test_port(uri, expected_port): def test_port(uri, expected_port):

View File

@@ -0,0 +1,68 @@
# coding: UTF-8
from vcr.stubs import VCRHTTPResponse
def test_response_should_have_headers_field():
recorded_response = {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"access-control-allow-credentials": ["true"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"access-control-allow-origin": ["*"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers is not None
def test_response_headers_should_be_equal_to_msg():
recorded_response = {
"status": {
"message": b"OK",
"code": 200
},
"headers": {
"content-length": ["0"],
"server": ["gunicorn/18.0"],
"connection": ["Close"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers == response.msg
def test_response_headers_should_have_correct_values():
recorded_response = {
"status": {
"message": "OK",
"code": 200
},
"headers": {
"content-length": ["10806"],
"date": ["Fri, 24 Oct 2014 18:35:37 GMT"],
"content-type": ["text/html; charset=utf-8"],
},
"body": {
"string": b""
}
}
response = VCRHTTPResponse(recorded_response)
assert response.headers.get('content-length') == "10806"
assert response.headers.get('date') == "Fri, 24 Oct 2014 18:35:37 GMT"

View File

@@ -1,21 +1,35 @@
import mock
import pytest import pytest
from vcr.serialize import deserialize from vcr.serialize import deserialize
from vcr.serializers import yamlserializer, jsonserializer from vcr.serializers import yamlserializer, jsonserializer
def test_deserialize_old_yaml_cassette(): def test_deserialize_old_yaml_cassette():
with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f: with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f:
with pytest.raises(ValueError): with pytest.raises(ValueError):
deserialize(f.read(), yamlserializer) deserialize(f.read(), yamlserializer)
def test_deserialize_old_json_cassette(): def test_deserialize_old_json_cassette():
with open('tests/fixtures/migration/old_cassette.json', 'r') as f: with open('tests/fixtures/migration/old_cassette.json', 'r') as f:
with pytest.raises(ValueError): with pytest.raises(ValueError):
deserialize(f.read(), jsonserializer) deserialize(f.read(), jsonserializer)
def test_deserialize_new_yaml_cassette(): def test_deserialize_new_yaml_cassette():
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f: with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
deserialize(f.read(), yamlserializer) deserialize(f.read(), yamlserializer)
def test_deserialize_new_json_cassette(): def test_deserialize_new_json_cassette():
with open('tests/fixtures/migration/new_cassette.json', 'r') as f: with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
deserialize(f.read(), jsonserializer) 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({})

View File

@@ -3,6 +3,7 @@ import pytest
from vcr import VCR, use_cassette from vcr import VCR, use_cassette
from vcr.request import Request from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection
def test_vcr_use_cassette(): def test_vcr_use_cassette():
@@ -74,3 +75,18 @@ def test_fixtures_with_use_cassette(random_fixture):
# fixtures. It is admittedly a bit strange because the test would never even # fixtures. It is admittedly a bit strange because the test would never even
# run if the relevant feature were broken. # run if the relevant feature were broken.
pass pass
def test_custom_patchers():
class Test(object):
attribute = None
attribute2 = None
test_vcr = VCR(custom_patches=((Test, 'attribute', VCRHTTPSConnection),))
with test_vcr.use_cassette('custom_patches'):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
with test_vcr.use_cassette('custom_patches', custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)):
assert issubclass(Test.attribute, VCRHTTPSConnection)
assert VCRHTTPSConnection is not Test.attribute
assert Test.attribute is Test.attribute2

195
tox.ini
View File

@@ -1,188 +1,27 @@
# 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] [tox]
envlist = envlist = {py26,py27,py33,py34,pypy}-{requests25,requests24,requests23,requests22,requests1,httplib2,urllib3,boto}
py26,
py27,
py33,
py34,
pypy,
py26requests24,
py27requests24,
py34requests24,
pypyrequests24,
py26requests23,
py27requests23,
py34requests23,
pypyrequests23,
py26requests22,
py27requests22,
py34requests22,
pypyrequests22,
py26requests1,
py27requests1,
py33requests1,
pypyrequests1,
py26httplib2,
py27httplib2,
py33httplib2,
py34httplib2,
pypyhttplib2,
[testenv] [testenv]
commands = commands =
py.test {posargs} py.test {posargs}
basepython =
py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
pypy: pypy
deps = deps =
mock mock
pytest pytest
pytest-localserver pytest-localserver
PyYAML PyYAML
ipdb requests1: requests==1.2.3
requests25: requests==2.5.0
[testenv:py26requests1] requests24: requests==2.4.0
basepython = python2.6 requests23: requests==2.3.0
deps = requests22: requests==2.2.1
{[testenv]deps} httplib2: httplib2
requests==1.2.3 urllib317: urllib3==1.7.1
urllib319: urllib3==1.9.1
[testenv:py27requests1] urllib3110: urllib3==1.10.2
basepython = python2.7 boto: boto
deps =
{[testenv]deps}
requests==1.2.3
[testenv:py33requests1]
basepython = python3.3
deps =
{[testenv]deps}
requests==1.2.3
[testenv:pypyrequests1]
basepython = pypy
deps =
{[testenv]deps}
requests==1.2.3
[testenv:py26requests24]
basepython = python2.6
deps =
{[testenv]deps}
requests==2.4.0
[testenv:py27requests24]
basepython = python2.7
deps =
{[testenv]deps}
requests==2.4.0
[testenv:py33requests24]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.4.0
[testenv:py34requests24]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.4.0
[testenv:pypyrequests24]
basepython = pypy
deps =
{[testenv]deps}
requests==2.4.0
[testenv:py26requests23]
basepython = python2.6
deps =
{[testenv]deps}
requests==2.3.0
[testenv:py27requests23]
basepython = python2.7
deps =
{[testenv]deps}
requests==2.3.0
[testenv:py33requests23]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.3.0
[testenv:py34requests23]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.3.0
[testenv:pypyrequests23]
basepython = pypy
deps =
{[testenv]deps}
requests==2.3.0
[testenv:py26requests22]
basepython = python2.6
deps =
{[testenv]deps}
requests==2.2.1
[testenv:py27requests22]
basepython = python2.7
deps =
{[testenv]deps}
requests==2.2.1
[testenv:py33requests22]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.2.1
[testenv:py34requests22]
basepython = python3.4
deps =
{[testenv]deps}
requests==2.2.1
[testenv:pypyrequests22]
basepython = pypy
deps =
{[testenv]deps}
requests==2.2.1
[testenv:py26httplib2]
basepython = python2.6
deps =
{[testenv]deps}
httplib2
[testenv:py27httplib2]
basepython = python2.7
deps =
{[testenv]deps}
httplib2
[testenv:py33httplib2]
basepython = python3.4
deps =
{[testenv]deps}
httplib2
[testenv:py34httplib2]
basepython = python3.4
deps =
{[testenv]deps}
httplib2
[testenv:pypyhttplib2]
basepython = pypy
deps =
{[testenv]deps}
httplib2

View File

@@ -1,4 +1,4 @@
'''The container for recorded requests and responses''' """The container for recorded requests and responses"""
import logging import logging
import contextlib2 import contextlib2
@@ -24,7 +24,7 @@ class CassetteContextDecorator(object):
removing cassettes. removing cassettes.
This class defers the creation of a new cassette instance until the point at This class defers the creation of a new cassette instance until the point at
which it is installned by context manager or decorator. The fact that a new 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 cassette is used with each application prevents the state of any cassette
from interfering with another. from interfering with another.
""" """
@@ -45,7 +45,8 @@ class CassetteContextDecorator(object):
log.debug('Entered context for cassette at {0}.'.format(cassette._path)) log.debug('Entered context for cassette at {0}.'.format(cassette._path))
yield cassette yield cassette
log.debug('Exiting context for cassette at {0}.'.format(cassette._path)) log.debug('Exiting context for cassette at {0}.'.format(cassette._path))
# TODO(@IvanMalison): Hmmm. it kind of feels like this should be somewhere else. # TODO(@IvanMalison): Hmmm. it kind of feels like this should be
# somewhere else.
cassette._save() cassette._save()
def __enter__(self): def __enter__(self):
@@ -65,11 +66,11 @@ class CassetteContextDecorator(object):
class Cassette(object): class Cassette(object):
'''A container for recorded requests and responses''' """A container for recorded requests and responses"""
@classmethod @classmethod
def load(cls, path, **kwargs): def load(cls, path, **kwargs):
'''Instantiate and load the cassette stored at the specified path.''' """Instantiate and load the cassette stored at the specified path."""
new_cassette = cls(path, **kwargs) new_cassette = cls(path, **kwargs)
new_cassette._load() new_cassette._load()
return new_cassette return new_cassette
@@ -85,7 +86,8 @@ class Cassette(object):
def __init__(self, path, serializer=yamlserializer, record_mode='once', def __init__(self, path, serializer=yamlserializer, record_mode='once',
match_on=(uri, method), filter_headers=(), match_on=(uri, method), filter_headers=(),
filter_query_parameters=(), before_record_request=None, filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(), ignore_localhost=()): before_record_response=None, ignore_hosts=(),
ignore_localhost=(), custom_patches=()):
self._path = path self._path = path
self._serializer = serializer self._serializer = serializer
self._match_on = match_on self._match_on = match_on
@@ -98,6 +100,7 @@ class Cassette(object):
self.dirty = False self.dirty = False
self.rewound = False self.rewound = False
self.record_mode = record_mode self.record_mode = record_mode
self.custom_patches = custom_patches
@property @property
def play_count(self): def play_count(self):
@@ -105,9 +108,7 @@ class Cassette(object):
@property @property
def all_played(self): 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) return self.play_count == len(self)
@property @property
@@ -124,7 +125,7 @@ class Cassette(object):
self.record_mode == 'none' self.record_mode == 'none'
def append(self, request, response): def append(self, request, response):
'''Add a request, response pair to this cassette''' """Add a request, response pair to this cassette"""
request = self._before_record_request(request) request = self._before_record_request(request)
if not request: if not request:
return return
@@ -153,10 +154,10 @@ class Cassette(object):
self.rewound self.rewound
def play_response(self, request): def play_response(self, request):
''' """
Get the response corresponding to a request, but only if it Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played hasn't been played back before, and mark it as played
''' """
for index, response in self._responses(request): for index, response in self._responses(request):
if self.play_counts[index] == 0: if self.play_counts[index] == 0:
self.play_counts[index] += 1 self.play_counts[index] += 1
@@ -168,11 +169,11 @@ class Cassette(object):
) )
def responses_of(self, request): def responses_of(self, request):
''' """
Find the responses corresponding to a request. Find the responses corresponding to a request.
This function isn't actually used by VCR internally, but is This function isn't actually used by VCR internally, but is
provided as an external API. provided as an external API.
''' """
responses = [response for index, response in self._responses(request)] responses = [response for index, response in self._responses(request)]
if responses: if responses:
@@ -214,11 +215,12 @@ class Cassette(object):
) )
def __len__(self): 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) return len(self.data)
def __contains__(self, request): 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): for index, response in self._responses(request):
return True if self.play_counts[index] == 0:
return True
return False return False

View File

@@ -12,7 +12,7 @@ from . import filters
class VCR(object): class VCR(object):
def __init__(self, serializer='yaml', cassette_library_dir=None, def __init__(self, serializer='yaml', cassette_library_dir=None,
record_mode="once", filter_headers=(), record_mode="once", filter_headers=(), custom_patches=(),
filter_query_parameters=(), before_record_request=None, filter_query_parameters=(), before_record_request=None,
before_record_response=None, ignore_hosts=(), before_record_response=None, ignore_hosts=(),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',), match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
@@ -43,6 +43,7 @@ class VCR(object):
self.before_record_response = before_record_response self.before_record_response = before_record_response
self.ignore_hosts = ignore_hosts self.ignore_hosts = ignore_hosts
self.ignore_localhost = ignore_localhost self.ignore_localhost = ignore_localhost
self._custom_patches = tuple(custom_patches)
def _get_serializer(self, serializer_name): def _get_serializer(self, serializer_name):
try: try:
@@ -68,6 +69,9 @@ class VCR(object):
def use_cassette(self, path, with_current_defaults=False, **kwargs): def use_cassette(self, path, with_current_defaults=False, **kwargs):
if with_current_defaults: if with_current_defaults:
return Cassette.use(path, self.get_path_and_merged_config(path, **kwargs)) 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) args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
return Cassette.use_arg_getter(args_getter) return Cassette.use_arg_getter(args_getter)
@@ -86,7 +90,8 @@ class VCR(object):
'match_on': self._get_matchers(matcher_names), 'match_on': self._get_matchers(matcher_names),
'record_mode': kwargs.get('record_mode', self.record_mode), 'record_mode': kwargs.get('record_mode', self.record_mode),
'before_record_request': self._build_before_record_request(kwargs), 'before_record_request': self._build_before_record_request(kwargs),
'before_record_response': self._build_before_record_response(kwargs) 'before_record_response': self._build_before_record_response(kwargs),
'custom_patches': self._custom_patches + kwargs.get('custom_patches', ())
} }
return path, merged_config return path, merged_config

View File

@@ -38,16 +38,16 @@ def headers(r1, r2):
return r1.headers == r2.headers 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]] differences = [m for m in matches if not m[0]]
if differences: if differences:
log.debug( log.debug(
'Requests differ according to the following matchers: ' + "Requests {0} and {1} differ according to "
str(differences) "the following matchers: {2}".format(r1, r2, differences)
) )
def requests_match(r1, r2, matchers): def requests_match(r1, r2, matchers):
matches = [(m(r1, r2), m) for m in 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]) return all([m[0] for m in matches])

View File

@@ -58,7 +58,7 @@ PARTS = [
def build_uri(**parts): def build_uri(**parts):
port = parts['port'] port = parts['port']
scheme = parts['protocol'] 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 '' parts['port'] = ':{0}'.format(port) if port != default_port else ''
return "{protocol}://{host}{port}{path}".format(**parts) return "{protocol}://{host}{port}{path}".format(**parts)

View File

@@ -59,7 +59,9 @@ class CassettePatcherBuilder(object):
def _build_patchers_from_mock_triples_decorator(function): def _build_patchers_from_mock_triples_decorator(function):
@functools.wraps(function) @functools.wraps(function)
def wrapped(self, *args, **kwargs): def wrapped(self, *args, **kwargs):
return self._build_patchers_from_mock_triples(function(self, *args, **kwargs)) return self._build_patchers_from_mock_triples(
function(self, *args, **kwargs)
)
return wrapped return wrapped
def __init__(self, cassette): def __init__(self, cassette):
@@ -67,9 +69,12 @@ class CassettePatcherBuilder(object):
self._class_to_cassette_subclass = {} self._class_to_cassette_subclass = {}
def build(self): def build(self):
return itertools.chain(self._httplib(), self._requests(), return itertools.chain(
self._urllib3(), self._httplib2(), self._httplib(), self._requests(), self._urllib3(), self._httplib2(),
self._boto()) self._boto(), self._build_patchers_from_mock_triples(
self._cassette.custom_patches
)
)
def _build_patchers_from_mock_triples(self, mock_triples): def _build_patchers_from_mock_triples(self, mock_triples):
for args in mock_triples: for args in mock_triples:
@@ -86,6 +91,23 @@ class CassettePatcherBuilder(object):
replacement_class)) replacement_class))
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj): 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 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, 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): if isinstance(replacement_dict_or_obj, dict):
for key, replacement_obj in replacement_dict_or_obj.items(): for key, replacement_obj in replacement_dict_or_obj.items():
replacement_obj = self._recursively_apply_get_cassette_subclass( replacement_obj = self._recursively_apply_get_cassette_subclass(
@@ -122,38 +144,8 @@ class CassettePatcherBuilder(object):
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
return () return ()
from .stubs.requests_stubs import VCRRequestsHTTPConnection, VCRRequestsHTTPSConnection from .stubs import requests_stubs
http_connection_remover = ConnectionRemover( return self._urllib3_patchers(cpool, requests_stubs)
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): def _patched_get_conn(self, connection_pool_class, connection_class_getter):
get_conn = connection_pool_class._get_conn get_conn = connection_pool_class._get_conn
@@ -162,6 +154,12 @@ class CassettePatcherBuilder(object):
connection = get_conn(pool, timeout) connection = get_conn(pool, timeout)
connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \ connection_class = pool.ConnectionCls if hasattr(pool, 'ConnectionCls') \
else connection_class_getter() 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): while not isinstance(connection, connection_class):
connection = get_conn(pool, timeout) connection = get_conn(pool, timeout)
return connection return connection
@@ -176,17 +174,13 @@ class CassettePatcherBuilder(object):
return new_connection return new_connection
return patched_new_conn return patched_new_conn
@_build_patchers_from_mock_triples_decorator
def _urllib3(self): def _urllib3(self):
try: try:
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass return ()
else: from .stubs import urllib3_stubs
from .stubs.urllib3_stubs import VCRVerifiedHTTPSConnection return self._urllib3_patchers(cpool, urllib3_stubs)
yield cpool, 'VerifiedHTTPSConnection', VCRVerifiedHTTPSConnection
yield cpool, 'HTTPConnection', VCRHTTPConnection
@_build_patchers_from_mock_triples_decorator @_build_patchers_from_mock_triples_decorator
def _httplib2(self): def _httplib2(self):
@@ -213,6 +207,40 @@ class CassettePatcherBuilder(object):
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
yield cpool, 'CertValidatingHTTPSConnection', 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): class ConnectionRemover(object):
@@ -234,7 +262,7 @@ class ConnectionRemover(object):
def __exit__(self, *args): def __exit__(self, *args):
for pool, connections in self._connection_pool_to_connections.items(): for pool, connections in self._connection_pool_to_connections.items():
readd_connections = [] readd_connections = []
while not pool.pool.empty() and connections: while pool.pool and not pool.pool.empty() and connections:
connection = pool.pool.get() connection = pool.pool.get()
if isinstance(connection, self._connection_class): if isinstance(connection, self._connection_class):
connections.remove(connection) connections.remove(connection)
@@ -273,8 +301,9 @@ def reset_patchers():
yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection) yield mock.patch.object(cpool, 'VerifiedHTTPSConnection', _VerifiedHTTPSConnection)
yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection) yield mock.patch.object(cpool, 'HTTPConnection', _HTTPConnection)
yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection) yield mock.patch.object(cpool, 'HTTPSConnection', _HTTPSConnection)
yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection) if hasattr(cpool.HTTPConnectionPool, 'ConnectionCls'):
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection) yield mock.patch.object(cpool.HTTPConnectionPool, 'ConnectionCls', _HTTPConnection)
yield mock.patch.object(cpool.HTTPSConnectionPool, 'ConnectionCls', _HTTPSConnection)
try: try:
import httplib2 as cpool import httplib2 as cpool

View File

@@ -51,7 +51,7 @@ class Request(object):
parse_uri = urlparse(self.uri) parse_uri = urlparse(self.uri)
port = parse_uri.port port = parse_uri.port
if port is None: if port is None:
port = {'https': 433, 'http': 80}[parse_uri.scheme] port = {'https': 443, 'http': 80}[parse_uri.scheme]
return port return port
@property @property

View File

@@ -11,10 +11,14 @@ def deserialize(cassette_string):
def serialize(cassette_dict): def serialize(cassette_dict):
try: try:
return json.dumps(cassette_dict, indent=4) return json.dumps(cassette_dict, indent=4)
except UnicodeDecodeError: except UnicodeDecodeError as original:
raise UnicodeDecodeError( raise UnicodeDecodeError(
"Error serializing cassette to JSON. ", original.encoding,
"Does this HTTP interaction contain binary data? ", b"Error serializing cassette to JSON",
"If so, use a different serializer (like the yaml serializer) ", original.start,
"for this request" 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?")
) )

View File

@@ -76,7 +76,7 @@ class VCRHTTPResponse(HTTPResponse):
self._closed = False self._closed = False
headers = self.recorded_response['headers'] headers = self.recorded_response['headers']
self.msg = parse_headers(headers) self.headers = self.msg = parse_headers(headers)
self.length = compat.get_header(self.msg, 'content-length') or None self.length = compat.get_header(self.msg, 'content-length') or None
@@ -128,7 +128,7 @@ class VCRConnection(object):
Returns empty string for the default port and ':port' otherwise Returns empty string for the default port and ':port' otherwise
""" """
port = self.real_connection.port 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 '' return ':{0}'.format(port) if port != default_port else ''
def _uri(self, url): def _uri(self, url):
@@ -217,11 +217,15 @@ class VCRConnection(object):
response = self.cassette.play_response(self._vcr_request) response = self.cassette.play_response(self._vcr_request)
return VCRHTTPResponse(response) return VCRHTTPResponse(response)
else: 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( raise CannotOverwriteExistingCassetteException(
"No match for the request (%r) was found. "
"Can't overwrite existing cassette (%r) in " "Can't overwrite existing cassette (%r) in "
"your current record mode (%r)." "your current record mode (%r)."
% (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 # Otherwise, we should send the request, then get the response
@@ -232,12 +236,16 @@ class VCRConnection(object):
self._vcr_request self._vcr_request
) )
) )
self.real_connection.request( # This is imported here to avoid circular import.
method=self._vcr_request.method, # TODO(@IvanMalison): Refactor to allow normal import.
url=self._url(self._vcr_request.uri), from vcr.patch import force_reset
body=self._vcr_request.body, with force_reset():
headers=self._vcr_request.headers, self.real_connection.request(
) method=self._vcr_request.method,
url=self._url(self._vcr_request.uri),
body=self._vcr_request.body,
headers=self._vcr_request.headers,
)
# get the response # get the response
response = self.real_connection.getresponse() response = self.real_connection.getresponse()
@@ -310,3 +318,4 @@ class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests''' '''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection _baseclass = HTTPSConnection
_protocol = 'https' _protocol = 'https'
is_verified = True

View File

@@ -1,8 +1,13 @@
'''Stubs for urllib3''' '''Stubs for urllib3'''
from urllib3.connectionpool import VerifiedHTTPSConnection from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
from ..stubs import VCRHTTPSConnection 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 _baseclass = VerifiedHTTPSConnection