1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Major Refactor of Stubs

So the stubs were getting out of hand, and while trying to add support for the
putrequest and putheader methods, I had an idea for a cleaner way to handle
the stubs using the VCRHTTPConnection more as a proxy object.  So
VCRHTTPConnection and VCRHTTPSConnection no longer inherit from HTTPConnection
and HTTPSConnection.  This allowed me to get rid of quite a bit of
copy-and-pasted stdlib code.
This commit is contained in:
Kevin McCarthy
2014-03-08 19:22:58 -10:00
parent c0b88c2201
commit e84cd6f059
3 changed files with 65 additions and 106 deletions

View File

@@ -12,8 +12,8 @@ try:
# Try to save the original types for requests # Try to save the original types for requests
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection _VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
_HTTPConnection = cpool.HTTPConnection _cpoolHTTPConnection = cpool.HTTPConnection
_HTTPSConnection = cpool.HTTPSConnection _cpoolHTTPSConnection = cpool.HTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
@@ -72,10 +72,13 @@ def reset():
_HTTPSConnection _HTTPSConnection
try: try:
import requests.packages.urllib3.connectionpool as cpool import requests.packages.urllib3.connectionpool as cpool
# unpatch requests v1.x
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection cpool.HTTPConnection = _cpoolHTTPConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection # unpatch requests v2.x
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
cpool.HTTPSConnection = _cpoolHTTPSConnection
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
pass pass
@@ -83,6 +86,7 @@ def reset():
import urllib3.connectionpool as cpool import urllib3.connectionpool as cpool
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
cpool.HTTPConnection = _HTTPConnection cpool.HTTPConnection = _HTTPConnection
cpool.HTTPSConnection = _HTTPSConnection
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
except ImportError: # pragma: no cover except ImportError: # pragma: no cover

View File

@@ -10,6 +10,11 @@ class Request(object):
# make headers a frozenset so it will be hashable # make headers a frozenset so it will be hashable
self.headers = frozenset(headers.items()) self.headers = frozenset(headers.items())
def add_header(self, key, value):
tmp = dict(self.headers)
tmp[key] = value
self.headers = frozenset(tmp.iteritems())
@property @property
def url(self): def url(self):
return "{0}://{1}{2}".format(self.protocol, self.host, self.path) return "{0}://{1}{2}".format(self.protocol, self.host, self.path)

View File

@@ -49,8 +49,6 @@ class VCRHTTPResponse(object):
self.length = self.msg.getheader('content-length') or None self.length = self.msg.getheader('content-length') or None
def read(self, *args, **kwargs): def read(self, *args, **kwargs):
# Note: I'm pretty much ignoring any chunking stuff because
# I don't really understand what it is or how it works.
return self._content.read(*args, **kwargs) return self._content.read(*args, **kwargs)
def close(self): def close(self):
@@ -58,29 +56,28 @@ class VCRHTTPResponse(object):
return True return True
def isclosed(self): def isclosed(self):
# Urllib3 seems to call this because it actually uses
# the weird chunking support in httplib
return self.closed return self.closed
def getheaders(self): def getheaders(self):
headers = parse_headers(self.recorded_response['headers']) headers = parse_headers(self.recorded_response['headers'])
return headers.dict.iteritems() return headers.dict.iteritems()
def getheader(self, header, default=None):
headers = dict(((k, v) for k, v in self.getheaders()))
return headers.get(header, default)
class VCRConnectionMixin:
class VCRConnection:
# A reference to the cassette that's currently being patched in # A reference to the cassette that's currently being patched in
cassette = None cassette = None
def request(self, method, url, body=None, headers=None): def request(self, method, url, body=None, headers=None):
'''Persist the request metadata in self._vcr_request''' '''Persist the request metadata in self._vcr_request'''
# see VCRConnectionMixin._restore_socket for the motivation here
if hasattr(self, 'sock'):
del self.sock
self._vcr_request = Request( self._vcr_request = Request(
protocol=self._protocol, protocol=self._protocol,
host=self.host, host=self.real_connection.host,
port=self.port, port=self.real_connection.port,
method=method, method=method,
path=url, path=url,
body=body, body=body,
@@ -92,6 +89,26 @@ class VCRConnectionMixin:
# allows me to compare the entire length of the response to see if it # allows me to compare the entire length of the response to see if it
# exists in the cassette. # exists in the cassette.
def putrequest(self, method, url, *args, **kwargs):
"""
httplib gives you more than one way to do it. This is a way
to start building up a request. Usually followed by a bunch
of putheader() calls.
"""
self._vcr_request = Request(
protocol=self._protocol,
host=self.real_connection.host,
port=self.real_connection.port,
method=method,
path=url,
body="",
headers={}
)
def putheader(self, header, *values):
for value in values:
self._vcr_request.add_header(header, value)
def send(self, data): def send(self, data):
''' '''
This method is called after request(), to add additional data to the This method is called after request(), to add additional data to the
@@ -101,82 +118,17 @@ class VCRConnectionMixin:
self._vcr_request.body = (self._vcr_request.body or '') + data self._vcr_request.body = (self._vcr_request.body or '') + data
def close(self): def close(self):
self._restore_socket() # Note: the real connection will only close if it's open, so
self._baseclass.close(self) # no need to check that here.
self.real_connection.close()
def _restore_socket(self): def endheaders(self, *args, **kwargs):
""" """
Since some libraries (REQUESTS!!) decide to set options on Normally, this would atually send the request to the server.
connection.socket, I need to delete the socket attribute We are not sending the request until getting the response,
(which makes requests think this is a AppEngine connection) so bypass this method for now.
and then restore it when I want to make the actual request.
This function restores it to its standard initial value
(which is None)
""" """
if not hasattr(self, 'sock'): pass
self.sock = None
def _send_request(self, method, url, body, headers):
"""
Copy+pasted from python stdlib 2.6 source because it
has a call to self.send() which I have overridden
#stdlibproblems #fml
"""
header_names = dict.fromkeys([k.lower() for k in headers])
skips = {}
if 'host' in header_names:
skips['skip_host'] = 1
if 'accept-encoding' in header_names:
skips['skip_accept_encoding'] = 1
self.putrequest(method, url, **skips)
if body and ('content-length' not in header_names):
thelen = None
try:
thelen = str(len(body))
except TypeError, te:
# If this is a file-like object, try to
# fstat its file descriptor
import os
try:
thelen = str(os.fstat(body.fileno()).st_size)
except (AttributeError, OSError):
# Don't send a length if this failed
if self.debuglevel > 0:
print "Cannot stat!!"
if thelen is not None:
self.putheader('Content-Length', thelen)
for hdr, value in headers.iteritems():
self.putheader(hdr, value)
self.endheaders()
if body:
self._baseclass.send(self, body)
def _send_output(self, message_body=None):
"""
Copy-and-pasted from httplib, just so I can modify the self.send()
calls to call the superclass's send(), since I had to override the
send() behavior, since send() is both an external and internal
httplib API.
"""
self._buffer.extend(("", ""))
msg = "\r\n".join(self._buffer)
del self._buffer[:]
# If msg and message_body are sent in a single send() call,
# it will avoid performance problems caused by the interaction
# between delayed ack and the Nagle algorithm.
if isinstance(message_body, str):
msg += message_body
message_body = None
self._restore_socket()
self._baseclass.send(self, msg)
if message_body is not None:
#message_body was not a string (i.e. it is a file) and
#we must run the risk of Nagle
self._baseclass.send(self, message_body)
def getresponse(self, _=False): def getresponse(self, _=False):
'''Retrieve a the response''' '''Retrieve a the response'''
@@ -198,12 +150,7 @@ class VCRConnectionMixin:
# Otherwise, we should send the request, then get the response # Otherwise, we should send the request, then get the response
# and return it. # and return it.
# restore sock's value to None, since we need a real socket self.real_connection.request(
self._restore_socket()
#make the actual request
self._baseclass.request(
self,
method=self._vcr_request.method, method=self._vcr_request.method,
url=self._vcr_request.path, url=self._vcr_request.path,
body=self._vcr_request.body, body=self._vcr_request.body,
@@ -211,7 +158,7 @@ class VCRConnectionMixin:
) )
# get the response # get the response
response = self._baseclass.getresponse(self) response = self.real_connection.getresponse()
# put the response into the cassette # put the response into the cassette
response = { response = {
@@ -225,23 +172,26 @@ class VCRConnectionMixin:
self.cassette.append(self._vcr_request, response) self.cassette.append(self._vcr_request, response)
return VCRHTTPResponse(response) return VCRHTTPResponse(response)
def set_debuglevel(self, *args, **kwargs):
self.real_connection.set_debuglevel(*args, **kwargs)
class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection): def __init__(self, *args, **kwargs):
# need to temporarily reset here because the real connection
# inherits from the thing that we are mocking out. Take out
# the reset if you want to see what I mean :)
from vcr.patch import install, reset
reset()
self.real_connection = self._baseclass(*args, **kwargs)
install(self.cassette)
class VCRHTTPConnection(VCRConnection):
'''A Mocked class for HTTP requests''' '''A Mocked class for HTTP requests'''
# Can't use super since this is an old-style class
_baseclass = HTTPConnection _baseclass = HTTPConnection
_protocol = 'http' _protocol = 'http'
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection): class VCRHTTPSConnection(VCRConnection):
'''A Mocked class for HTTPS requests''' '''A Mocked class for HTTPS requests'''
_baseclass = HTTPSConnection _baseclass = HTTPSConnection
_protocol = 'https' _protocol = 'https'
def __init__(self, *args, **kwargs):
'''I overrode the init and copied a lot of the code from the parent
class because HTTPConnection when this happens has been replaced by
VCRHTTPConnection, but doing it here lets us use the original one.'''
HTTPConnection.__init__(self, *args, **kwargs)
self.key_file = kwargs.pop('key_file', None)
self.cert_file = kwargs.pop('cert_file', None)