From cd64f821e934fe525e9efba02b89a2f9ff221f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Falc=C3=A3o?= Date: Fri, 28 Jan 2011 23:16:39 -0200 Subject: [PATCH] starting with a proof of concept. It IS possible to monkey patch socket module --- .gitignore | 10 +++ COPYING | 22 +++++ Makefile | 40 +++++++++ README.md | 89 +++++++++++++++++++ httpretty/__init__.py | 144 +++++++++++++++++++++++++++++++ setup.py | 49 +++++++++++ tests/functional/__init__.py | 26 ++++++ tests/functional/test_urllib2.py | 42 +++++++++ tests/integration/__init__.py | 26 ++++++ tests/unit/__init__.py | 26 ++++++ 10 files changed, 474 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 Makefile create mode 100644 README.md create mode 100644 httpretty/__init__.py create mode 100755 setup.py create mode 100644 tests/functional/__init__.py create mode 100644 tests/functional/test_urllib2.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98b9059 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.pyc +.coverage +docs/_build/ +httpretty.egg-info/ +build/ +dist/ +.DS_Store +*.swp +.#* +#* diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..116064c --- /dev/null +++ b/COPYING @@ -0,0 +1,22 @@ +Copyright (C) <2011> Gabriel Falcão + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2fb8461 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +all: check_dependencies unit functional integration doctest + +filename=httpretty-`python -c 'import httpretty;print httpretty.version'`.tar.gz + +#export PYTHONPATH:= ${PWD} +export HTTPRETTY_DEPENDENCIES:= nose sure + +check_dependencies: + @echo "Checking for dependencies to run tests ..." + @for dependency in `echo $$HTTPRETTY_DEPENDENCIES`; do \ + python -c "import $$dependency" 2>/dev/null || (echo "You must install $$dependency in order to run httpretty's tests" && exit 3) ; \ + done + +unit: clean + @echo "Running unit tests ..." + @nosetests -s --verbosity=2 --with-coverage --cover-erase --cover-inclusive tests/unit --cover-package=httpretty + +functional: clean + @echo "Running functional tests ..." + @nosetests -s --verbosity=2 --with-coverage --cover-erase --cover-inclusive tests/functional --cover-package=httpretty + +integration: clean + @echo "Running integration tests ..." + @nosetests -s --verbosity=2 tests/integration + +doctest: clean + @cd docs && make doctest + +documentation: + @cd docs && make html + +clean: + @printf "Cleaning up files that are already in .gitignore... " + @for pattern in `cat .gitignore`; do rm -rf $$pattern; done + @echo "OK!" + +release: clean unit functional integration doctest deploy-documentation + @printf "Exporting to $(filename)... " + @tar czf $(filename) httpretty setup.py README.md COPYING + @echo "DONE!" diff --git a/README.md b/README.md new file mode 100644 index 0000000..076b77c --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# HTTPretty +> Version 0.1 + +# What + +HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's [FakeWeb](http://fakeweb.rubyforge.org/) + +# Motivation + +When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem: + + "I'm gonna need to mock all those requests" + +It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on. + +## The idea behind HTTPretty (how it works) + +HTTPretty [monkey matches](http://en.wikipedia.org/wiki/Monkey_patch) +Python's [socket](http://docs.python.org/library/socket.html) core +module, reimplementing the HTTP protocol, by mocking requests and +responses. + +This is a nice thing, if you consider that mostly python http module +will be "mockable". + +# Usage + + from httpretty import HTTPretty + HTTPretty.register_uri(HTTPretty.GET, "http://globo.com/", + body="The biggest portal in Brazil") + + fd = urllib2.urlopen('http://globo.com') + got = fd.read() + fd.close() + + print got + +**:: output ::** + + The biggest portal in Brazil + + +# Dependencies + +you will need **ONLY** if you decide to contribute to HTTPretty which means you're gonna need run our test suite + +* [nose](http://code.google.com/p/python-nose/) + > [sudo] pip install nose +* [sure](http://github.com/gabrielfalcao/sure/) + > [sudo] pip install sure + +# Contributing + +1. fork and clone the project +2. install the dependencies above +3. run the tests with make: + > make unit functional integration +4. hack at will +5. commit, push etc +6. send a pull request + +# License + + + Copyright (C) <2011> Gabriel Falcão + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/httpretty/__init__.py b/httpretty/__init__.py new file mode 100644 index 0000000..3d616fc --- /dev/null +++ b/httpretty/__init__.py @@ -0,0 +1,144 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +import re +import socket +from urlparse import urlsplit +from StringIO import StringIO + +class fakesock(object): + old_socket = socket.socket + + class socket(object): + _entry = None + def __init__(self, family, type, protocol): + self.family = family + self.type = type + self.protocol = protocol + + def connect(self, address): + self._address = (self._host, self._port) = address + self._closed = False + + def close(self): + self._closed = True + + def makefile(self, mode, bufsize): + self._mode = mode + self._bufsize = bufsize + fd = StringIO() + + if self._entry: + fd.write(self._entry.body.strip()) + fd.seek(0) + + + return fd + + def sendall(self, data): + verb, headers_string = data.split('\n', 1) + method, path, version = re.split('\s+', verb.strip(), 3) + + info = URIInfo(hostname=self._host, port=self._port, path=path) + + entry = HTTPretty._entries.get(info, None) + if not entry: + return + + if entry.method == method: + self._entry = entry + +def create_fake_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + s = fakesock.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + s.settimeout(timeout) + + s.connect(address) + return s + + + +class Entry(object): + def __init__(self, method, uri, body): + self.method = method + self.uri = uri + self.body = body + +class URIInfo(object): + def __init__(self, username='', password='', hostname='', port=80, path='/', query='', fragment=''): + self.username = username or '' + self.password = password or '' + self.hostname = hostname or '' + self.port = int(port) != 80 and str(port) or '' + self.path = path or '' + self.query = query or '' + self.fragment = fragment or '' + + def __unicode__(self): + attrs = 'username', 'password', 'hostname', 'port', 'path', 'query', 'fragment' + return ur'' % ", ".join(['%s="%s"' % (k, getattr(self, k, '')) for k in attrs]) + + def __repr__(self): + return unicode(self) + + def __hash__(self): + return hash(unicode(self)) + + def __eq__(self, other): + return unicode(self) == unicode(other) + + @classmethod + def from_uri(cls, uri): + result = urlsplit(uri) + return cls(result.username, + result.password, + result.hostname, + result.port or 80, + result.path, + result.query, + result.fragment) + + +class HTTPretty(object): + u"""The URI registration class""" + _entries = {} + + GET = 'GET' + PUT = 'PUT' + POST = 'POST' + DELETE = 'DELETE' + HEAD = 'HEAD' + + @classmethod + def register_uri(self, method, uri, body): + self._entries[URIInfo.from_uri(uri)] = Entry(method, uri, body) + + def __repr__(self): + return u'' % len(self._entries) + +socket.socket = fakesock.socket +socket.create_connection = create_fake_connection +socket.create = fakesock.socket +socket.__dict__.update(fakesock.__dict__) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..6736fcc --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +import os +from httpretty import version +from setuptools import setup + +def get_packages(): + # setuptools can't do the job :( + packages = [] + for root, dirnames, filenames in os.walk('httpretty'): + if '__init__.py' in filenames: + packages.append(".".join(os.path.split(root)).strip(".")) + + return packages + +setup(name='httpretty', + version=version, + description='HTTP client mock for Python', + author=u'Gabriel Falcao', + author_email='gabriel@nacaolivre.org', + url='http://github.com/gabrielfalcao/httpretty', + packages=get_packages() +) diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000..1e59f4f --- /dev/null +++ b/tests/functional/__init__.py @@ -0,0 +1,26 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + diff --git a/tests/functional/test_urllib2.py b/tests/functional/test_urllib2.py new file mode 100644 index 0000000..0eb6993 --- /dev/null +++ b/tests/functional/test_urllib2.py @@ -0,0 +1,42 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +import urllib2 +from sure import that, within, microseconds +from httpretty import HTTPretty + +#@within(five=microseconds) # ensure time to timeout actual request +def test_httpretty_should_mock_a_simple_get_with_urllib2_read(): + u"HTTPretty should mock a simple GET with urllib2.read()" + + HTTPretty.register_uri(HTTPretty.GET, "http://globo.com/", + body="The biggest portal in Brazil") + + fd = urllib2.urlopen('http://globo.com') + got = fd.read() + fd.close() + + assert that(got).equals('The biggest portal in Brazil') diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..1e59f4f --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,26 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..1e59f4f --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,26 @@ +# #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) <2011> Gabriel Falcão +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +