diff --git a/.gitignore b/.gitignore index 2b468069..b7650420 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ _build build dist +/docs/html MANIFEST .coverage coverage diff --git a/README.rst b/README.rst index 166fbc37..fd15489a 100644 --- a/README.rst +++ b/README.rst @@ -64,17 +64,15 @@ Lint: pep8 treq pyflakes treq -Build docs: +Build docs:: -:: - - cd docs; make html + tox -e docs -.. |build| image:: https://secure.travis-ci.org/twisted/treq.svg?branch=master -.. _build: http://travis-ci.org/twisted/treq +.. |build| image:: https://api.travis-ci.org/twisted/treq.svg?branch=master +.. _build: https://travis-ci.org/twisted/treq .. |coverage| image:: https://codecov.io/github/twisted/treq/coverage.svg?branch=master .. _coverage: https://codecov.io/github/twisted/treq -.. |pypi| image:: http://img.shields.io/pypi/v/treq.svg +.. |pypi| image:: https://img.shields.io/pypi/v/treq.svg .. _pypi: https://pypi.python.org/pypi/treq diff --git a/docs/api.rst b/docs/api.rst index 31f8f946..105f510c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,5 +1,10 @@ +API Reference +============= + +This page lists all of the interfaces exposed by the `treq` package. + Making Requests -=============== +--------------- .. module:: treq @@ -12,73 +17,63 @@ Making Requests .. autofunction:: delete Accessing Content -================= +----------------- .. autofunction:: collect .. autofunction:: content .. autofunction:: text_content .. autofunction:: json_content -Responses -========= - -.. module:: treq.response - -.. class:: Response - - .. method:: collect(collector) - - Incrementally collect the body of the response. - - :param collector: A single argument callable that will be called - with chunks of body data as it is received. +HTTPClient Objects +------------------ - :returns: A `Deferred` that fires when the entire body has been - received. +.. module:: treq.client - .. method:: content() +The :class:`treq.client.HTTPClient` class provides the same interface as the :mod:`treq` module itself. - Read the entire body all at once. +.. autoclass:: HTTPClient + :members: + :undoc-members: - :returns: A `Deferred` that fires with a `bytes` object when the entire - body has been received. +Augmented Response Objects +-------------------------- - .. method:: text(encoding='ISO-8859-1') +:func:`treq.request`, :func:`treq.get`, etc. return an object which implements :class:`twisted.web.iweb.IResponse`, plus a few additional convenience methods: - Read the entire body all at once as text. - :param encoding: An encoding for the body, if none is given the - encoding will be guessed, defaulting to this argument. - - :returns: A `Deferred` that fires with a `unicode` object when the - entire body has been received. - - .. method:: json() +.. module:: treq.response - Read the entire body all at once and decode it as JSON. +.. class:: _Response - :returns: A `Deferred` that fires with the result of `json.loads` on - the body after it has been received. + .. automethod:: collect + .. automethod:: content + .. automethod:: json + .. automethod:: text + .. automethod:: history + .. automethod:: cookies - .. method:: history() + Inherited from :class:`twisted.web.iweb.IResponse`: - Get a list of all responses that (such as intermediate redirects), - that ultimately ended in the current response. + :ivar version: + :ivar code: + :ivar phrase: + :ivar headers: + :ivar length: + :ivar request: + :ivar previousResponse: - :returns: A `list` of :class:`treq.response.Response` objects. + .. method:: deliverBody(protocol) + .. method:: setPreviousResponse(response) - .. method:: cookies() +Test Helpers +------------ - :returns: A `CookieJar`. +.. automodule:: treq.testing + :members: - Inherited from twisted.web.iweb.IResponse. +MultiPartProducer Objects +------------------------- - .. attribute:: version - .. attribute:: code - .. attribute:: phrase - .. attribute:: headers - .. attribute:: length - .. attribute:: request - .. attribute:: previousResponse +:class:`treq.multipart.MultiPartProducer` is used internally when making requests which involve files. - .. method:: deliverBody(protocol) - .. method:: setPreviousResponse(response) +.. automodule:: treq.multipart + :members: diff --git a/docs/conf.py b/docs/conf.py index c36efd2f..38fe6512 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.viewcode', 'sphinx.ext.autodoc'] +extensions = ['sphinx.ext.viewcode', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -48,7 +48,7 @@ # built documents. # # The full version, including alpha/beta/rc tags. -release = open('../treq/_version').readline().strip() +from treq import __version__ as release version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation @@ -241,3 +241,8 @@ #texinfo_show_urls = 'footnote' RTD_NEW_THEME = True + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3.5', None), + 'twisted': ('https://twistedmatrix.com/documents/current/api/', None), +} diff --git a/docs/howto.rst b/docs/howto.rst index 3b597227..f1f519b0 100644 --- a/docs/howto.rst +++ b/docs/howto.rst @@ -1,16 +1,18 @@ +Use Cases +========= + Handling Streaming Responses ---------------------------- -In addition to `receiving responses `_ -with ``IResponse.deliverBody``. - -treq provides a helper function :py:func:`treq.collect` which takes a -``response``, and a single argument function which will be called with all new -data available from the response. Much like ``IProtocol.dataReceived``, +In addition to `receiving responses `_ +with :meth:`IResponse.deliverBody`, treq provides a helper function +:py:func:`treq.collect` which takes a +``response`` and a single argument function which will be called with all new +data available from the response. Much like :meth:`IProtocol.dataReceived`, :py:func:`treq.collect` knows nothing about the framing of your data and will simply call your collector function with any data that is currently available. -Here is an example which simply a ``file`` object's write method to +Here is an example which simply a file object's write method to :py:func:`treq.collect` to save the response body to a file. .. literalinclude:: examples/download_file.py @@ -23,7 +25,7 @@ Query Parameters ---------------- :py:func:`treq.request` supports a ``params`` keyword argument which will -be urlencoded and added to the ``url`` argument in addition to any query +be URL-encoded and added to the ``url`` argument in addition to any query parameters that may already exist. The ``params`` argument may be either a ``dict`` or a ``list`` of @@ -41,7 +43,7 @@ Full example: :download:`query_params.py ` Auth ---- -HTTP Basic authentication as specified in `RFC 2617`_ is easily supported by +HTTP Basic authentication as specified in :rfc:`2617` is easily supported by passing an ``auth`` keyword argument to any of the request functions. The ``auth`` argument should be a tuple of the form ``('username', 'password')``. @@ -52,8 +54,6 @@ The ``auth`` argument should be a tuple of the form ``('username', 'password')`` Full example: :download:`basic_auth.py ` -.. _RFC 2617: http://www.ietf.org/rfc/rfc2617.txt - Redirects --------- @@ -77,7 +77,7 @@ any of the request methods. Full example: :download:`disable_redirects.py ` You can even access the complete history of treq response objects by calling -the `history()` method on the the response. +the :meth:`~treq.response._Response.history()` method on the response. .. literalinclude:: examples/response_history.py :linenos: @@ -91,10 +91,10 @@ Cookies Cookies can be set by passing a ``dict`` or ``cookielib.CookieJar`` instance via the ``cookies`` keyword argument. Later cookies set by the server can be -retrieved using the :py:func:`treq.cookies` function. +retrieved using the :py:meth:`~treq.response._Response.cookies()` method. -The the object returned by :py:func:`treq.cookies` supports the same key/value -access as `requests cookies `_ +The object returned by :py:meth:`~treq.response._Response.cookies()` supports the same key/value +access as `requests cookies `_. .. literalinclude:: examples/using_cookies.py :linenos: diff --git a/docs/index.rst b/docs/index.rst index 8982dba8..ed42afa6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,8 @@ treq: High-level Twisted HTTP Client API ======================================== -treq depends on Twisted 12.3.0 or later and optionally pyOpenSSL. -Python 3 support requires at least Twisted 15.5. +treq depends on Twisted 16.0.0 or later and optionally pyOpenSSL. +It functions on Python 2.7 and Python 3.3+. Why? ---- @@ -10,7 +10,7 @@ Why? `requests`_ by `Kenneth Reitz`_ is a wonderful library. I want the same ease of use when writing Twisted applications. treq is not of course a perfect clone of `requests`. -I have tried to stay true to the do-what-I-mean spirit of the `requests` API and also kept the API familiar to users of `Twisted`_ and ``twisted.web.client.Agent`` on which treq is based. +I have tried to stay true to the do-what-I-mean spirit of the `requests` API and also kept the API familiar to users of `Twisted`_ and :class:`twisted.web.client.Agent` on which treq is based. .. _requests: http://python-requests.org/ .. _Kenneth Reitz: https://www.gittip.com/kennethreitz/ @@ -28,7 +28,7 @@ GET .. literalinclude:: examples/basic_get.py :linenos: - :lines: 7-11 + :lines: 7-10 Full example: :download:`basic_get.py ` @@ -45,17 +45,17 @@ Full example: :download:`basic_post.py ` Why not 100% requests-alike? ---------------------------- -Initially when I started off working on treq I thought the API should look exactly like `requests`_ except anything that would involve the network would return a ``Deferred``. +Initially when I started off working on treq I thought the API should look exactly like `requests`_ except anything that would involve the network would return a `Deferred`. Over time while attempting to mimic the `requests`_ API it became clear that not enough code could be shared between `requests`_ and treq for it to be worth the effort to translate many of the usage patterns from `requests`_. With the current version of treq I have tried to keep the API simple, yet remain familiar to users of Twisted and its lower-level HTTP libraries. -Feature Parity w/ Requests --------------------------- +Feature Parity with Requests +---------------------------- -Even though mimicing the `requests`_ API is not a goal, supporting most of it's features is. +Even though mimicking the `requests`_ API is not a goal, supporting most of its features is. Here is a list of `requests`_ features and their status in treq. +----------------------------------+----------+----------+ @@ -92,21 +92,14 @@ Here is a list of `requests`_ features and their status in treq. | Python 3.x | yes | yes | +----------------------------------+----------+----------+ -Howto ------ +Table of Contents +----------------- .. toctree:: :maxdepth: 3 howto - -API Documentation ------------------ - -.. toctree:: - :maxdepth: 2 - - api + api Indices and tables ================== diff --git a/setup.py b/setup.py index d36ab6fc..6f829308 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,6 @@ classifiers=classifiers, description="A requests-like API built on top of twisted.web's Agent", license="MIT/X", - url="http://github.com/twisted/treq", + url="https://github.com/twisted/treq", long_description=readme ) diff --git a/src/treq/api.py b/src/treq/api.py index 5262e6d3..0939cdc1 100644 --- a/src/treq/api.py +++ b/src/treq/api.py @@ -101,6 +101,9 @@ def request(method, url, **kwargs): (i.e. Ignore RFC2616 section 10.3 and follow redirects from POST requests). Default: ``False`` + :param bool unbuffered: Pass ``True`` to to disable response buffering. By + default treq buffers the entire response body in memory. + :rtype: Deferred that fires with an IResponse provider. """ diff --git a/src/treq/multipart.py b/src/treq/multipart.py index f93fcfe5..04699b38 100644 --- a/src/treq/multipart.py +++ b/src/treq/multipart.py @@ -22,39 +22,37 @@ @implementer(IBodyProducer) class MultiPartProducer(object): """ - L{MultiPartProducer} takes parameters for HTTP Request - produces bytes in multipart/form-data format defined - - U{Multipart} - and - U{Mime format} + :class:`MultiPartProducer` takes parameters for a HTTP request and + produces bytes in multipart/form-data format defined in :rfc:`2388` and + :rfc:`2046`. The encoded request is produced incrementally and the bytes are written to a consumer. - Fields should have form: [(parameter name, value), ...] + Fields should have form: ``[(parameter name, value), ...]`` Accepted values: * Unicode strings (in this case parameter will be encoded with utf-8) - * Tuples with (file name, content-type, L{IBodyProducer} objects) + * Tuples with (file name, content-type, + :class:`~twisted.web.iweb.IBodyProducer` objects) - Since MultiPart producer can accept L{IBodyProducer} like objects - and these objects sometimes cannot be read from in an event-driven manner - (e.g. L{FileBodyProducer} is passed in) - L{FileBodyProducer} uses a L{Cooperator} instance to schedule reads from - the undelying producers. This process is also paused and resumed based - on notifications from the L{IConsumer} provider being written to. + Since :class:`MultiPartProducer` can accept objects like + :class:`~twisted.web.iweb.IBodyProducer` which cannot be read from in an + event-driven manner it uses uses a + :class:`~twisted.internet.task.Cooperator` instance to schedule reads + from the underlying producers. Reading is also paused and resumed based on + notifications from the :class:`IConsumer` provider being written to. - @ivar _fileds: Sorted parameters, where all strings are enforced to be - unicode and file objects stacked on bottom (to produce a human readable - form-data request) + :ivar _fields: Sorted parameters, where all strings are enforced to be + unicode and file objects stacked on bottom (to produce a human readable + form-data request) - @ivar _cooperate: A method like L{Cooperator.cooperate} which is used to + :ivar _cooperate: A method like `Cooperator.cooperate` which is used to schedule all reads. - @ivar boundary: The generated boundary used in form-data encoding - @type boundary: L{bytes} + :ivar boundary: The generated boundary used in form-data encoding + :type boundary: `bytes` """ def __init__(self, fields, boundary=None, cooperator=task): @@ -72,10 +70,10 @@ def __init__(self, fields, boundary=None, cooperator=task): def startProducing(self, consumer): """ Start a cooperative task which will read bytes from the input file and - write them to C{consumer}. Return a L{Deferred} which fires after all + write them to `consumer`. Return a `Deferred` which fires after all bytes have been written. - @param consumer: Any L{IConsumer} provider + :param consumer: Any `IConsumer` provider """ self._task = self._cooperate(self._writeLoop(consumer)) d = self._task.whenDone() @@ -89,7 +87,7 @@ def maybeStopped(reason): def stopProducing(self): """ Permanently stop writing bytes from the file to the consumer by - stopping the underlying L{CooperativeTask}. + stopping the underlying `CooperativeTask`. """ if self._currentProducer: self._currentProducer.stopProducing() @@ -98,7 +96,7 @@ def stopProducing(self): def pauseProducing(self): """ Temporarily suspend copying bytes from the input file to the consumer - by pausing the L{CooperativeTask} which drives that activity. + by pausing the `CooperativeTask` which drives that activity. """ if self._currentProducer: # Having a current producer means that we are in @@ -113,8 +111,8 @@ def pauseProducing(self): def resumeProducing(self): """ - Undo the effects of a previous C{pauseProducing} and resume copying - bytes to the consumer by resuming the L{CooperativeTask} which drives + Undo the effects of a previous `pauseProducing` and resume copying + bytes to the consumer by resuming the `CooperativeTask` which drives the write activity. """ if self._currentProducer: @@ -125,9 +123,9 @@ def resumeProducing(self): def _calculateLength(self): """ Determine how many bytes the overall form post would consume. - The easiest way is to calculate is to generate of C{fObj} + The easiest way is to calculate is to generate of `fObj` (assuming it is not modified from this point on). - If the determination cannot be made, return C{UNKNOWN_LENGTH}. + If the determination cannot be made, return `UNKNOWN_LENGTH`. """ consumer = _LengthConsumer() for i in list(self._writeLoop(consumer)): @@ -234,7 +232,7 @@ def _enforce_unicode(value): This function enforces the stings passed to be unicode, so we won't need to guess what's the encoding of the binary strings passed in. If someone needs to pass the binary string, use BytesIO and wrap it with - L{FileBodyProducer} + `FileBodyProducer`. """ if isinstance(value, unicode): return value @@ -282,13 +280,13 @@ def _converted(fields): class _LengthConsumer(object): """ - L{_LengthConsumer} is used to calculate the length of the multi-part + `_LengthConsumer` is used to calculate the length of the multi-part request. The easiest way to do that is to consume all the fields, but instead writing them to the string just accumulate the request length. - @ivar length: The length of the request. Can be UNKNOWN_LENGTH - if consumer finds the field that has length that can not be calculated + :ivar length: The length of the request. Can be `UNKNOWN_LENGTH` + if consumer finds the field that has length that can not be calculated """ @@ -312,7 +310,7 @@ def write(self, value): class _Header(object): """ - L{_Header} This class is a tiny wrapper that produces + `_Header` This class is a tiny wrapper that produces request headers. We can't use standard python header class because it encodes unicode fields using =? bla bla ?= encoding, which is correct, but no one in HTTP world expects diff --git a/src/treq/response.py b/src/treq/response.py index c71423b9..5e6e52f1 100644 --- a/src/treq/response.py +++ b/src/treq/response.py @@ -5,24 +5,68 @@ from requests.cookies import cookiejar_from_dict -from treq.content import content, json_content, text_content +from treq.content import collect, content, json_content, text_content class _Response(proxyForInterface(IResponse)): + """ + A wrapper for :class:`twisted.web.iweb.IResponse` which manages cookies and + adds a few convenience methods. + """ + def __init__(self, original, cookiejar): self.original = original self._cookiejar = cookiejar + def collect(self, collector): + """ + Incrementally collect the body of the response, per + :func:`treq.collect()`. + + :param collector: A single argument callable that will be called + with chunks of body data as it is received. + + :returns: A `Deferred` that fires when the entire body has been + received. + """ + return collect(self.original, collector) + def content(self): + """ + Read the entire body all at once, per :func:`treq.content()`. + + :returns: A `Deferred` that fires with a `bytes` object when the entire + body has been received. + """ return content(self.original) - def json(self, *args, **kwargs): - return json_content(self.original, *args, **kwargs) + def json(self): + """ + Collect the response body as JSON per :func:`treq.json_content()`. - def text(self, *args, **kwargs): - return text_content(self.original, *args, **kwargs) + :rtype: Deferred that fires with the decoded JSON when the entire body + has been read. + """ + return json_content(self.original) + + def text(self, encoding='ISO-8859-1'): + """ + Read the entire body all at once as text, per + :func:`treq.text_content()`. + + :rtype: A `Deferred` that fires with a unicode string when the entire + body has been received. + """ + return text_content(self.original, encoding) def history(self): + """ + Get a list of all responses that (such as intermediate redirects), + that ultimately ended in the current response. The responses are + ordered chronologically. + + :returns: A `list` of :class:`~treq.response._Response` objects + """ if not hasattr(self, "previousResponse"): raise NotImplementedError( "Twisted < 13.1.0 does not support response history.") @@ -39,6 +83,11 @@ def history(self): return history def cookies(self): + """ + Get a copy of this response's cookies. + + :rtype: :class:`requests.cookies.RequestsCookieJar` + """ jar = cookiejar_from_dict({}) if self._cookiejar is not None: diff --git a/src/treq/test/test_response.py b/src/treq/test/test_response.py index 9886d8ce..010b0c30 100644 --- a/src/treq/test/test_response.py +++ b/src/treq/test/test_response.py @@ -1,8 +1,9 @@ -from twisted.trial.unittest import TestCase +from twisted.trial.unittest import SynchronousTestCase from twisted import version +from twisted.python.failure import Failure from twisted.python.versions import Version - +from twisted.web.client import ResponseDone from twisted.web.http_headers import Headers from treq.response import _Response @@ -15,16 +16,51 @@ class FakeResponse(object): - def __init__(self, code, headers): + def __init__(self, code, headers, body=()): self.code = code self.headers = headers self.previousResponse = None + self._body = body + self.length = sum(len(c) for c in body) def setPreviousResponse(self, response): self.previousResponse = response + def deliverBody(self, protocol): + for chunk in self._body: + protocol.dataReceived(chunk) + protocol.connectionLost(Failure(ResponseDone())) + + +class ResponseTests(SynchronousTestCase): + def test_collect(self): + original = FakeResponse(200, Headers(), body=[b'foo', b'bar', b'baz']) + calls = [] + _Response(original, None).collect(calls.append) + self.assertEqual([b'foo', b'bar', b'baz'], calls) + + def test_content(self): + original = FakeResponse(200, Headers(), body=[b'foo', b'bar', b'baz']) + self.assertEqual( + b'foobarbaz', + self.successResultOf(_Response(original, None).content()), + ) + + def test_json(self): + original = FakeResponse(200, Headers(), body=[b'{"foo": ', b'"bar"}']) + self.assertEqual( + {'foo': 'bar'}, + self.successResultOf(_Response(original, None).json()), + ) + + def test_text(self): + headers = Headers({b'content-type': [b'text/plain;charset=utf-8']}) + original = FakeResponse(200, headers, body=[b'\xe2\x98', b'\x83']) + self.assertEqual( + u'\u2603', + self.successResultOf(_Response(original, None).text()), + ) -class ResponseTests(TestCase): def test_history(self): redirect1 = FakeResponse( 301, diff --git a/src/treq/testing.py b/src/treq/testing.py index 9447ab60..7794d127 100644 --- a/src/treq/testing.py +++ b/src/treq/testing.py @@ -38,8 +38,8 @@ class RequestTraversalAgent(object): def __init__(self, rootResource): """ - :param rootResource: The twisted IResource at the root of the resource - tree. + :param rootResource: The Twisted `IResource` at the root of the + resource tree. """ self._memoryReactor = MemoryReactor() self._realAgent = Agent(reactor=self._memoryReactor) @@ -143,8 +143,8 @@ class _SynchronousProducer(object): This does not implement the :func:`IBodyProducer.stopProducing` method, because that is very difficult to trigger. (The request from - RequestTraversalAgent would have to be canceled while it is still in the - transmitting state), and the intent is to use RequestTraversalAgent to + `RequestTraversalAgent` would have to be canceled while it is still in the + transmitting state), and the intent is to use `RequestTraversalAgent` to make synchronous requests. """ @@ -184,7 +184,7 @@ def wrapper(*args, **kwargs): class StubTreq(object): """ A fake version of the treq module that can be used for testing that - provides all the function calls exposed in treq.__all__. + provides all the function calls exposed in :obj:`treq.__all__`. :ivar resource: A :obj:`Resource` object that provides the fake responses """ @@ -216,30 +216,31 @@ class StringStubbingResource(Resource): The resource uses the callable to return a real response as a result of a request. - The parameters for the callable are:: + The parameters for the callable are: - :param bytes method: An HTTP method - :param bytes url: The full URL of the request - :param dict params: A dictionary of query parameters mapping query keys - lists of values (sorted alphabetically) - :param dict headers: A dictionary of headers mapping header keys to - a list of header values (sorted alphabetically) - :param str data: The request body. - :return: a ``tuple`` of (code, headers, body) where the code is - the HTTP status code, the headers is a dictionary of bytes - (unlike the `headers` parameter, which is a dictionary of lists), - and body is a string that will be returned as the response body. + - ``method``, the HTTP method as `bytes`. + - ``url``, the the full URL of the request as `bytes`. + - ``params``, a dictionary of query parameters mapping query keys + lists of values (sorted alphabetically). + - ``headers``, a dictionary of headers mapping header keys to + a list of header values (sorted alphabetically). + - ``data``, the request body as `bytes`. + + The callable must return a ``tuple`` of (code, headers, body) where the + code is the HTTP status code, the headers is a dictionary of bytes (unlike + the `headers` parameter, which is a dictionary of lists), and body is + a string that will be returned as the response body. If there is a stubbing error, the return value is undefined (if an - exception is raised, :obj:`Resource` will just eat it and return 500 - in its place). The callable, or whomever creates the callable, should - have a way to handle error reporting. + exception is raised, :obj:`~twisted.web.resource.Resource` will just eat it + and return 500 in its place). The callable, or whomever creates the + callable, should have a way to handle error reporting. """ isLeaf = True def __init__(self, get_response_for): """ - See ``StringStubbingResource``. + See `StringStubbingResource`. """ Resource.__init__(self) self._get_response_for = get_response_for @@ -338,28 +339,28 @@ class RequestSequence(object): responses, or the request's paramters do not match the next item's expected request paramters, raises :obj:`AssertionError`. - For the expected request arguments:: + For the expected request arguments: - ``method`` should be `bytes` normalized to lowercase. - ``url`` should be normalized as per the transformations in - https://en.wikipedia.org/wiki/URL_normalization that (usually) preserve - semantics. A url to `http://something-that-looks-like-a-directory` - would be normalized to `http://something-that-looks-like-a-directory/` - and a url to `http://something-that-looks-like-a-page/page.html` - remains unchanged. + https://en.wikipedia.org/wiki/URL_normalization that (usually) preserve + semantics. A url to `http://something-that-looks-like-a-directory` + would be normalized to `http://something-that-looks-like-a-directory/` + and a url to `http://something-that-looks-like-a-page/page.html` + remains unchanged. - ``params`` is a dictionary mapping `bytes` to `lists` of `bytes` - ``headers`` is a dictionary mapping `bytes` to `lists` of `bytes` - note - that :obj:`twisted.web.client.Agent` may add its own headers though, - which are not guaranteed (for instance, `user-agent` or - `content-length`), so it's better to use some kind of matcher like - :obj:`HasHeaders`. + that :obj:`twisted.web.client.Agent` may add its own headers though, + which are not guaranteed (for instance, `user-agent` or + `content-length`), so it's better to use some kind of matcher like + :obj:`HasHeaders`. - ``data`` is a `bytes` - For the response:: + For the response: - ``code`` is an integer representing the HTTP status code to return - ``headers`` is a dictionary mapping `bytes` to `bytes` or `lists` of - `bytes` + `bytes` - ``body`` is a `bytes` :ivar list sequence: The sequence of expected request arguments mapped to diff --git a/tox.ini b/tox.ini index e87224af..40a66125 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = {pypy,py27,py33,py34,py35}-twisted_{lowest,latest}-pyopenssl_{lowest,latest}, {pypy,py27,py33,py34,py35}-twisted_trunk-pyopenssl_trunk, - pypi-readme, check-manifest, flake8 + pypi-readme, check-manifest, flake8, docs [testenv] deps = @@ -16,6 +16,8 @@ deps = {pypy,py27,py33,py34,py35}-pyopenssl_lowest: pyOpenSSL==0.15.1 pyopenssl_latest: pyOpenSSL pyopenssl_trunk: https://github.com/pyca/pyopenssl/archive/master.zip + + docs: Sphinx>=1.4.8 passenv = TERM # ensure colors commands = pip list @@ -38,3 +40,8 @@ deps = check-manifest commands = check-manifest + +[testenv:docs] +changedir = docs +commands = + sphinx-build -b html . html