Skip to content

Commit fb14739

Browse files
committed
Merge pull request #133 from IvanMalison/custom_patches
Custom patches
2 parents 83aed99 + a7c7e4e commit fb14739

File tree

7 files changed

+67
-9
lines changed

7 files changed

+67
-9
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,8 @@ API in version 1.0.x
457457

458458

459459
## Changelog
460+
* 1.2.0 Add custom_patches argument to VCR/Cassette objects to allow
461+
users to stub custom classes when cassettes become active.
460462
* 1.1.4 Add force reset around calls to actual connection from stubs, to ensure
461463
compatibility with the version of httplib/urlib2 in python 2.7.9.
462464
* 1.1.3 Fix python3 headers field (thanks @rtaboada), fix boto test (thanks

Diff for: setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python
1+
##!/usr/bin/env python
22

33
import sys
44
from setuptools import setup
@@ -20,7 +20,7 @@ def run_tests(self):
2020

2121
setup(
2222
name='vcrpy',
23-
version='1.1.4',
23+
version='1.2.0',
2424
description=(
2525
"Automatically mock your HTTP interactions to simplify and "
2626
"speed up testing"

Diff for: tests/unit/test_cassettes.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import yaml
88

99
from vcr.cassette import Cassette
10-
from vcr.patch import force_reset
1110
from vcr.errors import UnhandledHTTPRequestError
11+
from vcr.patch import force_reset
12+
from vcr.stubs import VCRHTTPSConnection
13+
1214

1315

1416
def test_cassette_load(tmpdir):
@@ -181,3 +183,21 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
181183
assert httplib.HTTPConnection is original
182184
assert httplib.HTTPConnection is second_cassette_HTTPConnection
183185
assert httplib.HTTPConnection is first_cassette_HTTPConnection
186+
187+
188+
def test_custom_patchers():
189+
class Test(object):
190+
attribute = None
191+
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
192+
assert issubclass(Test.attribute, VCRHTTPSConnection)
193+
assert VCRHTTPSConnection is not Test.attribute
194+
old_attribute = Test.attribute
195+
196+
with Cassette.use('custom_patches', custom_patches=((Test, 'attribute', VCRHTTPSConnection),)):
197+
assert issubclass(Test.attribute, VCRHTTPSConnection)
198+
assert VCRHTTPSConnection is not Test.attribute
199+
assert Test.attribute is not old_attribute
200+
201+
assert issubclass(Test.attribute, VCRHTTPSConnection)
202+
assert VCRHTTPSConnection is not Test.attribute
203+
assert Test.attribute is old_attribute

Diff for: tests/unit/test_vcr.py

+16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from vcr import VCR, use_cassette
55
from vcr.request import Request
6+
from vcr.stubs import VCRHTTPSConnection
67

78

89
def test_vcr_use_cassette():
@@ -74,3 +75,18 @@ def test_fixtures_with_use_cassette(random_fixture):
7475
# fixtures. It is admittedly a bit strange because the test would never even
7576
# run if the relevant feature were broken.
7677
pass
78+
79+
80+
def test_custom_patchers():
81+
class Test(object):
82+
attribute = None
83+
attribute2 = None
84+
test_vcr = VCR(custom_patches=((Test, 'attribute', VCRHTTPSConnection),))
85+
with test_vcr.use_cassette('custom_patches'):
86+
assert issubclass(Test.attribute, VCRHTTPSConnection)
87+
assert VCRHTTPSConnection is not Test.attribute
88+
89+
with test_vcr.use_cassette('custom_patches', custom_patches=((Test, 'attribute2', VCRHTTPSConnection),)):
90+
assert issubclass(Test.attribute, VCRHTTPSConnection)
91+
assert VCRHTTPSConnection is not Test.attribute
92+
assert Test.attribute is Test.attribute2

Diff for: vcr/cassette.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def __init__(self, path, serializer=yamlserializer, record_mode='once',
8787
match_on=(uri, method), filter_headers=(),
8888
filter_query_parameters=(), before_record_request=None,
8989
before_record_response=None, ignore_hosts=(),
90-
ignore_localhost=()):
90+
ignore_localhost=(), custom_patches=()):
9191
self._path = path
9292
self._serializer = serializer
9393
self._match_on = match_on
@@ -100,6 +100,7 @@ def __init__(self, path, serializer=yamlserializer, record_mode='once',
100100
self.dirty = False
101101
self.rewound = False
102102
self.record_mode = record_mode
103+
self.custom_patches = custom_patches
103104

104105
@property
105106
def play_count(self):

Diff for: vcr/config.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class VCR(object):
1313

1414
def __init__(self, serializer='yaml', cassette_library_dir=None,
15-
record_mode="once", filter_headers=(),
15+
record_mode="once", filter_headers=(), custom_patches=(),
1616
filter_query_parameters=(), before_record_request=None,
1717
before_record_response=None, ignore_hosts=(),
1818
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
@@ -43,6 +43,7 @@ def __init__(self, serializer='yaml', cassette_library_dir=None,
4343
self.before_record_response = before_record_response
4444
self.ignore_hosts = ignore_hosts
4545
self.ignore_localhost = ignore_localhost
46+
self._custom_patches = tuple(custom_patches)
4647

4748
def _get_serializer(self, serializer_name):
4849
try:
@@ -68,6 +69,9 @@ def _get_matchers(self, matcher_names):
6869
def use_cassette(self, path, with_current_defaults=False, **kwargs):
6970
if with_current_defaults:
7071
return Cassette.use(path, self.get_path_and_merged_config(path, **kwargs))
72+
# This is made a function that evaluates every time a cassette is made so that
73+
# changes that are made to this VCR instance that occur AFTER the use_cassette
74+
# decorator is applied still affect subsequent calls to the decorated function.
7175
args_getter = functools.partial(self.get_path_and_merged_config, path, **kwargs)
7276
return Cassette.use_arg_getter(args_getter)
7377

@@ -86,7 +90,8 @@ def get_path_and_merged_config(self, path, **kwargs):
8690
'match_on': self._get_matchers(matcher_names),
8791
'record_mode': kwargs.get('record_mode', self.record_mode),
8892
'before_record_request': self._build_before_record_request(kwargs),
89-
'before_record_response': self._build_before_record_response(kwargs)
93+
'before_record_response': self._build_before_record_response(kwargs),
94+
'custom_patches': self._custom_patches + kwargs.get('custom_patches', ())
9095
}
9196
return path, merged_config
9297

Diff for: vcr/patch.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,12 @@ def __init__(self, cassette):
6969
self._class_to_cassette_subclass = {}
7070

7171
def build(self):
72-
return itertools.chain(self._httplib(), self._requests(),
73-
self._urllib3(), self._httplib2(),
74-
self._boto())
72+
return itertools.chain(
73+
self._httplib(), self._requests(), self._urllib3(), self._httplib2(),
74+
self._boto(), self._build_patchers_from_mock_triples(
75+
self._cassette.custom_patches
76+
)
77+
)
7578

7679
def _build_patchers_from_mock_triples(self, mock_triples):
7780
for args in mock_triples:
@@ -88,6 +91,17 @@ def _build_patcher(self, obj, patched_attribute, replacement_class):
8891
replacement_class))
8992

9093
def _recursively_apply_get_cassette_subclass(self, replacement_dict_or_obj):
94+
"""One of the subtleties of this class is that it does not directly
95+
replace HTTPSConnection with VCRRequestsHTTPSConnection, but a
96+
subclass of this class that has cassette assigned to the
97+
appropriate value. This behavior is necessary to properly
98+
support nested cassette contexts
99+
100+
This function exists to ensure that we use the same class
101+
object (reference) to patch everything that replaces
102+
VCRRequestHTTP[S]Connection, but that we can talk about
103+
patching them with the raw references instead.
104+
"""
91105
if isinstance(replacement_dict_or_obj, dict):
92106
for key, replacement_obj in replacement_dict_or_obj.items():
93107
replacement_obj = self._recursively_apply_get_cassette_subclass(

0 commit comments

Comments
 (0)