From 431e385ded95a6f5c80e90ae20f78e927eb96f0a Mon Sep 17 00:00:00 2001 From: kevin1024 Date: Mon, 28 May 2012 17:18:22 -1000 Subject: [PATCH] initial commit --- .gitignore | 1 + README.md | 21 +++++++++++++++++++ setup.py | 11 ++++++++++ test.py | 28 +++++++++++++++++++++++++ vcr/__init__.py | 1 + vcr/cassette.py | 16 ++++++++++++++ vcr/files.py | 17 +++++++++++++++ vcr/patch.py | 25 ++++++++++++++++++++++ vcr/stubs.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 175 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 setup.py create mode 100644 test.py create mode 100644 vcr/__init__.py create mode 100644 vcr/cassette.py create mode 100644 vcr/files.py create mode 100644 vcr/patch.py create mode 100644 vcr/stubs.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dd1653 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +#VCR.py + +This is a proof-of-concept start at a python version of [Ruby's VCR +library](https://github.com/myronmarston/vcr). It +doesn't actually work. + +#What it is supposed to do +Simplify testing by recording all HTTP interactions and saving them to +"cassette" files, which are just yaml files. Then when you run your tests +again, they all just hit the text files instead of the internet. This speeds up +your tests and lets you work offline. + +#What it actually does +Uses up all your memory + + +#Similar libraries in Python +Neither of these really implement the API I want, but I have cribbed some code +from them. + * https://github.com/bbangert/Dalton + * https://github.com/storborg/replaylib diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f710b8d --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='VCR.py', + version='1.0', + description='VCR.py', + author='Kevin McCarthy', + author_email='mc@kevinmccarthy.org', + packages=['vcr'], + ) diff --git a/test.py b/test.py new file mode 100644 index 0000000..54362d5 --- /dev/null +++ b/test.py @@ -0,0 +1,28 @@ +import os +import unittest +import vcr +import urllib2 + +TEST_CASSETTE_FILE = 'test/test_req.yaml' + +class TestHttpRequest(unittest.TestCase): + + def setUp(self): + try: + os.remove(TEST_CASSETTE_FILE) + except OSError: + pass + + def test_response_code(self): + with vcr.use_cassette(TEST_CASSETTE_FILE): + code = urllib2.urlopen('http://www.iana.org/domains/example/').getcode() + self.assertEqual(code,urllib2.urlopen('http://www.iana.org/domains/example/').getcode()) + + def test_response_body(self): + with vcr.use_cassette('test/synopsis.yaml'): + body = urllib2.urlopen('http://www.iana.org/domains/example/').read() + self.assertEqual(body,urllib2.urlopen('http://www.iana.org/domains/example/').read()) + + +if __name__ == '__main__': + unittest.main() diff --git a/vcr/__init__.py b/vcr/__init__.py new file mode 100644 index 0000000..becd6b6 --- /dev/null +++ b/vcr/__init__.py @@ -0,0 +1 @@ +from .patch import use_cassette diff --git a/vcr/cassette.py b/vcr/cassette.py new file mode 100644 index 0000000..d2f1a2e --- /dev/null +++ b/vcr/cassette.py @@ -0,0 +1,16 @@ +import yaml + + +class Cassette(object): + def __init__(self): + self.requests = [] + self.responses = [] + + def serialize(self): + return yaml.dump([{ + 'request': req, + 'response': res, + } for req,res in zip(self.requests,self.responses)]) + + + diff --git a/vcr/files.py b/vcr/files.py new file mode 100644 index 0000000..7e036c0 --- /dev/null +++ b/vcr/files.py @@ -0,0 +1,17 @@ +import os +import yaml + +def load_cassette(cassette_path): + try: + return yaml.load(open(cassette_path)) + except IOError: + return None + +def save_cassette(cassette_path,cassette): + dirname,filename = os.path.split(cassette_path) + if not os.path.exists(dirname): + os.makedirs(dirname) + with open(cassette_path,'wc') as cassette_file: + cassette_file.write(cassette.serialize()) + + diff --git a/vcr/patch.py b/vcr/patch.py new file mode 100644 index 0000000..4f26307 --- /dev/null +++ b/vcr/patch.py @@ -0,0 +1,25 @@ +import httplib +from contextlib import contextmanager +from .stubs import VCRHTTPConnection, VCRHTTPSConnection + +_HTTPConnection = httplib.HTTPConnection +_HTTPSConnection = httplib.HTTPSConnection + + +def install(cassette_path): + httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection + httplib.HTTPSConnection = httplib.HTTPS._connection_class = VCRHTTPSConnection + httplib.HTTPConnection._vcr_cassette_path = cassette_path + httplib.HTTPSConnection._vcr_cassette_path = cassette_path + +def reset(): + httplib.HTTPConnection = httplib.HTTP._connection_class = _HTTPConnection + httplib.HTTPSConnection = httplib.HTTPS._connection_class = \ + _HTTPSConnection + +@contextmanager +def use_cassette(cassette_path): + install(cassette_path) + yield + reset() + diff --git a/vcr/stubs.py b/vcr/stubs.py new file mode 100644 index 0000000..ff3a28e --- /dev/null +++ b/vcr/stubs.py @@ -0,0 +1,55 @@ +from httplib import HTTPConnection +from .files import save_cassette, load_cassette +from .cassette import Cassette + +class VCRHTTPResponse(object): + def __init__(self,recorded_response): + self.recorded_response = recorded_response + self.msg = recorded_response['status']['message'] + self.reason = recorded_response['status']['message'] + self.status = recorded_response['status']['code'] + + def read(self,chunked=False): + return self.recorded_response['body']['string'] + + def getheaders(self): + return self.recorded_response['headers'] + + +class VCRHTTPConnection(HTTPConnection): + + def __init__(self,*args,**kwargs): + self._cassette = Cassette() + HTTPConnection.__init__(self,*args,**kwargs) + + def _save_cassette(self): + save_cassette(self._vcr_cassette_path,self._cassette) + + def request(self,method,url,body=None,headers={}): + old_cassette = load_cassette(self._vcr_cassette_path) + if old_cassette: + return + self._cassette.requests.append(dict( + method = method, + url = url, + body = body, + headers = headers + )) + return HTTPConnection.request(self,method,url,body=body,headers=headers) + + def getresponse(self,buffering=False): + old_cassette = load_cassette(self._vcr_cassette_path) + if old_cassette: + return VCRHTTPResponse(old_cassette[0]['response']) + response = HTTPConnection.getresponse(self,buffering=buffering) + self._cassette.responses.append({ + 'status':{'code':response.status,'message':response.reason}, + 'headers':dict(response.getheaders()), + 'body':{'string':response.read()}, + }) + self._save_cassette() + return response + + +class VCRHTTPSConnection(VCRHTTPConnection): + pass