Skip to content

Commit

Permalink
Merge pull request #133 from IvanMalison/custom_patches
Browse files Browse the repository at this point in the history
Custom patches
  • Loading branch information
colonelpanic8 committed Jan 8, 2015
2 parents 83aed99 + a7c7e4e commit fb14739
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 9 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ API in version 1.0.x


## Changelog
* 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
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
##!/usr/bin/env python

import sys
from setuptools import setup
Expand All @@ -20,7 +20,7 @@ def run_tests(self):

setup(
name='vcrpy',
version='1.1.4',
version='1.2.0',
description=(
"Automatically mock your HTTP interactions to simplify and "
"speed up testing"
Expand Down
22 changes: 21 additions & 1 deletion tests/unit/test_cassettes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import yaml

from vcr.cassette import Cassette
from vcr.patch import force_reset
from vcr.errors import UnhandledHTTPRequestError
from vcr.patch import force_reset
from vcr.stubs import VCRHTTPSConnection



def test_cassette_load(tmpdir):
Expand Down Expand Up @@ -181,3 +183,21 @@ def test_nesting_context_managers_by_checking_references_of_http_connection():
assert httplib.HTTPConnection is original
assert httplib.HTTPConnection is second_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
16 changes: 16 additions & 0 deletions tests/unit/test_vcr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from vcr import VCR, use_cassette
from vcr.request import Request
from vcr.stubs import VCRHTTPSConnection


def test_vcr_use_cassette():
Expand Down Expand Up @@ -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
# run if the relevant feature were broken.
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
3 changes: 2 additions & 1 deletion vcr/cassette.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ 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=()):
ignore_localhost=(), custom_patches=()):
self._path = path
self._serializer = serializer
self._match_on = match_on
Expand All @@ -100,6 +100,7 @@ def __init__(self, path, serializer=yamlserializer, record_mode='once',
self.dirty = False
self.rewound = False
self.record_mode = record_mode
self.custom_patches = custom_patches

@property
def play_count(self):
Expand Down
9 changes: 7 additions & 2 deletions vcr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class VCR(object):

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,
before_record_response=None, ignore_hosts=(),
match_on=('method', 'scheme', 'host', 'port', 'path', 'query',),
Expand Down Expand Up @@ -43,6 +43,7 @@ def __init__(self, serializer='yaml', cassette_library_dir=None,
self.before_record_response = before_record_response
self.ignore_hosts = ignore_hosts
self.ignore_localhost = ignore_localhost
self._custom_patches = tuple(custom_patches)

def _get_serializer(self, serializer_name):
try:
Expand All @@ -68,6 +69,9 @@ def _get_matchers(self, matcher_names):
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)
return Cassette.use_arg_getter(args_getter)

Expand All @@ -86,7 +90,8 @@ def get_path_and_merged_config(self, path, **kwargs):
'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)
'before_record_response': self._build_before_record_response(kwargs),
'custom_patches': self._custom_patches + kwargs.get('custom_patches', ())
}
return path, merged_config

Expand Down
20 changes: 17 additions & 3 deletions vcr/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ def __init__(self, cassette):
self._class_to_cassette_subclass = {}

def build(self):
return itertools.chain(self._httplib(), self._requests(),
self._urllib3(), self._httplib2(),
self._boto())
return itertools.chain(
self._httplib(), self._requests(), self._urllib3(), self._httplib2(),
self._boto(), self._build_patchers_from_mock_triples(
self._cassette.custom_patches
)
)

def _build_patchers_from_mock_triples(self, mock_triples):
for args in mock_triples:
Expand All @@ -88,6 +91,17 @@ def _build_patcher(self, obj, patched_attribute, replacement_class):
replacement_class))

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
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.
"""
if isinstance(replacement_dict_or_obj, dict):
for key, replacement_obj in replacement_dict_or_obj.items():
replacement_obj = self._recursively_apply_get_cassette_subclass(
Expand Down

0 comments on commit fb14739

Please sign in to comment.