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:
14
vcr/patch.py
14
vcr/patch.py
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user