diff options
| author | Gabriel Falcão <gabriel@nacaolivre.org> | 2018-04-26 20:49:36 -0300 |
|---|---|---|
| committer | Gabriel Falcão <gabriel@nacaolivre.org> | 2018-04-26 20:49:36 -0300 |
| commit | ba3a893d4d3feed5b3b9d57e4d3f3d1bfc6db810 (patch) | |
| tree | d45ed6e23c5e54c4a6e0e4c9296a58c2f87a00a9 | |
| parent | b2a6666bd5f45a08467bbc0bf00fea165bc7e5a4 (diff) | |
| download | httpretty-ba3a893d4d3feed5b3b9d57e4d3f3d1bfc6db810.tar.gz | |
more documentation
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | Pipfile.lock | 4 | ||||
| -rw-r--r-- | docs/source/api.rst | 135 | ||||
| -rw-r--r-- | docs/source/conf.py | 6 | ||||
| -rw-r--r-- | docs/source/contributing.rst | 12 | ||||
| -rw-r--r-- | httpretty/__init__.py | 54 | ||||
| -rw-r--r-- | httpretty/core.py | 263 | ||||
| -rw-r--r-- | tests/unit/test_httpretty.py | 15 |
8 files changed, 411 insertions, 82 deletions
@@ -42,6 +42,10 @@ clean: @printf "Cleaning up files that are already in .gitignore... " @for pattern in `cat .gitignore`; do rm -rf $$pattern; done @echo "OK!" + @printf "Deleting built documentation" + @rm -rf docs/build + @printf "Deleting dist files" + @rm -rf dist release: lint unit functional docs @rm -rf dist/* diff --git a/Pipfile.lock b/Pipfile.lock index 49f6273..387e867 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5fee12257c42c2db084c56469f71433f38ac66d58e299f2dfd9cd7822298b22e" + "sha256": "2598ebbdf276f48b6164053d0f52af007f02ae6224a4cb156d6bdd2791a5bbcb" }, "pipfile-spec": 6, "requires": { @@ -146,7 +146,7 @@ "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" ], - "markers": "python_version == '2.7'", + "markers": "python_version < '3.4'", "version": "==1.1.6" }, "flake8": { diff --git a/docs/source/api.rst b/docs/source/api.rst index 9896a76..1f34a37 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -3,11 +3,129 @@ API Reference ============= -.. autosummary:: - :toctree: stubs +.. _httpretty: +register_uri +------------ -.. _api core: +.. automethod:: httpretty.core.httpretty.register_uri + +enable +------ + +.. automethod:: httpretty.core.httpretty.enable + +disable +------- + +.. automethod:: httpretty.core.httpretty.disable + +is_enabled +---------- + +.. automethod:: httpretty.core.httpretty.is_enabled + +last_request +------------ + +.. autofunction:: httpretty.last_request + + +.. _httprettified: + +httprettified +------------- + +.. autofunction:: httpretty.core.httprettified + + +.. _httprettized: + +httprettized +------------ + +.. autoclass:: httpretty.core.httprettized + :members: + + +.. _HTTPrettyRequest: + +HTTPrettyRequest +---------------- + +.. autoclass:: httpretty.core.HTTPrettyRequest + :members: + + +.. _HTTPrettyRequestEmpty: + +HTTPrettyRequestEmpty +--------------------- + +.. autoclass:: httpretty.core.HTTPrettyRequestEmpty + :members: + +.. _FakeSockFile: + +FakeSockFile +------------ + +.. autoclass:: httpretty.core.FakeSockFile + :members: + + +.. _FakeSSLSocket: + +FakeSSLSocket +------------ + +.. autoclass:: httpretty.core.FakeSSLSocket + :members: + + +.. _FakeSockFile: + +FakeSockFile +------------ + +.. autoclass:: httpretty.core.FakeSockFile + :members: + + + +.. _URIInfo: + +URIInfo +------- + +.. autoclass:: httpretty.URIInfo + :members: + + +.. _URIMatcher: + +URIMatcher +---------- + +.. autoclass:: httpretty.URIMatcher + :members: + + +.. _Entry: + +Entry +----- + +.. autoclass:: httpretty.Entry + :members: + + +.. _api modules: + +Modules +======= + +.. _api module core: Core ---- @@ -15,12 +133,15 @@ Core .. automodule:: httpretty.core :members: +.. _api module http: + Http ---- .. automodule:: httpretty.http :members: +.. _api module utils: Utils ----- @@ -28,16 +149,10 @@ Utils .. automodule:: httpretty.utils :members: +.. _api module exceptions: Exceptions ---------- .. automodule:: httpretty.errors :members: - - -Compat ------- - -.. automodule:: httpretty.compat - :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 733f5e1..8679c2d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + import sys import sphinx_rtd_theme try: @@ -14,8 +15,8 @@ from httpretty.version import version # noqa project = 'HTTPretty' -copyright = '2018, Gabriel Falcão' -author = 'Gabriel Falcão' +copyright = '2018, Gabriel Falcao' +author = 'Gabriel Falcao' # The short X.Y version version = version @@ -31,6 +32,7 @@ extensions = [ 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', + 'sphinx.ext.autosummary', ] templates_path = ['_templates'] diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index e8588bc..6246852 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -59,11 +59,11 @@ License Main contributors ================= -HTTPretty has received `many contributions <https://github.com/gabrielfalcao/HTTPretty/graphs/contributors>` +HTTPretty has received `many contributions <https://github.com/gabrielfalcao/HTTPretty/graphs/contributors>`_ but some folks made remarkable contributions and deserve extra credit: -- Andrew Gross ~> @andrewgross -- Hugh Saunders ~> @hughsaunders -- James Rowe ~> @JNRowe -- Matt Luongo ~> @mhluongo -- Steve Pulec ~> @spulec +- Andrew Gross ~> `@andrewgross <https://github.com/andrewgross>`_ +- Hugh Saunders ~> `@hughsaunders <https://github.com/hughsaunders>`_ +- James Rowe ~> `@JNRowe <https://github.com/JNRowe>`_ +- Matt Luongo ~> `@mhluongo <https://github.com/mhluongo>`_ +- Steve Pulec ~> `@spulec <https://github.com/spulec>`_ diff --git a/httpretty/__init__.py b/httpretty/__init__.py index b306a3e..e4f2bed 100644 --- a/httpretty/__init__.py +++ b/httpretty/__init__.py @@ -23,21 +23,29 @@ # 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. -from __future__ import unicode_literals +# flake8: noqa -from .core import httpretty -from .core import httprettified -from .core import EmptyRequestHeaders -from .core import HTTPrettyRequestEmpty # noqa -from .core import URIInfo +from . import core from .errors import HTTPrettyError, UnmockedError from .version import version __version__ = version +# aliases +EmptyRequestHeaders = core.EmptyRequestHeaders +Entry = core.Entry +HTTPrettyRequestEmpty = core.HTTPrettyRequestEmpty +URIInfo = core.URIInfo +URIMatcher = core.URIMatcher +httprettified = core.httprettified +httprettized = core.httprettized +httpretty = core.httpretty + HTTPretty = httpretty activate = httprettified +enabled = httprettized + enable = httpretty.enable register_uri = httpretty.register_uri disable = httpretty.disable @@ -56,36 +64,14 @@ CONNECT = httpretty.CONNECT def last_request(): - """returns the last request""" + """ + :returns: the last :py:class:`~httpretty.core.HTTPrettyRequest` + """ return httpretty.last_request def has_request(): - """returns a boolean indicating whether any request has been made""" + """ + :returns: bool - whether any request has been made + """ return not isinstance(httpretty.last_request.headers, EmptyRequestHeaders) - - -__all__ = [ - 'last_request', - 'has_request', - 'GET', - 'PUT', - 'POST', - 'DELETE', - 'HEAD', - 'PATCH', - 'OPTIONS', - 'CONNECT', - 'register_uri', - 'enable', - 'disable', - 'is_enabled', - 'reset', - 'Response', - 'URIInfo', - 'UnmockedError', - 'HTTPrettyError', - 'httpretty', - 'httprettified', - 'EmptyRequestHeaders', -] diff --git a/httpretty/core.py b/httpretty/core.py index da07206..9c604b0 100644 --- a/httpretty/core.py +++ b/httpretty/core.py @@ -122,9 +122,10 @@ def FALLBACK_FUNCTION(x): class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): - """Represents a HTTP request. It takes a valid multi-line, ``\r\n`` - separated string with HTTP headers and parse them out using the - internal `parse_request` method. + """ + Represents a HTTP request. It takes a valid multi-line, + ``\r\n`` separated string with HTTP headers and parse them out using + the internal `parse_request` method. It also replaces the `rfile` and `wfile` attributes with StringIO instances so that we guarantee that it won't make any I/O, neighter @@ -141,17 +142,14 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): attributes. Please notice that if you need a single value from a query string you will need to get it manually like: - :: + .. testcode:: >>> request.querystring {'name': ['Gabriel Falcao']} >>> print request.querystring['name'][0] - ``parsed_body`` -> a dictionary containing parsed request body or - None if HTTPrettyRequest doesn't know how to parse it. It - currently supports parsing body data that was sent under the - ``content`-type` headers values: ``application/json`` or - ``application/x-www-form-urlencoded`` + + """ def __init__(self, headers, body=''): # first of all, lets make sure that if headers or body are @@ -197,6 +195,12 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): # And the body will be attempted to be parsed as # `application/json` or `application/x-www-form-urlencoded` + """a dictionary containing parsed request body or None if + HTTPrettyRequest doesn't know how to parse it. It currently + supports parsing body data that was sent under the + ``content`-type` headers values: ``application/json`` or + ``application/x-www-form-urlencoded`` + """ self.parsed_body = self.parse_request_body(self._body) @property @@ -585,6 +589,8 @@ class fakesock(object): def fake_wrap_socket(orig_wrap_socket_fn, *args, **kw): + """drop-in replacement for py:func:`ssl.wrap_socket` + """ server_hostname = kw.get('server_hostname') if server_hostname is not None: matcher = httpretty.match_https_hostname(server_hostname) @@ -1028,7 +1034,8 @@ class URIMatcher(object): class httpretty(HttpBaseClass): - """The URI registration class""" + """manages HTTPretty's internal request/response registry and request matching. + """ _entries = {} latest_requests = [] @@ -1038,6 +1045,10 @@ class httpretty(HttpBaseClass): @classmethod def match_uriinfo(cls, info): + """ + :param info: an :py:class:`~httpretty.core.URIInfo` + :returns: a 2-item tuple: (:py:class:`~httpretty.core.URLMatcher`, :py:class:`~httpretty.core.URIInfo`) or ``(None, [])`` + """ items = sorted( cls._entries.items(), key=lambda matcher_entries: matcher_entries[0].priority, @@ -1051,6 +1062,10 @@ class httpretty(HttpBaseClass): @classmethod def match_https_hostname(cls, hostname): + """ + :param hostname: a string + :returns: an :py:class:`~httpretty.core.URLMatcher` or ``None`` + """ items = sorted( cls._entries.items(), key=lambda matcher_entries: matcher_entries[0].priority, @@ -1071,6 +1086,11 @@ class httpretty(HttpBaseClass): @classmethod def match_http_address(cls, hostname, port): + """ + :param hostname: a string + :param port: an integer + :returns: an :py:class:`~httpretty.core.URLMatcher` or ``None`` + """ items = sorted( cls._entries.items(), key=lambda matcher_entries: matcher_entries[0].priority, @@ -1099,6 +1119,26 @@ class httpretty(HttpBaseClass): @classmethod @contextlib.contextmanager def record(cls, filename, indentation=4, encoding='utf-8'): + """ + .. testcode:: + + import io + import json + import requests + import httpretty + + with httpretty.record('/tmp/ip.json'): + data = requests.get('https://httpbin.org/ip').json() + + with io.open('/tmp/ip.json') as fd: + assert data == json.load(fd) + + :param filename: a string + :param indentation: an integer, defaults to **4** + :param encoding: a string, defaults to **"utf-8"** + + :returns: a `context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_ + """ try: import urllib3 except ImportError: @@ -1147,10 +1187,27 @@ class httpretty(HttpBaseClass): @classmethod @contextlib.contextmanager - def playback(cls, origin): + def playback(cls, filename): + """ + .. testcode:: + + import io + import json + import requests + import httpretty + + with httpretty.record('/tmp/ip.json'): + data = requests.get('https://httpbin.org/ip').json() + + with io.open('/tmp/ip.json') as fd: + assert data == json.load(fd) + + :param filename: a string + :returns: a `context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_ + """ cls.enable() - data = json.loads(open(origin).read()) + data = json.loads(open(filename).read()) for item in data: uri = item['request']['uri'] method = item['request']['method'] @@ -1163,6 +1220,8 @@ class httpretty(HttpBaseClass): @classmethod def reset(cls): + """resets the internal state of HTTPretty, unregistering all URLs + """ POTENTIAL_HTTP_PORTS.intersection_update(DEFAULT_HTTP_PORTS) POTENTIAL_HTTPS_PORTS.intersection_update(DEFAULT_HTTPS_PORTS) cls._entries.clear() @@ -1171,6 +1230,18 @@ class httpretty(HttpBaseClass): @classmethod def historify_request(cls, headers, body='', append=True): + """appends request to a list for later retrieval + + .. testcode:: + + import httpretty + + httpretty.register_uri(httpretty.GET, 'https://httpbin.org/ip', body='') + with httpretty.enabled(): + requests.get('https://httpbin.org/ip') + + assert httpretty.latest_requests[-1].url == 'https://httpbin.org/ip' + """ request = HTTPrettyRequest(headers, body) cls.last_request = request if append or not cls.latest_requests: @@ -1180,14 +1251,38 @@ class httpretty(HttpBaseClass): return request @classmethod - def register_uri(cls, method, uri, body='HTTPretty :)', + def register_uri(cls, method, uri, body='{"message": "HTTPretty :)"}', adding_headers=None, forcing_headers=None, status=200, - responses=None, match_querystring=False, + responses=None, + match_querystring=False, priority=0, **headers): + """ + .. testcode:: + + import httpretty + + httpretty.register_uri(httpretty.GET, 'https://httpbin.org/ip', body='') + with httpretty.enabled(): + requests.get('https://httpbin.org/ip') + + assert httpretty.latest_requests[-1].url == 'https://httpbin.org/ip' + + + :param method: one of ``httpretty.GET``, ``httpretty.PUT``, ``httpretty.POST``, ``httpretty.DELETE``, ``httpretty.HEAD``, ``httpretty.PATCH``, ``httpretty.OPTIONS``, ``httpretty.CONNECT`` + :param uri: a string (e.g.: **"https://httpbin.org/ip"**) + :param body: a string, defaults to ``{"message": "HTTPretty :)"}`` + :param adding_headers: dict - headers to be added to the response + :param forcing_headers: dict - headers to be forcefully set in the response + :param status: an integer, defaults to **200** + :param responses: a list of entries, ideally each created with :py:meth:`~httpretty.core.httpretty.Response` + :param priority: an integer, useful for setting higher priority over previously registered urls. defaults to zero + :param match_querystring: bool - whether to take the querystring into account when matching an URL + :param headers: headers to be added to the response + """ uri_is_string = isinstance(uri, basestring) if uri_is_string and re.search(r'^\w+://[^/]+[.]\w{2,}$', uri): @@ -1228,17 +1323,49 @@ class httpretty(HttpBaseClass): forcing_headers=None, status=200, streaming=False, - **headers): - - headers[str('body')] = body - headers[str('adding_headers')] = adding_headers - headers[str('forcing_headers')] = forcing_headers - headers[str('status')] = int(status) - headers[str('streaming')] = streaming - return Entry(method, uri, **headers) + **kw): + """ + shortcut to create an :py:class:`~httpretty.core.Entry` that takes the body as first positional argument + + .. seealso:: the parameters of this function match those of the :py:class:`~httpretty.core.Entry` constructor + :param body: + :param method: one of ``httpretty.GET``, ``httpretty.PUT``, ``httpretty.POST``, ``httpretty.DELETE``, ``httpretty.HEAD``, ``httpretty.PATCH``, ``httpretty.OPTIONS``, ``httpretty.CONNECT`` + :param uri: + :param adding_headers: + :param forcing_headers: + :param status: defaults to **200** + :param streaming: defaults to **False** + :param kw: keyword-arguments passed onto the :py:class:`~httpretty.core.Entry` + :returns: an :py:class:`~httpretty.core.Entry` + """ + kw['body'] = body + kw['adding_headers'] = adding_headers + kw['forcing_headers'] = forcing_headers + kw['status'] = int(status) + kw['streaming'] = streaming + return Entry(method, uri, **kw) @classmethod def disable(cls): + """Disables HTTPretty entirely, putting the original :py:mod:`socket` + module back in its place. + + + .. code:: + + import re, json + import httpretty + + httpretty.enable() + # request passes through fake socket + response = requests.get('https://httpbin.org') + + httpretty.disable() + # request uses real python socket module + response = requests.get('https://httpbin.org') + + .. note:: This method does not call :py:meth:`httpretty.core.reset` automatically. + """ cls._is_enabled = False socket.socket = old_socket socket.SocketType = old_SocketType @@ -1280,10 +1407,48 @@ class httpretty(HttpBaseClass): @classmethod def is_enabled(cls): + """Check if HTTPretty is enabled + + :returns: bool + + .. testcode:: + + import httpretty + + httpretty.enable() + assert httpretty.is_enabled() == True + + httpretty.disable() + assert httpretty.is_enabled() == False + """ return cls._is_enabled @classmethod def enable(cls): + """Enables HTTPretty. + + .. testcode:: + + import re, json + import httpretty + + httpretty.enable() + + httpretty.register_uri( + httpretty.GET, + re.compile(r'http://.*'), + body=json.dumps({'man': 'in', 'the': 'middle'}) + ) + + response = requests.get('https://foo.bar/foo/bar') + + response.json().should.equal({ + "man": "in", + "the": "middle", + }) + + .. warning:: after calling this method the original :py:mod:`socket` is replaced with :py:class:`httpretty.core.fakesock`. Make sure to call :py:meth:`~httpretty.disable` after done with your tests or use the :py:class:`httpretty.enabled` as decorator or `context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_ + """ cls._is_enabled = True # Some versions of python internally shadowed the # SocketType variable incorrectly https://bugs.python.org/issue20386 @@ -1332,7 +1497,18 @@ class httpretty(HttpBaseClass): class httprettized(object): + """`context-manager <https://docs.python.org/3/reference/datamodel.html#context-managers>`_ for enabling HTTPretty. + + .. testcode:: + + import httpretty + + httpretty.register_uri(httpretty.GET, 'https://httpbin.org/ip', body='') + with httpretty.enabled(): + requests.get('https://httpbin.org/ip') + assert httpretty.latest_requests[-1].url == 'https://httpbin.org/ip' + """ def __enter__(self): httpretty.reset() httpretty.enable() @@ -1343,7 +1519,50 @@ class httprettized(object): def httprettified(test): + """decorator for test functions + + :param test: a callable + + + example usage with `nosetests <https://nose.readthedocs.io/en/latest/>`_ + + .. testcode:: + import sure + from httpretty import httprettified + + @httprettified + def test_using_nosetests(): + httpretty.register_uri( + httpretty.GET, + 'https://httpbin.org/ip' + ) + + response = requests.get('https://httpbin.org/ip') + + response.json().should.equal({ + "message": "HTTPretty :)" + }) + + example usage with `unittest module <https://docs.python.org/3/library/unittest.html>`_ + + .. testcode:: + + import unittest + from sure import expect + from httpretty import httprettified + + @httprettified + class TestWithPyUnit(unittest.TestCase): + def test_httpbin(self): + httpretty.register_uri(httpretty.GET, 'https://httpbin.org/ip') + response = requests.get('https://httpbin.org/ip') + expect(response.json()).to.equal({ + "message": "HTTPretty :)" + }) + + + """ def decorate_unittest_TestCase_setUp(klass): # Prefer addCleanup (added in python 2.7), but fall back diff --git a/tests/unit/test_httpretty.py b/tests/unit/test_httpretty.py index 097efae..a99bd03 100644 --- a/tests/unit/test_httpretty.py +++ b/tests/unit/test_httpretty.py @@ -27,7 +27,10 @@ from __future__ import unicode_literals import json from sure import expect -from httpretty import HTTPretty, HTTPrettyError, core +import httpretty +from httpretty import HTTPretty +from httpretty import HTTPrettyError +from httpretty import core from httpretty.core import URIInfo, BaseClass, Entry, FakeSockFile, HTTPrettyRequest from httpretty.http import STATUSES @@ -298,11 +301,11 @@ def test_fake_socket_passes_through_setblocking(): def test_fake_socket_passes_through_fileno(): import socket - HTTPretty.enable() - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.truesock = MagicMock() - expect(s.fileno).called_with().should_not.throw(AttributeError) - s.truesock.fileno.assert_called_with() + with httpretty.enabled(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.truesock = MagicMock() + expect(s.fileno).called_with().should_not.throw(AttributeError) + s.truesock.fileno.assert_called_with() def test_fake_socket_passes_through_getsockopt(): |
