WebOb-1.8.6/0000755000076600000240000000000013611756053013501 5ustar xistencestaff00000000000000WebOb-1.8.6/.coveragerc0000644000076600000240000000021713357731131015617 0ustar xistencestaff00000000000000[run] parallel = true source = webob [paths] source = src/webob */site-packages/webob [report] show_missing = true precision = 2 WebOb-1.8.6/.gitignore0000644000076600000240000000026213261302253015457 0ustar xistencestaff00000000000000*$py.class *.egg *.pyc *.pyo *.swp *~ .*.swp .tox/ __pycache__/ _build/ build/ dist/ env*/ .coverage .coverage.* .cache/ WebOb.egg-info/ pytest*.xml coverage*.xml .pytest_cache/ WebOb-1.8.6/.travis.yml0000644000076600000240000000123013611751037015603 0ustar xistencestaff00000000000000# Wire up travis language: python sudo: false matrix: include: - python: 2.7 env: TOXENV=py27 - python: 3.4 env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.6 env: TOXENV=docs - python: nightly env: TOXENV=py37 - python: pypy env: TOXENV=pypy - python: 3.6 env: TOXENV=py27,py36,coverage allow_failures: - env: TOXENV=py37 install: - travis_retry pip install tox script: - travis_retry tox notifications: email: - pyramid-checkins@lists.repoze.org WebOb-1.8.6/CHANGES.txt0000644000076600000240000001723213611752074015316 0ustar xistencestaff000000000000001.8.6 (2020-01-21) ------------------ Experimental Features ~~~~~~~~~~~~~~~~~~~~~ - The SameSite value now includes a new option named "None", this is a new change that was introduced in https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 Please be aware that older clients are incompatible with this change: https://www.chromium.org/updates/same-site/incompatible-clients, WebOb does not enable SameSite on cookies by default, so there is no backwards incompatible change here. See https://github.com/Pylons/webob/issues/406 - Validation of SameSite values can be disabled by toggling a module flag. This is in anticipation of future changes in evolving cookie standards. The discussion in https://github.com/Pylons/webob/pull/407 (which initially expanded the allowed options) notes the sudden change to browser cookie implementation details may happen again. In May 2019, Google announced a new model for privacy controls in their browsers, which affected the list of valid options for the SameSite attribute of cookies. In late 2019, the company began to roll out these changes to their browsers to force developer adoption of the new specification. See https://www.chromium.org/updates/same-site and https://blog.chromium.org/2019/10/developers-get-ready-for-new.html for more details on this change. See https://github.com/Pylons/webob/pull/409 1.8.5 (2019-01-03) ------------------ Warnings ~~~~~~~~ - Fixed one last remaining invalid escape sequence in a docstring. 1.8.4 (2018-11-11) ------------------ Bugfix ~~~~~~ - Response.content_type now accepts unicode strings on Python 2 and encodes them to latin-1. See https://github.com/Pylons/webob/pull/389 and https://github.com/Pylons/webob/issues/388 - Accept header classes now support a .copy() function that may be used to create a copy. This allows ``create_accept_header`` and other like functions to accept an pre-existing Accept header. See https://github.com/Pylons/webob/pull/386 and https://github.com/Pylons/webob/issues/385 Warnings ~~~~~~~~ - Some backslashes introduced with the new accept handling code were causing DeprecationWarnings upon compiling the source to pyc files, all of the backslashes have been reigned in as appropriate, and users should no longer see DeprecationWarnings for invalid escape sequence. See https://github.com/Pylons/webob/issues/384 1.8.3 (2018-10-14) ------------------ Bugfix ~~~~~~ - ``acceptparse.AcceptValidHeader``, ``acceptparse.AcceptInvalidHeader``, and ``acceptparse.AcceptNoHeader`` will now always ignore offers that do not match the required media type grammar when calling ``.acceptable_offers()``. Previous versions raised a ``ValueError`` for invalid offers in ``AcceptValidHeader`` and returned them as acceptable in the others. See https://github.com/Pylons/webob/pull/372 Feature ~~~~~~~ - Add Request.remote_host, exposing REMOTE_HOST environment variable. - Added ``acceptparse.Accept.parse_offer`` to codify what types of offers are compatible with ``acceptparse.AcceptValidHeader.acceptable_offers``, ``acceptparse.AcceptMissingHeader.acceptable_offers``, and ``acceptparse.AcceptInvalidHeader.acceptable_offers``. This API also normalizes the offer with lowercased type/subtype and parameter names. See https://github.com/Pylons/webob/pull/376 and https://github.com/Pylons/webob/pull/379 1.8.2 (2018-06-05) ------------------ Bugfix ~~~~~~ - SameSite may now be passed as str or bytes to `Response.set_cookie` and `cookies.make_cookie`. This was an oversight as all other arguments would be correctly coerced before being serialized. See https://github.com/Pylons/webob/issues/361 and https://github.com/Pylons/webob/pull/362 1.8.1 (2018-04-10) ------------------ Bugfix ~~~~~~ - acceptparse.MIMEAccept which is deprecated in WebOb 1.8.0 made a backwards incompatible change that led to it raising on an invalid Accept header. This behaviour has now been reversed, as well as some other fixes to allow MIMEAccept to behave more like the old version. See https://github.com/Pylons/webob/pull/356 1.8.0 (2018-04-04) ------------------ Feature ~~~~~~~ - ``request.POST`` now supports any requests with the appropriate Content-Type. Allowing any HTTP method to access form encoded content, including DELETE, PUT, and others. See https://github.com/Pylons/webob/pull/352 Compatibility ~~~~~~~~~~~~~ - WebOb is no longer officially supported on Python 3.3 which was EOL'ed on 2017-09-29. Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Many changes have been made to the way WebOb does Accept handling, not just for the Accept header itself, but also for Accept-Charset, Accept-Encoding and Accept-Language. This was a `Google Summer of Code `_ project completed by Whiteroses (https://github.com/whiteroses). Many thanks to Google for running GSoC, the Python Software Foundation for organising and a huge thanks to Ira for completing the work. See https://github.com/Pylons/webob/pull/338 and https://github.com/Pylons/webob/pull/335. Documentation is available at https://docs.pylonsproject.org/projects/webob/en/master/api/webob.html - When calling a ``@wsgify`` decorated function, the default arguments passed to ``@wsgify`` are now used when called with the request, and not as a `start_response` .. code:: def hello(req, name): return "Hello, %s!" % name app = wsgify(hello, args=("Fred",)) req = Request.blank('/') resp = req.get_response(app) # => "Hello, Fred" resp2 = app(req) # => "Hello, Fred" Previously the ``resp2`` line would have failed with a ``TypeError``. With this change there is no way to override the default arguments with no arguments. See https://github.com/Pylons/webob/pull/203 - When setting ``app_iter`` on a ``Response`` object the ``content_md5`` header is no longer cleared. This behaviour is odd and disallows setting the ``content_md5`` and then returning an iterator for chunked content encoded responses. See https://github.com/Pylons/webob/issues/86 Experimental Features ~~~~~~~~~~~~~~~~~~~~~ These features are experimental and may change at any point in the future. - The cookie APIs now have the ability to set the SameSite attribute on a cookie in both ``webob.cookies.make_cookie`` and ``webob.cookies.CookieProfile``. See https://github.com/Pylons/webob/pull/255 Bugfix ~~~~~~ - Exceptions now use string.Template.safe_substitute rather than string.Template.substitute. The latter would raise for missing mappings, the former will simply not substitute the missing variable. This is safer in case the WSGI environ does not contain the keys necessary for the body template. See https://github.com/Pylons/webob/issues/345. - Request.host_url, Request.host_port, Request.domain correctly parse IPv6 Host headers as provided by a browser. See https://github.com/Pylons/webob/pull/332 - Request.authorization would raise ValueError for unusual or malformed header values. See https://github.com/Pylons/webob/issues/231 - Allow unnamed fields in form data to be properly transcoded when calling request.decode with an alternate encoding. See https://github.com/Pylons/webob/pull/309 - ``Response.__init__`` would discard ``app_iter`` when a ``Response`` had no body, this would cause issues when ``app_iter`` was an object that was tied to the life-cycle of a web application and had to be properly closed. ``app_iter`` is more advanced API for ``Response`` and thus even if it contains a body and is thus against the HTTP RFC's, we should let the users shoot themselves by returning a body. See https://github.com/Pylons/webob/issues/305 WebOb-1.8.6/HISTORY.txt0000644000076600000240000014367213611751037015415 0ustar xistencestaff000000000000001.7rc1 (2016-11-18) ------------------- Compatibility ~~~~~~~~~~~~~ - WebOb is no longer supported on Python 2.6 and PyPy3 (due to pip no longer supporting Python 3.2 even on PyPy) Backwards Incompatibility ~~~~~~~~~~~~~~~~~~~~~~~~~ - ``Response.set_cookie`` no longer accepts a key argument. This was deprecated in WebOb 1.5 and as mentioned in the deprecation, is being removed in 1.7 - ``Response.__init__`` will no longer set the default Content-Type, nor Content-Length on Responses that don't have a body. This allows WebOb to return proper responses for things like `Response(status='204 No Content')`. - ``Response.text`` will no longer raise if the Content-Type does not have a charset, it will fall back to using the new ``default_body_encoding`. To get the old behaviour back please sub-class ``Response`` and set ``default_body_encoding`` to ``None``. See https://github.com/Pylons/webob/pull/287 - WebOb no longer supports Chunked Encoding, this means that if you are using WebOb and need Chunked Encoding you will be required to have a proxy that unchunks the request for you. Please read https://github.com/Pylons/webob/issues/279 for more background. Feature ~~~~~~~ - ``Response`` has a new ``default_body_encoding`` which may be used to allow getting/setting ``Response.text`` when a Content-Type has no charset. See https://github.com/Pylons/webob/pull/287 - ``webob.Request`` with any HTTP method is now allowed to have a body. This allows DELETE to have a request body for passing extra information. See https://github.com/Pylons/webob/pull/283 and https://github.com/Pylons/webob/pull/274 - Add ``tell()`` to ``ResponseBodyFile`` so that it may be used for example for zipfile support. See https://github.com/Pylons/webob/pull/117 - Allow the return from ``wsgify.middleware`` to be used as a decorator. See https://github.com/Pylons/webob/pull/228 Bugfix ~~~~~~ - Fixup ``cgi.FieldStorage`` on Python 3.x to work-around issue reported in Python bug report 27777 and 24764. This is currently applied for Python versions less than 3.7. See https://github.com/Pylons/webob/pull/294 - ``Response.set_cookie`` now accepts ``datetime`` objects for the ``expires`` kwarg and will correctly convert them to UTC with no tzinfo for use in calculating the ``max_age``. See https://github.com/Pylons/webob/issues/254 and https://github.com/Pylons/webob/pull/292 - Fixes ``request.PATH_SAFE`` to contain all of the path safe characters according to RFC3986. See https://github.com/Pylons/webob/pull/291 - WebOb's exceptions will lazily read underlying variables when inserted into templates to avoid expensive computations/crashes when inserting into the template. This had a bad performance regression on Py27 because of the way the lazified class was created and returned. See https://github.com/Pylons/webob/pull/284 - ``wsgify.__call__`` raised a ``TypeError`` with an unhelpful message, it will now return the ``repr`` for the wrapped function: https://github.com/Pylons/webob/issues/119 - ``Response.content_type`` removes the charset content-type parameter unless the new content-type is a text like type that has a charset parameter. See https://github.com/Pylons/webob/pull/261 and https://github.com/Pylons/webob/issues/130 - ``Response.json``'s ``json.dumps``/``json.loads`` are now always UTF-8. It no longer tries to use the charset. - The ``Response.__init__`` will by default no longer set the Content-Type to the default if a ``headerlist`` is provided. This fixes issues whereby ``Request.get_response()`` would return a Response that didn't match the actual response. See https://github.com/Pylons/webob/pull/261 and https://github.com/Pylons/webob/issues/205 - Cleans up the remainder of the issues with the updated WebOb exceptions that were taught to return JSON in version 1.6. See https://github.com/Pylons/webob/issues/237 and https://github.com/Pylons/webob/issues/236 - ``Response.from_file`` now parses the status line correctly when the status line contains an HTTP with version, as well as a status text that contains multiple white spaces (e.g HTTP/1.1 404 Not Found). See https://github.com/Pylons/webob/issues/250 - ``Response`` now has a new property named ``has_body`` that may be used to interrogate the ``Response`` to find out if ``Response.body`` is or isn't set. This is used in the exception handling code so that if you use a WebOb HTTP Exception and pass a generator to ``app_iter`` WebOb won't attempt to read the whole thing and instead allows it to be returned to the WSGI server. See https://github.com/Pylons/webob/pull/259 1.6.0 (2016-03-15) ------------------ Compatibility ~~~~~~~~~~~~~ - Python 3.2 is no longer supported by WebOb Bugfix ~~~~~~ - Request.decode attempted to read from the an already consumed stream, it has now been redirected to another stream to read from. See https://github.com/Pylons/webob/pull/183 - The application/json media type does not allow for a charset as discovery of the encoding is done at the JSON layer. Upon initialization of a Response WebOb will no longer add a charset if the content-type is set to JSON. See https://github.com/Pylons/webob/pull/197 and https://github.com/Pylons/pyramid/issues/1611 Features ~~~~~~~~ - Lazily HTML escapes environment keys in HTTP Exceptions so that those keys in the environ that are not used in the output of the page don't raise an exception due to inability to be properly escaped. See https://github.com/Pylons/webob/pull/139 - MIMEAccept now accepts comparisons against wildcards, this allows one to match on just the media type or sub-type, without having to explicitly match on both the media type and sub-type at the same time. See https://github.com/Pylons/webob/pull/185 - Add the ability to return a JSON body from an exception. Using the Accept information in the request, the exceptions will now automatically return a JSON version of the exception instead of just HTML or text. See https://github.com/Pylons/webob/pull/230 and https://github.com/Pylons/webob/issues/209 Security ~~~~~~~~ - exc._HTTPMove and any subclasses will now raise a ValueError if the location field contains a line feed or carriage return. These values may lead to possible HTTP Response Splitting. The header_getter descriptor has also been modified to no longer accept headers with a line feed or carriage return. See: https://github.com/Pylons/webob/pull/229 and https://github.com/Pylons/webob/issues/217 1.5.1 (2015-10-30) ------------------ Bug Fixes ~~~~~~~~~ - The exceptions HTTPNotAcceptable, HTTPUnsupportedMediaType and HTTPNotImplemented will now correctly use the sub-classed template rather than the default error template. See https://github.com/Pylons/webob/issues/221 - Response's from_file now correctly deals with a status line that contains an HTTP version identifier. HTTP/1.1 200 OK is now correctly parsed, whereas before this would raise an error upon setting the Response.status in from_file. See https://github.com/Pylons/webob/issues/121 1.5.0 (2015-10-11) ------------------ Bug Fixes ~~~~~~~~~ - The cookie API functions will now make sure that `max_age` is an integer or an string that can convert to an integer. Previously passing in max_age='test' would have silently done the wrong thing. Features ~~~~~~~~ - Unbreak req.POST when the request method is PATCH. Instead of returning something cmpletely unrelated we return NoVar. See: https://github.com/Pylons/webob/pull/215 - HTTP Status Code 308 is now supported as a Permanent Redirect. See https://github.com/Pylons/webob/pull/207 Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``Response.set_cookie`` renamed the only required parameter from "key" to "name". The code will now still accept "key" as a keyword argument, and will issue a DeprecationWarning until WebOb 1.7. - The ``status`` attribute of a ``Response`` object no longer takes a string like ``None None`` and allows that to be set as the status. It now has to at least match the pattern of `` ``. Invalid status strings will now raise a ``ValueError``. 1.5.0a0 (2015-07-25) -------------------- Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``Morsel`` will no longer accept a cookie value that does not meet RFC6265's cookie-octet specification. Upon calling ``Morsel.serialize`` a warning will be issued, in the future this will raise a ``ValueError``, please update your cookie handling code. See https://github.com/Pylons/webob/pull/172 The cookie-octet specification in RFC6265 states the following characters are valid in a cookie value: =============== ======================================= Hex Range Actual Characters =============== ======================================= ``[0x21 ]`` ``!`` ``[0x25-0x2B]`` ``#$%&'()*+`` ``[0x2D-0x3A]`` ``-./0123456789:`` ``[0x3C-0x5B]`` ``<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[`` ``[0x5D-0x7E]`` ``]^_`abcdefghijklmnopqrstuvwxyz{|}~`` =============== ======================================= RFC6265 suggests using base 64 to serialize data before storing data in a cookie. Cookies that meet the RFC6265 standard will no longer be quoted, as this is unnecessary. This is a no-op as far as browsers and cookie storage is concerned. - ``Response.set_cookie`` now uses the internal ``make_cookie`` API, which will issue warnings if cookies are set with invalid bytes. See https://github.com/Pylons/webob/pull/172 Features ~~~~~~~~ - Add support for some new caching headers, stale-while-revalidate and stale-if-error that can be used by reverse proxies to cache stale responses temporarily if the backend disappears. From RFC5861. See https://github.com/Pylons/webob/pull/189 Bug Fixes ~~~~~~~~~ - Response.status now uses duck-typing for integers, and has also learned to raise a ValueError if the status isn't an integer followed by a space, and then the reason. See https://github.com/Pylons/webob/pull/191 - Fixed a bug in ``webob.multidict.GetDict`` which resulted in the QUERY_STRING not being updated when changes were made to query params using ``Request.GET.extend()``. - Read the body of a request if we think it might have a body. This fixes PATCH to support bodies. See https://github.com/Pylons/webob/pull/184 - Response.from_file returns HTTP headers as latin1 rather than UTF-8, this fixes the usage on Google AppEngine. See https://github.com/Pylons/webob/issues/99 and https://github.com/Pylons/webob/pull/150 - Fix a bug in parsing the auth parameters that contained bad white space. This makes the parsing fall in line with what's required in RFC7235. See https://github.com/Pylons/webob/issues/158 - Use '\r\n' line endings in ``Response.__str__``. See: https://github.com/Pylons/webob/pull/146 Documentation Changes ~~~~~~~~~~~~~~~~~~~~~ - ``response.set_cookie`` now has proper documentation for ``max_age`` and ``expires``. The code has also been refactored to use ``cookies.make_cookie`` instead of duplicating the code. This fixes https://github.com/Pylons/webob/issues/166 and https://github.com/Pylons/webob/issues/171 - Documentation didn't match the actual code for the wsgify function signature. See https://github.com/Pylons/webob/pull/167 - Remove the WebDAV only from certain HTTP Exceptions, these exceptions may also be used by REST services for example. 1.4 (2014-05-14) ---------------- Features ~~~~~~~~ - Remove ``webob.__version__``, the version number had not been kept in sync with the official pkg version. To obtain the WebOb version number, use ``pkg_resources.get_distribution('webob').version`` instead. Bug Fixes ~~~~~~~~~ - Fix a bug in ``EmptyResponse`` that prevents it from setting self.close as appropriate due to testing truthiness of object rather than if it is something other than ``None``. - Fix a bug in ``SignedSerializer`` preventing secrets from containing higher-order characters. See https://github.com/Pylons/webob/issues/136 - Use the ``hmac.compare_digest`` method when available for constant-time comparisons. 1.3.1 (2013-12-13) ------------------ Bug Fixes ~~~~~~~~~ - Fix a bug in ``SignedCookieProfile`` whereby we didn't keep the original serializer around, this would cause us to have ``SignedSerializer`` be added on top of a ``SignedSerializer`` which would cause it to be run twice when attempting to verify a cookie. See https://github.com/Pylons/webob/pull/127 Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - When ``CookieProfile.get_value`` and ``SignedCookieProfile.get_value`` fails to deserialize a badly encoded value, we now return ``None`` as if the cookie was never set in the first place instead of allowing a ``ValueError`` to be raised to the calling code. See https://github.com/Pylons/webob/pull/126 1.3 (2013-12-10) ---------------- Features ~~~~~~~~ - Added a read-only ``domain`` property to ``BaseRequest``. This property returns the domain portion of the host value. For example, if the environment contains an ``HTTP_HOST`` value of ``foo.example.com:8000``, ``request.domain`` will return ``foo.example.com``. - Added five new APIs: ``webob.cookies.CookieProfile``, ``webob.cookies.SignedCookieProfile``, ``webob.cookies.JSONSerializer`` and ``webob.cookies.SignedSerializer``, and ``webob.cookies.make_cookie``. These APIs are convenience APIs for generating and parsing cookie headers as well as dealing with signing cookies. - Cookies generated via webob.cookies quoted characters in cookie values that did not need to be quoted per RFC 6265. The following characters are no longer quoted in cookie values: ``~/=<>()[]{}?@`` . The full set of non-letter-or-digit unquoted cookie value characters is now ``!#$%&'*+-.^_`|~/: =<>()[]{}?@``. See http://tools.ietf.org/html/rfc6265#section-4.1.1 for more information. - Cookie names are now restricted to the set of characters expected by RFC 6265. Previously they could contain unsupported characters such as ``/``. - Older versions of Webob escaped the doublequote to ``\"`` and the backslash to ``\\`` when quoting cookie values. Now, instead, cookie serialization generates ``\042`` for the doublequote and ``\134`` for the backslash. This is what is expected as per RFC 6265. Note that old cookie values that do have the older style quoting in them will still be unquoted correctly, however. - Added support for draft status code 451 ("Unavailable for Legal Reasons"). See http://tools.ietf.org/html/draft-tbray-http-legally-restricted-status-00 - Added status codes 428, 429, 431 and 511 to ``util.status_reasons`` (they were already present in a previous release as ``webob.exc`` exceptions). Bug Fixes ~~~~~~~~~ - MIMEAccept happily parsed malformed wildcard strings like "image/pn*" at parse time, but then threw an AssertionError during matching. See https://github.com/Pylons/webob/pull/83 . - Preserve document ordering of GET and POST request data when POST data passed to Request.blank is a MultiDict. See https://github.com/Pylons/webob/pull/96 - Allow query strings attached to PATCH requests to populate request.params. See https://github.com/Pylons/webob/pull/106 - Added Python 3.3 trove classifier. 1.2.3 ------------ * Maintainership transferred to `Pylons Project ` * Fix parsing of form submissions where fields have transfer-content-encoding headers. 1.2.2 ------------ * Fix multiple calls to ``cache_expires()`` not fully overriding the previously set headers. * Fix parsing of form submissions where fields have different encodings. 1.2.1 ------------ * Add index page (e.g., ``index.html``) support for :class:`webob.static.DirectoryApp`. * Detect mime-type when creating a test request with file uploads (``Request.blank("/", POST=dict(file1=("foo.jpg", "xxx")))``) * Relax parsing of ``Accept`` and ``Range`` headers to allow uppercase and extra whitespace. * Fix docs references to some deprecated classes. 1.2 ------------ * Fix :mod:`webob.client` handling of connection-refused on Windows. * Use ``simplejson`` in :mod:`webob.request` if present. * Fix ``resp.retry_after = `` interpreting value as a UNIX timestamp (should interpret as time delta in seconds). 1.2rc1 ------------ * Add ``Response.json`` and ``Request.json`` which reads and sets the body using a JSON encoding (previously only the readable attribute ``Request.json_body`` existed). ``Request.json_body`` is still available as an alias. * Rename ``Response.status_int`` to ``Response.status_code`` (the ``.status_int`` name is still available and will be supported indefinitely). * Add ``Request.text``, the unicode version of the request body (similar to ``Response.text``). * Add :mod:`webob.client` which contains the WSGI application ``send_request_app`` and ``SendRequest``. All requests sent to this application are turned into HTTP requests. * Renamed ``Request.get_response(app)`` to ``Request.send(app)``. The ``.get_response()`` name is still available. * Use ``send_request_app`` as the default application for ``Request.send()``, so you can do: ``resp = Request.blank("http://python.org").send()`` * Add :mod:`webob.static` which contains two new WSGI applications, :class:`FileApp` serve one static file and :class:`DirectoryApp` to serve the content of a directory. They should provide a reusable implementation of :doc:`file-example`. It also comes with support for ``wsgi.file_wrapper``. The implementation has been imported and simplified from :mod:`PasteOb.fileapp`. * Add ``dev`` and ``docs`` setup.py aliases (to install development and docs dependencies respectively, e.g. "python setup.py dev"). 1.2b3 ------------ * Added ``request.host_port`` API (returns port number implied by HTTP_HOST, falling back to SERVER_PORT). * Added ``request.client_addr`` API (returns IP address implied by HTTP_X_FORWARDED_FOR, falling back to REMOTE_ADDR). * Fix corner-case ``response.status_int`` and ``response.status`` mutation bug on py3 (use explicit floor division). * Backwards incompatibility: Request and BaseRequest objects now return Unicode for ``request.path_info`` and ``request.script_name`` under Python 2. Rationale: the legacy behavior of returning the respective raw environ values was nonsensical on Python 3. Working with non-ascii encoded environ variables as raw WSGI values under Python 3 makes no sense, as PEP 3333 specifies that environ variables are bytes-tunneled-as-latin-1 strings. If you don't care about Python 3, and you need strict backwards compatibility, to get legacy behavior of returning bytes on Python 2 for these attributes, use ``webob.LegacyRequest`` instead of ``webob.Request``. Although it's possible to use ``webob.LegacyRequest`` under Python 3, it makes no sense, and it should not be used there. * The above backwards incompatibility fixed nonsensical behavior of ``request.host_url``, ``request.application_url``, ``request.path_url``, ``request.path``, ``request.path_qs``, ``request.url``, ``request.relative_url``, ``request.path_info_peek``, ``request.path_info_pop`` under Python 3. These methods previously dealt with raw SCRIPT_NAME and PATH_INFO values, which caused nonsensical results. * The WebOb Request object now respects an additional WSGI environment variable: ``webob.url_encoding``. ``webob.url_encoding`` will be used to decode the raw WSGI PATH_INFO and SCRIPT_NAME variables when the ``request.path_info`` and ``request.script_name`` APIs are used. * Request objects now accept an additional constructor parameter: ``url_encoding``. ``url_encoding`` will be used to decode PATH_INFO and SCRIPT_NAME from its WSGI-encoded values. If ``webob.url_encoding`` is not set in the environ and ``url_encoding`` is not passed to the Request constructor, the default value ``utf-8`` will be used to decode the PATH_INFO and SCRIPT_NAME. Note that passing ``url_encoding`` will cause the WSGI environment variable ``webob.url_encoding`` to be set. * Fix ``webob.response._request_uri`` internal function to generate sensible request URI under Python 3. This fixed a problem under Python 3 if you were using non-absolute Location headers in responses. 1.2b2 ------ * Fix ``request.cookies.get('name', 'default')``. Previously ``default`` was ignored. 1.2b1 --------- * Mutating the ``request.cookies`` property now reflects the mutations into the ``HTTP_COOKIES`` environ header. * ``Response.etag = (tag, False)`` sets weak etag. * ``Range`` only parses single range now. * ``Range.satisfiable(..)`` is gone. * ``Accept.best_matches()`` is gone; use ``list(request.accept)`` or ``request.accept.best_match(..)`` instead (applies to all Accept-* headers) or similar with ``request.accept_language``. * ``Response.request`` and ``Response.environ`` attrs are undeprecated and no longer raise exceptions when used. These can also be passed to the Response constructor. This is to support codebases that pass them to the constructor or assign them to a response instance. However, some behavior differences from 1.1 exist. In particular, synchronization is no longer done between environ and request attribute properties of Response; you may pass either to the constructor (or both) or assign one or the other or both, but they wont be managed specially and will remain the same over the lifetime of the response just as you passed them. Default values for both ``request`` and ``environ`` on any given response are ``None`` now. * Undeprecated ``uscript_name`` and ``upath_info``. * For backwards compatibility purposes, switch ``req.script_name`` and ``path_info`` back again to contain "raw" undecoded native strings rather than text. Use ``uscript_name`` and ``upath_info`` to get the text version of SCRIPT_NAME and PATH_INFO. * Don't raise an exception if ``unicode_errors`` or ``decode_param_names`` is passed to the Request constructor. Instead, emit a warning. For benefit of Pylons 1.X, which passes both. * Don't raise an exception if HTTPException.exception is used; instead emit a warning. For benefit of Pylons 1.X, which uses it. 1.2a2 --------- * ``req.script_name`` and ``path_info`` now contain text, not bytes. * Deprecated ``uscript_name`` and ``upath_info``. * ``charset`` argument to ``Request`` as well as the attribute can only be set to UTF-8 or the value already present in the ``Content-Type`` header. * ``unicode_errors`` attribute of ``Request`` and related functionality is gone. * To process requests that come in an encoding different from UTF-8, the request needs to be transcoded like this: ``req = req.decode('windows-1251')`` * Added support for weak ETag matching in conditional responses. * Most of etag-related functionality was refactored. 1.2a1 --------- * Python 3.2 compatibility. * No longer compatible with Python 2.5 (only 2.6, 2.7, and 3.2 are supported). * Switched VCS from Mercurial to Git * Moved development to `GitHub `_ * Added full history from PyCon 2011 sprint to the repository * Change ``LimitedLengthFile`` and ``FakeCGIBody`` to inherit from ``io.RawIOBase`` and benefit from ``io.BufferedReader``. * Do not set ``resp.request`` in ``req.get_response(app)`` * ``Response.request`` and ``.environ`` attrs are deprecated and raise exceptions when used. * Deprecated request attributes ``str_GET``, ``str_POST``, ``str_cookies`` and ``str_params`` now raise exceptions when touched. * Remove testing dependency on WebTest. * Remove UnicodeMultiDict class; the result of ``Request.GET`` and ``Request.POST`` is now just a plain ``MultiDict``. * The ``decode_param_names`` Request constructor argument has been removed, along with the ``Request.decode_param_names`` attribute. * The ``Request.as_string()`` method is now better known as ``Request.as_bytes()``. * The ``Request.from_string()`` method is now better known as ``Request.from_bytes()``. * A new method named ``Request.as_text()`` now exists. * A new method named ``Request.from_text()`` now exists. * The ``webob.dec.wsgify`` repr() is now much less informative, but a lot easier to test and maintain. 1.1.1 --------- * Fix disconnect detection being incorrect in some cases (`issue 21 `_). * Fix exception when calling ``.accept.best_match(..)`` on a header containing ``'*'`` (instead of ``'*/*'``). * Extract some of the ``Accept`` code into subclasses (``AcceptCharset``, ``AcceptLanguage``). * Improve language matching so that the app can now offer a generic language code and it will match any of the accepted dialects (``'en' in AcceptLanguage('en-gb')``). * Normalize locale names when matching (``'en_GB' in AcceptLanguage('en-gb')``). * Deprecate ``etag.weak_match(..)``. * Deprecate ``Response.request`` and ``Response.environ`` attrs. 1.1 --------- * Remove deprecation warnings for ``unicode_body`` and ``ubody``. 1.1rc1 --------- * Deprecate ``Response.ubody`` / ``.unicode_body`` in favor of new ``.text`` attribute (the old names will be removed in 1.3 or even later). * Make ``Response.write`` much more efficient (`issue 18 `_). * Make sure copying responses does not reset Content-Length or Content-MD5 of the original (and that of future copies). * Change ``del res.body`` semantics so that it doesn't make the response invalid, but only removes the response body. * Remove ``Response._body`` so the ``_app_iter`` is the only representation. 1.1b2 --------- * Add detection for browser / user-agent disconnects. If the client disconnected before sending the entire request body (POST / PUT), ``req.POST``, ``req.body`` and other related properties and methods will raise an exception. Previously this caused the application get a truncated request with no indication that it is incomplete. * Make ``Response.body_file`` settable. This is now valid: ``Response(body_file=open('foo.bin'), content_type=...)`` * Revert the restriction on req.body not being settable for GET and some other requests. Such requests actually can have a body according to HTTP BIS (see also `commit message `_) * Add support for file upload testing via ``Request.blank(POST=..)``. Patch contributed by Tim Perevezentsev. See also: `ticket `_, `changeset `_. * Deprecate ``req.str_GET``, ``str_POST``, ``str_params`` and ``str_cookies`` (warning). * Deprecate ``req.decode_param_names`` (warning). * Change ``req.decode_param_names`` default to ``True``. This means that ``.POST``, ``.GET``, ``.params`` and ``.cookies`` keys are now unicode. This is necessary for WebOb to behave as close as possible on Python 2 and Python 3. 1.1b1 --------- * We have acquired the webob.org domain, docs are now hosted at `docs.webob.org `_ * Make ``accept.quality(..)`` return best match quality, not first match quality. * Fix ``Range.satisfiable(..)`` edge cases. * Make sure ``WSGIHTTPException`` instances return the same headers for ``HEAD`` and ``GET`` requests. * Drop Python 2.4 support * Deprecate ``HTTPException.exception`` (warning on use). * Deprecate ``accept.first_match(..)`` (warning on use). Use ``.best_match(..)`` instead. * Complete deprecation of ``req.[str_]{post|query}vars`` properties (exception on use). * Remove ``FakeCGIBody.seek`` hack (no longer necessary). 1.0.8 ------ * Escape commas in cookie values (see also: `stdlib Cookie bug `_) * Change cookie serialization to more closely match how cookies usually are serialized (unquoted expires, semicolon separators even between morsels) * Fix some rare cases in cookie parsing * Enhance the req.is_body_readable to always guess GET, HEAD, DELETE and TRACE as unreadable and PUT and POST as readable (`issue 12 `_) * Deny setting req.body or req.body_file to non-empty values for GET, HEAD and other bodiless requests * Fix running nosetests with arguments on UNIX systems (`issue 11 `_) 1.0.7 ------ * Fix ``Accept`` header matching for items with zero-quality (`issue 10 `_) * Hide password values in ``MultiDict.__repr__`` 1.0.6 ------ * Use ``environ['wsgi.input'].read()`` instead of ``.read(-1)`` because the former is explicitly mentioned in PEP-3333 and CherryPy server does not support the latter. * Add new ``environ['webob.is_body_readable']`` flag which specifies if the input stream is readable even if the ``CONTENT_LENGTH`` is not set. WebOb now only ever reads the input stream if the content-length is known or this flag is set. * The two changes above fix a hangup with CherryPy and wsgiref servers (`issue 6 `_) * ``req.body_file`` is now safer to read directly. For ``GET`` and other similar requests it returns an empty ``StringIO`` or ``BytesIO`` object even if the server passed in something else. * Setting ``req.body_file`` to a string now produces a PendingDeprecationWarning. It will produce DeprecationWarning in 1.1 and raise an error in 1.2. Either set ``req.body_file`` to a file-like object or set ``req.body`` to a string value. * Fix ``.pop()`` and ``.setdefault(..)`` methods of ``req/resp.cache_control`` * Thanks to the participants of `Pyramid sprint at the PyCon US 2011 `_ WebOb now has 100% test coverage. 1.0.5 ------ * Restore Python 2.4 compatibility. 1.0.4 ------ * The field names escaping bug semi-fixed in 1.0.3 and originally blamed on cgi module was in fact a ``webob.request._encode_multipart`` bug (also in Google Chrome) and was lurking in webob code for quite some time -- 1.0.2 just made it trigger more often. Now it is fixed properly. * Make sure that req.url and related properties do not unnecessarily escape some chars (``:@&+$``) in the URI path (`issue 5 `_) * Revert some changes from 1.0.3 that have broken backwards compatibility for some apps. Getting ``req.body_file`` does not make input stream seekable, but there's a new property ``req.body_file_seekable`` that does. * ``Request.get_response`` and ``Request.call_application`` seek the input body to start before calling the app (if possible). * Accessing ``req.body`` 'rewinds' the input stream back to pos 0 as well. * When accessing ``req.POST`` we now avoid making the body seekable as the input stream data are preserved in ``FakeCGIBody`` anyway. * Add new method ``Request.from_string``. * Make sure ``Request.as_string()`` uses CRLF to separate headers. * Improve parity between ``Request.as_string()`` and ``.from_file``/``.from_string`` methods, so that the latter can parse output of the former and create a similar request object which wasn't always the case previously. 1.0.3 ------ * Correct a caching issue introduced in WebOb 1.0.2 that was causing unnecessary reparsing of POST requests. * Fix a bug regarding field names escaping for forms submitted as ``multipart/form-data``. For more infromation see `the bug report and discussion `_ and 1.0.4 notes for further fix. * Add ``req.http_version`` attribute. 1.0.2 ------ * Primary maintainer is now `Sergey Schetinin `_. * Issue tracker moved from `Trac `_ to bitbucket's `issue tracker `_ * WebOb 1.0.1 changed the behavior of ``MultiDict.update`` to be more in line with other dict-like objects. We now also issue a warning when we detect that the client code seems to expect the old, extending semantics. * Make ``Response.set_cookie(key, None)`` set the 'delete-cookie' (same as ``.delete_cookie(key)``) * Make ``req.upath_info`` and ``req.uscript_name`` settable * Add :meth:``Request.as_string()`` method * Add a ``req.is_body_seekable`` property * Support for the ``deflate`` method with ``resp.decode_content()`` * To better conform to WSGI spec we no longer attempt to use seek on ``wsgi.input`` file instead we assume it is not seekable unless ``env['webob.is_body_seekable']`` is set. When making the body seekable we set that flag. * A call to ``req.make_body_seekable()`` now guarantees that the body is seekable, is at 0 position and that a correct ``req.content_length`` is present. * ``req.body_file`` is always seekable. To access ``env['wsgi.input']`` without any processing, use ``req.body_file_raw``. (Partially reverted in 1.0.4) * Fix responses to HEAD requests with Range. * Fix ``del resp.content_type``, ``del req.body``, ``del req.cache_control`` * Fix ``resp.merge_cookies()`` when called with an argument that is not a Response instance. * Fix ``resp.content_body = None`` (was removing Cache-Control instead) * Fix ``req.body_file = f`` setting ``CONTENT_LENGTH`` to ``-1`` (now removes from environ) * Fix: make sure req.copy() leaves the original with seekable body * Fix handling of WSGI environs with missing ``SCRIPT_NAME`` * A lot of tests were added by Mariano Mara and Danny Navarro. 1.0.1 ----- * As WebOb requires Python 2.4 or later, drop some compatibility modules and update the code to use the decorator syntax. * Implement optional on-the-fly response compression (``resp.encode_content(lazy=True)``) * Drop ``util.safezip`` module and make ``util`` a module instead of a subpackage. Merge ``statusreasons`` into it. * Instead of using stdlib ``Cookie`` with monkeypatching, add a derived but thoroughly rewritten, cleaner, safer and faster ``webob.cookies`` module. * Fix: ``Response.merge_cookies`` now copies the headers before modification instead of doing it in-place. * Fix: setting request header attribute to ``None`` deletes that header. (Bug only affected the 1.0 release). * Use ``io.BytesIO`` for the request body file on Python 2.7 and newer. * If a UnicodeMultiDict was used as the ``multi`` argument of another UnicodeMultiDict, and a ``cgi.FieldStorage`` with a ``filename`` with high-order characters was present in the underlying UnicodeMultiDict, a ``UnicodeEncodeError`` would be raised when any helper method caused the ``_decode_value`` method to be called, because the method would try to decode an already decoded string. * Fix tests to pass under Python 2.4. * Add descriptive docstrings to each exception in ``webob.exc``. * Change the behaviour of ``MultiDict.update`` to overwrite existing header values instead of adding new headers. The extending semantics are now available via the ``extend`` method. * Fix a bug in ``webob.exc.WSGIHTTPException.__init__``. If a list of ``headers`` was passed as a sequence which contained duplicate keys (for example, multiple ``Set-Cookie`` headers), all but one of those headers would be lost, because the list was effectively flattened into a dictionary as the result of calling ``self.headers.update``. Fixed via calling ``self.headers.extend`` instead. 1.0 --- * 1.0, yay! * Pull in werkzeug Cookie fix for malformed cookie bug. * Implement :meth:`Request.from_file` and :meth:`Response.from_file` which are kind of the inversion of ``str(req)`` and ``str(resp)`` * Add optional ``pattern`` argument to :meth:`Request.path_info_pop` that requires the ``path_info`` segment to match the passed regexp to get popped and returned. * Rewrite most of descriptor implementations for speed. * Reorder descriptor declarations to group them by their semantics. * Move code around so that there are fewer compat modules. * Change :meth:``HTTPError.__str__`` to better conform to PEP 352. * Make :attr:`Request.cache_control` a view on the headers. * Correct Accept-Language and Accept-Charset matching to fully conform to the HTTP spec. * Expose parts of :meth:`Request.blank` as :func:`environ_from_url` and :func:`environ_add_POST` * Fix Authorization header parsing for some corner cases. * Fix an error generated if the user-agent sends a 'Content_Length' header (note the underscore). * Kill :attr:`Request.default_charset`. Request charset defaults to UTF-8. This ensures that all values in ``req.GET``, ``req.POST`` and ``req.params`` are always unicode. * Fix the ``headerlist`` and ``content_type`` constructor arguments priorities for :class:`HTTPError` and subclasses. * Add support for weak etags to conditional Response objects. * Fix locale-dependence for some cookie dates strings. * Improve overall test coverage. * Rename class ``webob.datastruct.EnvironHeaders`` to ``webob.headers.EnvironHeaders`` * Rename class ``webob.headerdict.HeaderDict`` to ``webob.headers.ResponseHeaders`` * Rename class ``webob.updatedict.UpdateDict`` to ``webob.cachecontrol.UpdateDict`` 0.9.8 ----- * Fix issue with WSGIHTTPException inadvertently generating unicode body and failing to encode it * WWW-Authenticate response header is accessible as ``response.www_authenticate`` * ``response.www_authenticate`` and ``request.authorization`` hold None or tuple ``(auth_method, params)`` where ``params`` is a dictionary (or a string when ``auth_method`` is not one of known auth schemes and for Authenticate: Basic ...) * Don't share response headers when getting a response like ``resp = req.get_response(some_app)``; this can avoid some funny errors with modifying headers and reusing Response objects. * Add `overwrite` argument to :meth:`Response.set_cookie` that make the new value overwrite the previously set. `False` by default. * Add `strict` argument to :meth:`Response.unset_cookie` that controls if an exception should be raised in case there are no cookies to unset. `True` by default. * Fix ``req.GET.copy()`` * Make sure that 304 Not Modified responses generated by :meth:`Response.conditional_response_app` exclude Content-{Length/Type} headers * Fix ``Response.copy()`` not being an independent copy * When the requested range is not satisfiable, return a 416 error (was returning entire body) * Truncate response for range requests that go beyond the end of body (was treating as invalid). 0.9.7.1 ------- * Fix an import problem with Pylons 0.9.7 ----- * Moved repository from svn location to http://bitbucket.org/ianb/webob/ * Arguments to :meth:`Accept.best_match` must be specific types, not wildcards. The server should know a list of specic types it can offer and use ``best_match`` to select a specific one. * With ``req.accept.best_match([types])`` prefer the first type in the list (previously it preferred later types). * Also, make sure that if the user-agent accepts multiple types and there are multiple matches to the types that the application offers, ``req.accept.best_match([..])`` returns the most specific match. So if the server can satisfy either ``image/*`` or ``text/plain`` types, the latter will be picked independent from the order the accepted or offered types are listed (given they have the same quality rating). * Fix Range, Content-Range and AppIter support all of which were broken in many ways, incorrectly parsing ranges, reporting incorrect content-ranges, failing to generate the correct body to satisfy the range from ``app_iter`` etc. * Fix assumption that presense of a ``seek`` method means that the stream is seekable. * Add ``ubody`` alias for ``Response.unicode_body`` * Add Unicode versions of ``Request.script_name`` and ``path_info``: ``uscript_name`` and ``upath_info``. * Split __init__.py into four modules: request, response, descriptors and datetime_utils. * Fix ``Response.body`` access resetting Content-Length to zero for HEAD responses. * Support passing Unicode bodies to :class:`WSGIHTTPException` constructors. * Make ``bool(req.accept)`` return ``False`` for requests with missing Accept header. * Add HTTP version to :meth:`Request.__str__` output. * Resolve deprecation warnings for parse_qsl on Python 2.6 and newer. * Fix :meth:`Response.md5_etag` setting Content-MD5 in incorrect format. * Add ``Request.authorization`` property for Authorization header. * Make sure ETag value is always quoted (required by RFC) * Moved most ``Request`` behavior into a new class named ``BaseRequest``. The ``Request`` class is now a superclass for ``BaseRequest`` and a simple mixin which manages ``environ['webob.adhoc_attrs']`` when ``__setitem__``, ``__delitem__`` and ``__getitem__`` are called. This allows framework developers who do not want the ``environ['webob.adhoc_attrs']`` mutation behavior from ``__setattr__``. (chrism) * Added response attribute ``response.content_disposition`` for its associated header. * Changed how ``charset`` is determined on :class:`webob.Request` objects. Now the ``charset`` parameter is read on the Content-Type header, if it is present. Otherwise a ``default_charset`` parameter is read, or the ``charset`` argument to the Request constructor. This is more similar to how :class:`webob.Response` handles the charset. * Made the case of the Content-Type header consistent (note: this might break some doctests). * Make ``req.GET`` settable, such that ``req.environ['QUERY_STRING']`` is updated. * Fix problem with ``req.POST`` causing a re-parse of the body when you instantiate multiple ``Request`` objects over the same environ (e.g., when using middleware that looks at ``req.POST``). * Recreate the request body properly when a ``POST`` includes file uploads. * When ``req.POST`` is updated, the generated body will include the new values. * Added a ``POST`` parameter to :meth:`webob.Request.blank`; when given this will create a request body for the POST parameters (list of two-tuples or dictionary-like object). Note: this does not handle unicode or file uploads. * Added method :meth:`webob.Response.merge_cookies`, which takes the ``Set-Cookie`` headers from a Response, and merges them with another response or WSGI application. (This is useful for flash messages.) * Fix a problem with creating exceptions like ``webob.exc.HTTPNotFound(body='', content_type='application/xml')`` (i.e., non-HTML exceptions). * When a Location header is not absolute in a Response, it will be made absolute when the Response is called as a WSGI application. This makes the response less bound to a specific request. * Added :mod:`webob.dec`, a decorator for making WSGI applications from functions with the signature ``resp = app(req)``. 0.9.6.1 ------- * Fixed :meth:`Response.__init__`, which for some content types would raise an exception. * The ``req.body`` property will not recreate a StringIO object unnecessarily when rereading the body. 0.9.6 ----- * Removed `environ_getter` from :class:`webob.Request`. This largely-unused option allowed a Request object to be instantiated with a dynamic underlying environ. Since it wasn't used much, and might have been ill-advised from the beginning, and affected performance, it has been removed (from Chris McDonough). * Speed ups for :meth:`webob.Response.__init__` and :meth:`webob.Request.__init__` * Fix defaulting of ``CONTENT_TYPE`` instead of ``CONTENT_LENGTH`` to 0 in ``Request.str_POST``. * Added :meth:`webob.Response.copy` 0.9.5 ----- * Fix ``Request.blank('/').copy()`` raising an exception. * Fix a potential memory leak with HEAD requests and 304 responses. * Make :func:`webob.html_escape` respect the ``.__html__()`` magic method, which allows you to use HTML in :class:`webob.exc.HTTPException` instances. * Handle unicode values for ``resp.location``. * Allow arbitrary keyword arguments to ``exc.HTTP*`` (the same keywords you can send to :class:`webob.Response`). * Allow setting :meth:`webob.Response.cache_expires` (usually it is called as a method). This is primarily to allow ``Response(cache_expires=True)``. 0.9.4 ----- * Quiet Python 2.6 deprecation warnings. * Added an attribute ``unicode_errors`` to :class:`webob.Response` -- if set to something like ``unicode_errors='replace'`` it will decode ``resp.body`` appropriately. The default is ``strict`` (which was the former un-overridable behavior). 0.9.3 ----- * Make sure that if changing the body the Content-MD5 header is removed. (Otherwise a lot of middleware would accidentally corrupt responses). * Fixed ``Response.encode_content('identity')`` case (was a no-op even for encoded bodies). * Fixed :meth:`Request.remove_conditional_headers` that was removing If-Match header instead of If-None-Match. * Fixed ``resp.set_cookie(max_age=timedelta(...))`` * ``request.POST`` now supports PUT requests with the appropriate Content-Type. 0.9.2 ----- * Add more arguments to :meth:`Request.remove_conditional_headers` for more fine-grained control: `remove_encoding`, `remove_range`, `remove_match`, `remove_modified`. All of them are `True` by default. * Add an `set_content_md5` argument to :meth:`Response.md5_etag` that calculates and sets Content-MD5 reponse header from current body. * Change formatting of cookie expires, to use the more traditional format ``Wed, 5-May-2001 15:34:10 GMT`` (dashes instead of spaces). Browsers should deal with either format, but some other code expects dashes. * Added in ``sorted`` function for backward compatibility with Python 2.3. * Allow keyword arguments to :class:`webob.Request`, which assign attributes (possibly overwriting values in the environment). * Added methods :meth:`webob.Request.make_body_seekable` and :meth:`webob.Request.copy_body`, which make it easier to share a request body among different consuming applications, doing something like `req.make_body_seekable(); req.body_file.seek(0)` 0.9.1 ----- * ``request.params.copy()`` now returns a writable MultiDict (before it returned an unwritable object). * There were several things broken with ``UnicodeMultiDict`` when ``decode_param_names`` is turned on (when the dictionary keys are unicode). * You can pass keyword arguments to ``Request.blank()`` that will be used to construct ``Request`` (e.g., ``Request.blank('/', decode_param_names=True)``). * If you set headers like ``response.etag`` to a unicode value, they will be encoded as ISO-8859-1 (however, they will remain encoded, and ``response.etag`` will not be a unicode value). * When parsing, interpret times with no timezone as UTC (previously they would be interpreted as local time). * Set the Expires property on cookies when using ``response.set_cookie()``. This is inherited from ``max_age``. * Support Unicode cookie values 0.9 --- * Added ``req.urlarg``, which represents positional arguments in ``environ['wsgiorg.routing_args']``. * For Python 2.4, added attribute get/set proxies on exception objects from, for example, ``webob.exc.HTTPNotFound().exception``, so that they act more like normal response objects (despite not being new-style classes or ``webob.Response`` objects). In Python 2.5 the exceptions are ``webob.Response`` objects. Backward Incompatible Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * The ``Response`` constructor has changed: it is now ``Response([body], [status], ...)`` (before it was ``Response([status], [body], ...)``). Body may be str or unicode. * The ``Response`` class defaults to ``text/html`` for the Content-Type, and ``utf8`` for the charset (charset is only set on ``text/*`` and ``application/*+xml`` responses). Bugfixes and Small Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~ * Use ``BaseCookie`` instead of ``SimpleCookie`` for parsing cookies. * Added ``resp.write(text)`` method, which is equivalent to ``resp.body += text`` or ``resp.unicode_body += text``, depending on the type of ``text``. * The ``decode_param_names`` argument (used like ``Request(decode_param_names=True)``) was being ignored. * Unicode decoding of file uploads and file upload filenames were causing errors when decoding non-file-upload fields (both fixes from Ryan Barrett). 0.8.5 ----- * Added response methods ``resp.encode_content()`` and ``resp.decode_content()`` to gzip or ungzip content. * ``Response(status=404)`` now works (before you would have to use ``status="404 Not Found"``). * Bugfix (typo) with reusing POST body. * Added ``226 IM Used`` response status. * Backport of ``string.Template`` included for Python 2.3 compatibility. 0.8.4 ----- * ``__setattr__`` would keep ``Request`` subclasses from having properly settable environ proxies (like ``req.path_info``). 0.8.3 ----- * ``request.POST`` was giving FieldStorage objects for *every* attribute, not just file uploads. This is fixed now. * Added request attributes ``req.server_name`` and ``req.server_port`` for the environ keys ``SERVER_NAME`` and ``SERVER_PORT``. * Avoid exceptions in ``req.content_length``, even if ``environ['CONTENT_LENGTH']`` is somehow invalid. 0.8.2 ----- * Python 2.3 compatibility: backport of ``reversed(seq)`` * Made separate ``.exception`` attribute on ``webob.exc`` objects, since new-style classes can't be raised as exceptions. * Deprecate ``req.postvars`` and ``req.queryvars``, instead using the sole names ``req.GET`` and ``req.POST`` (also ``req.str_GET`` and ``req.str_POST``). The old names give a warning; will give an error in next release, and be completely gone in the following release. * ``req.user_agent`` is now just a simple string (parsing the User-Agent header was just too volatile, and required too much knowledge about current browsers). Similarly, ``req.referer_search_query()`` is gone. * Added parameters ``version`` and ``comment`` to ``Response.set_cookie()``, per William Dode's suggestion. * Was accidentally consuming file uploads, instead of putting the ``FieldStorage`` object directly in the parameters. 0.8.1 ----- * Added ``res.set_cookie(..., httponly=True)`` to set the ``HttpOnly`` attribute on the cookie, which keeps Javascript from reading the cookie. * Added some WebDAV-related responses to ``webob.exc`` * Set default ``Last-Modified`` when using ``response.cache_expire()`` (fixes issue with Opera) * Generally fix ``.cache_control`` 0.8 --- First release. Nothing is new, or everything is new, depending on how you think about it. WebOb-1.8.6/PKG-INFO0000644000076600000240000002752313611756053014607 0ustar xistencestaff00000000000000Metadata-Version: 2.1 Name: WebOb Version: 1.8.6 Summary: WSGI request and response object Home-page: http://webob.org/ Author: Ian Bicking Author-email: ianb@colorstudy.com Maintainer: Pylons Project License: MIT Description: WebOb ===== .. image:: https://travis-ci.org/Pylons/webob.png?branch=master :target: https://travis-ci.org/Pylons/webob .. image:: https://readthedocs.org/projects/webob/badge/?version=stable :target: https://docs.pylonsproject.org/projects/webob/en/stable/ :alt: Documentation Status WebOb provides objects for HTTP requests and responses. Specifically it does this by wrapping the `WSGI `_ request environment and response status/headers/app_iter(body). The request and response objects provide many conveniences for parsing HTTP request and forming HTTP responses. Both objects are read/write: as a result, WebOb is also a nice way to create HTTP requests and parse HTTP responses. Support and Documentation ------------------------- See the `WebOb Documentation website `_ to view documentation, report bugs, and obtain support. License ------- WebOb is offered under the `MIT-license `_. Authors ------- WebOb was authored by Ian Bicking and is currently maintained by the `Pylons Project `_ and a team of contributors. 1.8.6 (2020-01-21) ------------------ Experimental Features ~~~~~~~~~~~~~~~~~~~~~ - The SameSite value now includes a new option named "None", this is a new change that was introduced in https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 Please be aware that older clients are incompatible with this change: https://www.chromium.org/updates/same-site/incompatible-clients, WebOb does not enable SameSite on cookies by default, so there is no backwards incompatible change here. See https://github.com/Pylons/webob/issues/406 - Validation of SameSite values can be disabled by toggling a module flag. This is in anticipation of future changes in evolving cookie standards. The discussion in https://github.com/Pylons/webob/pull/407 (which initially expanded the allowed options) notes the sudden change to browser cookie implementation details may happen again. In May 2019, Google announced a new model for privacy controls in their browsers, which affected the list of valid options for the SameSite attribute of cookies. In late 2019, the company began to roll out these changes to their browsers to force developer adoption of the new specification. See https://www.chromium.org/updates/same-site and https://blog.chromium.org/2019/10/developers-get-ready-for-new.html for more details on this change. See https://github.com/Pylons/webob/pull/409 1.8.5 (2019-01-03) ------------------ Warnings ~~~~~~~~ - Fixed one last remaining invalid escape sequence in a docstring. 1.8.4 (2018-11-11) ------------------ Bugfix ~~~~~~ - Response.content_type now accepts unicode strings on Python 2 and encodes them to latin-1. See https://github.com/Pylons/webob/pull/389 and https://github.com/Pylons/webob/issues/388 - Accept header classes now support a .copy() function that may be used to create a copy. This allows ``create_accept_header`` and other like functions to accept an pre-existing Accept header. See https://github.com/Pylons/webob/pull/386 and https://github.com/Pylons/webob/issues/385 Warnings ~~~~~~~~ - Some backslashes introduced with the new accept handling code were causing DeprecationWarnings upon compiling the source to pyc files, all of the backslashes have been reigned in as appropriate, and users should no longer see DeprecationWarnings for invalid escape sequence. See https://github.com/Pylons/webob/issues/384 1.8.3 (2018-10-14) ------------------ Bugfix ~~~~~~ - ``acceptparse.AcceptValidHeader``, ``acceptparse.AcceptInvalidHeader``, and ``acceptparse.AcceptNoHeader`` will now always ignore offers that do not match the required media type grammar when calling ``.acceptable_offers()``. Previous versions raised a ``ValueError`` for invalid offers in ``AcceptValidHeader`` and returned them as acceptable in the others. See https://github.com/Pylons/webob/pull/372 Feature ~~~~~~~ - Add Request.remote_host, exposing REMOTE_HOST environment variable. - Added ``acceptparse.Accept.parse_offer`` to codify what types of offers are compatible with ``acceptparse.AcceptValidHeader.acceptable_offers``, ``acceptparse.AcceptMissingHeader.acceptable_offers``, and ``acceptparse.AcceptInvalidHeader.acceptable_offers``. This API also normalizes the offer with lowercased type/subtype and parameter names. See https://github.com/Pylons/webob/pull/376 and https://github.com/Pylons/webob/pull/379 1.8.2 (2018-06-05) ------------------ Bugfix ~~~~~~ - SameSite may now be passed as str or bytes to `Response.set_cookie` and `cookies.make_cookie`. This was an oversight as all other arguments would be correctly coerced before being serialized. See https://github.com/Pylons/webob/issues/361 and https://github.com/Pylons/webob/pull/362 1.8.1 (2018-04-10) ------------------ Bugfix ~~~~~~ - acceptparse.MIMEAccept which is deprecated in WebOb 1.8.0 made a backwards incompatible change that led to it raising on an invalid Accept header. This behaviour has now been reversed, as well as some other fixes to allow MIMEAccept to behave more like the old version. See https://github.com/Pylons/webob/pull/356 1.8.0 (2018-04-04) ------------------ Feature ~~~~~~~ - ``request.POST`` now supports any requests with the appropriate Content-Type. Allowing any HTTP method to access form encoded content, including DELETE, PUT, and others. See https://github.com/Pylons/webob/pull/352 Compatibility ~~~~~~~~~~~~~ - WebOb is no longer officially supported on Python 3.3 which was EOL'ed on 2017-09-29. Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Many changes have been made to the way WebOb does Accept handling, not just for the Accept header itself, but also for Accept-Charset, Accept-Encoding and Accept-Language. This was a `Google Summer of Code `_ project completed by Whiteroses (https://github.com/whiteroses). Many thanks to Google for running GSoC, the Python Software Foundation for organising and a huge thanks to Ira for completing the work. See https://github.com/Pylons/webob/pull/338 and https://github.com/Pylons/webob/pull/335. Documentation is available at https://docs.pylonsproject.org/projects/webob/en/master/api/webob.html - When calling a ``@wsgify`` decorated function, the default arguments passed to ``@wsgify`` are now used when called with the request, and not as a `start_response` .. code:: def hello(req, name): return "Hello, %s!" % name app = wsgify(hello, args=("Fred",)) req = Request.blank('/') resp = req.get_response(app) # => "Hello, Fred" resp2 = app(req) # => "Hello, Fred" Previously the ``resp2`` line would have failed with a ``TypeError``. With this change there is no way to override the default arguments with no arguments. See https://github.com/Pylons/webob/pull/203 - When setting ``app_iter`` on a ``Response`` object the ``content_md5`` header is no longer cleared. This behaviour is odd and disallows setting the ``content_md5`` and then returning an iterator for chunked content encoded responses. See https://github.com/Pylons/webob/issues/86 Experimental Features ~~~~~~~~~~~~~~~~~~~~~ These features are experimental and may change at any point in the future. - The cookie APIs now have the ability to set the SameSite attribute on a cookie in both ``webob.cookies.make_cookie`` and ``webob.cookies.CookieProfile``. See https://github.com/Pylons/webob/pull/255 Bugfix ~~~~~~ - Exceptions now use string.Template.safe_substitute rather than string.Template.substitute. The latter would raise for missing mappings, the former will simply not substitute the missing variable. This is safer in case the WSGI environ does not contain the keys necessary for the body template. See https://github.com/Pylons/webob/issues/345. - Request.host_url, Request.host_port, Request.domain correctly parse IPv6 Host headers as provided by a browser. See https://github.com/Pylons/webob/pull/332 - Request.authorization would raise ValueError for unusual or malformed header values. See https://github.com/Pylons/webob/issues/231 - Allow unnamed fields in form data to be properly transcoded when calling request.decode with an alternate encoding. See https://github.com/Pylons/webob/pull/309 - ``Response.__init__`` would discard ``app_iter`` when a ``Response`` had no body, this would cause issues when ``app_iter`` was an object that was tied to the life-cycle of a web application and had to be properly closed. ``app_iter`` is more advanced API for ``Response`` and thus even if it contains a body and is thus against the HTTP RFC's, we should let the users shoot themselves by returning a body. See https://github.com/Pylons/webob/issues/305 Keywords: wsgi request web http Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.* Provides-Extra: testing Provides-Extra: docs WebOb-1.8.6/README.rst0000644000076600000240000000234713611751037015173 0ustar xistencestaff00000000000000WebOb ===== .. image:: https://travis-ci.org/Pylons/webob.png?branch=master :target: https://travis-ci.org/Pylons/webob .. image:: https://readthedocs.org/projects/webob/badge/?version=stable :target: https://docs.pylonsproject.org/projects/webob/en/stable/ :alt: Documentation Status WebOb provides objects for HTTP requests and responses. Specifically it does this by wrapping the `WSGI `_ request environment and response status/headers/app_iter(body). The request and response objects provide many conveniences for parsing HTTP request and forming HTTP responses. Both objects are read/write: as a result, WebOb is also a nice way to create HTTP requests and parse HTTP responses. Support and Documentation ------------------------- See the `WebOb Documentation website `_ to view documentation, report bugs, and obtain support. License ------- WebOb is offered under the `MIT-license `_. Authors ------- WebOb was authored by Ian Bicking and is currently maintained by the `Pylons Project `_ and a team of contributors.WebOb-1.8.6/RELEASING.rst0000644000076600000240000000323413204653630015541 0ustar xistencestaff00000000000000Releasing WebOb =============== - For clarity, we define releases as follows. - Alpha, beta, dev and similar statuses do not qualify whether a release is major or minor. The term "pre-release" means alpha, beta, or dev. - A release is final when it is no longer pre-release. - A *major* release is where the first number either before or after the first dot increases. Examples: 1.6.0 to 1.7.0a1, or 1.8.0 to 2.0.0. - A *minor* or *bug fix* release is where the number after the second dot increases. Example: 1.6.0 to 1.6.1. Releasing --------- - First install the required pre-requisites:: $ pip install setuptools_git twine - Edit ``CHANGES.txt`` to add a release number and data and then modify ``setup.py`` to update the version number as well. - Run ``python setup.py sdist bdist_wheel``, then verify ``dist/*`` hasn't increased dramatically compared to previous versions (for example, accidentally including a large file in the release or pyc files). - Upload the resulting package to PyPi: ``twine upload dist/WebOb-*{whl,tar.gz}`` Marketing and communications ---------------------------- - Announce to Twitter:: WebOb 1.x released. PyPI https://pypi.python.org/pypi/webob/1.x.y Changes http://docs.webob.org/en/1.x-branch/ Issues https://github.com/Pylons/webob/issues - Announce to maillist:: WebOb 1.x.y has been released. Here are the changes: <> You can install it via PyPI: pip install webob==1.x.y Enjoy, and please report any issues you find to the issue tracker at https://github.com/Pylons/webob/issues Thanks! - WebOb developers WebOb-1.8.6/appveyor.yml0000644000076600000240000000064013611742767016100 0ustar xistencestaff00000000000000environment: matrix: - PYTHON: "C:\\Python35" TOXENV: "py35" - PYTHON: "C:\\Python27" TOXENV: "py27" - PYTHON: "C:\\Python27-x64" TOXENV: "py27" - PYTHON: "C:\\Python35-x64" TOXENV: "py35" cache: - '%LOCALAPPDATA%\pip\Cache' version: '{branch}.{build}' install: - "%PYTHON%\\python.exe -m pip install tox" build: off test_script: - "%PYTHON%\\Scripts\\tox.exe" WebOb-1.8.6/contributing.md0000644000076600000240000000734713204653630016540 0ustar xistencestaff00000000000000Contributing ============ All projects under the Pylons Projects, including this one, follow the guidelines established at [How to Contribute](http://www.pylonsproject.org/community/how-to-contribute) and [Coding Style and Standards](http://docs.pylonsproject.org/en/latest/community/codestyle.html). You can contribute to this project in several ways. * [File an Issue on GitHub](https://github.com/Pylons/webob/issues) * Fork this project and create a branch with your suggested change. When ready, submit a pull request for consideration. [GitHub Flow](https://guides.github.com/introduction/flow/index.html) describes the workflow process and why it's a good practice. * Join the IRC channel #pyramid on irc.freenode.net. Git Branches ------------ Git branches and their purpose and status at the time of this writing are listed below. * [master](https://github.com/Pylons/webob/) - The branch on which further development takes place. The default branch on GitHub. * [1.6-branch](https://github.com/Pylons/webob/tree/1.6-branch) - The branch classified as "stable" or "latest". Actively maintained. * [1.5-branch](https://github.com/Pylons/webob/tree/1.5-branch) - The oldest actively maintained and stable branch. Older branches are not actively maintained. In general, two stable branches and one or two development branches are actively maintained. Running Tests ------------- *Note:* This section needs better instructions. Run `tox` from within your checkout. This will run the tests across all supported systems and attempt to build the docs. To run the tests for Python 2.x only: $ tox py2-cover To build the docs for Python 3.x only: $ tox py3-docs See the `tox.ini` file for details. Building documentation for a Pylons Project project --------------------------------------------------- *Note:* These instructions might not work for Windows users. Suggestions to improve the process for Windows users are welcome by submitting an issue or a pull request. 1. Fork the repo on GitHub by clicking the [Fork] button. 2. Clone your fork into a workspace on your local machine. git clone git@github.com:/webob.git 3. Add a git remote "upstream" for the cloned fork. git remote add upstream git@github.com:Pylons/webob.git 4. Set an environment variable to your virtual environment. # Mac and Linux $ export VENV=~/hack-on-webob/env # Windows set VENV=c:\hack-on-webob\env 5. Try to build the docs in your workspace. # Mac and Linux $ make clean html SPHINXBUILD=$VENV/bin/sphinx-build # Windows c:\> make clean html SPHINXBUILD=%VENV%\bin\sphinx-build If successful, then you can make changes to the documentation. You can load the built documentation in the `/_build/html/` directory in a web browser. 6. From this point forward, follow the typical [git workflow](https://help.github.com/articles/what-is-a-good-git-workflow/). Start by pulling from the upstream to get the most current changes. git pull upstream master 7. Make a branch, make changes to the docs, and rebuild them as indicated in step 5. To speed up the build process, you can omit `clean` from the above command to rebuild only those pages that depend on the files you have changed. 8. Once you are satisfied with your changes and the documentation builds successfully without errors or warnings, then git commit and push them to your "origin" repository on GitHub. git commit -m "commit message" git push -u origin --all # first time only, subsequent can be just 'git push'. 9. Create a [pull request](https://help.github.com/articles/using-pull-requests/). 10. Repeat the process starting from Step 6.WebOb-1.8.6/docs/0000755000076600000240000000000013611756053014431 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/Makefile0000644000076600000240000001635513157035764016107 0ustar xistencestaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WebOb.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WebOb.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/WebOb" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WebOb" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." WebOb-1.8.6/docs/_static/0000755000076600000240000000000013611756053016057 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/_static/.empty0000644000076600000240000000000013157035764017210 0ustar xistencestaff00000000000000WebOb-1.8.6/docs/api/0000755000076600000240000000000013611756053015202 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/api/client.txt0000644000076600000240000000033713157035764017230 0ustar xistencestaff00000000000000:mod:`webob.client` -- Send WSGI requests over HTTP =================================================== .. automodule:: webob.client Client ------ .. autoclass:: SendRequest :members: .. autoclass:: send_request_app WebOb-1.8.6/docs/api/cookies.txt0000644000076600000240000000055113611746633017403 0ustar xistencestaff00000000000000:mod:`webob.cookies` -- Cookies =============================== Cookies ------- .. autoclass:: webob.cookies.CookieProfile :members: .. autoclass:: webob.cookies.SignedCookieProfile :members: .. autoclass:: webob.cookies.SignedSerializer :members: .. autoclass:: webob.cookies.JSONSerializer :members: .. autofunction:: webob.cookies.make_cookie WebOb-1.8.6/docs/api/dec.txt0000644000076600000240000000023613157035764016503 0ustar xistencestaff00000000000000:mod:`webob.dec` -- WSGIfy decorator ==================================== .. automodule:: webob.dec Decorator --------- .. autoclass:: wsgify :members: WebOb-1.8.6/docs/api/exceptions.txt0000644000076600000240000000427713157035764020142 0ustar xistencestaff00000000000000:mod:`webob.exc` -- WebOb Exceptions ==================================== .. automodule:: webob.exc HTTP Exceptions --------------- .. autoexception:: HTTPException .. autoexception:: WSGIHTTPException .. autoexception:: HTTPError .. autoexception:: HTTPRedirection .. autoexception:: HTTPOk .. autoexception:: HTTPCreated .. autoexception:: HTTPAccepted .. autoexception:: HTTPNonAuthoritativeInformation .. autoexception:: HTTPNoContent .. autoexception:: HTTPResetContent .. autoexception:: HTTPPartialContent .. autoexception:: _HTTPMove .. autoexception:: HTTPMultipleChoices .. autoexception:: HTTPMovedPermanently .. autoexception:: HTTPFound .. autoexception:: HTTPSeeOther .. autoexception:: HTTPNotModified .. autoexception:: HTTPUseProxy .. autoexception:: HTTPTemporaryRedirect .. autoexception:: HTTPClientError .. autoexception:: HTTPBadRequest .. autoexception:: HTTPUnauthorized .. autoexception:: HTTPPaymentRequired .. autoexception:: HTTPForbidden .. autoexception:: HTTPNotFound .. autoexception:: HTTPMethodNotAllowed .. autoexception:: HTTPNotAcceptable .. autoexception:: HTTPProxyAuthenticationRequired .. autoexception:: HTTPRequestTimeout .. autoexception:: HTTPConflict .. autoexception:: HTTPGone .. autoexception:: HTTPLengthRequired .. autoexception:: HTTPPreconditionFailed .. autoexception:: HTTPRequestEntityTooLarge .. autoexception:: HTTPRequestURITooLong .. autoexception:: HTTPUnsupportedMediaType .. autoexception:: HTTPRequestRangeNotSatisfiable .. autoexception:: HTTPExpectationFailed .. autoexception:: HTTPUnprocessableEntity .. autoexception:: HTTPLocked .. autoexception:: HTTPFailedDependency .. autoexception:: HTTPPreconditionRequired .. autoexception:: HTTPTooManyRequests .. autoexception:: HTTPRequestHeaderFieldsTooLarge .. autoexception:: HTTPUnavailableForLegalReasons .. autoexception:: HTTPServerError .. autoexception:: HTTPInternalServerError .. autoexception:: HTTPNotImplemented .. autoexception:: HTTPBadGateway .. autoexception:: HTTPServiceUnavailable .. autoexception:: HTTPGatewayTimeout .. autoexception:: HTTPVersionNotSupported .. autoexception:: HTTPInsufficientStorage .. autoexception:: HTTPNetworkAuthenticationRequired .. autoexception:: HTTPExceptionMiddleware WebOb-1.8.6/docs/api/multidict.txt0000644000076600000240000000247113204653630017740 0ustar xistencestaff00000000000000:mod:`webob.multidict` -- multi-value dictionary object ======================================================= multidict --------- Several parts of WebOb use a "multidict", which is a dictionary where a key can have multiple values. The quintessential example is a query string like ``?pref=red&pref=blue``. The ``pref`` variable has two values, ``red`` and ``blue``. In a multidict, when you do ``request.GET['pref']``, you'll get back only ``'blue'`` (the last value of ``pref``). Sometimes returning a string and other times returning a list is a cause of frequent exceptions. If you want *all* the values back, use ``request.GET.getall('pref')``. If you want to be sure there is *one and only one* value, use ``request.GET.getone('pref')``, which will raise an exception if there is zero or more than one value for ``pref``. When you use operations like ``request.GET.items()``, you'll get back something like ``[('pref', 'red'), ('pref', 'blue')]``. All the key/value pairs will show up. Similarly ``request.GET.keys()`` returns ``['pref', 'pref']``. Multidict is a view on a list of tuples; all the keys are ordered, and all the values are ordered. .. automodule:: webob.multidict .. autoclass:: MultiDict :members: :inherited-members: .. autoclass:: NestedMultiDict :members: .. autoclass:: NoVars :members: WebOb-1.8.6/docs/api/request.txt0000644000076600000240000000031513157035764017436 0ustar xistencestaff00000000000000:mod:`webob.request` -- Request =============================== Request ------- .. automodule:: webob.request .. autoclass:: webob.request.Request .. autoclass:: webob.request.BaseRequest :members: WebOb-1.8.6/docs/api/response.txt0000644000076600000240000000044013157035764017603 0ustar xistencestaff00000000000000:mod:`webob.response` -- Response ================================= Response -------- .. automodule:: webob.response .. autoclass:: webob.response.Response :members: .. autoclass:: webob.response.ResponseBodyFile :members: .. autoclass:: webob.response.AppIterRange :members: WebOb-1.8.6/docs/api/static.txt0000644000076600000240000000034113157035764017234 0ustar xistencestaff00000000000000:mod:`webob.static` -- Serving static files =========================================== .. automodule:: webob.static .. autoclass:: webob.static.FileApp :members: .. autoclass:: webob.static.DirectoryApp :members: WebOb-1.8.6/docs/api/webob.txt0000644000076600000240000001124313372142442017035 0ustar xistencestaff00000000000000:mod:`webob` -- Request/Response objects ======================================== Headers ------- .. _acceptheader: Accept* ~~~~~~~ .. automodule:: webob.acceptparse Convenience functions to automatically create the appropriate header objects of a certain type: .. autofunction:: create_accept_header .. autofunction:: create_accept_charset_header .. autofunction:: create_accept_encoding_header .. autofunction:: create_accept_language_header The classes that may be returned by one of the functions above, and their methods: .. autoclass:: Accept :members: parse .. autoclass:: AcceptOffer :members: __str__ .. autoclass:: AcceptValidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, accept_html, accepts_html, acceptable_offers, best_match, quality .. autoclass:: AcceptNoHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, accept_html, accepts_html, acceptable_offers, best_match, quality .. autoclass:: AcceptInvalidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, accept_html, accepts_html, acceptable_offers, best_match, quality .. autoclass:: AcceptCharset :members: parse .. autoclass:: AcceptCharsetValidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptCharsetNoHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptCharsetInvalidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptEncoding :members: parse .. autoclass:: AcceptEncodingValidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptEncodingNoHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptEncodingInvalidHeader :members: parse, header_value, parsed, __init__, __add__, __bool__, __contains__, __iter__, __nonzero__, __radd__, __repr__, __str__, acceptable_offers, best_match, quality .. autoclass:: AcceptLanguage :members: parse .. autoclass:: AcceptLanguageValidHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, __radd__, __str__, parse, basic_filtering, best_match, lookup, quality .. autoclass:: AcceptLanguageNoHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, __radd__, __str__, parse, basic_filtering, best_match, lookup, quality .. autoclass:: AcceptLanguageInvalidHeader :members: header_value, parsed, __init__, __add__, __contains__, __iter__, __radd__, __str__, parse, basic_filtering, best_match, lookup, quality Deprecated: .. autoclass:: MIMEAccept Cache-Control ~~~~~~~~~~~~~ .. autoclass:: webob.cachecontrol.CacheControl :members: Range and related headers ~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: webob.byterange.Range :members: .. autoclass:: webob.byterange.ContentRange :members: .. autoclass:: webob.etag.IfRange :members: ETag ~~~~ .. autoclass:: webob.etag.ETagMatcher :members: Misc Functions and Internals ---------------------------- .. autofunction:: webob.html_escape .. comment: not sure what to do with these constants; not autoclass .. autoclass:: webob.day .. autoclass:: webob.week .. autoclass:: webob.hour .. autoclass:: webob.minute .. autoclass:: webob.second .. autoclass:: webob.month .. autoclass:: webob.year .. autoclass:: webob.headers.ResponseHeaders :members: .. autoclass:: webob.headers.EnvironHeaders :members: .. autoclass:: webob.cachecontrol.UpdateDict :members: .. comment: Descriptors ----------- .. autoclass:: webob.descriptors.environ_getter .. autoclass:: webob.descriptors.header_getter .. autoclass:: webob.descriptors.converter .. autoclass:: webob.descriptors.deprecated_property WebOb-1.8.6/docs/changes.txt0000644000076600000240000000016413157035764016607 0ustar xistencestaff00000000000000.. _changelog: WebOb Change History ==================== .. include:: ../CHANGES.txt .. include:: ../HISTORY.txt WebOb-1.8.6/docs/comment-example-code/0000755000076600000240000000000013611756053020434 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/comment-example-code/example.py0000644000076600000240000001174713157035764022457 0ustar xistencestaff00000000000000import os import urllib import time import re from cPickle import load, dump from webob import Request, Response, html_escape from webob import exc class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) def __call__(self, environ, start_response): req = Request(environ) if req.path_info_peek() == '.comments': return self.process_comment(req)(environ, start_response) # This is the base path of *this* middleware: base_url = req.application_url resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: # Not an HTML response, we don't want to # do anything to it return resp(environ, start_response) # Make sure the content isn't gzipped: resp.decode_content() comments = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(comments)) body = self.add_to_end(body, self.submit_form(base_url, req)) resp.body = body return resp(environ, start_response) def get_data(self, url): # Double-quoting makes the filename safe filename = self.url_filename(url) if not os.path.exists(filename): return [] else: f = open(filename, 'rb') data = load(f) f.close() return data def save_data(self, url, data): filename = self.url_filename(url) f = open(filename, 'wb') dump(data, f) f.close() def url_filename(self, url): return os.path.join(self.storage_dir, urllib.quote(url, '')) _end_body_re = re.compile(r'', re.I|re.S) def add_to_end(self, html, extra_html): """ Adds extra_html to the end of the html page (before ) """ match = self._end_body_re.search(html) if not match: return html + extra_html else: return html[:match.start()] + extra_html + html[match.start():] def format_comments(self, comments): if not comments: return '' text = [] text.append('
') text.append('

Comments (%s):

' % len(comments)) for comment in comments: text.append('

%s at %s:

' % ( html_escape(comment['homepage']), html_escape(comment['name']), time.strftime('%c', comment['time']))) # Susceptible to XSS attacks!: text.append(comment['comments']) return ''.join(text) def submit_form(self, base_path, req): return '''

Leave a comment:

Name:
URL:
Comments:

''' % (base_path, html_escape(req.url)) def process_comment(self, req): try: url = req.params['url'] name = req.params['name'] homepage = req.params['homepage'] comments = req.params['comments'] except KeyError, e: resp = exc.HTTPBadRequest('Missing parameter: %s' % e) return resp data = self.get_data(url) data.append(dict( name=name, homepage=homepage, comments=comments, time=time.gmtime())) self.save_data(url, data) resp = exc.HTTPSeeOther(location=url+'#comment-area') return resp if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT BASE_DIRECTORY' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--comment-data', default='./comments', dest='comment_data', help='Place to put comment data into (default ./comments/)') options, args = parser.parse_args() if not args: parser.error('You must give a BASE_DIRECTORY') base_dir = args[0] from paste.urlparser import StaticURLParser app = StaticURLParser(base_dir) app = Commenter(app, options.comment_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' WebOb-1.8.6/docs/comment-example.txt0000644000076600000240000003324113204653630020263 0ustar xistencestaff00000000000000Comment Example =============== .. contents:: Introduction ------------ This is an example of how to write WSGI middleware with WebOb. The specific example adds a simple comment form to HTML web pages; any page served through the middleware that is HTML gets a comment form added to it, and shows any existing comments. Code ---- The finished code for this is available in `docs/comment-example-code/example.py `_ -- you can run that file as a script to try it out. Instantiating Middleware ------------------------ Middleware of any complexity at all is usually best created as a class with its configuration as arguments to that class. Every middleware needs an application (``app``) that it wraps. This middleware also needs a location to store the comments; we'll put them all in a single directory. .. code-block:: python import os class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) When you use this middleware, you'll use it like: .. code-block:: python app = ... make the application ... app = Commenter(app, storage_dir='./comments') For our application we'll use a simple static file server that is included with `Paste `_ (use ``easy_install Paste`` to install this). The setup is all at the bottom of ``example.py``, and looks like this: .. code-block:: python if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT BASE_DIRECTORY' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--comment-data', default='./comments', dest='comment_data', help='Place to put comment data into (default ./comments/)') options, args = parser.parse_args() if not args: parser.error('You must give a BASE_DIRECTORY') base_dir = args[0] from paste.urlparser import StaticURLParser app = StaticURLParser(base_dir) app = Commenter(app, options.comment_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' I won't explain it here, but basically it takes some options, creates an application that serves static files (``StaticURLParser(base_dir)``), wraps it with ``Commenter(app, options.comment_data)`` then serves that. The Middleware -------------- While we've created the class structure for the middleware, it doesn't actually do anything. Here's a kind of minimal version of the middleware (using WebOb): .. code-block:: python from webob import Request class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) def __call__(self, environ, start_response): req = Request(environ) resp = req.get_response(self.app) return resp(environ, start_response) This doesn't modify the response it any way. You could write it like this without WebOb: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): return self.app(environ, start_response) But it won't be as convenient later. First, lets create a little bit of infrastructure for our middleware. We need to save and load per-url data (the comments themselves). We'll keep them in pickles, where each url has a pickle named after the url (but double-quoted, so ``http://localhost:8080/index.html`` becomes ``http%3A%2F%2Flocalhost%3A8080%2Findex.html``). .. code-block:: python from cPickle import load, dump class Commenter(object): ... def get_data(self, url): filename = self.url_filename(url) if not os.path.exists(filename): return [] else: f = open(filename, 'rb') data = load(f) f.close() return data def save_data(self, url, data): filename = self.url_filename(url) f = open(filename, 'wb') dump(data, f) f.close() def url_filename(self, url): # Double-quoting makes the filename safe return os.path.join(self.storage_dir, urllib.quote(url, '')) You can get the full request URL with ``req.url``, so to get the comment data with these methods you do ``data = self.get_data(req.url)``. Now we'll update the ``__call__`` method to filter *some* responses, and get the comment data for those. We don't want to change responses that were error responses (anything but ``200``), nor do we want to filter responses that aren't HTML. So we get: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): req = Request(environ) resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: return resp(environ, start_response) data = self.get_data(req.url) ... do stuff with data, update resp ... return resp(environ, start_response) So far we're punting on actually adding the comments to the page. We also haven't defined what ``data`` will hold. Let's say it's a list of dictionaries, where each dictionary looks like ``{'name': 'John Doe', 'homepage': 'http://blog.johndoe.com', 'comments': 'Great site!'}``. We'll also need a simple method to add stuff to the page. We'll use a regular expression to find the end of the page and put text in: .. code-block:: python import re class Commenter(object): ... _end_body_re = re.compile(r'', re.I|re.S) def add_to_end(self, html, extra_html): """ Adds extra_html to the end of the html page (before ) """ match = self._end_body_re.search(html) if not match: return html + extra_html else: return html[:match.start()] + extra_html + html[match.start():] And then we'll use it like: .. code-block:: python data = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(data)) resp.body = body return resp(environ, start_response) We get the body, update it, and put it back in the response. This also updates ``Content-Length``. Then we define: .. code-block:: python from webob import html_escape class Commenter(object): ... def format_comments(self, comments): if not comments: return '' text = [] text.append('
') text.append('

Comments (%s):

' % len(comments)) for comment in comments: text.append('

%s at %s:

' % ( html_escape(comment['homepage']), html_escape(comment['name']), time.strftime('%c', comment['time']))) # Susceptible to XSS attacks!: text.append(comment['comments']) return ''.join(text) We put in a header (with an anchor we'll use later), and a section for each comment. Note that ``html_escape`` is the same as ``cgi.escape`` and just turns ``&`` into ``&``, etc. Because we put in some text without quoting it is susceptible to a `Cross-Site Scripting `_ attack. Fixing that is beyond the scope of this tutorial; you could quote it or clean it with something like `lxml.html.clean `_. Accepting Comments ------------------ All of those pieces *display* comments, but still no one can actually make comments. To handle this we'll take a little piece of the URL space for our own, everything under ``/.comments``, so when someone POSTs there it will add a comment. When the request comes in there are two parts to the path: ``SCRIPT_NAME`` and ``PATH_INFO``. Everything in ``SCRIPT_NAME`` has already been parsed, and everything in ``PATH_INFO`` has yet to be parsed. That means that the URL *without* ``PATH_INFO`` is the path to the middleware; we can intercept anything else below ``SCRIPT_NAME`` but nothing above it. The name for the URL without ``PATH_INFO`` is ``req.application_url``. We have to capture it early to make sure it doesn't change (since the WSGI application we are wrapping may update ``SCRIPT_NAME`` and ``PATH_INFO``). So here's what this all looks like: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): req = Request(environ) if req.path_info_peek() == '.comments': return self.process_comment(req)(environ, start_response) # This is the base path of *this* middleware: base_url = req.application_url resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: # Not an HTML response, we don't want to # do anything to it return resp(environ, start_response) # Make sure the content isn't gzipped: resp.decode_content() comments = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(comments)) body = self.add_to_end(body, self.submit_form(base_url, req)) resp.body = body return resp(environ, start_response) ``base_url`` is the path where the middleware is located (if you run the example server, it will be ``http://localhost:PORT/``). We use ``req.path_info_peek()`` to look at the next segment of the URL -- what comes after base_url. If it is ``.comments`` then we handle it internally and don't pass the request on. We also put in a little guard, ``resp.decode_content()`` in case the application returns a gzipped response. Then we get the data, add the comments, add the *form* to make new comments, and return the result. submit_form ~~~~~~~~~~~ Here's what the form looks like: .. code-block:: python class Commenter(object): ... def submit_form(self, base_path, req): return '''

Leave a comment:

Name:
URL:
Comments:

''' % (base_path, html_escape(req.url)) Nothing too exciting. It submits a form with the keys ``url`` (the URL being commented on), ``name``, ``homepage``, and ``comments``. process_comment ~~~~~~~~~~~~~~~ If you look at the method call, what we do is call the method then treat the result as a WSGI application: .. code-block:: python return self.process_comment(req)(environ, start_response) You could write this as: .. code-block:: python response = self.process_comment(req) return response(environ, start_response) A common pattern in WSGI middleware that *doesn't* use WebOb is to just do: .. code-block:: python return self.process_comment(environ, start_response) But the WebOb style makes it easier to modify the response if you want to; modifying a traditional WSGI response/application output requires changing your logic flow considerably. Here's the actual processing code: .. code-block:: python from webob import exc from webob import Response class Commenter(object): ... def process_comment(self, req): try: url = req.params['url'] name = req.params['name'] homepage = req.params['homepage'] comments = req.params['comments'] except KeyError, e: resp = exc.HTTPBadRequest('Missing parameter: %s' % e) return resp data = self.get_data(url) data.append(dict( name=name, homepage=homepage, comments=comments, time=time.gmtime())) self.save_data(url, data) resp = exc.HTTPSeeOther(location=url+'#comment-area') return resp We either give a Bad Request response (if the form submission is somehow malformed), or a redirect back to the original page. The classes in ``webob.exc`` (like ``HTTPBadRequest`` and ``HTTPSeeOther``) are Response subclasses that can be used to quickly create responses for these non-200 cases where the response body usually doesn't matter much. Conclusion ---------- This shows how to make response modifying middleware, which is probably the most difficult kind of middleware to write with WSGI -- modifying the request is quite simple in comparison, as you simply update ``environ``. WebOb-1.8.6/docs/conf.py0000644000076600000240000001042313372142442015723 0ustar xistencestaff00000000000000import pkg_resources import sys import os import shlex extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = ['.txt', '.rst'] # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'WebOb' copyright = u'2018, Ian Bicking, Pylons Project and contributors' author = u'Ian Bicking, Pylons Project, and contributors' version = release = pkg_resources.get_distribution('webob').version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'jsonrpc-example-code/*', 'file-example-code/*'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False modindex_common_prefix = ['webob.'] autodoc_member_order = 'bysource' # -- Options for HTML output --------------------------------------------- html_theme = 'alabaster' html_static_path = ['_static'] htmlhelp_basename = 'WebObdoc' smartquotes=False # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'WebOb.tex', u'WebOb Documentation', u'Ian Bicking and contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'webob', u'WebOb Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'WebOb', u'WebOb Documentation', author, 'WebOb', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } WebOb-1.8.6/docs/differences.txt0000644000076600000240000004627113204653630017454 0ustar xistencestaff00000000000000Differences Between WebOb and Other Systems +++++++++++++++++++++++++++++++++++++++++++ This document points out some of the API differences between the Request and Response object, and the objects in other systems. .. contents:: paste.wsgiwrappers and Pylons ============================= The Pylons ``request`` and ``response`` object are based on ``paste.wsgiwrappers.WSGIRequest`` and ``WSGIResponse`` There is no concept of ``defaults`` in WebOb. In Paste/Pylons these serve as threadlocal settings that control certain policies on the request and response object. In WebOb you should make your own subclasses to control policy (though in many ways simply being explicit elsewhere removes the need for this policy). Request ------- ``body``: This is a file-like object in WSGIRequest. In WebOb it is a string (to match Response.body) and the file-like object is available through ``req.body_file`` ``languages()``: This is available through ``req.accept_language``, particularly ``req.accept_language.best_match(supported_languages)`` ``match_accept(mimetypes)``: This is available through ``req.accept.first_match(mimetypes)``; or if you trust the client's quality ratings, you can use ``req.accept.best_match(mimetypes)`` ``errors``: This controls how unicode decode errors are handled; it is now named ``unicode_errors`` There are also many extra methods and attributes on WebOb Request objects. Response -------- ``determine_charset()``: Is now available as ``res.charset`` ``has_header(header)``: Should be done with ``header in res.headers`` ``get_content()`` and ``wsgi_response()``: These are gone; you should use ``res.body`` or ``res(environ, start_response)`` ``write(content)``: Available in ``res.body_file.write(content)``. ``flush()`` and ``tell()``: Not available. ``tell()``: Available in ``res.body_file.tell()``. There are also many extra methods and attributes on WebOb Response objects. Django ====== This is a quick summary from reading `the Django documentation `_. Request ------- ``encoding``: Is ``req.charset`` ``REQUEST``: Is ``req.params`` ``FILES``: File uploads are ``cgi.FieldStorage`` objects directly in ``res.POST`` ``META``: Is ``req.environ`` ``user``: No equivalent (too connected to application model for WebOb). There is ``req.remote_user``, which is only ever a string. ``session``: No equivalent ``raw_post_data``: Available with ``req.body`` ``__getitem__(key)``: You have to use ``req.params`` ``is_secure()``: No equivalent; you could use ``req.scheme == 'https'``. QueryDict --------- QueryDict is the way Django represents the multi-key dictionary-like objects that are request variables (query string and POST body variables). The equivalent in WebOb is MultiDict. Mutability: WebOb dictionaries are sometimes mutable (req.GET is, req.params is not) Ordering: I believe Django does not order the keys fully; MultiDict is a full ordering. Methods that iterate over the parameters iterate over keys in their order in the original request. ``keys()``, ``items()``, ``values()`` (plus ``iter*``): These return all values in MultiDict, but only the last value for a QueryDict. That is, given ``a=1&a=2`` with MultiDict ``d.items()`` returns ``[('a', '1'), ('a', '2')]``, but QueryDict returns ``[('a', '1')]`` ``getlist(key)``: Available as ``d.getall(key)`` ``setlist(key)``: No direct equivalent ``appendlist(key, value)``: Available as ``d.add(key, value)`` ``setlistdefault(key, default_list)``: No direct equivalent ``lists()``: Is ``d.dict_of_lists()`` The MultiDict object has a ``d.getone(key)`` method, that raises KeyError if there is not exactly one key. There is a method ``d.mixed()`` which returns a version where values are lists *if* there are multiple values for a list. This is similar to how many cgi-based request forms are represented. Response -------- Constructor: Somewhat different. WebOb takes any keyword arguments as attribute assignments. Django only takes a couple arguments. The ``mimetype`` argument is ``content_type``, and ``content_type`` is the entire ``Content-Type`` header (including charset). dictionary-like: The Django response object is somewhat dictionary-like, setting headers. The equivalent dictionary-like object is ``res.headers``. In WebOb this is a MultiDict. ``has_header(header)``: Use ``header in res.headers`` ``flush()``: Not available ``content``: Use ``res.body`` for the ``str`` value, ``res.text`` for the ``unicode`` value Response Subclasses ------------------- These are generally like ``webob.exc`` objects. ``HttpResponseNotModified`` is ``HTTPNotModified``; this naming translation generally works. CherryPy/TurboGears =================== The `CherryPy request object `_ is also used by TurboGears 1.x. Request ------- ``app``: No equivalent ``base``: ``req.application_url`` ``close()``: No equivalent ``closed``: No equivalent ``config``: No equivalent ``cookie``: A ``SimpleCookie`` object in CherryPy; a dictionary in WebOb (``SimpleCookie`` can represent cookie parameters, but cookie parameters are only sent with responses not requests) ``dispatch``: No equivalent (this is the object dispatcher in CherryPy). ``error_page``, ``error_response``, ``handle_error``: No equivalent ``get_resource()``: Similar to ``req.get_response(app)`` ``handler``: No equivalent ``headers``, ``header_list``: The WSGI environment represents headers as a dictionary, available through ``req.headers`` (no list form is available in the request). ``hooks``: No equivalent ``local``: No equivalent ``methods_with_bodies``: This represents methods where CherryPy will automatically try to read the request body. WebOb lazily reads POST requests with the correct content type, and no other bodies. ``namespaces``: No equivalent ``protocol``: As ``req.environ['SERVER_PROTOCOL']`` ``query_string``: As ``req.query_string`` ``remote``: ``remote.ip`` is like ``req.remote_addr``. ``remote.port`` is not available. ``remote.name`` is in ``req.environ.get('REMOTE_HOST')`` ``request_line``: No equivalent ``respond()``: A method that is somewhat similar to ``req.get_response()``. ``rfile``: ``req.body_file`` ``run``: No equivalent ``server_protocol``: As ``req.environ['SERVER_PROTOCOL']`` ``show_tracebacks``: No equivalent ``throw_errors``: No equivalent ``throws``: No equivalent ``toolmaps``: No equivalent ``wsgi_environ``: As ``req.environ`` Response -------- From information `from the wiki `_. ``body``: This is an iterable in CherryPy, a string in WebOb; ``res.app_iter`` gives an iterable in WebOb. ``check_timeout``: No equivalent ``collapse_body()``: This turns a stream/iterator body into a single string. Accessing ``res.body`` will do this automatically. ``cookie``: Accessible through ``res.set_cookie(...)``, ``res.delete_cookie``, ``res.unset_cookie()`` ``finalize()``: No equivalent ``header_list``: In ``res.headerlist`` ``stream``: This can make CherryPy stream the response body out directory. There is direct no equivalent; you can use a dynamically generated iterator to do something similar. ``time``: No equivalent ``timed_out``: No equivalent Yaro ==== `Yaro `_ is a small wrapper around the WSGI environment, much like WebOb in scope. The WebOb objects have many more methods and attributes. The Yaro Response object is a much smaller subset of WebOb's Response. Request ------- ``query``: As ``req.GET`` ``form``: As ``req.POST`` ``cookie``: A ``SimpleCookie`` object in Yaro; a dictionary in WebOb (``SimpleCookie`` can represent cookie parameters, but cookie parameters are only sent with responses not requests) ``uri``: Returns a URI object, no equivalent (only string URIs available). ``redirect``: Not available (response-related). ``webob.exc.HTTPFound()`` can be useful here. ``forward(yaroapp)``, ``wsgi_forward(wsgiapp)``: Available with ``req.get_response(app)`` and ``req.call_application(app)``. In both cases it is a WSGI application in WebOb, there is no special kind of communication; ``req.call_application()`` just returns a ``webob.Response`` object. ``res``: The request object in WebOb *may* have a ``req.response`` attribute. Werkzeug ======== An offshoot of `Pocoo `_, this library is based around WSGI, similar to Paste and Yaro. This is taken from the `wrapper documentation `_. Request ------- path: As ``req.path_info`` args: As ``req.GET`` form: As ``req.POST`` values: As ``req.params`` files: In ``req.POST`` (as FieldStorage objects) data: In ``req.body_file`` Response -------- response: In ``res.body`` (settable as ``res.body`` or ``res.app_iter``) status: In ``res.status_code`` mimetype: In ``res.content_type`` Zope 3 ====== From the Zope 3 interfaces for the `Request `_ and `Response `_. Request ------- ``locale``, ``setupLocale()``: This is not fully calculated, but information is available in ``req.accept_languages``. ``principal``, ``setPrincipal(principal)``: ``req.remote_user`` gives the username, but there is no standard place for a user *object*. ``publication``, ``setPublication()``, These are associated with the object publishing system in Zope. This kind of publishing system is outside the scope of WebOb. ``traverse(object)``, ``getTraversalStack()``, ``setTraversalStack()``: These all relate to traversal, which is part of the publishing system. ``processInputs()``, ``setPathSuffix(steps)``: Also associated with traversal and preparing the request. ``environment``: In ``req.environ`` ``bodyStream``: In ``req.body_file`` ``interaction``: This is the security context for the request; all the possible participants or principals in the request. There's no equivalent. ``annotations``: Extra information associated with the request. This would generally go in custom keys of ``req.environ``, or if you set attributes those attributes are stored in ``req.environ['webob.adhoc_attrs']``. ``debug``: There is no standard debug flag for WebOb. ``__getitem__(key)``, ``get(key)``, etc: These treat the request like a dictionary, which WebOb does not do. They seem to take values from the environment, not parameters. Also on the Zope request object is ``items()``, ``__contains__(key)``, ``__iter__()``, ``keys()``, ``__len__()``, ``values()``. ``getPositionalArguments()``: I'm not sure what the equivalent would be, as there are no positional arguments during instantiation (it doesn't fit into WSGI). Maybe ``wsgiorg.urlvars``? ``retry()``, ``supportsRetry()``: Creates a new request that can be used to retry a request. Similar to ``req.copy()``. ``close()``, ``hold(obj)``: This closes resources associated with the request, including any "held" objects. There's nothing similar. Response -------- ``authUser``: Not sure what this is or does. ``reset()``: No direct equivalent; you'd have to do ``res.headers = []; res.body = ''; res.status = 200`` ``setCookie(name, value, **kw)``: Is ``res.set_cookie(...)``. ``getCookie(name)``: No equivalent. Hm. ``expireCookie(name)``: Is ``res.delete_cookie(name)``. ``appendToCookie(name, value)``: This appends the value to any existing cookie (separating values with a colon). WebOb does not do this. ``setStatus(status)``: Availble by setting ``res.status`` (can be set to an integer or a string of "code reason"). ``getHeader(name, default=None)``: Is ``res.headers.get(name)``. ``getStatus()``: Is ``res.status_code`` (or ``res.status`` to include reason) ``addHeader(name, value)``: Is ``res.headers.add(name, value)`` (in Zope and WebOb, this does not clobber any previous value). ``getHeaders()``: Is ``res.headerlist``. ``setHeader(name, value)``: Is ``res.headers[name] = value``. ``getStatusString()``: Is ``res.status``. ``consumeBody()``: This consumes any non-string body to turn the body into a single string. Any access to ``res.body`` will do this (e.g., when you have set the ``res.app_iter``). ``internalError()``: This is available with ``webob.exc.HTTP*()``. ``handleException(exc_info)``: This is provided with a tool like ``paste.exceptions``. ``consumeBodyIter()``: This returns the iterable for the body, even if the body was a string. Anytime you access ``res.app_iter`` you will get an iterable. ``res.body`` and ``res.app_iter`` can be interchanged and accessed as many times as you want, unlike the Zope equivalents. ``setResult(result)``: You can achieve the same thing through ``res.body = result``, or ``res.app_iter = result``. ``res.body`` accepts None, a unicode string (*if* you have set a charset) or a normal string. ``res.app_iter`` only accepts None and an interable. You can't update all of a response with one call. Like in Zope, WebOb updates Content-Length. Unlike Zope, it does not automatically calculate a charset. mod_python ========== Some key attributes from the `mod_python `_ request object. Request ------- ``req.uri``: In ``req.path``. ``req.user``: In ``req.remote_user``. ``req.get_remote_host()``: In ``req.environ['REMOTE_ADDR']`` or ``req.remote_addr``. ``req.headers_in.get('referer')``: In ``req.headers.get('referer')`` or ``req.referer`` (same pattern for other request headers, presumably). Response -------- ``util.redirect`` or ``req.status = apache.HTTP_MOVED_TEMPORARILY``: .. code-block:: python from webob.exc import HTTPTemporaryRedirect exc = HTTPTemporaryRedirect(location=url) return exc(environ, start_response) ``req.content_type = "application/x-csv"`` and ``req.headers_out.add('Content-Disposition', 'attachment;filename=somefile.csv')``: .. code-block:: python res = req.ResponseClass() res.content_type = 'application/x-csv' res.headers.add('Content-Disposition', 'attachment;filename=somefile.csv') return res(environ, start_response) webapp Response =============== .. note:: Google App Engine released the successor to webapp, `webapp2 `_. The Google App Engine `webapp `_ framework uses the WebOb Request object, but does not use its Response object. The constructor for ``webapp.Response`` does not take any arguments. The response is created by the framework, so you don't use it like ``return Response(...)``, instead you use ``self.response``. Also the response object automatically has ``Cache-Control: no-cache`` set, while the WebOb response does not set any cache headers. ``resp.set_status(code, message=None)``: This is handled by setting the ``resp.status`` attribute. ``resp.clear()``: You'd do ``resp.body = ""`` ``resp.wsgi_write(start_response)``: This writes the response using the ``start_response`` callback, and using the ``start_response`` writer. The WebOb response object is called as a WSGI app (``resp(environ, start_response)``) to do the equivalent. ``resp.out.write(text)``: This writes to an internal ``StringIO`` instance of the response. This uses the ability of the standard StringIO object to hold either unicode or ``str`` text, and so long as you are always consistent it will encode your content (but it does not respect your preferred encoding, it always uses UTF-8). The WebOb method ``resp.write(text)`` is basically equivalent, and also accepts unicode (using ``resp.charset`` for the encoding). You can also write to ``resp.body_file``, but it does not allow unicode. Besides exposing a ``.headers`` attribute (based on :py:class:`wsgiref.headers.Headers`) there is no other API for the webapp response object. This means the response lacks: * A usefully readable body or status. * A useful constructor that makes it easy to treat responses like objects. * Providing a non-string ``app_iter`` for the body (like a generator). * Parsing of the Content-Type charset. * Getter/setters for parsed forms of headers, specifically cache_control and last_modified. * The ``cache_expires`` method * ``set_cookie``, ``delete_cookie``, and ``unset_cookie``. Instead you have to simply manually set the Set-Cookie header. * ``encode_content`` and ``decode_content`` for handling gzip encoding. * ``md5_etag()`` for generating an etag from the body. * Conditional responses that will return 304 based on the response and request headers. * The ability to serve Range request automatically. PHP === PHP does not have anything really resembling a request and response object. Instead these are encoded in a set of global objects for the request and functions for the response. ``$_POST``, ``$_GET``, ``$_FILES`` ---------------------------------- These represent ``req.POST`` and ``req.GET``. PHP uses the variable names to tell whether a variable can hold multiple values. For instance ``$_POST['name[]']``, which will be an array. In WebOb any variable can have multiple values, and you can get these through ``req.POST.getall('name')``. The files in ``$_FILES`` are simply in ``req.POST`` in WebOb, as FieldStorage instances. ``$_COOKIES`` ------------- This is in ``req.cookies``. ``$_SERVER``, ``$_REQUEST``, ``$_ENV`` -------------------------------------- These are all in ``req.environ``. These are not split up like they are in PHP, it's all just one dictionary. Everything that would typically be in ``$_ENV`` is technically optional, and outside of a couple CGI-standard keys in ``$_SERVER`` most of those are also optional, but it is common for WSGI servers to populate the request with similar information as PHP. ``$HTTP_RAW_POST_DATA`` ----------------------- This contains the unparsed data in the request body. This is in ``req.body``. The response ------------ Response headers in PHP are sent with ``header("Header-Name: value")``. In WebOb there is a dictionary in ``resp.headers`` that can have values set; the headers aren't actually sent until you send the response. You can add headers without overwriting (the equivalent of ``header("...", false)``) with ``resp.headers.add('Header-Name', 'value')``. The status in PHP is sent with ``http_send_status(code)``. In WebOb this is ``resp.status = code``. The body in PHP is sent implicitly through the rendering of the PHP body (or with ``echo`` or any other functions that send output). WebOb-1.8.6/docs/do-it-yourself.txt0000644000076600000240000006471013204653630020057 0ustar xistencestaff00000000000000Another Do-It-Yourself Framework ================================ .. contents:: Introduction and Audience ------------------------- It's been over two years since I wrote the `first version of this tutorial `_. I decided to give it another run with some of the tools that have come about since then (particularly `WebOb `_). Sometimes Python is accused of having too many web frameworks. And it's true, there are a lot. That said, I think writing a framework is a useful exercise. It doesn't let you skip over too much without understanding it. It removes the magic. So even if you go on to use another existing framework (which I'd probably advise you do), you'll be able to understand it better if you've written something like it on your own. This tutorial shows you how to create a web framework of your own, using WSGI and WebOb. No other libraries will be used. For the longer sections I will try to explain any tricky parts on a line-by line basis following the example. What Is WSGI? ------------- At its simplest WSGI is an interface between web servers and web applications. We'll explain the mechanics of WSGI below, but a higher level view is to say that WSGI lets code pass around web requests in a fairly formal way. That's the simplest summary, but there is more -- WSGI lets you add annotation to the request, and adds some more metadata to the request. WSGI more specifically is made up of an *application* and a *server*. The application is a function that receives the request and produces the response. The server is the thing that calls the application function. A very simple application looks like this: .. code-block:: python >>> def application(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/html')]) ... return ['Hello World!'] The ``environ`` argument is a dictionary with values like the environment in a CGI request. The header ``Host:``, for instance, goes in ``environ['HTTP_HOST']``. The path is in ``environ['SCRIPT_NAME']`` (which is the path leading *up to* the application), and ``environ['PATH_INFO']`` (the remaining path that the application should interpret). We won't focus much on the server, but we will use WebOb to handle the application. WebOb in a way has a simple server interface. To use it you create a new request with ``req = webob.Request.blank('http://localhost/test')``, and then call the application with ``resp = req.get_response(app)``. For example: .. code-block:: python >>> from webob import Request >>> req = Request.blank('http://localhost/test') >>> resp = req.get_response(application) >>> print resp 200 OK Content-Type: text/html Hello World! This is an easy way to test applications, and we'll use it to test the framework we're creating. About WebOb ----------- WebOb is a library to create a request and response object. It's centered around the WSGI model. Requests are wrappers around the environment. For example: .. code-block:: python >>> req = Request.blank('http://localhost/test') >>> req.environ['HTTP_HOST'] 'localhost:80' >>> req.host 'localhost:80' >>> req.path_info '/test' Responses are objects that represent the... well, response. The status, headers, and body: .. code-block:: python >>> from webob import Response >>> resp = Response(body='Hello World!') >>> resp.content_type 'text/html' >>> resp.content_type = 'text/plain' >>> print resp 200 OK Content-Length: 12 Content-Type: text/plain; charset=UTF-8 Hello World! Responses also happen to be WSGI applications. That means you can call ``resp(environ, start_response)``. Of course it's much less *dynamic* than a normal WSGI application. These two pieces solve a lot of the more tedious parts of making a framework. They deal with parsing most HTTP headers, generating valid responses, and a number of unicode issues. Serving Your Application ------------------------ While we can test the application using WebOb, you might want to serve the application. Here's the basic recipe, using the `Paste `_ HTTP server: .. code-block:: python if __name__ == '__main__': from paste import httpserver httpserver.serve(app, host='127.0.0.1', port=8080) You could also use :py:mod:`wsgiref.simple_server` from the standard library, but this is mostly appropriate for testing as it is single-threaded: .. code-block:: python if __name__ == '__main__': from wsgiref.simple_server import make_server server = make_server('127.0.0.1', 8080, app) server.serve_forever() Making A Framework ------------------ Well, now we need to start work on our framework. Here's the basic model we'll be creating: * We'll define routes that point to controllers * We'll create a simple framework for creating controllers Routing ------- We'll use explicit routes using URI templates (minus the domains) to match paths. We'll add a little extension that you can use ``{name:regular expression}``, where the named segment must then match that regular expression. The matches will include a "controller" variable, which will be a string like "module_name:function_name". For our examples we'll use a simple blog. So here's what a route would look like: .. code-block:: python app = Router() app.add_route('/', controller='controllers:index') app.add_route('/{year:\d\d\d\d}/', controller='controllers:archive') app.add_route('/{year:\d\d\d\d}/{month:\d\d}/', controller='controllers:archive') app.add_route('/{year:\d\d\d\d}/{month:\d\d}/{slug}', controller='controllers:view') app.add_route('/post', controller='controllers:post') To do this we'll need a couple pieces: * Something to match those URI template things. * Something to load the controller * The object to patch them together (``Router``) Routing: Templates ~~~~~~~~~~~~~~~~~~ To do the matching, we'll compile those templates to regular expressions. .. code-block:: python :linenos: >>> import re >>> var_regex = re.compile(r''' ... \{ # The exact character "{" ... (\w+) # The variable name (restricted to a-z, 0-9, _) ... (?::([^}]+))? # The optional :regex part ... \} # The exact character "}" ... ''', re.VERBOSE) >>> def template_to_regex(template): ... regex = '' ... last_pos = 0 ... for match in var_regex.finditer(template): ... regex += re.escape(template[last_pos:match.start()]) ... var_name = match.group(1) ... expr = match.group(2) or '[^/]+' ... expr = '(?P<%s>%s)' % (var_name, expr) ... regex += expr ... last_pos = match.end() ... regex += re.escape(template[last_pos:]) ... regex = '^%s$' % regex ... return regex **line 2:** Here we create the regular expression. The ``re.VERBOSE`` flag makes the regular expression parser ignore whitespace and allow comments, so we can avoid some of the feel of line-noise. This matches any variables, i.e., ``{var:regex}`` (where ``:regex`` is optional). Note that there are two groups we capture: ``match.group(1)`` will be the variable name, and ``match.group(2)`` will be the regular expression (or None when there is no regular expression). Note that ``(?:...)?`` means that the section is optional. **line 9**: This variable will hold the regular expression that we are creating. **line 10**: This contains the position of the end of the last match. **line 11**: The ``finditer`` method yields all the matches. **line 12**: We're getting all the non-``{}`` text from after the last match, up to the beginning of this match. We call ``re.escape`` on that text, which escapes any characters that have special meaning. So ``.html`` will be escaped as ``\.html``. **line 13**: The first match is the variable name. **line 14**: ``expr`` is the regular expression we'll match against, the optional second match. The default is ``[^/]+``, which matches any non-empty, non-/ string. Which seems like a reasonable default to me. **line 15**: Here we create the actual regular expression. ``(?P...)`` is a grouped expression that is named. When you get a match, you can look at ``match.groupdict()`` and get the names and values. **line 16, 17**: We add the expression on to the complete regular expression and save the last position. **line 18**: We add remaining non-variable text to the regular expression. **line 19**: And then we make the regular expression match the complete string (``^`` to force it to match from the start, ``$`` to make sure it matches up to the end). To test it we can try some translations. You could put these directly in the docstring of the ``template_to_regex`` function and use :py:mod:`doctest` to test that. But I'm using doctest to test *this* document, so I can't put a docstring doctest inside the doctest itself. Anyway, here's what a test looks like: .. code-block:: python >>> print template_to_regex('/a/static/path') ^\/a\/static\/path$ >>> print template_to_regex('/{year:\d\d\d\d}/{month:\d\d}/{slug}') ^\/(?P\d\d\d\d)\/(?P\d\d)\/(?P[^/]+)$ Routing: controller loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To load controllers we have to import the module, then get the function out of it. We'll use the ``__import__`` builtin to import the module. The return value of ``__import__`` isn't very useful, but it puts the module into ``sys.modules``, a dictionary of all the loaded modules. Also, some people don't know how exactly the string method ``split`` works. It takes two arguments -- the first is the character to split on, and the second is the maximum number of splits to do. We want to split on just the first ``:`` character, so we'll use a maximum number of splits of 1. .. code-block:: python >>> import sys >>> def load_controller(string): ... module_name, func_name = string.split(':', 1) ... __import__(module_name) ... module = sys.modules[module_name] ... func = getattr(module, func_name) ... return func Routing: putting it together ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, the ``Router`` class. The class has the ``add_route`` method, and also a ``__call__`` method. That ``__call__`` method makes the Router object itself a WSGI application. So when a request comes in, it looks at ``PATH_INFO`` (also known as ``req.path_info``) and hands off the request to the controller that matches that path. .. code-block:: python :linenos: >>> from webob import Request >>> from webob import exc >>> class Router(object): ... def __init__(self): ... self.routes = [] ... ... def add_route(self, template, controller, **vars): ... if isinstance(controller, basestring): ... controller = load_controller(controller) ... self.routes.append((re.compile(template_to_regex(template)), ... controller, ... vars)) ... ... def __call__(self, environ, start_response): ... req = Request(environ) ... for regex, controller, vars in self.routes: ... match = regex.match(req.path_info) ... if match: ... req.urlvars = match.groupdict() ... req.urlvars.update(vars) ... return controller(environ, start_response) ... return exc.HTTPNotFound()(environ, start_response) **line 5**: We are going to keep the route options in an ordered list. Each item will be ``(regex, controller, vars)``: ``regex`` is the regular expression object to match against, ``controller`` is the controller to run, and ``vars`` are any extra (constant) variables. **line 8, 9**: We will allow you to call ``add_route`` with a string (that will be imported) or a controller object. We test for a string here, and then import it if necessary. **line 14**: Here we add a ``__call__`` method. This is the method used when you call an object like a function. You should recognize this as the WSGI signature. **line 15**: We create a request object. Note we'll only use this request object in this function; if the controller wants a request object it'll have to make on of its own. **line 17**: We test the regular expression against ``req.path_info``. This is the same as ``environ['PATH_INFO']``. That's all the request path left to be processed. **line 19**: We set ``req.urlvars`` to the dictionary of matches in the regular expression. This variable actually maps to ``environ['wsgiorg.routing_args']``. Any attributes you set on a request will, in one way or another, map to the environment dictionary: the request holds no state of its own. **line 20**: We also add in any explicit variables passed in through ``add_route()``. **line 21**: Then we call the controller as a WSGI application itself. Any fancy framework stuff the controller wants to do, it'll have to do itself. **line 22**: If nothing matches, we return a 404 Not Found response. ``webob.exc.HTTPNotFound()`` is a WSGI application that returns 404 responses. You could add a message too, like ``webob.exc.HTTPNotFound('No route matched')``. Then, of course, we call the application. Controllers ----------- The router just passes the request on to the controller, so the controllers are themselves just WSGI applications. But we'll want to set up something to make those applications friendlier to write. To do that we'll write a `decorator `_. A decorator is a function that wraps another function. After decoration the function will be a WSGI application, but it will be decorating a function with a signature like ``controller_func(req, **urlvars)``. The controller function will return a response object (which, remember, is a WSGI application on its own). .. code-block:: python :linenos: >>> from webob import Request, Response >>> from webob import exc >>> def controller(func): ... def replacement(environ, start_response): ... req = Request(environ) ... try: ... resp = func(req, **req.urlvars) ... except exc.HTTPException, e: ... resp = e ... if isinstance(resp, basestring): ... resp = Response(body=resp) ... return resp(environ, start_response) ... return replacement **line 3**: This is the typical signature for a decorator -- it takes one function as an argument, and returns a wrapped function. **line 4**: This is the replacement function we'll return. This is called a `closure `_ -- this function will have access to ``func``, and everytime you decorate a new function there will be a new ``replacement`` function with its own value of ``func``. As you can see, this is a WSGI application. **line 5**: We create a request. **line 6**: Here we catch any ``webob.exc.HTTPException`` exceptions. This is so you can do ``raise webob.exc.HTTPNotFound()`` in your function. These exceptions are themselves WSGI applications. **line 7**: We call the function with the request object, any variables in ``req.urlvars``. And we get back a response. **line 10**: We'll allow the function to return a full response object, or just a string. If they return a string, we'll create a ``Response`` object with that (and with the standard ``200 OK`` status, ``text/html`` content type, and ``utf8`` charset/encoding). **line 12**: We pass the request on to the response. Which *also* happens to be a WSGI application. WSGI applications are falling from the sky! **line 13**: We return the function object itself, which will take the place of the function. You use this controller like: .. code-block:: python >>> @controller ... def index(req): ... return 'This is the index' Putting It Together ------------------- Now we'll show a basic application. Just a hello world application for now. Note that this document is the module ``__main__``. .. code-block:: python >>> @controller ... def hello(req): ... if req.method == 'POST': ... return 'Hello %s!' % req.params['name'] ... elif req.method == 'GET': ... return '''
... Your name: ... ...
''' >>> hello_world = Router() >>> hello_world.add_route('/', controller=hello) Now let's test that application: .. code-block:: python >>> req = Request.blank('/') >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 131
Your name:
>>> req.method = 'POST' >>> req.body = 'name=Ian' >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 10 Hello Ian! Another Controller ------------------ There's another pattern that might be interesting to try for a controller. Instead of a function, we can make a class with methods like ``get``, ``post``, etc. The ``urlvars`` will be used to instantiate the class. We could do this as a superclass, but the implementation will be more elegant as a wrapper, like the decorator is a wrapper. Python 3.0 will add `class decorators `_ which will work like this. We'll allow an extra ``action`` variable, which will define the method (actually ``action_method``, where ``_method`` is the request method). If no action is given, we'll use just the method (i.e., ``get``, ``post``, etc). .. code-block:: python :linenos: >>> def rest_controller(cls): ... def replacement(environ, start_response): ... req = Request(environ) ... try: ... instance = cls(req, **req.urlvars) ... action = req.urlvars.get('action') ... if action: ... action += '_' + req.method.lower() ... else: ... action = req.method.lower() ... try: ... method = getattr(instance, action) ... except AttributeError: ... raise exc.HTTPNotFound("No action %s" % action) ... resp = method() ... if isinstance(resp, basestring): ... resp = Response(body=resp) ... except exc.HTTPException, e: ... resp = e ... return resp(environ, start_response) ... return replacement **line 1**: Here we're kind of decorating a class. But really we'll just create a WSGI application wrapper. **line 2-4**: The replacement WSGI application, also a closure. And we create a request and catch exceptions, just like in the decorator. **line 5**: We instantiate the class with both the request and ``req.urlvars`` to initialize it. The instance will only be used for one request. (Note that the *instance* then doesn't have to be thread safe.) **line 6**: We get the action variable out, if there is one. **line 7, 8**: If there was one, we'll use the method name ``{action}_{method}``... **line 9, 10**: ... otherwise we'll use just the method for the method name. **line 11-14**: We'll get the method from the instance, or respond with a 404 error if there is not such method. **line 15**: Call the method, get the response **line 16, 17**: If the response is just a string, create a full response object from it. **line 20**: and then we forward the request... **line 21**: ... and return the wrapper object we've created. Here's the hello world: .. code-block:: python >>> class Hello(object): ... def __init__(self, req): ... self.request = req ... def get(self): ... return '''
... Your name: ... ...
''' ... def post(self): ... return 'Hello %s!' % self.request.params['name'] >>> hello = rest_controller(Hello) We'll run the same test as before: .. code-block:: python >>> hello_world = Router() >>> hello_world.add_route('/', controller=hello) >>> req = Request.blank('/') >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 131
Your name:
>>> req.method = 'POST' >>> req.body = 'name=Ian' >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 10 Hello Ian! URL Generation and Request Access --------------------------------- You can use hard-coded links in your HTML, but this can have problems. Relative links are hard to manage, and absolute links presume that your application lives at a particular location. WSGI gives a variable ``SCRIPT_NAME``, which is the portion of the path that led up to this application. If you are writing a blog application, for instance, someone might want to install it at ``/blog/``, and then SCRIPT_NAME would be ``"/blog"``. We should generate links with that in mind. The base URL using SCRIPT_NAME is ``req.application_url``. So, if we have access to the request we can make a URL. But what if we don't have access? We can use thread-local variables to make it easy for any function to get access to the current request. A "thread-local" variable is a variable whose value is tracked separately for each thread, so if there are multiple requests in different threads, their requests won't clobber each other. The basic means of using a thread-local variable is ``threading.local()``. This creates a blank object that can have thread-local attributes assigned to it. I find the best way to get *at* a thread-local value is with a function, as this makes it clear that you are fetching the object, as opposed to getting at some global object. Here's the basic structure for the local: .. code-block:: python >>> import threading >>> class Localized(object): ... def __init__(self): ... self.local = threading.local() ... def register(self, object): ... self.local.object = object ... def unregister(self): ... del self.local.object ... def __call__(self): ... try: ... return self.local.object ... except AttributeError: ... raise TypeError("No object has been registered for this thread") >>> get_request = Localized() Now we need some *middleware* to register the request object. Middleware is something that wraps an application, possibly modifying the request on the way in or the way out. In a sense the ``Router`` object was middleware, though not exactly because it didn't wrap a single application. This registration middleware looks like: .. code-block:: python >>> class RegisterRequest(object): ... def __init__(self, app): ... self.app = app ... def __call__(self, environ, start_response): ... req = Request(environ) ... get_request.register(req) ... try: ... return self.app(environ, start_response) ... finally: ... get_request.unregister() Now if we do: >>> hello_world = RegisterRequest(hello_world) then the request will be registered each time. Now, lets create a URL generation function: .. code-block:: python >>> import urllib >>> def url(*segments, **vars): ... base_url = get_request().application_url ... path = '/'.join(str(s) for s in segments) ... if not path.startswith('/'): ... path = '/' + path ... if vars: ... path += '?' + urllib.urlencode(vars) ... return base_url + path Now, to test: .. code-block:: python >>> get_request.register(Request.blank('http://localhost/')) >>> url('article', 1) 'http://localhost/article/1' >>> url('search', q='some query') 'http://localhost/search?q=some+query' Templating ---------- Well, we don't *really* need to factor templating into our framework. After all, you return a string from your controller, and you can figure out on your own how to get a rendered string from a template. But we'll add a little helper, because I think it shows a clever trick. We'll use `Tempita `_ for templating, mostly because it's very simplistic about how it does loading. The basic form is: .. code-block:: python import tempita template = tempita.HTMLTemplate.from_filename('some-file.html') But we'll be implementing a function ``render(template_name, **vars)`` that will render the named template, treating it as a path *relative to the location of the render() call*. That's the trick. To do that we use ``sys._getframe``, which is a way to look at information in the calling scope. Generally this is frowned upon, but I think this case is justifiable. We'll also let you pass an instantiated template in instead of a template name, which will be useful in places like a doctest where there aren't other files easily accessible. .. code-block:: python >>> import os >>> import tempita #doctest: +SKIP >>> def render(template, **vars): ... if isinstance(template, basestring): ... caller_location = sys._getframe(1).f_globals['__file__'] ... filename = os.path.join(os.path.dirname(caller_location), template) ... template = tempita.HTMLTemplate.from_filename(filename) ... vars.setdefault('request', get_request()) ... return template.substitute(vars) Conclusion ---------- Well, that's a framework. Ta-da! Of course, this doesn't deal with some other stuff. In particular: * Configuration * Making your routes debuggable * Exception catching and other basic infrastructure * Database connections * Form handling * Authentication But, for now, that's outside the scope of this document. WebOb-1.8.6/docs/doctests.py0000644000076600000240000000126713157035764016645 0ustar xistencestaff00000000000000import unittest import doctest def test_suite(): flags = doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE return unittest.TestSuite(( doctest.DocFileSuite('test_request.txt', optionflags=flags), doctest.DocFileSuite('test_response.txt', optionflags=flags), doctest.DocFileSuite('test_dec.txt', optionflags=flags), doctest.DocFileSuite('do-it-yourself.txt', optionflags=flags), doctest.DocFileSuite('file-example.txt', optionflags=flags), doctest.DocFileSuite('index.txt', optionflags=flags), doctest.DocFileSuite('reference.txt', optionflags=flags), )) if __name__ == '__main__': unittest.main(defaultTest='test_suite') WebOb-1.8.6/docs/experimental/0000755000076600000240000000000013611756053017126 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/experimental/samesite.txt0000644000076600000240000000173613611751632021506 0ustar xistencestaff00000000000000.. _samesiteexp: Same-site Cookies ================= The `Same-site cookie RFC `_ updates `RFC6265 `_ to include a new cookie attribute named ``SameSite``. WebOb provides support for setting the ``SameSite`` attribute in its cookie APIs, using the ``samesite`` keyword argument. In `Incrementally Better Cookies `_ the standard was altered to add an additional option for the ``SameSite`` attribute. This new option has `known incompatible clients `_, please be aware that WebOb does not attempt to sniff the user agent to know if setting the ``SameSite`` attribute to ``None`` will cause compatibility issues. Please refer to the API documentation for :func:`webob.cookies.make_cookie` and :class:`webob.cookies.CookieProfile` for the keyword arguments. WebOb-1.8.6/docs/file-example-code/0000755000076600000240000000000013611756053017711 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/file-example-code/test-file.txt0000644000076600000240000000004313157035764022347 0ustar xistencestaff00000000000000This is a test. Hello test people!WebOb-1.8.6/docs/file-example.txt0000644000076600000240000001714313157035764017554 0ustar xistencestaff00000000000000WebOb File-Serving Example ========================== This document shows how you can make a static-file-serving application using WebOb. We'll quickly build this up from minimal functionality to a high-quality file serving application. .. note:: Starting from 1.2b4, WebOb ships with a :mod:`webob.static` module which implements a :class:`webob.static.FileApp` WSGI application similar to the one described below. This document stays as a didactic example how to serve files with WebOb, but you should consider using applications from :mod:`webob.static` in production. .. comment: >>> import webob, os >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__)) >>> doc_dir = os.path.join(base_dir, 'docs') >>> from doctest import ELLIPSIS First we'll setup a really simple shim around our application, which we can use as we improve our application: .. code-block:: python >>> from webob import Request, Response >>> import os >>> class FileApp(object): ... def __init__(self, filename): ... self.filename = filename ... def __call__(self, environ, start_response): ... res = make_response(self.filename) ... return res(environ, start_response) >>> import mimetypes >>> def get_mimetype(filename): ... type, encoding = mimetypes.guess_type(filename) ... # We'll ignore encoding, even though we shouldn't really ... return type or 'application/octet-stream' Now we can make different definitions of ``make_response``. The simplest version: .. code-block:: python >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename)) ... res.body = open(filename, 'rb').read() ... return res We'll test it out with a file ``test-file.txt`` in the WebOb doc directory, which has the following content: .. literalinclude:: file-example-code/test-file.txt :language: text Let's give it a shot: .. code-block:: python >>> fn = os.path.join(doc_dir, 'file-example-code/test-file.txt') >>> open(fn).read() 'This is a test. Hello test people!' >>> app = FileApp(fn) >>> req = Request.blank('/') >>> print req.get_response(app) 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 This is a test. Hello test people! Well, that worked. But it's not a very fancy object. First, it reads everything into memory, and that's bad. We'll create an iterator instead: .. code-block:: python >>> class FileIterable(object): ... def __init__(self, filename): ... self.filename = filename ... def __iter__(self): ... return FileIterator(self.filename) >>> class FileIterator(object): ... chunk_size = 4096 ... def __init__(self, filename): ... self.filename = filename ... self.fileobj = open(self.filename, 'rb') ... def __iter__(self): ... return self ... def next(self): ... chunk = self.fileobj.read(self.chunk_size) ... if not chunk: ... raise StopIteration ... return chunk ... __next__ = next # py3 compat >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename)) ... res.app_iter = FileIterable(filename) ... res.content_length = os.path.getsize(filename) ... return res And testing: .. code-block:: python >>> req = Request.blank('/') >>> print req.get_response(app) 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 This is a test. Hello test people! Well, that doesn't *look* different, but lets *imagine* that it's different because we know we changed some code. Now to add some basic metadata to the response: .. code-block:: python >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename), ... conditional_response=True) ... res.app_iter = FileIterable(filename) ... res.content_length = os.path.getsize(filename) ... res.last_modified = os.path.getmtime(filename) ... res.etag = '%s-%s-%s' % (os.path.getmtime(filename), ... os.path.getsize(filename), hash(filename)) ... return res Now, with ``conditional_response`` on, and with ``last_modified`` and ``etag`` set, we can do conditional requests: .. code-block:: python >>> req = Request.blank('/') >>> res = req.get_response(app) >>> print res 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 Last-Modified: ... GMT ETag: ...-... This is a test. Hello test people! >>> req2 = Request.blank('/') >>> req2.if_none_match = res.etag >>> req2.get_response(app) >>> req3 = Request.blank('/') >>> req3.if_modified_since = res.last_modified >>> req3.get_response(app) We can even do Range requests, but it will currently involve iterating through the file unnecessarily. When there's a range request (and you set ``conditional_response=True``) the application will satisfy that request. But with an arbitrary iterator the only way to do that is to run through the beginning of the iterator until you get to the chunk that the client asked for. We can do better because we can use ``fileobj.seek(pos)`` to move around the file much more efficiently. So we'll add an extra method, ``app_iter_range``, that ``Response`` looks for: .. code-block:: python >>> class FileIterable(object): ... def __init__(self, filename, start=None, stop=None): ... self.filename = filename ... self.start = start ... self.stop = stop ... def __iter__(self): ... return FileIterator(self.filename, self.start, self.stop) ... def app_iter_range(self, start, stop): ... return self.__class__(self.filename, start, stop) >>> class FileIterator(object): ... chunk_size = 4096 ... def __init__(self, filename, start, stop): ... self.filename = filename ... self.fileobj = open(self.filename, 'rb') ... if start: ... self.fileobj.seek(start) ... if stop is not None: ... self.length = stop - start ... else: ... self.length = None ... def __iter__(self): ... return self ... def next(self): ... if self.length is not None and self.length <= 0: ... raise StopIteration ... chunk = self.fileobj.read(self.chunk_size) ... if not chunk: ... raise StopIteration ... if self.length is not None: ... self.length -= len(chunk) ... if self.length < 0: ... # Chop off the extra: ... chunk = chunk[:self.length] ... return chunk ... __next__ = next # py3 compat Now we'll test it out: .. code-block:: python >>> req = Request.blank('/') >>> res = req.get_response(app) >>> req2 = Request.blank('/') >>> # Re-fetch the first 5 bytes: >>> req2.range = (0, 5) >>> res2 = req2.get_response(app) >>> res2 >>> # Let's check it's our custom class: >>> res2.app_iter >>> res2.body 'This ' >>> # Now, conditional range support: >>> req3 = Request.blank('/') >>> req3.if_range = res.etag >>> req3.range = (0, 5) >>> req3.get_response(app) >>> req3.if_range = 'invalid-etag' >>> req3.get_response(app) WebOb-1.8.6/docs/index.txt0000644000076600000240000002763413222443707016312 0ustar xistencestaff00000000000000.. _index: .. module:: webob .. toctree:: :hidden: self WebOb +++++ WebOb provides objects for HTTP requests and responses. Specifically it does this by wrapping the `WSGI `_ request environment and response status/headers/app_iter(body). The request and response objects provide many conveniences for parsing HTTP request and forming HTTP responses. Both objects are read/write: as a result, WebOb is also a nice way to create HTTP requests and parse HTTP responses; however, we won't cover that use case in this document. The :ref:`reference documentation ` shows many examples of creating requests. .. toctree:: :maxdepth: 2 reference differences license API Documentation ================= Reference material for every public API exposed by WebOb: .. toctree:: :maxdepth: 1 :glob: api/* .. _experimental-api: Experimental API ================ There are a variety of features that are considered experimental in WebOb, these features may change without any notice in future versions of WebOb, or be removed entirely. If you are relying on these features, please pin your version of WebOb and carefully watch for changes. .. toctree:: :maxdepth: 1 :glob: experimental/* Request ======= The request object is a wrapper around the `WSGI environ dictionary `_. This dictionary contains keys for each header, keys that describe the request (including the path and query string), a file-like object for the request body, and a variety of custom keys. You can always access the environ with ``req.environ``. Some of the most important and interesting attributes of a request object are the following: - :attr:`req.method ` The request method, e.g., ``GET``, ``POST``, ``PUT``. - :attr:`req.GET ` A :mod:`dictionary-like object ` with all the variables in the query string. - :attr:`req.POST ` A :mod:`dictionary-like object ` with all the variables in the request body. This only has variables if the request was a ``POST`` and it is a form submission. - :attr:`req.params `: A :mod:`dictionary-like object ` with a combination of everything in ``req.GET`` and ``req.POST``. - :attr:`req.body `: The contents of the body of the request. This contains the entire request body as a string. This is useful when the request is a ``POST`` that is *not* a form submission, or a request like a ``PUT``. You can also get ``req.body_file`` for a file-like object. - :attr:`req.cookies `: A simple dictionary of all the cookies. - :attr:`req.headers `: A dictionary of all the headers. This dictionary is case-insensitive. Also for standard HTTP request headers, there are usually attributes, e.g., :attr:`req.accept_language `, :attr:`req.content_length `, and :attr:`req.user_agent `. These properties expose the *parsed* form of each header, for whatever parsing makes sense. For instance, :attr:`req.if_modified_since ` returns a :class:`~datetime.datetime` object (or ``None`` if the header is was not provided). Details are in the :mod:`Request object API documentation `. URLs ---- In addition to these attributes, there are several ways to get the URL of the request. I'll show various values for an example URL ``http://localhost/app-root/doc?article_id=10``, where the application is mounted at ``http://localhost/app-root``. - :attr:`req.url `: The full request URL, with query string, e.g., ``'http://localhost/app-root/doc?article_id=10'``. - :attr:`req.application_url `: The URL of the application (just the ``SCRIPT_NAME`` portion of the path, not ``PATH_INFO``), e.g., ``'http://localhost/app-root'``. - :attr:`req.host_url `: The URL with the host, e.g., ``'http://localhost'``. - :func:`req.relative_url(url, to_application=False) `: Gives a URL, relative to the current URL. If ``to_application`` is True, then the URL is resolved relative to ``req.application_url``. Methods ------- There are several methods in :class:`~webob.request.Request` but only a few you'll use often: - :func:`Request.blank(uri) `: Creates a new request with blank information, based at the given URL. This can be useful for subrequests and artificial requests. You can also use ``req.copy()`` to copy an existing request, or for subrequests ``req.copy_get()`` which copies the request but always turns it into a GET (which is safer to share for subrequests). - :func:`req.get_response(wsgi_application) `: This method calls the given WSGI application with this request, and returns a `Response`_ object. You can also use this for subrequests or testing. Unicode ------- Many of the properties in the request object will return unicode values if the request encoding/charset is provided. The client *can* indicate the charset with something like ``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but browsers seldom set this. You can set the charset with ``req.charset = 'utf8'``, or during instantiation with ``Request(environ, charset='utf8')``. If you subclass ``Request`` you can also set ``charset`` as a class-level attribute. If it is set, then ``req.POST``, ``req.GET``, ``req.params``, and ``req.cookies`` will contain unicode strings. Response ======== The response object looks a lot like the request object, though with some differences. The request object wraps a single ``environ`` object; the response object has three fundamental parts (based on WSGI): - :attr:`response.status `: The response code plus message, like ``'200 OK'``. To set the code without the reason, use ``response.status_code = 200``. - :attr:`response.headerlist `: A list of all the headers, like ``[('Content-Type', 'text/html')]``. There's a case-insensitive :mod:`dictionary-like object ` in ``response.headers`` that also allows you to access these same headers. - :attr:`response.app_iter `: An iterable (such as a list or generator) that will produce the content of the response. This is also accessible as ``response.body`` (a string), ``response.unicode_body`` (a unicode object, informed by ``response.charset``), and ``response.body_file`` (a file-like object; writing to it appends to ``app_iter``). Everything else in the object derives from this underlying state. Here are the highlights: - :attr:`response.content_type ` The content type *not* including the ``charset`` parameter. Typical use: ``response.content_type = 'text/html'``. You can subclass ``Response`` and add a class-level attribute ``default_content_type`` to set this automatically on instantiation. - :attr:`response.charset ` The ``charset`` parameter of the content-type, it also informs encoding in ``response.unicode_body``. ``response.content_type_params`` is a dictionary of all the parameters. - :func:`response.set_cookie(name=None, value='', max_age=None, ...) ` Set a cookie. The keyword arguments control the various cookie parameters. The ``max_age`` argument is the length for the cookie to live in seconds (you may also use a timedelta object). - :func:`response.delete_cookie(name, ...) ` Delete a cookie from the client. This sets ``max_age`` to 0 and the cookie value to ``''``. - :func:`response.cache_expires(seconds=0) ` This makes this response cacheable for the given number of seconds, or if ``seconds`` is 0 then the response is uncacheable (this also sets the ``Expires`` header). - :func:`response(environ, start_response) ` The response object is a WSGI application. As an application, it acts according to how you create it. It *can* do conditional responses if you pass ``conditional_response=True`` when instantiating (or set that attribute later). It can also do HEAD and Range requests. Headers ------- Like the request, most HTTP response headers are available as properties. These are parsed, so you can do things like ``response.last_modified = os.path.getmtime(filename)``. .. seealso:: The :class:`~webob.response.Response` object documentation for further information. Instantiating the Response -------------------------- Of course most of the time you just want to *make* a response. Generally any attribute of the response can be passed in as a keyword argument to the class, e.g.: .. code-block:: python response = Response(text='hello world!', content_type='text/plain') The status defaults to ``'200 OK'``. The ``content_type`` defaults to ``default_content_type`` which is set to ``text/html``, although if you subclass ``Response`` and set ``default_content_type``, you can override this behavior. Exceptions ========== To facilitate error responses like 404 Not Found, the module ``webob.exc`` contains classes for each kind of error response. These include boring but appropriate error bodies. Each class is named ``webob.exc.HTTP*``, where ``*`` is the reason for the error. For instance, ``webob.exc.HTTPNotFound``. It subclasses ``Response``, so you can manipulate the instances in the same way. A typical example is: .. code-block:: python response = HTTPNotFound('There is no such resource') # or: response = HTTPMovedPermanently(location=new_url) You can use this like: .. code-block:: python try: # ... stuff ... raise HTTPNotFound('No such resource') except HTTPException, e: return e(environ, start_response) Example ======= The `file-serving example `_ shows how to do more advanced HTTP techniques, while the `comment middleware example `_ shows middleware. For applications, it's more reasonable to use WebOb in the context of a larger framework. `Pyramid `_, and its predecessor `Pylons `_, both use WebOb. .. toctree:: :maxdepth: 1 file-example wiki-example comment-example jsonrpc-example do-it-yourself Change History ============== .. toctree:: :maxdepth: 1 whatsnew-1.5 whatsnew-1.6 whatsnew-1.7 whatsnew-1.8 changes Status and License ================== WebOb is an extraction and refinement of pieces from `Paste `_. It is under active development on `GitHub `_. It was originally written by `Ian Bicking `_, and is maintained by the `Pylons Project `_. You can clone the source code with: .. code-block:: bash $ git clone https://github.com/Pylons/webob.git Report issues on the `issue tracker `_. If you've got questions that aren't answered by this documentation, contact the `pylons-discuss mail list `_ or join the `#pyramid IRC channel `_. WebOb is released under an :doc:`MIT-style license `. WebOb-1.8.6/docs/jsonrpc-example-code/0000755000076600000240000000000013611756053020450 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/jsonrpc-example-code/jsonrpc.py0000644000076600000240000001352413157035764022511 0ustar xistencestaff00000000000000# A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751 from webob import Request, Response from webob import exc from simplejson import loads, dumps import traceback import sys class JsonRpcApp(object): """ Serve the given object via json-rpc (http://json-rpc.org/) """ def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) def process(self, req): if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) try: result = method(*params) except: text = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=text) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) return Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) class ServerProxy(object): """ JSON proxy to a remote service. """ def __init__(self, url, proxy=None): self._url = url if proxy is None: from wsgiproxy.exactproxy import proxy_exact_request proxy = proxy_exact_request self.proxy = proxy def __getattr__(self, name): if name.startswith('_'): raise AttributeError(name) return _Method(self, name) def __repr__(self): return '<%s for %s>' % ( self.__class__.__name__, self._url) class _Method(object): def __init__(self, parent, name): self.parent = parent self.name = name def __call__(self, *args): json = dict(method=self.name, id=None, params=list(args)) req = Request.blank(self.parent._url) req.method = 'POST' req.content_type = 'application/json' req.body = dumps(json) resp = req.get_response(self.parent.proxy) if resp.status_code != 200 and not ( resp.status_code == 500 and resp.content_type == 'application/json'): raise ProxyError( "Error from JSON-RPC client %s: %s" % (self.parent._url, resp.status), resp) json = loads(resp.body) if json.get('error') is not None: e = Fault( json['error'].get('message'), json['error'].get('code'), json['error'].get('error'), resp) raise e return json['result'] class ProxyError(Exception): """ Raised when a request via ServerProxy breaks """ def __init__(self, message, response): Exception.__init__(self, message) self.response = response class Fault(Exception): """ Raised when there is a remote error """ def __init__(self, message, code, error, response): Exception.__init__(self, message) self.code = code self.error = error self.response = response def __str__(self): return 'Method error calling %s: %s\n%s' % ( self.response.request.url, self.args[0], self.error) class DemoObject(object): """ Something interesting to attach to """ def add(self, *args): return sum(args) def average(self, *args): return sum(args) / float(len(args)) def divide(self, a, b): return a / b def make_app(expr): module, expression = expr.split(':', 1) __import__(module) module = sys.modules[module] obj = eval(expression, module.__dict__) return JsonRpcApp(obj) def main(args=None): import optparse from wsgiref import simple_server parser = optparse.OptionParser( usage='%prog [OPTIONS] MODULE:EXPRESSION') parser.add_option( '-p', '--port', default='8080', help='Port to serve on (default 8080)') parser.add_option( '-H', '--host', default='127.0.0.1', help='Host to serve on (default localhost; 0.0.0.0 to make public)') options, args = parser.parse_args() if not args or len(args) > 1: print 'You must give a single object reference' parser.print_help() sys.exit(2) app = make_app(args[0]) server = simple_server.make_server(options.host, int(options.port), app) print 'Serving on http://%s:%s' % (options.host, options.port) server.serve_forever() # Try python jsonrpc.py 'jsonrpc:DemoObject()' if __name__ == '__main__': main() WebOb-1.8.6/docs/jsonrpc-example-code/test_jsonrpc.py0000644000076600000240000000012713157035764023543 0ustar xistencestaff00000000000000if __name__ == '__main__': import doctest doctest.testfile('test_jsonrpc.txt') WebOb-1.8.6/docs/jsonrpc-example-code/test_jsonrpc.txt0000644000076600000240000000156713157035764023743 0ustar xistencestaff00000000000000This is a test of the ``jsonrpc.py`` module:: >>> class Divider(object): ... def divide(self, a, b): ... return a / b >>> from jsonrpc import * >>> app = JsonRpcApp(Divider()) >>> proxy = ServerProxy('http://localhost:8080', proxy=app) >>> proxy.divide(10, 4) 2 >>> proxy.divide(10, 4.0) 2.5 >>> proxy.divide(10, 0) # doctest: +ELLIPSIS Traceback (most recent call last): ... Fault: Method error calling http://localhost:8080: integer division or modulo by zero Traceback (most recent call last): File ... result = method(*params) File ... return a / b ZeroDivisionError: integer division or modulo by zero >>> proxy.add(1, 1) Traceback (most recent call last): ... ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request WebOb-1.8.6/docs/jsonrpc-example.txt0000644000076600000240000005120513204653630020277 0ustar xistencestaff00000000000000JSON-RPC Example ================ .. contents:: :author: Ian Bicking Introduction ------------ This is an example of how to write a web service using WebOb. The example shows how to create a `JSON-RPC `_ endpoint using WebOb and the `simplejson `_ JSON library. This also shows how to use WebOb as a client library using `WSGIProxy `_. While this example presents JSON-RPC, this is not an endorsement of JSON-RPC. In fact I don't like JSON-RPC. It's unnecessarily un-RESTful, and modelled too closely on `XML-RPC `_. Code ---- The finished code for this is available in `docs/json-example-code/jsonrpc.py `_ -- you can run that file as a script to try it out, or import it. Concepts -------- JSON-RPC wraps an object, allowing you to call methods on that object and get the return values. It also provides a way to get error responses. The `specification `_ goes into the details (though in a vague sort of way). Here's the basics: * All access goes through a POST to a single URL. * The POST contains a JSON body that looks like:: {"method": "methodName", "id": "arbitrary-something", "params": [arg1, arg2, ...]} * The ``id`` parameter is just a convenience for the client to keep track of which response goes with which request. This makes asynchronous calls (like an XMLHttpRequest) easier. We just send the exact same id back as we get, we never look at it. * The response is JSON. A successful response looks like:: {"result": the_result, "error": null, "id": "arbitrary-something"} * The error response looks like:: {"result": null, "error": {"name": "JSONRPCError", "code": (number 100-999), "message": "Some Error Occurred", "error": "whatever you want\n(a traceback?)"}, "id": "arbitrary-something"} * It doesn't seem to indicate if an error response should have a 200 response or a 500 response. So as not to be completely stupid about HTTP, we choose a 500 resonse, as giving an error with a 200 response is irresponsible. Infrastructure -------------- To make this easier to test, we'll set up a bit of infrastructure. This will open up a server (using :py:mod:`wsgiref.simple_server`) and serve up our application (note that *creating* the application is left out to start with): .. code-block:: python import sys def main(args=None): import optparse from wsgiref import simple_server parser = optparse.OptionParser( usage="%prog [OPTIONS] MODULE:EXPRESSION") parser.add_option( '-p', '--port', default='8080', help='Port to serve on (default 8080)') parser.add_option( '-H', '--host', default='127.0.0.1', help='Host to serve on (default localhost; 0.0.0.0 to make public)') if args is None: args = sys.argv[1:] options, args = parser.parse_args() if not args or len(args) > 1: print 'You must give a single object reference' parser.print_help() sys.exit(2) app = make_app(args[0]) server = simple_server.make_server( options.host, int(options.port), app) print 'Serving on http://%s:%s' % (options.host, options.port) server.serve_forever() if __name__ == '__main__': main() I won't describe this much. It starts a server, serving up just the app created by ``make_app(args[0])``. ``make_app`` will have to load up the object and wrap it in our WSGI/WebOb wrapper. We'll be calling that wrapper ``JSONRPC(obj)``, so here's how it'll go: .. code-block:: python def make_app(expr): module, expression = expr.split(':', 1) __import__(module) module = sys.modules[module] obj = eval(expression, module.__dict__) return JsonRpcApp(obj) We use ``__import__(module)`` to import the module, but its return value is wonky. We can find the thing it imported in ``sys.modules`` (a dictionary of all the loaded modules). Then we evaluate the second part of the expression in the namespace of the module. This lets you do something like ``smtplib:SMTP('localhost')`` to get a fully instantiated SMTP object. That's all the infrastructure we'll need for the server side. Now we just have to implement ``JsonRpcApp``. The Application Wrapper ----------------------- Note that I'm calling this an "application" because that's the terminology WSGI uses. Everything that gets *called* is an "application", and anything that calls an application is called a "server". The instantiation of the server is already figured out: .. code-block:: python class JsonRpcApp(object): def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): ... the WSGI interface ... So the server is an instance bound to the particular object being exposed, and ``__call__`` implements the WSGI interface. We'll start with a simple outline of the WSGI interface, using a kind of standard WebOb setup: .. code-block:: python from webob import Request, Response from webob import exc class JsonRpcApp(object): ... def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) We first create a request object. The request object just wraps the WSGI environment. Then we create the response object in the ``process`` method (which we still have to write). We also do some exception catching. We'll turn any ``ValueError`` into a ``400 Bad Request`` response. We'll also let ``process`` raise any ``web.exc.HTTPException`` exception. There's an exception defined in that module for all the HTTP error responses, like ``405 Method Not Allowed``. These exceptions are themselves WSGI applications (as is ``webob.Response``), and so we call them like WSGI applications and return the result. The ``process`` method ---------------------- The ``process`` method of course is where all the fancy stuff happens. We'll start with just the most minimal implementation, with no error checking or handling: .. code-block:: python from simplejson import loads, dumps class JsonRpcApp(object): ... def process(self, req): json = loads(req.body) method = json['method'] params = json['params'] id = json['id'] method = getattr(self.obj, method) result = method(*params) resp = Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) return resp As long as the request is properly formed and the method doesn't raise any exceptions, you are pretty much set. But of course that's not a reasonable expectation. There's a whole bunch of things that can go wrong. For instance, it has to be a POST method: .. code-block:: python if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') And maybe the request body doesn't contain valid JSON: .. code-block:: python try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) And maybe all the keys aren't in the dictionary: .. code-block:: python try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) And maybe it's trying to acces a private method (a method that starts with ``_``) -- that's not just a bad request, we'll call that case ``403 Forbidden``. .. code-block:: python if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) And maybe ``json['params']`` isn't a list: .. code-block:: python if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) And maybe the method doesn't exist: .. code-block:: python try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) The last case is the error we actually can expect: that the method raises some exception. .. code-block:: python try: result = method(*params) except: tb = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=tb) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) That's a complete server. The Complete Code ----------------- Since we showed all the error handling in pieces, here's the complete code: .. code-block:: python from webob import Request, Response from webob import exc from simplejson import loads, dumps import traceback import sys class JsonRpcApp(object): """ Serve the given object via json-rpc (http://json-rpc.org/) """ def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) def process(self, req): if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) try: result = method(*params) except: text = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=text) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) return Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) The Client ---------- It would be nice to have a client to test out our server. Using `WSGIProxy`_ we can use WebOb Request and Response to do actual HTTP connections. The basic idea is that you can create a blank Request: .. code-block:: python >>> from webob import Request >>> req = Request.blank('http://python.org') Then you can send that request to an application: .. code-block:: python >>> from wsgiproxy.exactproxy import proxy_exact_request >>> resp = req.get_response(proxy_exact_request) This particular application (``proxy_exact_request``) sends the request over HTTP: .. code-block:: python >>> resp.content_type 'text/html' >>> resp.body[:10] '`_ and you can run it with `docs/json-example-code/test_jsonrpc.py `_, which looks like: .. code-block:: python if __name__ == '__main__': import doctest doctest.testfile('test_jsonrpc.txt') As you can see, it's just a stub to run the doctest. We'll need a simple object to expose. We'll make it real simple: .. code-block:: python >>> class Divider(object): ... def divide(self, a, b): ... return a / b Then we'll get the app setup: .. code-block:: python >>> from jsonrpc import * >>> app = JsonRpcApp(Divider()) And attach the client *directly* to it: .. code-block:: python >>> proxy = ServerProxy('http://localhost:8080', proxy=app) Because we gave the app itself as the proxy, the URL doesn't actually matter. Now, if you are used to testing you might ask: is this kosher? That is, we are shortcircuiting HTTP entirely. Is this a realistic test? One thing you might be worried about in this case is that there are more shared objects than you'd have with HTTP. That is, everything over HTTP is serialized to headers and bodies. Without HTTP, we can send stuff around that can't go over HTTP. This *could* happen, but we're mostly protected because the only thing the application's share is the WSGI ``environ``. Even though we use a ``webob.Request`` object on both side, it's not the *same* request object, and all the state is studiously kept in the environment. We *could* share things in the environment that couldn't go over HTTP. For instance, we could set ``environ['jsonrpc.request_value'] = dict(...)``, and avoid ``simplejson.dumps`` and ``simplejson.loads``. We *could* do that, and if we did then it is possible our test would work even though the libraries were broken over HTTP. But of course inspection shows we *don't* do that. A little discipline is required to resist playing clever tricks (or else you can play those tricks and do more testing). Generally it works well. So, now we have a proxy, lets use it: .. code-block:: python >>> proxy.divide(10, 4) 2 >>> proxy.divide(10, 4.0) 2.5 Lastly, we'll test a couple error conditions. First a method error: .. code-block:: python >>> proxy.divide(10, 0) # doctest: +ELLIPSIS Traceback (most recent call last): ... Fault: Method error calling http://localhost:8080: integer division or modulo by zero Traceback (most recent call last): File ... result = method(*params) File ... return a / b ZeroDivisionError: integer division or modulo by zero It's hard to actually predict this exception, because the test of the exception itself contains the traceback from the underlying call, with filenames and line numbers that aren't stable. We use ``# doctest: +ELLIPSIS`` so that we can replace text we don't care about with ``...``. This is actually figured out through copy-and-paste, and visual inspection to make sure it looks sensible. The other exception can be: .. code-block:: python >>> proxy.add(1, 1) Traceback (most recent call last): ... ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request Here the exception isn't a JSON-RPC method exception, but a more basic ProxyError exception. Conclusion ---------- Hopefully this will give you ideas about how to implement web services of different kinds using WebOb. I hope you also can appreciate the elegance of the symmetry of the request and response objects, and the client and server for the protocol. Many of these techniques would be better used with a `RESTful `_ service, so do think about that direction if you are implementing your own protocol. WebOb-1.8.6/docs/license.txt0000644000076600000240000000210113157035764016612 0ustar xistencestaff00000000000000License ======= Copyright (c) 2007 Ian Bicking and Contributors 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. WebOb-1.8.6/docs/reference.txt0000644000076600000240000007562713204653630017144 0ustar xistencestaff00000000000000.. _reference: WebOb Reference +++++++++++++++ .. contents:: Introduction ============ This document covers all the details of the Request and Response objects. It is written to be testable with :py:mod:`doctest` -- this affects the flavor of the documentation, perhaps to its detriment. But it also means you can feel confident that the documentation is correct. .. note:: All of the code samples below are for Python 3, and will not function as-is on Python 2. This is a somewhat different approach to reference documentation compared to the extracted documentation for the :py:mod:`~webob.request` and :py:mod:`~webob.response`. .. _request-reference: Request ======= The primary object in WebOb is :py:class:`webob.Request`, a wrapper around a `WSGI environment `_. The basic way you create a request object is simple enough: .. code-block:: python >>> from webob import Request >>> environ = {'wsgi.url_scheme': 'http', ...} #doctest: +SKIP >>> req = Request(environ) #doctest: +SKIP (Note that the WSGI environment is a dictionary with a dozen required keys, so it's a bit lengthly to show a complete example of what it would look like -- usually your WSGI server will create it.) The request object *wraps* the environment; it has very little internal state of its own. Instead attributes you access read and write to the environment dictionary. You don't have to understand the details of WSGI to use this library; this library handles those details for you. You also don't have to use this exclusively of other libraries. If those other libraries also keep their state in the environment, multiple wrappers can coexist. Examples of libraries that can coexist include `paste.wsgiwrappers.Request `_ (used by Pylons) and `yaro.Request `_. The WSGI environment has a number of required variables. To make it easier to test and play around with, the ``Request`` class has a constructor that will fill in a minimal environment: .. code-block:: python >>> req = Request.blank('/article?id=1') >>> from pprint import pprint >>> pprint(req.environ) # doctest: +ELLIPSIS {'HTTP_HOST': 'localhost:80', 'PATH_INFO': '/article', 'QUERY_STRING': 'id=1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.errors': <...TextIOWrapper ...'' ...>, 'wsgi.input': <...IO object at 0x...>, 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0)} Request Body ------------ :py:meth:`req.body ` is a file-like object that gives the body of the request (e.g., a POST form, the body of a PUT, etc). It's kind of boring to start, but you can set it to a string and that will be turned into a file-like object. You can read the entire body with :py:meth:`req.body `. .. code-block:: python >>> hasattr(req.body_file, 'read') True >>> req.body b'' >>> req.method = 'PUT' >>> req.body = b'test' >>> hasattr(req.body_file, 'read') True >>> req.body b'test' Method & URL ------------ All the normal parts of a request are also accessible through the request object: .. code-block:: python >>> req.method 'PUT' >>> req.scheme 'http' >>> req.script_name # The base of the URL '' >>> req.script_name = '/blog' # make it more interesting >>> req.path_info # The yet-to-be-consumed part of the URL '/article' >>> req.content_type # Content-Type of the request body '' >>> print(req.remote_user) # The authenticated user (there is none set) None >>> print(req.remote_addr) # The remote IP None >>> req.host 'localhost:80' >>> req.host_url 'http://localhost' >>> req.application_url 'http://localhost/blog' >>> req.path_url 'http://localhost/blog/article' >>> req.url 'http://localhost/blog/article?id=1' >>> req.path '/blog/article' >>> req.path_qs '/blog/article?id=1' >>> req.query_string 'id=1' You can make new URLs: .. code-block:: python >>> req.relative_url('archive') 'http://localhost/blog/archive' For parsing the URLs, it is often useful to deal with just the next path segment on PATH_INFO: .. code-block:: python >>> req.path_info_peek() # Doesn't change request 'article' >>> req.path_info_pop() # Does change request! 'article' >>> req.script_name '/blog/article' >>> req.path_info '' Headers ------- All request headers are available through a dictionary-like object :py:meth:`req.headers `. Keys are case-insensitive. .. code-block:: python >>> req.headers['Content-Type'] = 'application/x-www-urlencoded' >>> sorted(req.headers.items()) [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')] >>> req.environ['CONTENT_TYPE'] 'application/x-www-urlencoded' Query & POST variables ---------------------- Requests can have variables in one of two locations: the query string (``?id=1``), or in the body of the request (generally a POST form). Note that even POST requests can have a query string, so both kinds of variables can exist at the same time. Also, a variable can show up more than once, as in ``?check=a&check=b``. For these variables WebOb uses a :py:class:`~webob.multidict.MultiDict`, which is basically a dictionary wrapper on a list of key/value pairs. It looks like a single-valued dictionary, but you can access all the values of a key with :py:meth:`.getall(key) ` (which always returns a list, possibly an empty list). You also get all key/value pairs when using :py:meth:`.items() ` and all values with :py:meth:`.values() `. Some examples: .. code-block:: python >>> req = Request.blank('/test?check=a&check=b&name=Bob') >>> req.GET GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) >>> req.GET['check'] 'b' >>> req.GET.getall('check') ['a', 'b'] >>> list(req.GET.items()) [('check', 'a'), ('check', 'b'), ('name', 'Bob')] We'll have to create a request body and change the method to get ``POST``. Until we do that, the variables are boring: .. code-block:: python >>> req.POST >>> list(req.POST.items()) # NoVars can be read like a dict, but not written [] >>> req.method = 'POST' >>> req.body = b'name=Joe&email=joe@example.com' >>> req.POST MultiDict([('name', 'Joe'), ('email', 'joe@example.com')]) >>> req.POST['name'] 'Joe' Often you won't care where the variables come from. (Even if you care about the method, the location of the variables might not be important.) There is a dictionary called :py:meth:`req.params ` that contains variables from both sources: .. code-block:: python >>> req.params NestedMultiDict([('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com')]) >>> req.params['name'] 'Bob' >>> req.params.getall('name') ['Bob', 'Joe'] >>> for name, value in req.params.items(): ... print('%s: %r' % (name, value)) check: 'a' check: 'b' name: 'Bob' name: 'Joe' email: 'joe@example.com' The ``POST`` and ``GET`` nomenclature is historical -- :py:meth:`req.GET ` can be used for non-GET requests to access query parameters, and :py:meth:`req.POST ` can also be used for PUT requests with the appropriate Content-Type. >>> req = Request.blank('/test?check=a&check=b&name=Bob') >>> req.method = 'PUT' >>> req.body = b'var1=value1&var2=value2&rep=1&rep=2' >>> req.environ['CONTENT_LENGTH'] = str(len(req.body)) >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' >>> req.GET GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) >>> req.POST MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')]) Unicode Variables ~~~~~~~~~~~~~~~~~ Submissions are by default UTF-8, you can force a different character set by setting the charset on the ``Request`` object explicitly. A client can indicate the character set with ``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but very few clients actually do this (sometimes XMLHttpRequest requests will do this, as JSON is always UTF8 even when a page is served with a different character set). You can force a charset, which will affect all the variables: .. code-block:: python >>> req.charset = 'utf8' >>> req.GET GET([('check', 'a'), ('check', 'b'), ('name', 'Bob')]) Cookies ------- Cookies are presented in a simple dictionary. Like other variables, they will be decoded into Unicode strings if you set the charset. .. code-block:: python >>> req.headers['Cookie'] = 'test=value' >>> req.cookies Modifying the request --------------------- The headers are all modifiable, as are other environmental variables (like :py:meth:`req.remote_user `, which maps to ``request.environ['REMOTE_USER']``). If you want to copy the request you can use :py:meth:`req.copy() `; this copies the ``environ`` dictionary, and the request body from ``environ['wsgi.input']``. The method :py:meth:`req.remove_conditional_headers(remove_encoding=True) ` can be used to remove headers that might result in a ``304 Not Modified`` response. If you are writing some intermediary it can be useful to avoid these headers. Also if ``remove_encoding`` is true (the default) then any ``Accept-Encoding`` header will be removed, which can result in gzipped responses. Header Getters -------------- In addition to :py:meth:`req.headers `, there are attributes for most of the request headers defined by the HTTP 1.1 specification. These attributes often return parsed forms of the headers. Accept-* headers ~~~~~~~~~~~~~~~~ There are several request headers that tell the server what the client accepts. These are ``accept`` (the Content-Type that is accepted), ``accept_charset`` (the charset accepted), ``accept_encoding`` (the Content-Encoding, like gzip, that is accepted), and ``accept_language`` (generally the preferred language of the client). The objects returned support containment to test for acceptability. E.g.: .. code-block:: python >>> 'text/html' in req.accept True Because no header means anything is potentially acceptable, this is returning True. We can set it to see more interesting behavior (the example means that ``text/html`` is okay, but ``application/xhtml+xml`` is preferred): .. code-block:: python >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' >>> req.accept >>> 'text/html' in req.accept True There are a few methods for different strategies of finding a match. .. code-block:: python >>> req.accept.best_match(['text/html', 'application/xhtml+xml']) 'application/xhtml+xml' If we just want to know everything the client prefers, in the order it is preferred: .. code-block:: python >>> list(req.accept) ['application/xhtml+xml', 'text/html'] For languages you'll often have a "fallback" language. E.g., if there's nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore any less preferrable languages): .. code-block:: python >>> req.accept_language = 'es, pt-BR' >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US') 'en-US' >>> req.accept_language.best_match(['es', 'en-US'], default_match='en-US') 'es' Your fallback language must appear both in the ``offers`` and as the ``default_match`` to insure that it is returned as a best match if the client specified a preference for it. .. code-block:: python >>> req.accept_language = 'en-US;q=0.5, en-GB;q=0.2' >>> req.accept_language.best_match(['en-GB'], default_match='en-US') 'en-GB' >>> req.accept_language.best_match(['en-GB', 'en-US'], default_match='en-US') 'en-US' Conditional Requests ~~~~~~~~~~~~~~~~~~~~ There a number of ways to make a conditional request. A conditional request is made when the client has a document, but it is not sure if the document is up to date. If it is not, it wants a new version. If the document is up to date then it doesn't want to waste the bandwidth, and expects a ``304 Not Modified`` response. ETags are generally the best technique for these kinds of requests; this is an opaque string that indicates the identity of the object. For instance, it's common to use the mtime (last modified) of the file, plus the number of bytes, and maybe a hash of the filename (if there's a possibility that the same URL could point to two different server-side filenames based on other variables). To test if a 304 response is appropriate, you can use: .. code-block:: python >>> server_token = 'opaque-token' >>> server_token in req.if_none_match # You shouldn't return 304 False >>> req.if_none_match = server_token >>> req.if_none_match >>> server_token in req.if_none_match # You *should* return 304 True For date-based comparisons If-Modified-Since is used: .. code-block:: python >>> from webob import UTC >>> from datetime import datetime >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.headers['If-Modified-Since'] 'Sun, 01 Jan 2006 12:00:00 GMT' >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_modified_since and req.if_modified_since >= server_modified True For range requests there are two important headers, If-Range (which is form of conditional request) and Range (which requests a range). If the If-Range header fails to match then the full response (not a range) should be returned: .. code-block:: python >>> req.if_range IfRange() >>> req.if_range = 'opaque-etag' >>> from webob import Response >>> res = Response(etag='opaque-etag') >>> res in req.if_range True To get the range information: >>> req.range = 'bytes=0-100' >>> req.range >>> cr = req.range.content_range(length=1000) >>> cr.start, cr.stop, cr.length (0, 101, 1000) Note that the range headers use *inclusive* ranges (the last byte indexed is included), where Python always uses a range where the last index is excluded from the range. The ``.stop`` index is in the Python form. Another kind of conditional request is a request (typically PUT) that includes If-Match or If-Unmodified-Since. In this case you are saying "here is an update to a resource, but don't apply it if someone else has done something since I last got the resource". If-Match means "do this if the current ETag matches the ETag I'm giving". If-Unmodified-Since means "do this if the resource has remained unchanged". .. code-block:: python >>> server_token in req.if_match # No If-Match means everything is ok True >>> req.if_match = server_token >>> server_token in req.if_match # Still OK True >>> req.if_match = 'other-token' >>> # Not OK, should return 412 Precondition Failed: >>> server_token in req.if_match False For more on this kind of conditional request, see `Detecting the Lost Update Problem Using Unreserved Checkout `_. Calling WSGI Applications ------------------------- The request object can be used to make handy subrequests or test requests against WSGI applications. If you want to make subrequests, you should copy the request (with ``req.copy()``) before sending it to multiple applications, since applications might modify the request when they are run. There's two forms of the subrequest. The more primitive form is this: .. code-block:: python >>> req = Request.blank('/') >>> def wsgi_app(environ, start_response): ... start_response('200 OK', [('Content-type', 'text/plain')]) ... return ['Hi!'] >>> req.call_application(wsgi_app) ('200 OK', [('Content-type', 'text/plain')], ['Hi!']) Note it returns ``(status_string, header_list, app_iter)``. If ``app_iter.close()`` exists, it is your responsibility to call it. A handier response can be had with: .. code-block:: python >>> res = req.get_response(wsgi_app) >>> res # doctest: +ELLIPSIS >>> res.status '200 OK' >>> res.headers ResponseHeaders([('Content-type', 'text/plain')]) >>> res.body 'Hi!' You can learn more about this response object in the Response_ section. Ad-Hoc Attributes ----------------- You can assign attributes to your request objects. They will all go in ``environ['webob.adhoc_attrs']`` (a dictionary). .. code-block:: python >>> req = Request.blank('/') >>> req.some_attr = 'blah blah blah' >>> new_req = Request(req.environ) >>> new_req.some_attr 'blah blah blah' >>> req.environ['webob.adhoc_attrs'] {'some_attr': 'blah blah blah'} .. _response-reference: Response ======== The :py:class:`webob.Response ` object contains everything necessary to make a WSGI response. Instances of it are in fact WSGI applications, but it can also represent the result of calling a WSGI application (as noted in `Calling WSGI Applications`_). It can also be a way of accumulating a response in your WSGI application. A WSGI response is made up of a status (like ``200 OK``), a list of headers, and a body (or iterator that will produce a body). Core Attributes --------------- The core attributes are unsurprising: .. code-block:: python >>> from webob import Response >>> res = Response() >>> res.status '200 OK' >>> res.headerlist [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')] >>> res.body b'' You can set any of these attributes, e.g.: .. code-block:: python >>> res.status = 404 >>> res.status '404 Not Found' >>> res.status_code 404 >>> res.headerlist = [('Content-Type', 'text/html')] >>> res.body = b'test' >>> print(res) 404 Not Found Content-Type: text/html Content-Length: 4 test >>> res.body = "test" # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: You cannot set Response.body to a text object (use Response.text) >>> res.text = "test" >>> res.body b'test' You can set any attribute with the constructor, like ``Response(charset='UTF-8')`` Headers ------- In addition to ``res.headerlist``, there is dictionary-like view on the list in ``res.headers``: .. code-block:: python >>> res.headers ResponseHeaders([('Content-Type', 'text/html'), ('Content-Length', '4')]) This is case-insensitive. It can support multiple values for a key, though only if you use ``res.headers.add(key, value)`` or read them with ``res.headers.getall(key)``. Body & app_iter --------------- The ``res.body`` attribute represents the entire body of the request as a single string (not unicode, though you can set it to unicode if you have a charset defined). There is also a ``res.app_iter`` attribute that reprsents the body as an iterator. WSGI applications return these ``app_iter`` iterators instead of strings, and sometimes it can be problematic to load the entire iterator at once (for instance, if it returns the contents of a very large file). Generally it is not a problem, and often the iterator is something simple like a one-item list containing a string with the entire body. If you set the body then Content-Length will also be set, and an ``res.app_iter`` will be created for you. If you set ``res.app_iter`` then Content-Length will be cleared, but it won't be set for you. There is also a file-like object you can access, which will update the app_iter in-place (turning the app_iter into a list if necessary): .. code-block:: python >>> res = Response(content_type='text/plain', charset=None) >>> f = res.body_file >>> f.write('hey') # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: You can only write text to Response if charset has been set >>> f.encoding >>> res.charset = 'UTF-8' >>> f.encoding 'UTF-8' >>> f.write('test') >>> res.app_iter [b'', b'test'] >>> res.body b'test' Header Getters -------------- Like Request, HTTP response headers are also available as individual properties. These represent parsed forms of the headers. Content-Type is a special case, as the type and the charset are handled through two separate properties: .. code-block:: python >>> res = Response() >>> res.content_type = 'text/html' >>> res.charset = 'utf8' >>> res.content_type 'text/html' >>> res.headers['content-type'] 'text/html; charset=utf8' >>> res.content_type = 'application/atom+xml' >>> res.content_type_params {'charset': 'UTF-8'} >>> res.content_type_params = {'type': 'entry', 'charset': 'UTF-8'} >>> res.headers['content-type'] 'application/atom+xml; charset=UTF-8; type=entry' Other headers: .. code-block:: python >>> # Used with a redirect: >>> res.location = 'http://localhost/foo' >>> # Indicates that the server accepts Range requests: >>> res.accept_ranges = 'bytes' >>> # Used by caching proxies to tell the client how old the >>> # response is: >>> res.age = 120 >>> # Show what methods the client can do; typically used in >>> # a 405 Method Not Allowed response: >>> res.allow = ['GET', 'PUT'] >>> # Set the cache-control header: >>> res.cache_control.max_age = 360 >>> res.cache_control.no_transform = True >>> # Tell the browser to treat the response as an attachment: >>> res.content_disposition = 'attachment; filename=foo.xml' >>> # Used if you had gzipped the body: >>> res.content_encoding = 'gzip' >>> # What language(s) are in the content: >>> res.content_language = ['en'] >>> # Seldom used header that tells the client where the content >>> # is from: >>> res.content_location = 'http://localhost/foo' >>> # Seldom used header that gives a hash of the body: >>> res.content_md5 = 'big-hash' >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total: >>> # you can also use the range setter shown earlier >>> res.content_range = (0, 501, 1000) >>> # The length of the content; set automatically if you set >>> # res.body: >>> res.content_length = 4 >>> # Used to indicate the current date as the server understands >>> # it: >>> res.date = datetime.now() >>> # The etag: >>> res.etag = 'opaque-token' >>> # You can generate it from the body too: >>> res.md5_etag() >>> res.etag '1B2M2Y8AsgTpgAmY7PhCfg' >>> # When this page should expire from a cache (Cache-Control >>> # often works better): >>> import time >>> res.expires = time.time() + 60*60 # 1 hour >>> # When this was last modified, of course: >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC) >>> # Used with 503 Service Unavailable to hint the client when to >>> # try again: >>> res.retry_after = 160 >>> # Indicate the server software: >>> res.server = 'WebOb/1.0' >>> # Give a list of headers that the cache should vary on: >>> res.vary = ['Cookie'] Note in each case you can general set the header to a string to avoid any parsing, and set it to None to remove the header (or do something like ``del res.vary``). In the case of date-related headers you can set the value to a ``datetime`` instance (ideally with a UTC timezone), a time tuple, an integer timestamp, or a properly-formatted string. After setting all these headers, here's the result: .. code-block:: python >>> for name, value in res.headerlist: # doctest: +ELLIPSIS ... print('%s: %s' % (name, value)) # doctest: +ELLIPSIS Content-Type: application/atom+xml; charset=UTF-8; type=entry Location: http://localhost/foo Accept-Ranges: bytes Age: 120 Allow: GET, PUT Cache-Control: max-age=360, no-transform Content-Disposition: attachment; filename=foo.xml Content-Encoding: gzip Content-Language: en Content-Location: http://localhost/foo Content-MD5: big-hash Content-Range: bytes 0-500/1000 Content-Length: 4 Date: ... GMT ETag: ... Expires: ... GMT Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT Retry-After: 160 Server: WebOb/1.0 Vary: Cookie You can also set Cache-Control related attributes with ``req.cache_expires(seconds, **attrs)``, like: .. code-block:: python >>> res = Response() >>> res.cache_expires(10) >>> res.headers['Cache-Control'] 'max-age=10' >>> res.cache_expires(0) >>> res.headers['Cache-Control'] 'max-age=0, must-revalidate, no-cache, no-store' >>> res.headers['Expires'] # doctest: +ELLIPSIS '... GMT' You can also use the :py:class:`~datetime.timedelta` constants defined, e.g.: .. code-block:: python >>> from webob import * >>> res = Response() >>> res.cache_expires(2*day+4*hour) >>> res.headers['Cache-Control'] 'max-age=187200' Cookies ------- Cookies (and the Set-Cookie header) are handled with a couple methods. Most importantly: .. code-block:: python >>> res.set_cookie('key', 'value', max_age=360, path='/', ... domain='example.org', secure=True) >>> res.headers['Set-Cookie'] # doctest: +ELLIPSIS 'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure' >>> # To delete a cookie previously set in the client: >>> res.delete_cookie('bad_cookie') >>> res.headers['Set-Cookie'] # doctest: +ELLIPSIS 'bad_cookie=; Max-Age=0; Path=/; expires=... GMT' The only other real method of note (note that this does *not* delete the cookie from clients, only from the response object): .. code-block:: python >>> res.unset_cookie('key') >>> res.unset_cookie('bad_cookie') >>> print(res.headers.get('Set-Cookie')) None Binding a Request ----------------- You can bind a request (or request WSGI environ) to the response object. This is available through ``res.request`` or ``res.environ``. This is currently only used in setting ``res.location``, to make the location absolute if necessary. Response as a WSGI application ------------------------------ A response is a WSGI application, in that you can do: .. code-block:: python >>> req = Request.blank('/') >>> status, headers, app_iter = req.call_application(res) A possible pattern for your application might be: .. code-block:: python >>> def my_app(environ, start_response): ... req = Request(environ) ... res = Response() ... res.charset = 'UTF-8' ... res.content_type = 'text/plain' ... parts = [] ... for name, value in sorted(req.environ.items()): ... parts.append('%s: %r' % (name, value)) ... res.text = '\n'.join(parts) ... return res(environ, start_response) >>> req = Request.blank('/') >>> res = req.get_response(my_app) >>> print(res) # doctest: +ELLIPSIS 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: ... HTTP_HOST: 'localhost:80' PATH_INFO: '/' QUERY_STRING: '' REQUEST_METHOD: 'GET' SCRIPT_NAME: '' SERVER_NAME: 'localhost' SERVER_PORT: '80' SERVER_PROTOCOL: 'HTTP/1.0' wsgi.errors: <...> wsgi.input: <...IO... object at ...> wsgi.multiprocess: False wsgi.multithread: False wsgi.run_once: False wsgi.url_scheme: 'http' wsgi.version: (1, 0) Exceptions ========== In addition to Request and Response objects, there are a set of Python exceptions for different HTTP responses (3xx, 4xx, 5xx codes). These provide a simple way to provide these non-200 response. A very simple body is provided. .. code-block:: python >>> from webob.exc import * >>> exc = HTTPTemporaryRedirect(location='foo') >>> req = Request.blank('/path/to/something') >>> print(str(req.get_response(exc)).strip()) 307 Temporary Redirect Location: http://localhost/path/to/foo Content-Length: 126 Content-Type: text/plain; charset=UTF-8 \r 307 Temporary Redirect The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically. Note that only if there's an ``Accept: text/html`` header in the request will an HTML response be given: .. code-block:: python >>> req.accept += 'text/html' >>> print(str(req.get_response(exc)).strip()) 307 Temporary Redirect Location: http://localhost/path/to/foo Content-Length: 270 Content-Type: text/html; charset=UTF-8 307 Temporary Redirect

307 Temporary Redirect

The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically. Conditional WSGI Application ---------------------------- The Response object can handle your conditional responses for you, checking If-None-Match, If-Modified-Since, and Range/If-Range. To enable this you must create the response like ``Response(conditional_response=True)``, or make a subclass like: .. code-block:: python >>> class AppResponse(Response): ... default_content_type = 'text/html' ... default_conditional_response = True >>> res = AppResponse(body='0123456789', ... last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC)) >>> req = Request.blank('/') >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.get_response(res) # doctest: +ELLIPSIS >>> del req.if_modified_since >>> res.etag = 'opaque-tag' >>> req.if_none_match = 'opaque-tag' >>> req.get_response(res) # doctest: +ELLIPSIS >>> req.if_none_match = '*' >>> 'x' in req.if_none_match True >>> req.if_none_match = req.if_none_match >>> 'x' in req.if_none_match True >>> req.if_none_match = None >>> 'x' in req.if_none_match False >>> req.if_match = None >>> 'x' in req.if_match True >>> req.if_match = req.if_match >>> 'x' in req.if_match True >>> req.headers.get('If-Match') '*' >>> del req.if_none_match >>> req.range = (1, 5) >>> result = req.get_response(res) >>> result.headers['content-range'] 'bytes 1-4/10' >>> result.body b'1234' WebOb-1.8.6/docs/whatsnew-1.5.txt0000644000076600000240000000734613157035764017351 0ustar xistencestaff00000000000000What's New in WebOb 1.5 ======================= Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``Response.set_cookie`` renamed the only required parameter from "key" to "name". The code will now still accept "key" as a keyword argument, and will issue a DeprecationWarning until WebOb 1.7. - The ``status`` attribute of a ``Response`` object no longer takes a string like ``None None`` and allows that to be set as the status. It now has to at least match the pattern of `` ``. Invalid status strings will now raise a ``ValueError``. - ``Morsel`` will no longer accept a cookie value that does not meet RFC6265's cookie-octet specification. Upon calling ``Morsel.serialize`` a warning will be issued, in the future this will raise a ``ValueError``, please update your cookie handling code. See https://github.com/Pylons/webob/pull/172 The cookie-octet specification in RFC6265 states the following characters are valid in a cookie value: =============== ======================================= Hex Range Actual Characters =============== ======================================= ``[0x21 ]`` ``!`` ``[0x25-0x2B]`` ``#$%&'()*+`` ``[0x2D-0x3A]`` ``-./0123456789:`` ``[0x3C-0x5B]`` ``<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[`` ``[0x5D-0x7E]`` ``]^_`abcdefghijklmnopqrstuvwxyz{|}~`` =============== ======================================= RFC6265 suggests using base 64 to serialize data before storing data in a cookie. Cookies that meet the RFC6265 standard will no longer be quoted, as this is unnecessary. This is a no-op as far as browsers and cookie storage is concerned. - ``Response.set_cookie`` now uses the internal ``make_cookie`` API, which will issue warnings if cookies are set with invalid bytes. See https://github.com/Pylons/webob/pull/172 Features ~~~~~~~~ - Add support for some new caching headers, stale-while-revalidate and stale-if-error that can be used by reverse proxies to cache stale responses temporarily if the backend disappears. From RFC5861. See https://github.com/Pylons/webob/pull/189 Bug Fixes ~~~~~~~~~ - Response.status now uses duck-typing for integers, and has also learned to raise a ValueError if the status isn't an integer followed by a space, and then the reason. See https://github.com/Pylons/webob/pull/191 - Fixed a bug in ``webob.multidict.GetDict`` which resulted in the QUERY_STRING not being updated when changes were made to query params using ``Request.GET.extend()``. - Read the body of a request if we think it might have a body. This fixes PATCH to support bodies. See https://github.com/Pylons/webob/pull/184 - Response.from_file returns HTTP headers as latin1 rather than UTF-8, this fixes the usage on Google AppEngine. See https://github.com/Pylons/webob/issues/99 and https://github.com/Pylons/webob/pull/150 - Fix a bug in parsing the auth parameters that contained bad white space. This makes the parsing fall in line with what's required in RFC7235. See https://github.com/Pylons/webob/issues/158 - Use '\r\n' line endings in ``Response.__str__``. See: https://github.com/Pylons/webob/pull/146 Documentation Changes ~~~~~~~~~~~~~~~~~~~~~ - ``response.set_cookie`` now has proper documentation for ``max_age`` and ``expires``. The code has also been refactored to use ``cookies.make_cookie`` instead of duplicating the code. This fixes https://github.com/Pylons/webob/issues/166 and https://github.com/Pylons/webob/issues/171 - Documentation didn't match the actual code for the wsgify function signature. See https://github.com/Pylons/webob/pull/167 - Remove the WebDAV only from certain HTTP Exceptions, these exceptions may also be used by REST services for example. WebOb-1.8.6/docs/whatsnew-1.6.txt0000644000076600000240000000607413157035764017347 0ustar xistencestaff00000000000000What's New in WebOb 1.6 ======================= Compatibility ~~~~~~~~~~~~~ - Python 3.2 is no longer a supported platform by WebOb Security ~~~~~~~~ - exc._HTTPMove and any subclasses will now raise a ValueError if the location field contains a line feed or carriage return. These values may lead to possible HTTP Response Splitting. The header_getter descriptor has also been modified to no longer accept headers with a line feed or carriage return. WebOb does not protect against all possible ways of injecting line feeds or carriage returns into headers, and should only be thought of as a single line of defense. Any user input should be sanitized. See https://github.com/Pylons/webob/pull/229 and https://github.com/Pylons/webob/issues/217 for more information. Features ~~~~~~~~ - When WebOb sends an HTTP Exception it will now lazily escape the keys in the environment, so that only those keys that are actually used in the HTTP exception are escaped. This solves the problem of keys that are not serializable as a string in the environment. See https://github.com/Pylons/webob/pull/139 for more information. - MIMEAccept now accepts comparisons against wildcards, this allows one to match on just the media type or sub-type. Example: .. code-block:: pycon >>> accept = MIMEAccept('text/html') >>> 'text/*' in accept True >>> '*/html' in accept True >>> '*' in accept True - WebOb uses the user agent's Accept header to change what type of information is returned to the client. This allows the HTTP Exception to return either HTML, text, or a JSON response. This allows WebOb HTTP Exceptions to be used in applications where the client is expecting a JSON response. See https://github.com/Pylons/webob/pull/230 and https://github.com/Pylons/webob/issues/209 for more information. Bugfixes ~~~~~~~~ - Response.from_file now parses the status line correctly when the status line contains an HTTP with version, as well as a status text that contains multiple white spaces (e.g HTTP/1.1 404 Not Found). See https://github.com/Pylons/webob/issues/250 - Request.decode would attempt to read from an already consumed stream, it is now reading from the correct stream. See https://github.com/Pylons/webob/pull/183 for more information. - The ``application/json`` media type does not allow for a ``charset`` because discovery of the encoding is done at the JSON layer, and it must always be UTF-{8,16,32}. See the IANA specification at https://www.iana.org/assignments/media-types/application/json, which notes: No "charset" parameter is defined for this registration. Adding one really has no effect on compliant recipients. `IETF RFC 4627 `_ describes the method for encoding discovery using the JSON content itself. Upon initialization of a Response, WebOb will no longer add a ``charset`` if the content-type is set to JSON. See https://github.com/Pylons/webob/pull/197, https://github.com/Pylons/webob/issues/237, and https://github.com/Pylons/pyramid/issues/1611 WebOb-1.8.6/docs/whatsnew-1.7.txt0000644000076600000240000002052513157035764017345 0ustar xistencestaff00000000000000What's New in WebOb 1.7 ======================= Compatibility ~~~~~~~~~~~~~ - WebOb is no longer supported on Python 2.6 and PyPy3. PyPy3 support will be re-introduced as soon as it supports a Python version higher than 3.2 and pip fully supports the platform again. If you would like Python 2.6 support, please pin to WebOb 1.6, which still has Python 2.6 support. Backwards Incompatibility ~~~~~~~~~~~~~~~~~~~~~~~~~ - :attr:`Response.content_type ` removes all existing Content-Type parameters, and if the new Content-Type is "texty" it adds a new charset (unless already provided) using the ``default_charset``, to emulate the old behaviour you may use the following: .. code-block:: python res = Response(content_type='text/html', charset='UTF-8') assert res.content_type == 'text/html' assert res.charset == 'UTF-8' params = res.content_type_params # Change the Content-Type res.content_type = 'application/unknown' assert res.content_type == 'application/unknown' assert res.charset == None # This will add the ``charset=UTF-8`` parameter to the Content-Type res.content_type_params = params assert res.headers['Content-Type'] == 'application/unknown; charset=UTF-8' See https://github.com/Pylons/webob/pull/301 for more information. - :class:`~webob.response.Response` no longer treats ``application/json`` as a special case that may also be treated as text. This means the following may no longer be used: .. code-block:: python res = Response(json.dumps({}), content_type='application/json') Since ``application/json`` does not have a ``charset``, this will now raise an error. Replacements are: .. code-block:: python res = Response(json_body={}) This will create a new :class:`~webob.response.Response` that automatically sets up the the Content-Type and converts the dictionary to a JSON object internally. If you want WebOb to the encoding but do the conversion to JSON yourself, the following would also work: .. code-block:: python res = Response(text=json.dumps({}), content_type='application/json') This uses :attr:`~webob.response.Response.default_body_encoding` to encode the text. - :func:`Response.set_cookie ` no longer accepts a key argument. This was deprecated in WebOb 1.5 and as mentioned in the deprecation, is being removed in 1.7 Use: .. code-block:: python res = Response() res.set_cookie(name='cookie_name', value='val') # or res.set_cookie('cookie_name', 'val') Instead of: .. code-block:: python res = Response() res.set_cookie(key='cookie_name', value='val') - :func:`Response.__init__ ` will no longer set the default Content-Type, nor Content-Length on Responses that don't have a body. This allows WebOb to return proper responses for things like `Response(status='204 No Content')`. - :attr:`Response.text ` will no longer raise if the Content-Type does not have a charset, it will fall back to using the new default_body_encoding. To get the old behaviour back please sub-class Response and set default_body_encoding to None. See https://github.com/Pylons/webob/pull/287 An example of a Response class that has the old behaviour: .. code-block:: python class MyResponse(Response): default_body_encoding = None res = MyResponse(content_type='application/json') # This will raise as application/json doesn't have a charset res.text = 'sometext' - WebOb no longer supports Chunked Encoding, this means that if you are using WebOb and need Chunked Encoding you will be required to have a proxy that unchunks the request for you. Please read https://github.com/Pylons/webob/issues/279 for more background. This changes the behaviour of ``request.is_body_readable``, it will no longer assume that a request has a body just because it is a particular HTTP verb. This change also allows any HTTP verb to be able to contain a body, which allows for example a HTTP body on DELETE or even GET. Feature ~~~~~~~ - :class:`~webob.response.Response` has a new ``default_body_encoding`` which may be used to allow getting/setting :attr:`Response.text ` when a Content-Type has no charset. See https://github.com/Pylons/webob/pull/287 .. code-block:: python res = Response() res.default_body_encoding = 'latin1' res.text = 'Will be encoded as latin1 and .body will be set' res = Response() res.default_body_encoding = 'latin1' res.body = b'A valid latin-1 string' res.text == 'A valid latin-1 string' - :class:`~webob.request.Request` with any HTTP method is now allowed to have a body. This allows DELETE to have a request body for passing extra information. See https://github.com/Pylons/webob/pull/283 and https://github.com/Pylons/webob/pull/274 - Add :func:`~webob.response.ResponseBodyFile.tell` to :class:`~webob.response.ResponseBodyFile` so that it may be used for example for zipfile support. See https://github.com/Pylons/webob/pull/117 - Allow the return from :func:`wsgify.middleware ` to be used as a decorator. See https://github.com/Pylons/webob/pull/228 .. code-block:: python @wsgify.middleware def restrict_ip(req, app, ips): if req.remote_addr not in ips: raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr) return app @restrict_ip(ips=['127.0.0.1']) @wsgify def app(req): return 'hi' Bugfix ~~~~~~ - Fixup :class:`cgi.FieldStorage` on Python 3.x to work-around issue reported in Python bug report 27777 and 24764. This is currently applied for Python versions less than 3.7. See https://github.com/Pylons/webob/pull/294 - :func:`Response.set_cookie ` now accepts :class:`~datetime.datetime` objects for the ``expires`` kwarg and will correctly convert them to UTC with no ``tzinfo`` for use in calculating the ``max_age``. See https://github.com/Pylons/webob/issues/254 and https://github.com/Pylons/webob/pull/292 - Fixes :attr:`request.PATH_SAFE ` to contain all of the path safe characters according to RFC3986. See https://github.com/Pylons/webob/pull/291 - WebOb's exceptions will lazily read underlying variables when inserted into templates to avoid expensive computations/crashes when inserting into the template. This had a bad performance regression on Py27 because of the way the lazified class was created and returned. See https://github.com/Pylons/webob/pull/284 - :func:`wsgify.__call__ ` raised a ``TypeError`` with an unhelpful message, it will now return the `repr` for the wrapped function: https://github.com/Pylons/webob/issues/119 - :attr:`Response.json `'s json.dumps/loads are now always UTF-8. It no longer tries to use the charset. - The :class:`~webob.response.Response` will by default no longer set the Content-Type to the default if a headerlist is provided. This fixes issues whereby `Request.get_response()` would return a Response that didn't match the actual response. See https://github.com/Pylons/webob/pull/261 and https://github.com/Pylons/webob/issues/205 - Cleans up the remainder of the issues with the updated WebOb exceptions that were taught to return JSON in version 1.6. See https://github.com/Pylons/webob/issues/237 and https://github.com/Pylons/webob/issues/236 - :func:`Response.from_file ` now parses the status line correctly when the status line contains an HTTP with version, as well as a status text that contains multiple white spaces (e.g HTTP/1.1 404 Not Found). See https://github.com/Pylons/webob/issues/250 - :class:`~webob.response.Response` now has a new property named :attr:`~webob.response.Response.has_body` that may be used to interrogate the Response to find out if the :attr:`~webob.response.Response.body` is or isn't set. This is used in the exception handling code so that if you use a WebOb HTTP Exception and pass a generator to ``app_iter`` WebOb won't attempt to read the whole thing and instead allows it to be returned to the WSGI server. See https://github.com/Pylons/webob/pull/259 WebOb-1.8.6/docs/whatsnew-1.8.txt0000644000076600000240000001041013261302253017320 0ustar xistencestaff00000000000000What's New in WebOb 1.8 ======================= Feature ~~~~~~~ - :attr:`Request.POST ` now supports any requests with the appropriate Content-Type. Allowing any HTTP method to access form encoded content, including DELETE, PUT, and others. See https://github.com/Pylons/webob/pull/352 Compatibility ~~~~~~~~~~~~~ - WebOb is no longer officially supported on Python 3.3 which was EOL'ed on 2017-09-29. Please pin to `WebOb~=1.7` which was tested against Python 3.3, and upgrade your Python version. Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Many changes have been made to the way WebOb does Accept handling, not just for the ``Accept`` header itself, but also for ``Accept-Charset``, ``Accept-Encoding`` and ``Accept-Language``. This was a `Google Summer of Code `_ project completed by Whiteroses (https://github.com/whiteroses). Many thanks to Google for running GSoC, the Python Software Foundation for organising and a huge thanks to Ira for completing the work. See https://github.com/Pylons/webob/pull/338 and https://github.com/Pylons/webob/pull/335. If you were previously using the ``Accept`` class or the ``MIMEAccept`` class directly, please take a look at the documentation for :func:`~webob.acceptparse.create_accept_header`, :func:`~webob.acceptparse.create_accept_charset_header`, :func:`~webob.acceptparse.create_accept_encoding_header` and :func:`~webob.acceptparse.create_accept_language_header`. These functions will accept a header value and create the appropriate object. The :ref:`API documentation for Accept* ` provides more information on the available API. - When calling a ``@wsgify`` decorated function, the default arguments passed to ``@wsgify`` are now used when called with the request, and not as a `start_response` .. code:: def hello(req, name): return "Hello, %s!" % name app = wsgify(hello, args=("Fred",)) req = Request.blank('/') resp = req.get_response(app) # => "Hello, Fred" resp2 = app(req) # => "Hello, Fred" Previously the ``resp2`` line would have failed with a ``TypeError``. With this change there is no way to override the default arguments with no arguments. See https://github.com/Pylons/webob/pull/203 - When setting ``app_iter`` on a ``Response`` object the ``content_md5`` header is no longer cleared. This behaviour is odd and disallows setting the ``content_md5`` and then returning an iterator for chunked content encoded responses. See https://github.com/Pylons/webob/issues/86 Experimental Features ~~~~~~~~~~~~~~~~~~~~~ These features are experimental and may change at any point in the future. The main page provides a list of :ref:`experimental-api` supported by WebOb. - The cookie APIs now have the ability to set the SameSite attribute on a cookie in both :func:`webob.cookies.make_cookie` and :class:`webob.cookies.CookieProfile`. See https://github.com/Pylons/webob/pull/255 Bugfix ~~~~~~ - :attr:`Request.host_url `, :attr:`Request.host_port `, :attr:`Request.domain ` correctly parse IPv6 Host headers as provided by a browser. See https://github.com/Pylons/webob/pull/332 - :attr:`Request.authorization ` would raise :class:`ValueError` for unusual or malformed header values. Now it simply returns an empty value. See https://github.com/Pylons/webob/issues/231 - Allow unnamed fields in form data to be properly transcoded when calling :func:`request.decode ` with an alternate encoding. See https://github.com/Pylons/webob/pull/309 - :class:`Response.__init__ ` would discard ``app_iter`` when a ``Response`` had no body, this would cause issues when ``app_iter`` was an object that was tied to the life-cycle of a web application and had to be properly closed. ``app_iter`` is more advanced API for ``Response`` and thus even if it contains a body and is thus against the HTTP RFC's, we should let the users shoot themselves in the foot by returning a body. See https://github.com/Pylons/webob/issues/305 WebOb-1.8.6/docs/wiki-example-code/0000755000076600000240000000000013611756053017735 5ustar xistencestaff00000000000000WebOb-1.8.6/docs/wiki-example-code/example.py0000644000076600000240000001331213157035764021746 0ustar xistencestaff00000000000000import os import re from webob import Request, Response from webob import exc from tempita import HTMLTemplate VIEW_TEMPLATE = HTMLTemplate("""\ {{page.title}}

{{page.title}}

{{if message}}
{{message}}
{{endif}}
{{page.content|html}}

Edit """) EDIT_TEMPLATE = HTMLTemplate("""\ Edit: {{page.title}} {{if page.exists}}

Edit: {{page.title}}

{{else}}

Create: {{page.title}}

{{endif}}
Title:
Content: Cancel

Cancel
""") class WikiApp(object): view_template = VIEW_TEMPLATE edit_template = EDIT_TEMPLATE def __init__(self, storage_dir): self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) def __call__(self, environ, start_response): req = Request(environ) action = req.params.get('action', 'view') page = self.get_page(req.path_info) try: try: meth = getattr(self, 'action_%s_%s' % (action, req.method)) except AttributeError: raise exc.HTTPBadRequest('No such action %r' % action) resp = meth(req, page) except exc.HTTPException, e: resp = e return resp(environ, start_response) def get_page(self, path): path = path.lstrip('/') if not path: path = 'index' path = os.path.join(self.storage_dir, path) path = os.path.normpath(path) if path.endswith('/'): path += 'index' if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path") path += '.html' return Page(path) def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') if req.cookies.get('message'): message = req.cookies['message'] else: message = None text = self.view_template.substitute( page=page, req=req, message=message) resp = Response(text) if message: resp.delete_cookie('message') else: resp.last_modified = page.mtime resp.conditional_response = True return resp def action_view_POST(self, req, page): submit_mtime = int(req.params.get('mtime') or '0') or None if page.mtime != submit_mtime: return exc.HTTPPreconditionFailed( "The page has been updated since you started editing it") page.set( title=req.params['title'], content=req.params['content']) resp = exc.HTTPSeeOther( location=req.path_url) resp.set_cookie('message', 'Page updated') return resp def action_edit_GET(self, req, page): text = self.edit_template.substitute( page=page, req=req) return Response(text) class Page(object): def __init__(self, filename): self.filename = filename @property def exists(self): return os.path.exists(self.filename) @property def title(self): if not self.exists: # we need to guess the title basename = os.path.splitext(os.path.basename(self.filename))[0] basename = re.sub(r'[_-]', ' ', basename) return basename.capitalize() content = self.full_content match = re.search(r'(.*?)', content, re.I|re.S) return match.group(1) @property def full_content(self): f = open(self.filename, 'rb') try: return f.read() finally: f.close() @property def content(self): if not self.exists: return '' content = self.full_content match = re.search(r']*>(.*?)', content, re.I|re.S) return match.group(1) @property def mtime(self): if not self.exists: return None else: return int(os.stat(self.filename).st_mtime) def set(self, title, content): dir = os.path.dirname(self.filename) if not os.path.exists(dir): os.makedirs(dir) new_content = """%s%s""" % ( title, content) f = open(self.filename, 'wb') f.write(new_content) f.close() if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--wiki-data', default='./wiki', dest='wiki_data', help='Place to put wiki data into (default ./wiki/)') options, args = parser.parse_args() print 'Writing wiki pages to %s' % options.wiki_data app = WikiApp(options.wiki_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' WebOb-1.8.6/docs/wiki-example.txt0000644000076600000240000005725113204653630017573 0ustar xistencestaff00000000000000Wiki Example ============ :author: Ian Bicking .. contents:: Introduction ------------ This is an example of how to write a WSGI application using WebOb. WebOb isn't itself intended to write applications -- it is not a web framework on its own -- but it is *possible* to write applications using just WebOb. The `file serving example `_ is a better example of advanced HTTP usage. The `comment middleware example `_ is a better example of using middleware. This example provides some completeness by showing an application-focused end point. This example implements a very simple wiki. Code ---- The finished code for this is available in `docs/wiki-example-code/example.py `_ -- you can run that file as a script to try it out. Creating an Application ----------------------- A common pattern for creating small WSGI applications is to have a class which is instantiated with the configuration. For our application we'll be storing the pages under a directory. .. code-block:: python class WikiApp(object): def __init__(self, storage_dir): self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) WSGI applications are callables like ``wsgi_app(environ, start_response)``. *Instances* of `WikiApp` are WSGI applications, so we'll implement a ``__call__`` method: .. code-block:: python class WikiApp(object): ... def __call__(self, environ, start_response): # what we'll fill in To make the script runnable we'll create a simple command-line interface: .. code-block:: python if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--wiki-data', default='./wiki', dest='wiki_data', help='Place to put wiki data into (default ./wiki/)') options, args = parser.parse_args() print 'Writing wiki pages to %s' % options.wiki_data app = WikiApp(options.wiki_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' There's not much to talk about in this code block. The application is instantiated and served with the built-in module :py:mod:`wsgiref.simple_server`. The WSGI Application -------------------- Of course all the interesting stuff is in that ``__call__`` method. WebOb lets you ignore some of the details of WSGI, like what ``start_response`` really is. ``environ`` is a CGI-like dictionary, but ``webob.Request`` gives an object interface to it. ``webob.Response`` represents a response, and is itself a WSGI application. Here's kind of the hello world of WSGI applications using these objects: .. code-block:: python from webob import Request, Response class WikiApp(object): ... def __call__(self, environ, start_response): req = Request(environ) resp = Response( 'Hello %s!' % req.params.get('name', 'World')) return resp(environ, start_response) ``req.params.get('name', 'World')`` gets any query string parameter (like ``?name=Bob``), or if it's a POST form request it will look for a form parameter ``name``. We instantiate the response with the body of the response. You could also give keyword arguments like ``content_type='text/plain'`` (``text/html`` is the default content type and ``200 OK`` is the default status). For the wiki application we'll support a couple different kinds of screens, and we'll make our ``__call__`` method dispatch to different methods depending on the request. We'll support an ``action`` parameter like ``?action=edit``, and also dispatch on the method (GET, POST, etc, in ``req.method``). We'll pass in the request and expect a response object back. Also, WebOb has a series of exceptions in ``webob.exc``, like ``webob.exc.HTTPNotFound``, ``webob.exc.HTTPTemporaryRedirect``, etc. We'll also let the method raise one of these exceptions and turn it into a response. One last thing we'll do in our ``__call__`` method is create our ``Page`` object, which represents a wiki page. All this together makes: .. code-block:: python from webob import Request, Response from webob import exc class WikiApp(object): ... def __call__(self, environ, start_response): req = Request(environ) action = req.params.get('action', 'view') # Here's where we get the Page domain object: page = self.get_page(req.path_info) try: try: # The method name is action_{action_param}_{request_method}: meth = getattr(self, 'action_%s_%s' % (action, req.method)) except AttributeError: # If the method wasn't found there must be # something wrong with the request: raise exc.HTTPBadRequest('No such action %r' % action) resp = meth(req, page) except exc.HTTPException, e: # The exception object itself is a WSGI application/response: resp = e return resp(environ, start_response) The Domain Object ----------------- The ``Page`` domain object isn't really related to the web, but it is important to implementing this. Each ``Page`` is just a file on the filesystem. Our ``get_page`` method figures out the filename given the path (the path is in ``req.path_info``, which is all the path after the base path). The ``Page`` class handles getting and setting the title and content. Here's the method to figure out the filename: .. code-block:: python import os class WikiApp(object): ... def get_page(self, path): path = path.lstrip('/') if not path: # The path was '/', the home page path = 'index' path = os.path.join(self.storage_dir) path = os.path.normpath(path) if path.endswith('/'): path += 'index' if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path") path += '.html' return Page(path) Mostly this is just the kind of careful path construction you have to do when mapping a URL to a filename. While the server *may* normalize the path (so that a path like ``/../../`` can't be requested), you can never really be sure. By using ``os.path.normpath`` we eliminate these, and then we make absolutely sure that the resulting path is under our ``self.storage_dir`` with ``if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path")``. Here's the actual domain object: .. code-block:: python class Page(object): def __init__(self, filename): self.filename = filename @property def exists(self): return os.path.exists(self.filename) @property def title(self): if not self.exists: # we need to guess the title basename = os.path.splitext(os.path.basename(self.filename))[0] basename = re.sub(r'[_-]', ' ', basename) return basename.capitalize() content = self.full_content match = re.search(r'(.*?)', content, re.I|re.S) return match.group(1) @property def full_content(self): f = open(self.filename, 'rb') try: return f.read() finally: f.close() @property def content(self): if not self.exists: return '' content = self.full_content match = re.search(r']*>(.*?)', content, re.I|re.S) return match.group(1) @property def mtime(self): if not self.exists: return None else: return int(os.stat(self.filename).st_mtime) def set(self, title, content): dir = os.path.dirname(self.filename) if not os.path.exists(dir): os.makedirs(dir) new_content = """%s%s""" % ( title, content) f = open(self.filename, 'wb') f.write(new_content) f.close() Basically it provides a ``.title`` attribute, a ``.content`` attribute, the ``.mtime`` (last modified time), and the page can exist or not (giving appropriate guesses for title and content when the page does not exist). It encodes these on the filesystem as a simple HTML page that is parsed by some regular expressions. None of this really applies much to the web or WebOb, so I'll leave it to you to figure out the details of this. URLs, PATH_INFO, and SCRIPT_NAME -------------------------------- This is an aside for the tutorial, but an important concept. In WSGI, and accordingly with WebOb, the URL is split up into several pieces. Some of these are obvious and some not. An example:: http://example.com:8080/wiki/article/12?version=10 There are several components here: * req.scheme: ``http`` * req.host: ``example.com:8080`` * req.server_name: ``example.com`` * req.server_port: 8080 * req.script_name: ``/wiki`` * req.path_info: ``/article/12`` * req.query_string: ``version=10`` One non-obvious part is ``req.script_name`` and ``req.path_info``. These correspond to the CGI environmental variables ``SCRIPT_NAME`` and ``PATH_INFO``. ``req.script_name`` points to the *application*. You might have several applications in your site at different paths: one at ``/wiki``, one at ``/blog``, one at ``/``. Each application doesn't necessarily know about the others, but it has to construct its URLs properly -- so any internal links to the wiki application should start with ``/wiki``. Just as there are pieces to the URL, there are several properties in WebOb to construct URLs based on these: * req.host_url: ``http://example.com:8080`` * req.application_url: ``http://example.com:8080/wiki`` * req.path_url: ``http://example.com:8080/wiki/article/12`` * req.path: ``/wiki/article/12`` * req.path_qs: ``/wiki/article/12?version=10`` * req.url: ``http://example.com:8080/wiki/article/12?version10`` You can also create URLs with ``req.relative_url('some/other/page')``. In this example that would resolve to ``http://example.com:8080/wiki/article/some/other/page``. You can also create a relative URL to the application URL (SCRIPT_NAME) like ``req.relative_url('some/other/page', True)`` which would be ``http://example.com:8080/wiki/some/other/page``. Back to the Application ----------------------- We have a dispatching function with ``__call__`` and we have a domain object with ``Page``, but we aren't actually doing anything. The dispatching goes to ``action_ACTION_METHOD``, where ACTION defaults to ``view``. So a simple page view will be ``action_view_GET``. Let's implement that: .. code-block:: python class WikiApp(object): ... def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') text = self.view_template.substitute( page=page, req=req) resp = Response(text) resp.last_modified = page.mtime resp.conditional_response = True return resp The first thing we do is redirect the user to the edit screen if the page doesn't exist. ``exc.HTTPTemporaryRedirect`` is a response that gives a ``307 Temporary Redirect`` response with the given location. Otherwise we fill in a template. The template language we're going to use in this example is `Tempita `_, a very simple template language with a similar interface to :py:class:`string.Template`. The template actually looks like this: .. code-block:: python from tempita import HTMLTemplate VIEW_TEMPLATE = HTMLTemplate("""\ {{page.title}}

{{page.title}}

{{page.content|html}}

Edit """) class WikiApp(object): view_template = VIEW_TEMPLATE ... As you can see it's a simple template using the title and the body, and a link to the edit screen. We copy the template object into a class method (``view_template = VIEW_TEMPLATE``) so that potentially a subclass could override these templates. ``tempita.HTMLTemplate`` is a template that does automatic HTML escaping. Our wiki will just be written in plain HTML, so we disable escaping of the content with ``{{page.content|html}}``. So let's look at the ``action_view_GET`` method again: .. code-block:: python def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') text = self.view_template.substitute( page=page, req=req) resp = Response(text) resp.last_modified = page.mtime resp.conditional_response = True return resp The template should be pretty obvious now. We create a response with ``Response(text)``, which already has a default Content-Type of ``text/html``. To allow conditional responses we set ``resp.last_modified``. You can set this attribute to a date, None (effectively removing the header), a time tuple (like produced by ``time.localtime()``), or as in this case to an integer timestamp. If you get the value back it will always be a :py:class:`~datetime.datetime` object (or None). With this header we can process requests with If-Modified-Since headers, and return ``304 Not Modified`` if appropriate. It won't actually do that unless you set ``resp.conditional_response`` to True. .. note:: If you subclass ``webob.Response`` you can set the class attribute ``default_conditional_response = True`` and this setting will be on by default. You can also set other defaults, like the ``default_charset`` (``"utf8"``), or ``default_content_type`` (``"text/html"``). The Edit Screen --------------- The edit screen will be implemented in the method ``action_edit_GET``. There's a template and a very simple method: .. code-block:: python EDIT_TEMPLATE = HTMLTemplate("""\ Edit: {{page.title}} {{if page.exists}}

Edit: {{page.title}}

{{else}}

Create: {{page.title}}

{{endif}}
Title:
Content: Cancel

Cancel
""") class WikiApp(object): ... edit_template = EDIT_TEMPLATE def action_edit_GET(self, req, page): text = self.edit_template.substitute( page=page, req=req) return Response(text) As you can see, all the action here is in the template. In ``
`` we submit to ``req.path_url``; that's everything *but* ``?action=edit``. So we are POSTing right over the view page. This has the nice side effect of automatically invalidating any caches of the original page. It also is vaguely `RESTful `_. We save the last modified time in a hidden ``mtime`` field. This way we can detect concurrent updates. If start editing the page who's mtime is 100000, and someone else edits and saves a revision changing the mtime to 100010, we can use this hidden field to detect that conflict. Actually resolving the conflict is a little tricky and outside the scope of this particular tutorial, we'll just note the conflict to the user in an error. From there we just have a very straight-forward HTML form. Note that we don't quote the values because that is done automatically by ``HTMLTemplate``; if you are using something like ``string.Template`` or a templating language that doesn't do automatic quoting, you have to be careful to quote all the field values. We don't have any error conditions in our application, but if there were error conditions we might have to re-display this form with the input values the user already gave. In that case we'd do something like:: This way we use the value in the request (``req.params`` is both the query string parameters and any variables in a POST response), but if there is no value (e.g., first request) then we use the page values. Processing the Form ------------------- The form submits to ``action_view_POST`` (``view`` is the default action). So we have to implement that method: .. code-block:: python class WikiApp(object): ... def action_view_POST(self, req, page): submit_mtime = int(req.params.get('mtime') or '0') or None if page.mtime != submit_mtime: return exc.HTTPPreconditionFailed( "The page has been updated since you started editing it") page.set( title=req.params['title'], content=req.params['content']) resp = exc.HTTPSeeOther( location=req.path_url) return resp The first thing we do is check the mtime value. It can be an empty string (when there's no mtime, like when you are creating a page) or an integer. ``int(req.params.get('time') or '0') or None`` basically makes sure we don't pass ``""`` to ``int()`` (which is an error) then turns 0 into None (``0 or None`` will evaluate to None in Python -- ``false_value or other_value`` in Python resolves to ``other_value``). If it fails we just give a not-very-helpful error message, using ``412 Precondition Failed`` (typically preconditions are HTTP headers like ``If-Unmodified-Since``, but we can't really get the browser to send requests like that, so we use the hidden field instead). .. note:: Error statuses in HTTP are often under-used because people think they need to either return an error (useful for machines) or an error message or interface (useful for humans). In fact you can do both: you can give any human readable error message with your error response. One problem is that Internet Explorer will replace error messages with its own incredibly unhelpful error messages. However, it will only do this if the error message is short. If it's fairly large (4Kb is large enough) it will show the error message it was given. You can load your error with a big HTML comment to accomplish this, like ``"" % ('x'*4000)``. You can change the status of any response with ``resp.status_int = 412``, or you can change the body of an ``exc.HTTPSomething`` with ``resp.body = new_body``. The primary advantage of using the classes in ``webob.exc`` is giving the response a clear name and a boilerplate error message. After we check the mtime we get the form parameters from ``req.params`` and issue a redirect back to the original view page. ``303 See Other`` is a good response to give after accepting a POST form submission, as it gets rid of the POST (no warning messages for the user if they try to go back). In this example we've used ``req.params`` for all the form values. If we wanted to be specific about where we get the values from, they could come from ``req.GET`` (the query string, a misnomer since the query string is present even in POST requests) or ``req.POST`` (a POST form body). While sometimes it's nice to distinguish between these two locations, for the most part it doesn't matter. If you want to check the request method (e.g., make sure you can't change a page with a GET request) there's no reason to do it by accessing these method-specific getters. It's better to just handle the method specifically. We do it here by including the request method in our dispatcher (dispatching to ``action_view_GET`` or ``action_view_POST``). Cookies ------- One last little improvement we can do is show the user a message when they update the page, so it's not quite so mysteriously just another page view. A simple way to do this is to set a cookie after the save, then display it in the page view. To set it on save, we add a little to ``action_view_POST``: .. code-block:: python def action_view_POST(self, req, page): ... resp = exc.HTTPSeeOther( location=req.path_url) resp.set_cookie('message', 'Page updated') return resp And then in ``action_view_GET``: .. code-block:: python VIEW_TEMPLATE = HTMLTemplate("""\ ... {{if message}}
{{message}}
{{endif}} ...""") class WikiApp(object): ... def action_view_GET(self, req, page): ... if req.cookies.get('message'): message = req.cookies['message'] else: message = None text = self.view_template.substitute( page=page, req=req, message=message) resp = Response(text) if message: resp.delete_cookie('message') else: resp.last_modified = page.mtime resp.conditional_response = True return resp ``req.cookies`` is just a dictionary, and we also delete the cookie if it is present (so the message doesn't keep getting set). The conditional response stuff only applies when there isn't any message, as messages are private. Another alternative would be to display the message with Javascript, like:: Then put ```` in the page somewhere. This has the advantage of being very cacheable and simple on the server side. Conclusion ---------- We're done, hurrah! WebOb-1.8.6/rtd.txt0000644000076600000240000000001313611742763015030 0ustar xistencestaff00000000000000-e .[docs] WebOb-1.8.6/setup.cfg0000644000076600000240000000035513611756053015325 0ustar xistencestaff00000000000000[wheel] universal = 1 [metadata] license_file = docs/license.txt [flake8] exclude = show-source = True max-line-length = 89 [tool:pytest] python_files = test_*.py testpaths = src/webob tests [egg_info] tag_build = tag_date = 0 WebOb-1.8.6/setup.py0000644000076600000240000000345613611752370015221 0ustar xistencestaff00000000000000import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) try: with open(os.path.join(here, 'README.rst')) as f: README = f.read() with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() except IOError: README = CHANGES = '' testing_extras = [ 'pytest >= 3.1.0', # >= 3.1.0 so we can use pytest.param 'coverage', 'pytest-cov', 'pytest-xdist', ] docs_extras = [ 'Sphinx >= 1.7.5', 'pylons-sphinx-themes', ] setup( name='WebOb', version='1.8.6', description="WSGI request and response object", long_description=README + '\n\n' + CHANGES, classifiers=[ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], keywords='wsgi request web http', author='Ian Bicking', author_email='ianb@colorstudy.com', maintainer='Pylons Project', url='http://webob.org/', license='MIT', packages=find_packages('src', exclude=['tests']), package_dir={'': 'src'}, python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*', zip_safe=True, extras_require={ 'testing': testing_extras, 'docs': docs_extras, }, ) WebOb-1.8.6/src/0000755000076600000240000000000013611756053014270 5ustar xistencestaff00000000000000WebOb-1.8.6/src/WebOb.egg-info/0000755000076600000240000000000013611756053016760 5ustar xistencestaff00000000000000WebOb-1.8.6/src/WebOb.egg-info/PKG-INFO0000644000076600000240000002752313611756053020066 0ustar xistencestaff00000000000000Metadata-Version: 2.1 Name: WebOb Version: 1.8.6 Summary: WSGI request and response object Home-page: http://webob.org/ Author: Ian Bicking Author-email: ianb@colorstudy.com Maintainer: Pylons Project License: MIT Description: WebOb ===== .. image:: https://travis-ci.org/Pylons/webob.png?branch=master :target: https://travis-ci.org/Pylons/webob .. image:: https://readthedocs.org/projects/webob/badge/?version=stable :target: https://docs.pylonsproject.org/projects/webob/en/stable/ :alt: Documentation Status WebOb provides objects for HTTP requests and responses. Specifically it does this by wrapping the `WSGI `_ request environment and response status/headers/app_iter(body). The request and response objects provide many conveniences for parsing HTTP request and forming HTTP responses. Both objects are read/write: as a result, WebOb is also a nice way to create HTTP requests and parse HTTP responses. Support and Documentation ------------------------- See the `WebOb Documentation website `_ to view documentation, report bugs, and obtain support. License ------- WebOb is offered under the `MIT-license `_. Authors ------- WebOb was authored by Ian Bicking and is currently maintained by the `Pylons Project `_ and a team of contributors. 1.8.6 (2020-01-21) ------------------ Experimental Features ~~~~~~~~~~~~~~~~~~~~~ - The SameSite value now includes a new option named "None", this is a new change that was introduced in https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 Please be aware that older clients are incompatible with this change: https://www.chromium.org/updates/same-site/incompatible-clients, WebOb does not enable SameSite on cookies by default, so there is no backwards incompatible change here. See https://github.com/Pylons/webob/issues/406 - Validation of SameSite values can be disabled by toggling a module flag. This is in anticipation of future changes in evolving cookie standards. The discussion in https://github.com/Pylons/webob/pull/407 (which initially expanded the allowed options) notes the sudden change to browser cookie implementation details may happen again. In May 2019, Google announced a new model for privacy controls in their browsers, which affected the list of valid options for the SameSite attribute of cookies. In late 2019, the company began to roll out these changes to their browsers to force developer adoption of the new specification. See https://www.chromium.org/updates/same-site and https://blog.chromium.org/2019/10/developers-get-ready-for-new.html for more details on this change. See https://github.com/Pylons/webob/pull/409 1.8.5 (2019-01-03) ------------------ Warnings ~~~~~~~~ - Fixed one last remaining invalid escape sequence in a docstring. 1.8.4 (2018-11-11) ------------------ Bugfix ~~~~~~ - Response.content_type now accepts unicode strings on Python 2 and encodes them to latin-1. See https://github.com/Pylons/webob/pull/389 and https://github.com/Pylons/webob/issues/388 - Accept header classes now support a .copy() function that may be used to create a copy. This allows ``create_accept_header`` and other like functions to accept an pre-existing Accept header. See https://github.com/Pylons/webob/pull/386 and https://github.com/Pylons/webob/issues/385 Warnings ~~~~~~~~ - Some backslashes introduced with the new accept handling code were causing DeprecationWarnings upon compiling the source to pyc files, all of the backslashes have been reigned in as appropriate, and users should no longer see DeprecationWarnings for invalid escape sequence. See https://github.com/Pylons/webob/issues/384 1.8.3 (2018-10-14) ------------------ Bugfix ~~~~~~ - ``acceptparse.AcceptValidHeader``, ``acceptparse.AcceptInvalidHeader``, and ``acceptparse.AcceptNoHeader`` will now always ignore offers that do not match the required media type grammar when calling ``.acceptable_offers()``. Previous versions raised a ``ValueError`` for invalid offers in ``AcceptValidHeader`` and returned them as acceptable in the others. See https://github.com/Pylons/webob/pull/372 Feature ~~~~~~~ - Add Request.remote_host, exposing REMOTE_HOST environment variable. - Added ``acceptparse.Accept.parse_offer`` to codify what types of offers are compatible with ``acceptparse.AcceptValidHeader.acceptable_offers``, ``acceptparse.AcceptMissingHeader.acceptable_offers``, and ``acceptparse.AcceptInvalidHeader.acceptable_offers``. This API also normalizes the offer with lowercased type/subtype and parameter names. See https://github.com/Pylons/webob/pull/376 and https://github.com/Pylons/webob/pull/379 1.8.2 (2018-06-05) ------------------ Bugfix ~~~~~~ - SameSite may now be passed as str or bytes to `Response.set_cookie` and `cookies.make_cookie`. This was an oversight as all other arguments would be correctly coerced before being serialized. See https://github.com/Pylons/webob/issues/361 and https://github.com/Pylons/webob/pull/362 1.8.1 (2018-04-10) ------------------ Bugfix ~~~~~~ - acceptparse.MIMEAccept which is deprecated in WebOb 1.8.0 made a backwards incompatible change that led to it raising on an invalid Accept header. This behaviour has now been reversed, as well as some other fixes to allow MIMEAccept to behave more like the old version. See https://github.com/Pylons/webob/pull/356 1.8.0 (2018-04-04) ------------------ Feature ~~~~~~~ - ``request.POST`` now supports any requests with the appropriate Content-Type. Allowing any HTTP method to access form encoded content, including DELETE, PUT, and others. See https://github.com/Pylons/webob/pull/352 Compatibility ~~~~~~~~~~~~~ - WebOb is no longer officially supported on Python 3.3 which was EOL'ed on 2017-09-29. Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Many changes have been made to the way WebOb does Accept handling, not just for the Accept header itself, but also for Accept-Charset, Accept-Encoding and Accept-Language. This was a `Google Summer of Code `_ project completed by Whiteroses (https://github.com/whiteroses). Many thanks to Google for running GSoC, the Python Software Foundation for organising and a huge thanks to Ira for completing the work. See https://github.com/Pylons/webob/pull/338 and https://github.com/Pylons/webob/pull/335. Documentation is available at https://docs.pylonsproject.org/projects/webob/en/master/api/webob.html - When calling a ``@wsgify`` decorated function, the default arguments passed to ``@wsgify`` are now used when called with the request, and not as a `start_response` .. code:: def hello(req, name): return "Hello, %s!" % name app = wsgify(hello, args=("Fred",)) req = Request.blank('/') resp = req.get_response(app) # => "Hello, Fred" resp2 = app(req) # => "Hello, Fred" Previously the ``resp2`` line would have failed with a ``TypeError``. With this change there is no way to override the default arguments with no arguments. See https://github.com/Pylons/webob/pull/203 - When setting ``app_iter`` on a ``Response`` object the ``content_md5`` header is no longer cleared. This behaviour is odd and disallows setting the ``content_md5`` and then returning an iterator for chunked content encoded responses. See https://github.com/Pylons/webob/issues/86 Experimental Features ~~~~~~~~~~~~~~~~~~~~~ These features are experimental and may change at any point in the future. - The cookie APIs now have the ability to set the SameSite attribute on a cookie in both ``webob.cookies.make_cookie`` and ``webob.cookies.CookieProfile``. See https://github.com/Pylons/webob/pull/255 Bugfix ~~~~~~ - Exceptions now use string.Template.safe_substitute rather than string.Template.substitute. The latter would raise for missing mappings, the former will simply not substitute the missing variable. This is safer in case the WSGI environ does not contain the keys necessary for the body template. See https://github.com/Pylons/webob/issues/345. - Request.host_url, Request.host_port, Request.domain correctly parse IPv6 Host headers as provided by a browser. See https://github.com/Pylons/webob/pull/332 - Request.authorization would raise ValueError for unusual or malformed header values. See https://github.com/Pylons/webob/issues/231 - Allow unnamed fields in form data to be properly transcoded when calling request.decode with an alternate encoding. See https://github.com/Pylons/webob/pull/309 - ``Response.__init__`` would discard ``app_iter`` when a ``Response`` had no body, this would cause issues when ``app_iter`` was an object that was tied to the life-cycle of a web application and had to be properly closed. ``app_iter`` is more advanced API for ``Response`` and thus even if it contains a body and is thus against the HTTP RFC's, we should let the users shoot themselves by returning a body. See https://github.com/Pylons/webob/issues/305 Keywords: wsgi request web http Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.* Provides-Extra: testing Provides-Extra: docs WebOb-1.8.6/src/WebOb.egg-info/SOURCES.txt0000644000076600000240000000407413611756053020651 0ustar xistencestaff00000000000000.coveragerc .gitignore .travis.yml CHANGES.txt HISTORY.txt README.rst RELEASING.rst appveyor.yml contributing.md rtd.txt setup.cfg setup.py tox.ini docs/Makefile docs/changes.txt docs/comment-example.txt docs/conf.py docs/differences.txt docs/do-it-yourself.txt docs/doctests.py docs/file-example.txt docs/index.txt docs/jsonrpc-example.txt docs/license.txt docs/reference.txt docs/whatsnew-1.5.txt docs/whatsnew-1.6.txt docs/whatsnew-1.7.txt docs/whatsnew-1.8.txt docs/wiki-example.txt docs/_static/.empty docs/api/client.txt docs/api/cookies.txt docs/api/dec.txt docs/api/exceptions.txt docs/api/multidict.txt docs/api/request.txt docs/api/response.txt docs/api/static.txt docs/api/webob.txt docs/comment-example-code/example.py docs/experimental/samesite.txt docs/file-example-code/test-file.txt docs/jsonrpc-example-code/jsonrpc.py docs/jsonrpc-example-code/test_jsonrpc.py docs/jsonrpc-example-code/test_jsonrpc.txt docs/wiki-example-code/example.py src/WebOb.egg-info/PKG-INFO src/WebOb.egg-info/SOURCES.txt src/WebOb.egg-info/dependency_links.txt src/WebOb.egg-info/requires.txt src/WebOb.egg-info/top_level.txt src/WebOb.egg-info/zip-safe src/webob/__init__.py src/webob/acceptparse.py src/webob/byterange.py src/webob/cachecontrol.py src/webob/client.py src/webob/compat.py src/webob/cookies.py src/webob/datetime_utils.py src/webob/dec.py src/webob/descriptors.py src/webob/etag.py src/webob/exc.py src/webob/headers.py src/webob/multidict.py src/webob/request.py src/webob/response.py src/webob/static.py src/webob/util.py tests/conftest.py tests/performance_test.py tests/test_acceptparse.py tests/test_byterange.py tests/test_cachecontrol.py tests/test_client.py tests/test_client_functional.py tests/test_compat.py tests/test_cookies.py tests/test_cookies_bw.py tests/test_datetime_utils.py tests/test_dec.py tests/test_descriptors.py tests/test_etag.py tests/test_etag_nose.py tests/test_exc.py tests/test_headers.py tests/test_in_wsgiref.py tests/test_misc.py tests/test_multidict.py tests/test_request.py tests/test_response.py tests/test_static.py tests/test_transcode.py tests/test_util.pyWebOb-1.8.6/src/WebOb.egg-info/dependency_links.txt0000644000076600000240000000000113611756053023026 0ustar xistencestaff00000000000000 WebOb-1.8.6/src/WebOb.egg-info/requires.txt0000644000076600000240000000014513611756053021360 0ustar xistencestaff00000000000000 [docs] Sphinx>=1.7.5 pylons-sphinx-themes [testing] pytest>=3.1.0 coverage pytest-cov pytest-xdist WebOb-1.8.6/src/WebOb.egg-info/top_level.txt0000644000076600000240000000000613611756053021506 0ustar xistencestaff00000000000000webob WebOb-1.8.6/src/WebOb.egg-info/zip-safe0000644000076600000240000000000113362210051020372 0ustar xistencestaff00000000000000 WebOb-1.8.6/src/webob/0000755000076600000240000000000013611756053015366 5ustar xistencestaff00000000000000WebOb-1.8.6/src/webob/__init__.py0000644000076600000240000000105613611751037017476 0ustar xistencestaff00000000000000from webob.datetime_utils import ( # noqa: F401 UTC, day, hour, minute, month, parse_date, parse_date_delta, second, serialize_date, serialize_date_delta, timedelta_to_seconds, week, year ) from webob.request import BaseRequest, LegacyRequest, Request from webob.response import Response from webob.util import html_escape __all__ = [ 'Request', 'LegacyRequest', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape' ] BaseRequest.ResponseClass = Response WebOb-1.8.6/src/webob/acceptparse.py0000644000076600000240000063244213611751037020242 0ustar xistencestaff00000000000000""" Parse four ``Accept*`` headers used in server-driven content negotiation. The four headers are ``Accept``, ``Accept-Charset``, ``Accept-Encoding`` and ``Accept-Language``. """ from collections import namedtuple import re import textwrap import warnings # RFC 7230 Section 3.2.3 "Whitespace" # OWS = *( SP / HTAB ) # ; optional whitespace OWS_re = '[ \t]*' # RFC 7230 Section 3.2.6 "Field Value Components": # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" # / DIGIT / ALPHA tchar_re = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]" # token = 1*tchar token_re = tchar_re + '+' token_compiled_re = re.compile('^' + token_re + '$') # RFC 7231 Section 5.3.1 "Quality Values" # qvalue = ( "0" [ "." 0*3DIGIT ] ) # / ( "1" [ "." 0*3("0") ] ) qvalue_re = ( r'(?:0(?:\.[0-9]{0,3})?)' '|' r'(?:1(?:\.0{0,3})?)' ) # weight = OWS ";" OWS "q=" qvalue weight_re = OWS_re + ';' + OWS_re + '[qQ]=(' + qvalue_re + ')' def _item_n_weight_re(item_re): return '(' + item_re + ')(?:' + weight_re + ')?' def _item_qvalue_pair_to_header_element(pair): item, qvalue = pair if qvalue == 1.0: element = item elif qvalue == 0.0: element = '{};q=0'.format(item) else: element = '{};q={}'.format(item, qvalue) return element def _list_0_or_more__compiled_re(element_re): # RFC 7230 Section 7 "ABNF List Extension: #rule": # #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ] return re.compile( '^(?:$)|' + '(?:' + '(?:,|(?:' + element_re + '))' + '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*' + ')$', ) def _list_1_or_more__compiled_re(element_re): # RFC 7230 Section 7 "ABNF List Extension: #rule": # 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] ) # and RFC 7230 Errata ID: 4169 return re.compile( '^(?:,' + OWS_re + ')*' + element_re + '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*$', ) class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])): """ A pre-parsed offer tuple represeting a value in the format ``type/subtype;param0=value0;param1=value1``. :ivar type: The media type's root category. :ivar subtype: The media type's subtype. :ivar params: A tuple of 2-tuples containing parameter names and values. """ __slots__ = () def __str__(self): """ Return the properly quoted media type string. """ value = self.type + '/' + self.subtype return Accept._form_media_range(value, self.params) class Accept(object): """ Represent an ``Accept`` header. Base class for :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, and :class:`AcceptInvalidHeader`. """ # RFC 6838 describes syntax rules for media types that are different to # (and stricter than) those in RFC 7231, but if RFC 7231 intended us to # follow the rules in RFC 6838 for media ranges, it would not have # specified its own syntax rules for media ranges, so it appears we should # use the rules in RFC 7231 for now. # RFC 5234 Appendix B.1 "Core Rules": # VCHAR = %x21-7E # ; visible (printing) characters vchar_re = '\x21-\x7e' # RFC 7230 Section 3.2.6 "Field Value Components": # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE # qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text # obs-text = %x80-FF # quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) obs_text_re = '\x80-\xff' qdtext_re = '[\t \x21\x23-\x5b\\\x5d-\x7e' + obs_text_re + ']' # The '\\' between \x5b and \x5d is needed to escape \x5d (']') quoted_pair_re = r'\\' + '[\t ' + vchar_re + obs_text_re + ']' quoted_string_re = \ '"(?:(?:' + qdtext_re + ')|(?:' + quoted_pair_re + '))*"' # RFC 7231 Section 3.1.1.1 "Media Type": # type = token # subtype = token # parameter = token "=" ( token / quoted-string ) type_re = token_re subtype_re = token_re parameter_re = token_re + '=' + \ '(?:(?:' + token_re + ')|(?:' + quoted_string_re + '))' # Section 5.3.2 "Accept": # media-range = ( "*/*" # / ( type "/" "*" ) # / ( type "/" subtype ) # ) *( OWS ";" OWS parameter ) media_range_re = ( '(' + '(?:' + type_re + '/' + subtype_re + ')' + # '*' is included through type_re and subtype_re, so this covers */* # and type/* ')' + '(' + '(?:' + OWS_re + ';' + OWS_re + '(?![qQ]=)' + # media type parameter cannot be named "q" parameter_re + ')*' + ')' ) # accept-params = weight *( accept-ext ) # accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] accept_ext_re = ( OWS_re + ';' + OWS_re + token_re + '(?:' + '=(?:' + '(?:' + token_re + ')|(?:' + quoted_string_re + ')' + ')' + ')?' ) accept_params_re = weight_re + '((?:' + accept_ext_re + ')*)' media_range_n_accept_params_re = media_range_re + '(?:' + \ accept_params_re + ')?' media_range_n_accept_params_compiled_re = re.compile( media_range_n_accept_params_re, ) accept_compiled_re = _list_0_or_more__compiled_re( element_re=media_range_n_accept_params_re, ) # For parsing repeated groups within the media type parameters and # extension parameters segments parameters_compiled_re = re.compile( OWS_re + ';' + OWS_re + '(' + token_re + ')=(' + token_re + '|' + quoted_string_re + ')', ) accept_ext_compiled_re = re.compile( OWS_re + ';' + OWS_re + '(' + token_re + ')' + '(?:' + '=(' + '(?:' + '(?:' + token_re + ')|(?:' + quoted_string_re + ')' + ')' + ')' + ')?', ) # For parsing the media types in the `offers` argument to # .acceptable_offers(), we re-use the media range regex for media types. # This is not intended to be a validation of the offers; its main purpose # is to extract the media type and any media type parameters. media_type_re = media_range_re media_type_compiled_re = re.compile('^' + media_type_re + '$') @classmethod def _escape_and_quote_parameter_value(cls, param_value): """ Escape and quote parameter value where necessary. For media type and extension parameter values. """ if param_value == '': param_value = '""' else: param_value = param_value.replace('\\', '\\\\').replace( '"', r'\"', ) if not token_compiled_re.match(param_value): param_value = '"' + param_value + '"' return param_value @classmethod def _form_extension_params_segment(cls, extension_params): """ Convert iterable of extension parameters to str segment for header. `extension_params` is an iterable where each item is either a parameter string or a (name, value) tuple. """ extension_params_segment = '' for item in extension_params: try: extension_params_segment += (';' + item) except TypeError: param_name, param_value = item param_value = cls._escape_and_quote_parameter_value( param_value=param_value, ) extension_params_segment += ( ';' + param_name + '=' + param_value ) return extension_params_segment @classmethod def _form_media_range(cls, type_subtype, media_type_params): """ Combine `type_subtype` and `media_type_params` to form a media range. `type_subtype` is a ``str``, and `media_type_params` is an iterable of (parameter name, parameter value) tuples. """ media_type_params_segment = '' for param_name, param_value in media_type_params: param_value = cls._escape_and_quote_parameter_value( param_value=param_value, ) media_type_params_segment += (';' + param_name + '=' + param_value) return type_subtype + media_type_params_segment @classmethod def _iterable_to_header_element(cls, iterable): """ Convert iterable of tuples into header element ``str``. Each tuple is expected to be in one of two forms: (media_range, qvalue, extension_params_segment), or (media_range, qvalue). """ try: media_range, qvalue, extension_params_segment = iterable except ValueError: media_range, qvalue = iterable extension_params_segment = '' if qvalue == 1.0: if extension_params_segment: element = '{};q=1{}'.format( media_range, extension_params_segment, ) else: element = media_range elif qvalue == 0.0: element = '{};q=0{}'.format(media_range, extension_params_segment) else: element = '{};q={}{}'.format( media_range, qvalue, extension_params_segment, ) return element @classmethod def _parse_media_type_params(cls, media_type_params_segment): """ Parse media type parameters segment into list of (name, value) tuples. """ media_type_params = cls.parameters_compiled_re.findall( media_type_params_segment, ) for index, (name, value) in enumerate(media_type_params): if value.startswith('"') and value.endswith('"'): value = cls._process_quoted_string_token(token=value) media_type_params[index] = (name, value) return media_type_params @classmethod def _process_quoted_string_token(cls, token): """ Return unescaped and unquoted value from quoted token. """ # RFC 7230, section 3.2.6 "Field Value Components": "Recipients that # process the value of a quoted-string MUST handle a quoted-pair as if # it were replaced by the octet following the backslash." return re.sub(r'\\(?![\\])', '', token[1:-1]).replace('\\\\', '\\') @classmethod def _python_value_to_header_str(cls, value): """ Convert Python value to header string for __add__/__radd__. """ if isinstance(value, str): return value if hasattr(value, 'items'): if value == {}: value = [] else: value_list = [] for media_range, item in value.items(): # item is either (media range, (qvalue, extension # parameters segment)), or (media range, qvalue) (supported # for backward compatibility) if isinstance(item, (float, int)): value_list.append((media_range, item, '')) else: value_list.append((media_range, item[0], item[1])) value = sorted( value_list, key=lambda item: item[1], # qvalue reverse=True, ) if isinstance(value, (tuple, list)): header_elements = [] for item in value: if isinstance(item, (tuple, list)): item = cls._iterable_to_header_element(iterable=item) header_elements.append(item) header_str = ', '.join(header_elements) else: header_str = str(value) return header_str @classmethod def parse(cls, value): """ Parse an ``Accept`` header. :param value: (``str``) header value :return: If `value` is a valid ``Accept`` header, returns an iterator of (*media_range*, *qvalue*, *media_type_params*, *extension_params*) tuples, as parsed from the header from left to right. | *media_range* is the media range, including any media type parameters. The media range is returned in a canonicalised form (except the case of the characters are unchanged): unnecessary spaces around the semicolons before media type parameters are removed; the parameter values are returned in a form where only the '``\\``' and '``"``' characters are escaped, and the values are quoted with double quotes only if they need to be quoted. | *qvalue* is the quality value of the media range. | *media_type_params* is the media type parameters, as a list of (parameter name, value) tuples. | *extension_params* is the extension parameters, as a list where each item is either a parameter string or a (parameter name, value) tuple. :raises ValueError: if `value` is an invalid header """ # Check if header is valid # Using Python stdlib's `re` module, there is currently no way to check # the match *and* get all the groups using the same regex, so we have # to do this in steps using multiple regexes. if cls.accept_compiled_re.match(value) is None: raise ValueError('Invalid value for an Accept header.') def generator(value): for match in ( cls.media_range_n_accept_params_compiled_re.finditer(value) ): groups = match.groups() type_subtype = groups[0] media_type_params = cls._parse_media_type_params( media_type_params_segment=groups[1], ) media_range = cls._form_media_range( type_subtype=type_subtype, media_type_params=media_type_params, ) # qvalue (groups[2]) and extension_params (groups[3]) are both # None if neither qvalue or extension parameters are found in # the match. qvalue = groups[2] qvalue = float(qvalue) if qvalue else 1.0 extension_params = groups[3] if extension_params: extension_params = cls.accept_ext_compiled_re.findall( extension_params, ) for index, (token_key, token_value) in enumerate( extension_params ): if token_value: if ( token_value.startswith('"') and token_value.endswith('"') ): token_value = cls._process_quoted_string_token( token=token_value, ) extension_params[index] = ( token_key, token_value, ) else: extension_params[index] = token_key else: extension_params = [] yield ( media_range, qvalue, media_type_params, extension_params, ) return generator(value=value) @classmethod def parse_offer(cls, offer): """ Parse an offer into its component parts. :param offer: A media type or range in the format ``type/subtype[;params]``. :return: A named tuple containing ``(*type*, *subtype*, *params*)``. | *params* is a list containing ``(*parameter name*, *value*)`` values. :raises ValueError: If the offer does not match the required format. """ if isinstance(offer, AcceptOffer): return offer match = cls.media_type_compiled_re.match(offer) if not match: raise ValueError('Invalid value for an Accept offer.') groups = match.groups() offer_type, offer_subtype = groups[0].split('/') offer_params = cls._parse_media_type_params( media_type_params_segment=groups[1], ) if offer_type == '*' or offer_subtype == '*': raise ValueError('Invalid value for an Accept offer.') return AcceptOffer( offer_type.lower(), offer_subtype.lower(), tuple((name.lower(), value) for name, value in offer_params), ) @classmethod def _parse_and_normalize_offers(cls, offers): """ Throw out any offers that do not match the media range ABNF. :return: A list of offers split into the format ``[offer_index, parsed_offer]``. """ parsed_offers = [] for index, offer in enumerate(offers): try: parsed_offer = cls.parse_offer(offer) except ValueError: continue parsed_offers.append([index, parsed_offer]) return parsed_offers class AcceptValidHeader(Accept): """ Represent a valid ``Accept`` header. A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptValidHeader.__add__`). """ @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. A list of (*media_range*, *qvalue*, *media_type_params*, *extension_params*) tuples, where *media_range* is the media range, including any media type parameters. The media range is returned in a canonicalised form (except the case of the characters are unchanged): unnecessary spaces around the semicolons before media type parameters are removed; the parameter values are returned in a form where only the '``\\``' and '``"``' characters are escaped, and the values are quoted with double quotes only if they need to be quoted. *qvalue* is the quality value of the media range. *media_type_params* is the media type parameters, as a list of (parameter name, value) tuples. *extension_params* is the extension parameters, as a list where each item is either a parameter string or a (parameter name, value) tuple. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptValidHeader` instance. :param header_value: (``str``) header value. :raises ValueError: if `header_value` is an invalid value for an ``Accept`` header. """ self._header_value = header_value self._parsed = list(self.parse(header_value)) self._parsed_nonzero = [item for item in self.parsed if item[1]] # item[1] is the qvalue def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with media ranges ``str``'s (including any media type parameters) as keys, and either qvalues ``float``'s or (*qvalues*, *extension_params*) tuples as values, where *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` or ``list`` where *media_range* is a ``str`` of the media range including any media type parameters, and *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or :class:`AcceptInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or another :class:`AcceptValidHeader` instance, and the header value it represents is not `''`, then the two header values are joined with ``', '``, and a new :class:`AcceptValidHeader` instance with the new header value is returned. If `other` is a valid header value or another :class:`AcceptValidHeader` instance representing a header value of `''`; or if it is ``None`` or an :class:`AcceptNoHeader` instance; or if it is an invalid header value, or an :class:`AcceptInvalidHeader` instance, then a new :class:`AcceptValidHeader` instance with the same header value as ``self`` is returned. """ if isinstance(other, AcceptValidHeader): if other.header_value == '': return self.__class__(header_value=self.header_value) else: return create_accept_header( header_value=self.header_value + ', ' + other.header_value, ) if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): return self.__class__(header_value=self.header_value) return self._add_instance_and_non_accept_type( instance=self, other=other, ) def __bool__(self): """ Return whether ``self`` represents a valid ``Accept`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``True``. """ return True __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of :meth:`AcceptValidHeader.__contains__` is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) media type offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. This uses the old criterion of a match in :meth:`AcceptValidHeader._old_match`, which is not as specified in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not correctly take into account media type parameters: >>> 'text/html;p=1' in AcceptValidHeader('text/html') False or media ranges with ``q=0`` in the header:: >>> 'text/html' in AcceptValidHeader('text/*, text/html;q=0') True >>> 'text/html' in AcceptValidHeader('text/html;q=0, */*') True (See the docstring for :meth:`AcceptValidHeader._old_match` for other problems with the old criterion for matching.) """ warnings.warn( 'The behavior of AcceptValidHeader.__contains__ is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) for ( media_range, quality, media_type_params, extension_params ) in self._parsed_nonzero: if self._old_match(media_range, offer): return True return False def __iter__(self): """ Return all the ranges with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the media ranges in the header with non-0 qvalues, in descending order of qvalue. If two ranges have the same qvalue, they are returned in the order of their positions in the header, from left to right. Please note that this is a simple filter for the ranges in the header with non-0 qvalues, and is not necessarily the same as what the client prefers, e.g. ``'audio/basic;q=0, */*'`` means 'everything but audio/basic', but ``list(instance)`` would return only ``['*/*']``. """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) for media_range, qvalue, media_type_params, extension_params in sorted( self._parsed_nonzero, key=lambda i: i[1], reverse=True ): yield media_range def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptValidHeader.__add__`. """ return self._add_instance_and_non_accept_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{} ({!r})>'.format(self.__class__.__name__, str(self)) def __str__(self): r""" Return a tidied up version of the header value. e.g. If ``self.header_value`` is ``r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,'``, ``str(instance)`` returns ``r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain'``. """ # self.parsed tuples are in the form: (media_range, qvalue, # media_type_params, extension_params) # self._iterable_to_header_element() requires iterable to be in the # form: (media_range, qvalue, extension_params_segment). return ', '.join( self._iterable_to_header_element( iterable=( tuple_[0], # media_range tuple_[1], # qvalue self._form_extension_params_segment( extension_params=tuple_[3], # extension_params ) ), ) for tuple_ in self.parsed ) def _add_instance_and_non_accept_type( self, instance, other, instance_on_the_right=False, ): if not other: return self.__class__(header_value=instance.header_value) other_header_value = self._python_value_to_header_str(value=other) if other_header_value == '': # if ``other`` is an object whose type we don't recognise, and # str(other) returns '' return self.__class__(header_value=instance.header_value) try: self.parse(value=other_header_value) except ValueError: # invalid header value return self.__class__(header_value=instance.header_value) new_header_value = ( (other_header_value + ', ' + instance.header_value) if instance_on_the_right else (instance.header_value + ', ' + other_header_value) ) return self.__class__(header_value=new_header_value) def _old_match(self, mask, offer): """ Check if the offer is covered by the mask ``offer`` may contain wildcards to facilitate checking if a ``mask`` would match a 'permissive' offer. Wildcard matching forces the match to take place against the type or subtype of the mask and offer (depending on where the wildcard matches) .. warning:: This is maintained for backward compatibility, and will be deprecated in the future. This method was WebOb's old criterion for deciding whether a media type matches a media range, used in - :meth:`AcceptValidHeader.__contains__` - :meth:`AcceptValidHeader.best_match` - :meth:`AcceptValidHeader.quality` It allows offers of *, */*, type/*, */subtype and types with no subtypes, which are not media types as specified in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. This is also undocumented in any of the public APIs that uses this method. """ # Match if comparisons are the same or either is a complete wildcard if (mask.lower() == offer.lower() or '*/*' in (mask, offer) or '*' == offer): return True # Set mask type with wildcard subtype for malformed masks try: mask_type, mask_subtype = [x.lower() for x in mask.split('/')] except ValueError: mask_type = mask mask_subtype = '*' # Set offer type with wildcard subtype for malformed offers try: offer_type, offer_subtype = [x.lower() for x in offer.split('/')] except ValueError: offer_type = offer offer_subtype = '*' if mask_subtype == '*': # match on type only if offer_type == '*': return True else: return mask_type.lower() == offer_type.lower() if mask_type == '*': # match on subtype only if offer_subtype == '*': return True else: return mask_subtype.lower() == offer_subtype.lower() if offer_subtype == '*': # match on type only return mask_type.lower() == offer_type.lower() if offer_type == '*': # match on subtype only return mask_subtype.lower() == offer_subtype.lower() return offer.lower() == mask.lower() def accept_html(self): """ Return ``True`` if any HTML-like type is accepted. The HTML-like types are 'text/html', 'application/xhtml+xml', 'application/xml' and 'text/xml'. """ return bool( self.acceptable_offers( offers=[ 'text/html', 'application/xhtml+xml', 'application/xml', 'text/xml', ], ) ) accepts_html = property(fget=accept_html, doc=accept_html.__doc__) # note the plural def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. The offers are returned in descending order of preference, where preference is indicated by the qvalue of the media range in the header that best matches the offer. This uses the matching rules described in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. Any offers that cannot be parsed via :meth:`.Accept.parse_offer` will be ignored. :param offers: ``iterable`` of ``str`` media types (media types can include media type parameters) or pre-parsed instances of :class:`.AcceptOffer`. :return: A list of tuples of the form (media type, qvalue), in descending order of qvalue. Where two offers have the same qvalue, they are returned in the same order as their order in `offers`. """ parsed = self.parsed # RFC 7231, section 3.1.1.1 "Media Type": # "The type, subtype, and parameter name tokens are case-insensitive. # Parameter values might or might not be case-sensitive, depending on # the semantics of the parameter name." lowercased_ranges = [ ( media_range.partition(';')[0].lower(), qvalue, tuple( (name.lower(), value) for name, value in media_type_params ), ) for media_range, qvalue, media_type_params, __ in parsed ] lowercased_offers_parsed = self._parse_and_normalize_offers(offers) acceptable_offers_n_quality_factors = {} for offer_index, parsed_offer in lowercased_offers_parsed: offer = offers[offer_index] offer_type, offer_subtype, offer_media_type_params = parsed_offer for ( range_type_subtype, range_qvalue, range_media_type_params, ) in lowercased_ranges: range_type, range_subtype = range_type_subtype.split('/', 1) # The specificity values below are based on the list in the # example in RFC 7231 section 5.3.2 explaining how "media # ranges can be overridden by more specific media ranges or # specific media types". We assign specificity to the list # items in reverse order, so specificity 4, 3, 2, 1 correspond # to 1, 2, 3, 4 in the list, respectively (so that higher # specificity has higher precedence). if ( offer_type == range_type and offer_subtype == range_subtype ): if range_media_type_params == (): # If offer_media_type_params == () the offer and the # range match exactly, with neither having media type # parameters. # If offer_media_type_params is not (), the offer and # the range are a match. See the table towards the end # of RFC 7231 section 5.3.2, where the media type # 'text/html;level=3' matches the range 'text/html' in # the header. # Both cases are a match with a specificity of 3. specificity = 3 elif offer_media_type_params == range_media_type_params: specificity = 4 else: # pragma: no cover # no cover because of # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement continue else: if range_subtype == '*' and offer_type == range_type: specificity = 2 elif range_type_subtype == '*/*': specificity = 1 else: # pragma: no cover # no cover because of # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement continue try: if specificity <= ( acceptable_offers_n_quality_factors[offer][2] ): continue except KeyError: # the entry for the offer is not already in # acceptable_offers_n_quality_factors pass acceptable_offers_n_quality_factors[offer] = ( range_qvalue, # qvalue of matched range offer_index, specificity, # specifity of matched range ) acceptable_offers_n_quality_factors = [ # key is offer, value[0] is qvalue, value[1] is offer_index (key, value[0], value[1]) for key, value in acceptable_offers_n_quality_factors.items() if value[0] # != 0.0 # We have to filter out the offers with qvalues of 0 here instead # of just skipping them early in the large ``for`` loop because # that would not work for e.g. when the header is 'text/html;q=0, # text/html' (which does not make sense, but is nonetheless valid), # and offers is ['text/html'] ] # sort by offer_index, ascending acceptable_offers_n_quality_factors.sort(key=lambda tuple_: tuple_[2]) # (stable) sort by qvalue, descending acceptable_offers_n_quality_factors.sort( key=lambda tuple_: tuple_[1], reverse=True, ) # drop offer_index acceptable_offers_n_quality_factors = [ (item[0], item[1]) for item in acceptable_offers_n_quality_factors ] return acceptable_offers_n_quality_factors # If a media range is repeated in the header (which would not make # sense, but would be valid according to the rules in the RFC), an # offer for which the media range is the most specific match would take # its qvalue from the first appearance of the range in the header. def best_match(self, offers, default_match=None): """ Return the best match from the sequence of media type `offers`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :meth:`AcceptValidHeader.best_match` uses its own algorithm (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a best match. The algorithm has many issues, and does not conform to :rfc:`RFC 7231 <7231>`. Each media type in `offers` is checked against each non-``q=0`` range in the header. If the two are a match according to WebOb's old criterion for a match, the quality value of the match is the qvalue of the media range from the header multiplied by the server quality value of the offer (if the server quality value is not supplied, it is 1). The offer in the match with the highest quality value is the best match. If there is more than one match with the highest qvalue, the match where the media range has a lower number of '*'s is the best match. If the two have the same number of '*'s, the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` media type, or a (media type, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if there is no match :return: (``str``, or the type of `default_match`) | The offer that is the best match. If there is no match, the value of `default_match` is returned. This uses the old criterion of a match in :meth:`AcceptValidHeader._old_match`, which is not as specified in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not correctly take into account media type parameters: >>> instance = AcceptValidHeader('text/html') >>> instance.best_match(offers=['text/html;p=1']) is None True or media ranges with ``q=0`` in the header:: >>> instance = AcceptValidHeader('text/*, text/html;q=0') >>> instance.best_match(offers=['text/html']) 'text/html' >>> instance = AcceptValidHeader('text/html;q=0, */*') >>> instance.best_match(offers=['text/html']) 'text/html' (See the docstring for :meth:`AcceptValidHeader._old_match` for other problems with the old criterion for matching.) Another issue is that this method considers the best matching range for an offer to be the matching range with the highest quality value, (where quality values are tied, the most specific media range is chosen); whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies that we should consider the best matching range for a media type offer to be the most specific matching range.:: >>> instance = AcceptValidHeader('text/html;q=0.5, text/*') >>> instance.best_match(offers=['text/html', 'text/plain']) 'text/html' """ warnings.warn( 'The behavior of AcceptValidHeader.best_match is currently being ' 'maintained for backward compatibility, but it will be deprecated' ' in the future, as it does not conform to the RFC.', DeprecationWarning, ) best_quality = -1 best_offer = default_match matched_by = '*/*' for offer in offers: if isinstance(offer, (tuple, list)): offer, server_quality = offer else: server_quality = 1 for item in self._parsed_nonzero: mask = item[0] quality = item[1] possible_quality = server_quality * quality if possible_quality < best_quality: continue elif possible_quality == best_quality: # 'text/plain' overrides 'message/*' overrides '*/*' # (if all match w/ the same q=) if matched_by.count('*') <= mask.count('*'): continue if self._old_match(mask, offer): best_quality = possible_quality best_offer = offer matched_by = mask return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :param offer: (``str``) media type offer :return: (``float`` or ``None``) | The highest quality value from the media range(s) that match the `offer`, or ``None`` if there is no match. This uses the old criterion of a match in :meth:`AcceptValidHeader._old_match`, which is not as specified in :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not correctly take into account media type parameters: >>> instance = AcceptValidHeader('text/html') >>> instance.quality('text/html;p=1') is None True or media ranges with ``q=0`` in the header:: >>> instance = AcceptValidHeader('text/*, text/html;q=0') >>> instance.quality('text/html') 1.0 >>> AcceptValidHeader('text/html;q=0, */*').quality('text/html') 1.0 (See the docstring for :meth:`AcceptValidHeader._old_match` for other problems with the old criterion for matching.) Another issue is that this method considers the best matching range for an offer to be the matching range with the highest quality value, whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies that we should consider the best matching range for a media type offer to be the most specific matching range.:: >>> instance = AcceptValidHeader('text/html;q=0.5, text/*') >>> instance.quality('text/html') 1.0 """ warnings.warn( 'The behavior of AcceptValidHeader.quality is currently being ' 'maintained for backward compatibility, but it will be deprecated ' 'in the future, as it does not conform to the RFC.', DeprecationWarning, ) bestq = 0 for item in self.parsed: media_range = item[0] qvalue = item[1] if self._old_match(media_range, offer): bestq = max(bestq, qvalue) return bestq or None class MIMEAccept(Accept): """ Backwards compatibility shim for the new functionality provided by AcceptValidHeader, AcceptInvalidHeader, or AcceptNoHeader, that acts like the old MIMEAccept from WebOb version 1.7 or lower. This shim does use the newer Accept header parsing, which will mean your application may be less liberal in what Accept headers are correctly parsed. It is recommended that user agents be updated to send appropriate Accept headers that are valid according to rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` .. deprecated:: 1.8 Instead of directly creating the Accept object, please see: :func:`create_accept_header(header_value) `, which will create the appropriate object. This shim has an extended deprecation period to allow for application developers to switch the to new API. """ def __init__(self, header_value): warnings.warn( 'The MIMEAccept class has been replaced by ' 'webob.acceptparse.create_accept_header. This compatibility shim ' 'will be deprecated in a future version of WebOb.', DeprecationWarning ) self._accept = create_accept_header(header_value) if self._accept.parsed: self._parsed = [(media, q) for (media, q, _, _) in self._accept.parsed] self._parsed_nonzero = [(m, q) for (m, q) in self._parsed if q] else: self._parsed = [] self._parsed_nonzero = [] @staticmethod def parse(value): try: parsed_accepted = Accept.parse(value) for (media, q, _, _) in parsed_accepted: yield (media, q) except ValueError: pass def __repr__(self): return self._accept.__repr__() def __iter__(self): return self._accept.__iter__() def __str__(self): return self._accept.__str__() def __add__(self, other): if isinstance(other, self.__class__): return self.__class__(str(self._accept.__add__(other._accept))) else: return self.__class__(str(self._accept.__add__(other))) def __radd__(self, other): return self.__class__(str(self._accept.__radd__(other))) def __contains__(self, offer): return offer in self._accept def quality(self, offer): return self._accept.quality(offer) def best_match(self, offers, default_match=None): return self._accept.best_match(offers, default_match=default_match) def accept_html(self): return self._accept.accept_html() class _AcceptInvalidOrNoHeader(Accept): """ Represent when an ``Accept`` header is invalid or not in request. This is the base class for the behaviour that :class:`.AcceptInvalidHeader` and :class:`.AcceptNoHeader` have in common. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept`` header has an invalid value. This implementation disregards the header when the header is invalid, so :class:`.AcceptInvalidHeader` and :class:`.AcceptNoHeader` have much behaviour in common. """ def __bool__(self): """ Return whether ``self`` represents a valid ``Accept`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``False``. """ return False __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of ``.__contains__`` for the ``Accept`` classes is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) media type offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. For this class, either there is no ``Accept`` header in the request, or the header is invalid, so any media type is acceptable, and this always returns ``True``. """ warnings.warn( 'The behavior of .__contains__ for the Accept classes is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) return True def __iter__(self): """ Return all the ranges with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the media ranges in the header with non-0 qvalues, in descending order of qvalue. If two ranges have the same qvalue, they are returned in the order of their positions in the header, from left to right. When there is no ``Accept`` header in the request or the header is invalid, there are no media ranges, so this always returns an empty iterator. """ warnings.warn( 'The behavior of AcceptValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) return iter(()) def accept_html(self): """ Return ``True`` if any HTML-like type is accepted. The HTML-like types are 'text/html', 'application/xhtml+xml', 'application/xml' and 'text/xml'. When the header is invalid, or there is no `Accept` header in the request, all `offers` are considered acceptable, so this always returns ``True``. """ return bool( self.acceptable_offers( offers=[ 'text/html', 'application/xhtml+xml', 'application/xml', 'text/xml', ], ) ) accepts_html = property(fget=accept_html, doc=accept_html.__doc__) # note the plural def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. Any offers that cannot be parsed via :meth:`.Accept.parse_offer` will be ignored. :param offers: ``iterable`` of ``str`` media types (media types can include media type parameters) :return: When the header is invalid, or there is no ``Accept`` header in the request, all `offers` are considered acceptable, so this method returns a list of (media type, qvalue) tuples where each offer in `offers` is paired with the qvalue of 1.0, in the same order as in `offers`. """ return [ (offers[offer_index], 1.0) for offer_index, _ # avoid returning any offers that don't match the grammar so # that the return values here are consistent with what would be # returned in AcceptValidHeader in self._parse_and_normalize_offers(offers) ] def best_match(self, offers, default_match=None): """ Return the best match from the sequence of language tag `offers`. This is the ``.best_match()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptValidHeader.best_match`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptValidHeader.best_match`). When the header is invalid, or there is no `Accept` header in the request, all `offers` are considered acceptable, so the best match is the media type in `offers` with the highest server quality value (if the server quality value is not supplied for a media type, it is 1). If more than one media type in `offers` have the same highest server quality value, then the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` media type, or a (media type, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if `offers` is empty. :return: (``str``, or the type of `default_match`) | The offer that has the highest server quality value. If `offers` is empty, the value of `default_match` is returned. """ warnings.warn( 'The behavior of .best_match for the Accept classes is currently ' 'being maintained for backward compatibility, but the method will' ' be deprecated in the future, as its behavior is not specified ' 'in (and currently does not conform to) RFC 7231.', DeprecationWarning, ) best_quality = -1 best_offer = default_match for offer in offers: if isinstance(offer, (list, tuple)): offer, quality = offer else: quality = 1 if quality > best_quality: best_offer = offer best_quality = quality return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. This is the ``.quality()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptValidHeader.quality`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptValidHeader.quality`). :param offer: (``str``) media type offer :return: (``float``) ``1.0``. When the ``Accept`` header is invalid or not in the request, all offers are equally acceptable, so 1.0 is always returned. """ warnings.warn( 'The behavior of .quality for the Accept classes is currently ' 'being maintained for backward compatibility, but the method will' ' be deprecated in the future, as its behavior does not conform to' 'RFC 7231.', DeprecationWarning, ) return 1.0 class AcceptNoHeader(_AcceptInvalidOrNoHeader): """ Represent when there is no ``Accept`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptNoHeader.__add__`). """ @property def header_value(self): """ (``str`` or ``None``) The header value. As there is no header in the request, this is ``None``. """ return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As there is no header in the request, this is ``None``. """ return self._parsed def __init__(self): """ Create an :class:`AcceptNoHeader` instance. """ self._header_value = None self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__() def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with media ranges ``str``'s (including any media type parameters) as keys, and either qvalues ``float``'s or (*qvalues*, *extension_params*) tuples as values, where *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` or ``list`` where *media_range* is a ``str`` of the media range including any media type parameters, and *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or :class:`AcceptInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptValidHeader` instance, a new :class:`AcceptValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid header value, or an :class:`AcceptInvalidHeader` instance, a new :class:`AcceptNoHeader` instance is returned. """ if isinstance(other, AcceptValidHeader): return AcceptValidHeader(header_value=other.header_value) if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): return self.__class__() return self._add_instance_and_non_accept_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptNoHeader.__add__`. """ return self.__add__(other=other) def __repr__(self): return '<{}>'.format(self.__class__.__name__) def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_type(self, instance, other): if other is None: return self.__class__() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptValidHeader(header_value=other_header_value) except ValueError: # invalid header value return self.__class__() class AcceptInvalidHeader(_AcceptInvalidOrNoHeader): """ Represent an invalid ``Accept`` header. An invalid header is one that does not conform to :rfc:`7231#section-5.3.2`. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept`` header has an invalid value. This implementation disregards the header, and treats it as if there is no ``Accept`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptInvalidHeader.__add__`). """ @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As the header is invalid and cannot be parsed, this is ``None``. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptInvalidHeader` instance. """ self._header_value = header_value self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with media ranges ``str``'s (including any media type parameters) as keys, and either qvalues ``float``'s or (*qvalues*, *extension_params*) tuples as values, where *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` or ``list`` where *media_range* is a ``str`` of the media range including any media type parameters, and *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or :class:`AcceptInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptValidHeader` instance, then a new :class:`AcceptValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid header value, or an :class:`AcceptInvalidHeader` instance, a new :class:`AcceptNoHeader` instance is returned. """ if isinstance(other, AcceptValidHeader): return AcceptValidHeader(header_value=other.header_value) if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)): return AcceptNoHeader() return self._add_instance_and_non_accept_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptValidHeader.__add__`. """ return self._add_instance_and_non_accept_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{}>'.format(self.__class__.__name__) # We do not display the header_value, as it is untrusted input. The # header_value could always be easily obtained from the .header_value # property. def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_type( self, instance, other, instance_on_the_right=False, ): if other is None: return AcceptNoHeader() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptValidHeader(header_value=other_header_value) except ValueError: # invalid header value return AcceptNoHeader() def create_accept_header(header_value): """ Create an object representing the ``Accept`` header in a request. :param header_value: (``str``) header value :return: If `header_value` is ``None``, an :class:`AcceptNoHeader` instance. | If `header_value` is a valid ``Accept`` header, an :class:`AcceptValidHeader` instance. | If `header_value` is an invalid ``Accept`` header, an :class:`AcceptInvalidHeader` instance. """ if header_value is None: return AcceptNoHeader() if isinstance(header_value, Accept): return header_value.copy() try: return AcceptValidHeader(header_value=header_value) except ValueError: return AcceptInvalidHeader(header_value=header_value) def accept_property(): doc = """ Property representing the ``Accept`` header. (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`) The header value in the request environ is parsed and a new object representing the header is created every time we *get* the value of the property. (*set* and *del* change the header value in the request environ, and do not involve parsing.) """ ENVIRON_KEY = 'HTTP_ACCEPT' def fget(request): """Get an object representing the header in the request.""" return create_accept_header( header_value=request.environ.get(ENVIRON_KEY) ) def fset(request, value): """ Set the corresponding key in the request environ. `value` can be: * ``None`` * a ``str`` header value * a ``dict``, with media ranges ``str``'s (including any media type parameters) as keys, and either qvalues ``float``'s or (*qvalues*, *extension_params*) tuples as values, where *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple`` or ``list`` where *media_range* is a ``str`` of the media range including any media type parameters, and *extension_params* is a ``str`` of the extension parameters segment of the header element, starting with the first '``;``' * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or :class:`AcceptInvalidHeader` instance * object of any other type that returns a value for ``__str__`` """ if value is None or isinstance(value, AcceptNoHeader): fdel(request=request) else: if isinstance(value, (AcceptValidHeader, AcceptInvalidHeader)): header_value = value.header_value else: header_value = Accept._python_value_to_header_str(value=value) request.environ[ENVIRON_KEY] = header_value def fdel(request): """Delete the corresponding key from the request environ.""" try: del request.environ[ENVIRON_KEY] except KeyError: pass return property(fget, fset, fdel, textwrap.dedent(doc)) class AcceptCharset(object): """ Represent an ``Accept-Charset`` header. Base class for :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, and :class:`AcceptCharsetInvalidHeader`. """ # RFC 7231 Section 3.1.1.2 "Charset": # charset = token charset_re = token_re # RFC 7231 Section 5.3.3 "Accept-Charset": # Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) charset_n_weight_re = _item_n_weight_re(item_re=charset_re) charset_n_weight_compiled_re = re.compile(charset_n_weight_re) accept_charset_compiled_re = _list_1_or_more__compiled_re( element_re=charset_n_weight_re, ) @classmethod def _python_value_to_header_str(cls, value): if isinstance(value, str): header_str = value else: if hasattr(value, 'items'): value = sorted( value.items(), key=lambda item: item[1], reverse=True, ) if isinstance(value, (tuple, list)): result = [] for item in value: if isinstance(item, (tuple, list)): item = _item_qvalue_pair_to_header_element(pair=item) result.append(item) header_str = ', '.join(result) else: header_str = str(value) return header_str @classmethod def parse(cls, value): """ Parse an ``Accept-Charset`` header. :param value: (``str``) header value :return: If `value` is a valid ``Accept-Charset`` header, returns an iterator of (charset, quality value) tuples, as parsed from the header from left to right. :raises ValueError: if `value` is an invalid header """ # Check if header is valid # Using Python stdlib's `re` module, there is currently no way to check # the match *and* get all the groups using the same regex, so we have # to use one regex to check the match, and another to get the groups. if cls.accept_charset_compiled_re.match(value) is None: raise ValueError('Invalid value for an Accept-Charset header.') def generator(value): for match in (cls.charset_n_weight_compiled_re.finditer(value)): charset = match.group(1) qvalue = match.group(2) qvalue = float(qvalue) if qvalue else 1.0 yield (charset, qvalue) return generator(value=value) class AcceptCharsetValidHeader(AcceptCharset): """ Represent a valid ``Accept-Charset`` header. A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptCharsetValidHeader.__add__`). """ @property def header_value(self): """(``str``) The header value.""" return self._header_value @property def parsed(self): """ (``list``) Parsed form of the header. A list of (charset, quality value) tuples. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptCharsetValidHeader` instance. :param header_value: (``str``) header value. :raises ValueError: if `header_value` is an invalid value for an ``Accept-Charset`` header. """ self._header_value = header_value self._parsed = list(self.parse(header_value)) self._parsed_nonzero = [ item for item in self.parsed if item[1] # item[1] is the qvalue ] def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, where keys are charsets and values are qvalues * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, or :class:`AcceptCharsetInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or another :class:`AcceptCharsetValidHeader` instance, the two header values are joined with ``', '``, and a new :class:`AcceptCharsetValidHeader` instance with the new header value is returned. If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an invalid header value, or an :class:`AcceptCharsetInvalidHeader` instance, a new :class:`AcceptCharsetValidHeader` instance with the same header value as ``self`` is returned. """ if isinstance(other, AcceptCharsetValidHeader): return create_accept_charset_header( header_value=self.header_value + ', ' + other.header_value, ) if isinstance( other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader) ): return self.__class__(header_value=self.header_value) return self._add_instance_and_non_accept_charset_type( instance=self, other=other, ) def __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Charset`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``True``. """ return True __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of :meth:`AcceptCharsetValidHeader.__contains__` is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) charset offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. This does not fully conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`: it incorrect interprets ``*`` to mean 'match any charset in the header', rather than 'match any charset that is not mentioned elsewhere in the header':: >>> 'UTF-8' in AcceptCharsetValidHeader('UTF-8;q=0, *') True """ warnings.warn( 'The behavior of AcceptCharsetValidHeader.__contains__ is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) for mask, quality in self._parsed_nonzero: if self._old_match(mask, offer): return True return False def __iter__(self): """ Return all the items with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the items (charset or ``*``) in the header with non-0 qvalues, in descending order of qvalue. If two items have the same qvalue, they are returned in the order of their positions in the header, from left to right. Please note that this is a simple filter for the items in the header with non-0 qvalues, and is not necessarily the same as what the client prefers, e.g. ``'utf-7;q=0, *'`` means 'everything but utf-7', but ``list(instance)`` would return only ``['*']``. """ warnings.warn( 'The behavior of AcceptCharsetValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) for m,q in sorted( self._parsed_nonzero, key=lambda i: i[1], reverse=True ): yield m def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptCharsetValidHeader.__add__`. """ return self._add_instance_and_non_accept_charset_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{} ({!r})>'.format(self.__class__.__name__, str(self)) def __str__(self): r""" Return a tidied up version of the header value. e.g. If the ``header_value`` is ``', \t,iso-8859-5;q=0.000 \t, utf-8;q=1.000, UTF-7, unicode-1-1;q=0.210 ,'``, ``str(instance)`` returns ``'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21'``. """ return ', '.join( _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed ) def _add_instance_and_non_accept_charset_type( self, instance, other, instance_on_the_right=False, ): if not other: return self.__class__(header_value=instance.header_value) other_header_value = self._python_value_to_header_str(value=other) try: self.parse(value=other_header_value) except ValueError: # invalid header value return self.__class__(header_value=instance.header_value) new_header_value = ( (other_header_value + ', ' + instance.header_value) if instance_on_the_right else (instance.header_value + ', ' + other_header_value) ) return self.__class__(header_value=new_header_value) def _old_match(self, mask, offer): """ Return whether charset offer matches header item (charset or ``*``). .. warning:: This is maintained for backward compatibility, and will be deprecated in the future. This method was WebOb's old criterion for deciding whether a charset matches a header item (charset or ``*``), used in - :meth:`AcceptCharsetValidHeader.__contains__` - :meth:`AcceptCharsetValidHeader.best_match` - :meth:`AcceptCharsetValidHeader.quality` It does not conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>` in that it does not interpret ``*`` values in the header correctly: ``*`` should only match charsets not mentioned elsewhere in the header. """ return mask == '*' or offer.lower() == mask.lower() def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. The offers are returned in descending order of preference, where preference is indicated by the qvalue of the charset or ``*`` in the header matching the offer. This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`. :param offers: ``iterable`` of ``str`` charsets :return: A list of tuples of the form (charset, qvalue), in descending order of qvalue. Where two offers have the same qvalue, they are returned in the same order as their order in `offers`. """ lowercased_parsed = [ (charset.lower(), qvalue) for (charset, qvalue) in self.parsed ] lowercased_offers = [offer.lower() for offer in offers] not_acceptable_charsets = set() acceptable_charsets = dict() asterisk_qvalue = None for charset, qvalue in lowercased_parsed: if charset == '*': if asterisk_qvalue is None: asterisk_qvalue = qvalue elif ( charset not in acceptable_charsets and charset not in not_acceptable_charsets # if we have not already encountered this charset in the header ): if qvalue == 0.0: not_acceptable_charsets.add(charset) else: acceptable_charsets[charset] = qvalue acceptable_charsets = list(acceptable_charsets.items()) # Sort acceptable_charsets by qvalue, descending order acceptable_charsets.sort(key=lambda tuple_: tuple_[1], reverse=True) filtered_offers = [] for index, offer in enumerate(lowercased_offers): # If offer matches a non-* charset with q=0, it is filtered out if any(( (offer == charset) for charset in not_acceptable_charsets )): continue matched_charset_qvalue = None for charset, qvalue in acceptable_charsets: if offer == charset: matched_charset_qvalue = qvalue break else: if asterisk_qvalue: matched_charset_qvalue = asterisk_qvalue if matched_charset_qvalue is not None: # if there was a match filtered_offers.append(( offers[index], matched_charset_qvalue, index )) # sort by position in `offers` argument, ascending filtered_offers.sort(key=lambda tuple_: tuple_[2]) # When qvalues are tied, position in `offers` is the tiebreaker. # sort by qvalue, descending filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True) return [(item[0], item[1]) for item in filtered_offers] # (offer, qvalue), dropping the position def best_match(self, offers, default_match=None): """ Return the best match from the sequence of charset `offers`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :meth:`AcceptCharsetValidHeader.best_match` has many issues, and does not conform to :rfc:`RFC 7231 <7231>`. Each charset in `offers` is checked against each non-``q=0`` item (charset or ``*``) in the header. If the two are a match according to WebOb's old criterion for a match, the quality value of the match is the qvalue of the item from the header multiplied by the server quality value of the offer (if the server quality value is not supplied, it is 1). The offer in the match with the highest quality value is the best match. If there is more than one match with the highest qvalue, the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` charset, or a (charset, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if there is no match :return: (``str``, or the type of `default_match`) | The offer that is the best match. If there is no match, the value of `default_match` is returned. The algorithm behind this method was written for the ``Accept`` header rather than the ``Accept-Charset`` header. It uses the old criterion of a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does not interpret ``*`` values in the header correctly: ``*`` should only match charsets not mentioned elsewhere in the header:: >>> AcceptCharsetValidHeader('utf-8;q=0, *').best_match(['utf-8']) 'utf-8' """ warnings.warn( 'The behavior of AcceptCharsetValidHeader.best_match is currently' ' being maintained for backward compatibility, but it will be ' 'deprecated in the future, as it does not conform to the RFC.', DeprecationWarning, ) best_quality = -1 best_offer = default_match matched_by = '*/*' for offer in offers: if isinstance(offer, (tuple, list)): offer, server_quality = offer else: server_quality = 1 for mask, quality in self._parsed_nonzero: possible_quality = server_quality * quality if possible_quality < best_quality: continue elif possible_quality == best_quality: # 'text/plain' overrides 'message/*' overrides '*/*' # (if all match w/ the same q=) # [We can see that this was written for the Accept header, # not the Accept-Charset header.] if matched_by.count('*') <= mask.count('*'): continue if self._old_match(mask, offer): best_quality = possible_quality best_offer = offer matched_by = mask return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :param offer: (``str``) charset offer :return: (``float`` or ``None``) | The quality value from the charset that matches the `offer`, or ``None`` if there is no match. This uses the old criterion of a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does not interpret ``*`` values in the header correctly: ``*`` should only match charsets not mentioned elsewhere in the header:: >>> AcceptCharsetValidHeader('utf-8;q=0, *').quality('utf-8') 1.0 >>> AcceptCharsetValidHeader('utf-8;q=0.9, *').quality('utf-8') 1.0 """ warnings.warn( 'The behavior of AcceptCharsetValidHeader.quality is currently ' 'being maintained for backward compatibility, but it will be ' 'deprecated in the future, as it does not conform to the RFC.', DeprecationWarning, ) bestq = 0 for mask, q in self.parsed: if self._old_match(mask, offer): bestq = max(bestq, q) return bestq or None class _AcceptCharsetInvalidOrNoHeader(AcceptCharset): """ Represent when an ``Accept-Charset`` header is invalid or not in request. This is the base class for the behaviour that :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader` have in common. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept-Charset`` header has an invalid value. This implementation disregards the header when the header is invalid, so :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader` have much behaviour in common. """ def __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Charset`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``False``. """ return False __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of ``.__contains__`` for the ``AcceptCharset`` classes is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) charset offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. For this class, either there is no ``Accept-Charset`` header in the request, or the header is invalid, so any charset is acceptable, and this always returns ``True``. """ warnings.warn( 'The behavior of .__contains__ for the AcceptCharset classes is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) return True def __iter__(self): """ Return all the items with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the items (charset or ``*``) in the header with non-0 qvalues, in descending order of qvalue. If two items have the same qvalue, they are returned in the order of their positions in the header, from left to right. When there is no ``Accept-Charset`` header in the request or the header is invalid, there are no items, and this always returns an empty iterator. """ warnings.warn( 'The behavior of AcceptCharsetValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) return iter(()) def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. The offers are returned in descending order of preference, where preference is indicated by the qvalue of the charset or ``*`` in the header matching the offer. This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`. :param offers: ``iterable`` of ``str`` charsets :return: A list of tuples of the form (charset, qvalue), in descending order of qvalue. Where two offers have the same qvalue, they are returned in the same order as their order in `offers`. | When the header is invalid or there is no ``Accept-Charset`` header in the request, all `offers` are considered acceptable, so this method returns a list of (charset, qvalue) tuples where each offer in `offers` is paired with the qvalue of 1.0, in the same order as `offers`. """ return [(offer, 1.0) for offer in offers] def best_match(self, offers, default_match=None): """ Return the best match from the sequence of charset `offers`. This is the ``.best_match()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptCharsetValidHeader.best_match`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptCharsetValidHeader.best_match`). When the header is invalid, or there is no `Accept-Charset` header in the request, all the charsets in `offers` are considered acceptable, so the best match is the charset in `offers` with the highest server quality value (if the server quality value is not supplied, it is 1). If more than one charsets in `offers` have the same highest server quality value, then the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` charset, or a (charset, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if `offers` is empty. :return: (``str``, or the type of `default_match`) | The charset that has the highest server quality value. If `offers` is empty, the value of `default_match` is returned. """ warnings.warn( 'The behavior of .best_match for the AcceptCharset classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior is not ' 'specified in (and currently does not conform to) RFC 7231.', DeprecationWarning, ) best_quality = -1 best_offer = default_match for offer in offers: if isinstance(offer, (list, tuple)): offer, quality = offer else: quality = 1 if quality > best_quality: best_offer = offer best_quality = quality return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. This is the ``.quality()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptCharsetValidHeader.quality`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptCharsetValidHeader.quality`). :param offer: (``str``) charset offer :return: (``float``) ``1.0``. When the ``Accept-Charset`` header is invalid or not in the request, all offers are equally acceptable, so 1.0 is always returned. """ warnings.warn( 'The behavior of .quality for the Accept-Charset classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior does not' ' conform to RFC 7231.', DeprecationWarning, ) return 1.0 class AcceptCharsetNoHeader(_AcceptCharsetInvalidOrNoHeader): """ Represent when there is no ``Accept-Charset`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptCharsetNoHeader.__add__`). """ @property def header_value(self): """ (``str`` or ``None``) The header value. As there is no header in the request, this is ``None``. """ return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As there is no header in the request, this is ``None``. """ return self._parsed def __init__(self): """ Create an :class:`AcceptCharsetNoHeader` instance. """ self._header_value = None self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__() def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, where keys are charsets and values are qvalues * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, or :class:`AcceptCharsetInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptCharsetValidHeader` instance, a new :class:`AcceptCharsetValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an invalid header value, or an :class:`AcceptCharsetInvalidHeader` instance, a new :class:`AcceptCharsetNoHeader` instance is returned. """ if isinstance(other, AcceptCharsetValidHeader): return AcceptCharsetValidHeader(header_value=other.header_value) if isinstance( other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader) ): return self.__class__() return self._add_instance_and_non_accept_charset_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptCharsetNoHeader.__add__`. """ return self.__add__(other=other) def __repr__(self): return '<{}>'.format(self.__class__.__name__) def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_charset_type(self, instance, other): if not other: return self.__class__() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptCharsetValidHeader(header_value=other_header_value) except ValueError: # invalid header value return self.__class__() class AcceptCharsetInvalidHeader(_AcceptCharsetInvalidOrNoHeader): """ Represent an invalid ``Accept-Charset`` header. An invalid header is one that does not conform to :rfc:`7231#section-5.3.3`. As specified in the RFC, an empty header is an invalid ``Accept-Charset`` header. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept-Charset`` header has an invalid value. This implementation disregards the header, and treats it as if there is no ``Accept-Charset`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptCharsetInvalidHeader.__add__`). """ @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As the header is invalid and cannot be parsed, this is ``None``. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptCharsetInvalidHeader` instance. """ self._header_value = header_value self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, where keys are charsets and values are qvalues * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, or :class:`AcceptCharsetInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptCharsetValidHeader` instance, a new :class:`AcceptCharsetValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an invalid header value, or an :class:`AcceptCharsetInvalidHeader` instance, a new :class:`AcceptCharsetNoHeader` instance is returned. """ if isinstance(other, AcceptCharsetValidHeader): return AcceptCharsetValidHeader(header_value=other.header_value) if isinstance( other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader) ): return AcceptCharsetNoHeader() return self._add_instance_and_non_accept_charset_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptCharsetValidHeader.__add__`. """ return self._add_instance_and_non_accept_charset_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{}>'.format(self.__class__.__name__) # We do not display the header_value, as it is untrusted input. The # header_value could always be easily obtained from the .header_value # property. def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_charset_type( self, instance, other, instance_on_the_right=False, ): if not other: return AcceptCharsetNoHeader() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptCharsetValidHeader(header_value=other_header_value) except ValueError: # invalid header value return AcceptCharsetNoHeader() def create_accept_charset_header(header_value): """ Create an object representing the ``Accept-Charset`` header in a request. :param header_value: (``str``) header value :return: If `header_value` is ``None``, an :class:`AcceptCharsetNoHeader` instance. | If `header_value` is a valid ``Accept-Charset`` header, an :class:`AcceptCharsetValidHeader` instance. | If `header_value` is an invalid ``Accept-Charset`` header, an :class:`AcceptCharsetInvalidHeader` instance. """ if header_value is None: return AcceptCharsetNoHeader() if isinstance(header_value, AcceptCharset): return header_value.copy() try: return AcceptCharsetValidHeader(header_value=header_value) except ValueError: return AcceptCharsetInvalidHeader(header_value=header_value) def accept_charset_property(): doc = """ Property representing the ``Accept-Charset`` header. (:rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`) The header value in the request environ is parsed and a new object representing the header is created every time we *get* the value of the property. (*set* and *del* change the header value in the request environ, and do not involve parsing.) """ ENVIRON_KEY = 'HTTP_ACCEPT_CHARSET' def fget(request): """Get an object representing the header in the request.""" return create_accept_charset_header( header_value=request.environ.get(ENVIRON_KEY) ) def fset(request, value): """ Set the corresponding key in the request environ. `value` can be: * ``None`` * a ``str`` header value * a ``dict``, where keys are charsets and values are qvalues * a ``tuple`` or ``list``, where each item is a charset ``str`` or a ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`, or :class:`AcceptCharsetInvalidHeader` instance * object of any other type that returns a value for ``__str__`` """ if value is None or isinstance(value, AcceptCharsetNoHeader): fdel(request=request) else: if isinstance( value, (AcceptCharsetValidHeader, AcceptCharsetInvalidHeader) ): header_value = value.header_value else: header_value = AcceptCharset._python_value_to_header_str( value=value, ) request.environ[ENVIRON_KEY] = header_value def fdel(request): """Delete the corresponding key from the request environ.""" try: del request.environ[ENVIRON_KEY] except KeyError: pass return property(fget, fset, fdel, textwrap.dedent(doc)) class AcceptEncoding(object): """ Represent an ``Accept-Encoding`` header. Base class for :class:`AcceptEncodingValidHeader`, :class:`AcceptEncodingNoHeader`, and :class:`AcceptEncodingInvalidHeader`. """ # RFC 7231 Section 3.1.2.1 "Content Codings": # content-coding = token # Section 5.3.4 "Accept-Encoding": # Accept-Encoding = #( codings [ weight ] ) # codings = content-coding / "identity" / "*" codings_re = token_re # "identity" (case-insensitive) and "*" are both already included in token # rule codings_n_weight_re = _item_n_weight_re(item_re=codings_re) codings_n_weight_compiled_re = re.compile(codings_n_weight_re) accept_encoding_compiled_re = _list_0_or_more__compiled_re( element_re=codings_n_weight_re, ) @classmethod def _python_value_to_header_str(cls, value): if isinstance(value, str): header_str = value else: if hasattr(value, 'items'): value = sorted( value.items(), key=lambda item: item[1], reverse=True, ) if isinstance(value, (tuple, list)): result = [] for item in value: if isinstance(item, (tuple, list)): item = _item_qvalue_pair_to_header_element(pair=item) result.append(item) header_str = ', '.join(result) else: header_str = str(value) return header_str @classmethod def parse(cls, value): """ Parse an ``Accept-Encoding`` header. :param value: (``str``) header value :return: If `value` is a valid ``Accept-Encoding`` header, returns an iterator of (codings, quality value) tuples, as parsed from the header from left to right. :raises ValueError: if `value` is an invalid header """ # Check if header is valid # Using Python stdlib's `re` module, there is currently no way to check # the match *and* get all the groups using the same regex, so we have # to use one regex to check the match, and another to get the groups. if cls.accept_encoding_compiled_re.match(value) is None: raise ValueError('Invalid value for an Accept-Encoding header.') def generator(value): for match in (cls.codings_n_weight_compiled_re.finditer(value)): codings = match.group(1) qvalue = match.group(2) qvalue = float(qvalue) if qvalue else 1.0 yield (codings, qvalue) return generator(value=value) class AcceptEncodingValidHeader(AcceptEncoding): """ Represent a valid ``Accept-Encoding`` header. A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptEncodingValidHeader.__add__`). """ @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. A list of (*codings*, *qvalue*) tuples, where *codings* (``str``) is a content-coding, the string "``identity``", or "``*``"; and *qvalue* (``float``) is the quality value of the codings. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptEncodingValidHeader` instance. :param header_value: (``str``) header value. :raises ValueError: if `header_value` is an invalid value for an ``Accept-Encoding`` header. """ self._header_value = header_value self._parsed = list(self.parse(header_value)) self._parsed_nonzero = [item for item in self.parsed if item[1]] # item[1] is the qvalue def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as keys, and qvalue ``float``'s as values * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple`` or ``list`` * an :class:`AcceptEncodingValidHeader`, :class:`AcceptEncodingNoHeader`, or :class:`AcceptEncodingInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or another :class:`AcceptEncodingValidHeader` instance, and the header value it represents is not ``''``, then the two header values are joined with ``', '``, and a new :class:`AcceptEncodingValidHeader` instance with the new header value is returned. If `other` is a valid header value or another :class:`AcceptEncodingValidHeader` instance representing a header value of ``''``; or if it is ``None`` or an :class:`AcceptEncodingNoHeader` instance; or if it is an invalid header value, or an :class:`AcceptEncodingInvalidHeader` instance, then a new :class:`AcceptEncodingValidHeader` instance with the same header value as ``self`` is returned. """ if isinstance(other, AcceptEncodingValidHeader): if other.header_value == '': return self.__class__(header_value=self.header_value) else: return create_accept_encoding_header( header_value=self.header_value + ', ' + other.header_value, ) if isinstance( other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader) ): return self.__class__(header_value=self.header_value) return self._add_instance_and_non_accept_encoding_type( instance=self, other=other, ) def __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Encoding`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``True``. """ return True __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of :meth:`AcceptEncodingValidHeader.__contains__` is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) a content-coding or ``identity`` offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. The behavior of this method does not fully conform to :rfc:`7231`. It does not correctly interpret ``*``:: >>> 'gzip' in AcceptEncodingValidHeader('gzip;q=0, *') True and does not handle the ``identity`` token correctly:: >>> 'identity' in AcceptEncodingValidHeader('gzip') False """ warnings.warn( 'The behavior of AcceptEncodingValidHeader.__contains__ is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) for mask, quality in self._parsed_nonzero: if self._old_match(mask, offer): return True def __iter__(self): """ Return all the ranges with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the (content-coding/``identity``/``*``) items in the header with non-0 qvalues, in descending order of qvalue. If two items have the same qvalue, they are returned in the order of their positions in the header, from left to right. Please note that this is a simple filter for the items in the header with non-0 qvalues, and is not necessarily the same as what the client prefers, e.g. ``'gzip;q=0, *'`` means 'everything but gzip', but ``list(instance)`` would return only ``['*']``. """ warnings.warn( 'The behavior of AcceptEncodingLanguageValidHeader.__iter__ is ' 'currently maintained for backward compatibility, but will change' ' in the future.', DeprecationWarning, ) for m,q in sorted( self._parsed_nonzero, key=lambda i: i[1], reverse=True ): yield m def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptEncodingValidHeader.__add__`. """ return self._add_instance_and_non_accept_encoding_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{} ({!r})>'.format(self.__class__.__name__, str(self)) def __str__(self): r""" Return a tidied up version of the header value. e.g. If the ``header_value`` is ``",\t, a ;\t q=0.20 , b ,',"``, ``str(instance)`` returns ``"a;q=0.2, b, '"``. """ return ', '.join( _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed ) def _add_instance_and_non_accept_encoding_type( self, instance, other, instance_on_the_right=False, ): if not other: return self.__class__(header_value=instance.header_value) other_header_value = self._python_value_to_header_str(value=other) if other_header_value == '': # if ``other`` is an object whose type we don't recognise, and # str(other) returns '' return self.__class__(header_value=instance.header_value) try: self.parse(value=other_header_value) except ValueError: # invalid header value return self.__class__(header_value=instance.header_value) new_header_value = ( (other_header_value + ', ' + instance.header_value) if instance_on_the_right else (instance.header_value + ', ' + other_header_value) ) return self.__class__(header_value=new_header_value) def _old_match(self, mask, offer): """ Return whether content-coding offer matches codings header item. .. warning:: This is maintained for backward compatibility, and will be deprecated in the future. This method was WebOb's old criterion for deciding whether a content-coding offer matches a header item (content-coding, ``identity`` or ``*``), used in - :meth:`AcceptCharsetValidHeader.__contains__` - :meth:`AcceptCharsetValidHeader.best_match` - :meth:`AcceptCharsetValidHeader.quality` It does not conform to :rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>` in that it does not interpret ``*`` values in the header correctly: ``*`` should only match content-codings not mentioned elsewhere in the header. """ return mask == '*' or offer.lower() == mask.lower() def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. The offers are returned in descending order of preference, where preference is indicated by the qvalue of the item (content-coding, "identity" or "*") in the header that matches the offer. This uses the matching rules described in :rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`. :param offers: ``iterable`` of ``str``s, where each ``str`` is a content-coding or the string ``identity`` (the token used to represent "no encoding") :return: A list of tuples of the form (content-coding or "identity", qvalue), in descending order of qvalue. Where two offers have the same qvalue, they are returned in the same order as their order in `offers`. Use the string ``'identity'`` (without the quotes) in `offers` to indicate an offer with no content-coding. From the RFC: 'If the representation has no content-coding, then it is acceptable by default unless specifically excluded by the Accept-Encoding field stating either "identity;q=0" or "\\*;q=0" without a more specific entry for "identity".' The RFC does not specify the qvalue that should be assigned to the representation/offer with no content-coding; this implementation assigns it a qvalue of 1.0. """ lowercased_parsed = [ (codings.lower(), qvalue) for (codings, qvalue) in self.parsed ] lowercased_offers = [offer.lower() for offer in offers] not_acceptable_codingss = set() acceptable_codingss = dict() asterisk_qvalue = None for codings, qvalue in lowercased_parsed: if codings == '*': if asterisk_qvalue is None: asterisk_qvalue = qvalue elif ( codings not in acceptable_codingss and codings not in not_acceptable_codingss # if we have not already encountered this codings in the header ): if qvalue == 0.0: not_acceptable_codingss.add(codings) else: acceptable_codingss[codings] = qvalue acceptable_codingss = list(acceptable_codingss.items()) # Sort acceptable_codingss by qvalue, descending order acceptable_codingss.sort(key=lambda tuple_: tuple_[1], reverse=True) filtered_offers = [] for index, offer in enumerate(lowercased_offers): # If offer matches a non-* codings with q=0, it is filtered out if any(( (offer == codings) for codings in not_acceptable_codingss )): continue matched_codings_qvalue = None for codings, qvalue in acceptable_codingss: if offer == codings: matched_codings_qvalue = qvalue break else: if asterisk_qvalue: matched_codings_qvalue = asterisk_qvalue elif asterisk_qvalue != 0.0 and offer == 'identity': matched_codings_qvalue = 1.0 if matched_codings_qvalue is not None: # if there was a match filtered_offers.append(( offers[index], matched_codings_qvalue, index )) # sort by position in `offers` argument, ascending filtered_offers.sort(key=lambda tuple_: tuple_[2]) # When qvalues are tied, position in `offers` is the tiebreaker. # sort by qvalue, descending filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True) return [(item[0], item[1]) for item in filtered_offers] # (offer, qvalue), dropping the position def best_match(self, offers, default_match=None): """ Return the best match from the sequence of `offers`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :meth:`AcceptEncodingValidHeader.best_match` uses its own algorithm (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a best match. The algorithm has many issues, and does not conform to the RFC. Each offer in `offers` is checked against each non-``q=0`` item (content-coding/``identity``/``*``) in the header. If the two are a match according to WebOb's old criterion for a match, the quality value of the match is the qvalue of the item from the header multiplied by the server quality value of the offer (if the server quality value is not supplied, it is 1). The offer in the match with the highest quality value is the best match. If there is more than one match with the highest qvalue, the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` *codings*, or a (*codings*, server quality value) ``tuple`` or ``list``, where *codings* is either a content-coding, or the string ``identity`` (which represents *no encoding*). ``str`` and ``tuple``/``list`` elements may be mixed within the iterable. :param default_match: (optional, any type) the value to be returned if there is no match :return: (``str``, or the type of `default_match`) | The offer that is the best match. If there is no match, the value of `default_match` is returned. This method does not conform to :rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`, in that it does not correctly interpret ``*``:: >>> AcceptEncodingValidHeader('gzip;q=0, *').best_match(['gzip']) 'gzip' and does not handle the ``identity`` token correctly:: >>> instance = AcceptEncodingValidHeader('gzip') >>> instance.best_match(['identity']) is None True """ warnings.warn( 'The behavior of AcceptEncodingValidHeader.best_match is ' 'currently being maintained for backward compatibility, but it ' 'will be deprecated in the future, as it does not conform to the' ' RFC.', DeprecationWarning, ) best_quality = -1 best_offer = default_match matched_by = '*/*' for offer in offers: if isinstance(offer, (tuple, list)): offer, server_quality = offer else: server_quality = 1 for item in self._parsed_nonzero: mask = item[0] quality = item[1] possible_quality = server_quality * quality if possible_quality < best_quality: continue elif possible_quality == best_quality: # 'text/plain' overrides 'message/*' overrides '*/*' # (if all match w/ the same q=) # [We can see that this was written for the Accept header, # not the Accept-Encoding header.] if matched_by.count('*') <= mask.count('*'): continue if self._old_match(mask, offer): best_quality = possible_quality best_offer = offer matched_by = mask return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :param offer: (``str``) A content-coding, or ``identity``. :return: (``float`` or ``None``) | The quality value from the header item (content-coding/``identity``/``*``) that matches the `offer`, or ``None`` if there is no match. The behavior of this method does not conform to :rfc:`RFC 7231, section 5.3.4<7231#section-5.3.4>`, in that it does not correctly interpret ``*``:: >>> AcceptEncodingValidHeader('gzip;q=0, *').quality('gzip') 1.0 and does not handle the ``identity`` token correctly:: >>> AcceptEncodingValidHeader('gzip').quality('identity') is None True """ warnings.warn( 'The behavior of AcceptEncodingValidHeader.quality is currently ' 'being maintained for backward compatibility, but it will be ' 'deprecated in the future, as it does not conform to the RFC.', DeprecationWarning, ) bestq = 0 for mask, q in self.parsed: if self._old_match(mask, offer): bestq = max(bestq, q) return bestq or None class _AcceptEncodingInvalidOrNoHeader(AcceptEncoding): """ Represent when an ``Accept-Encoding`` header is invalid or not in request. This is the base class for the behaviour that :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader` have in common. :rfc:`7231` does not provide any guidance on what should happen if the ``AcceptEncoding`` header has an invalid value. This implementation disregards the header when the header is invalid, so :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader` have much behaviour in common. """ def __bool__(self): """ Return whether ``self`` represents a valid ``Accept-Encoding`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``False``. """ return False __nonzero__ = __bool__ # Python 2 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of ``.__contains__`` for the ``Accept-Encoding`` classes is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) a content-coding or ``identity`` offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. For this class, either there is no ``Accept-Encoding`` header in the request, or the header is invalid, so any content-coding is acceptable, and this always returns ``True``. """ warnings.warn( 'The behavior of .__contains__ for the Accept-Encoding classes is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) return True def __iter__(self): """ Return all the header items with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the (content-coding/``identity``/``*``) items in the header with non-0 qvalues, in descending order of qvalue. If two items have the same qvalue, they are returned in the order of their positions in the header, from left to right. When there is no ``Accept-Encoding`` header in the request or the header is invalid, there are no items in the header, so this always returns an empty iterator. """ warnings.warn( 'The behavior of AcceptEncodingValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) return iter(()) def acceptable_offers(self, offers): """ Return the offers that are acceptable according to the header. :param offers: ``iterable`` of ``str``s, where each ``str`` is a content-coding or the string ``identity`` (the token used to represent "no encoding") :return: When the header is invalid, or there is no ``Accept-Encoding`` header in the request, all `offers` are considered acceptable, so this method returns a list of (content-coding or "identity", qvalue) tuples where each offer in `offers` is paired with the qvalue of 1.0, in the same order as in `offers`. """ return [(offer, 1.0) for offer in offers] def best_match(self, offers, default_match=None): """ Return the best match from the sequence of `offers`. This is the ``.best_match()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptEncodingValidHeader.best_match`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptEncodingValidHeader.best_match`). When the header is invalid, or there is no `Accept-Encoding` header in the request, all `offers` are considered acceptable, so the best match is the offer in `offers` with the highest server quality value (if the server quality value is not supplied for a media type, it is 1). If more than one offer in `offers` have the same highest server quality value, then the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` *codings*, or a (*codings*, server quality value) ``tuple`` or ``list``, where *codings* is either a content-coding, or the string ``identity`` (which represents *no encoding*). ``str`` and ``tuple``/``list`` elements may be mixed within the iterable. :param default_match: (optional, any type) the value to be returned if `offers` is empty. :return: (``str``, or the type of `default_match`) | The offer that has the highest server quality value. If `offers` is empty, the value of `default_match` is returned. """ warnings.warn( 'The behavior of .best_match for the Accept-Encoding classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior is not ' 'specified in (and currently does not conform to) RFC 7231.', DeprecationWarning, ) best_quality = -1 best_offer = default_match for offer in offers: if isinstance(offer, (list, tuple)): offer, quality = offer else: quality = 1 if quality > best_quality: best_offer = offer best_quality = quality return best_offer def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. This is the ``.quality()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptEncodingValidHeader.quality`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptEncodingValidHeader.quality`). :param offer: (``str``) A content-coding, or ``identity``. :return: (``float``) ``1.0``. When the ``Accept-Encoding`` header is invalid or not in the request, all offers are equally acceptable, so 1.0 is always returned. """ warnings.warn( 'The behavior of .quality for the Accept-Encoding classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior does ' 'not conform to RFC 7231.', DeprecationWarning, ) return 1.0 class AcceptEncodingNoHeader(_AcceptEncodingInvalidOrNoHeader): """ Represent when there is no ``Accept-Encoding`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptEncodingNoHeader.__add__`). """ @property def header_value(self): """ (``str`` or ``None``) The header value. As there is no header in the request, this is ``None``. """ return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As there is no header in the request, this is ``None``. """ return self._parsed def __init__(self): """ Create an :class:`AcceptEncodingNoHeader` instance. """ self._header_value = None self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__() def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as keys, and qvalue ``float``'s as values * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple`` or ``list`` * an :class:`AcceptEncodingValidHeader`, :class:`AcceptEncodingNoHeader`, or :class:`AcceptEncodingInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptEncodingValidHeader` instance, a new :class:`AcceptEncodingValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an invalid header value, or an :class:`AcceptEncodingInvalidHeader` instance, a new :class:`AcceptEncodingNoHeader` instance is returned. """ if isinstance(other, AcceptEncodingValidHeader): return AcceptEncodingValidHeader(header_value=other.header_value) if isinstance( other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader) ): return self.__class__() return self._add_instance_and_non_accept_encoding_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptEncodingNoHeader.__add__`. """ return self.__add__(other=other) def __repr__(self): return '<{}>'.format(self.__class__.__name__) def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_encoding_type(self, instance, other): if other is None: return self.__class__() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptEncodingValidHeader(header_value=other_header_value) except ValueError: # invalid header value return self.__class__() class AcceptEncodingInvalidHeader(_AcceptEncodingInvalidOrNoHeader): """ Represent an invalid ``Accept-Encoding`` header. An invalid header is one that does not conform to :rfc:`7231#section-5.3.4`. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept-Encoding`` header has an invalid value. This implementation disregards the header, and treats it as if there is no ``Accept-Encoding`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptEncodingInvalidHeader.__add__`). """ @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As the header is invalid and cannot be parsed, this is ``None``. """ return self._parsed def __init__(self, header_value): """ Create an :class:`AcceptEncodingInvalidHeader` instance. """ self._header_value = header_value self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` header value * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as keys, and qvalue ``float``'s as values * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple`` or ``list`` * an :class:`AcceptEncodingValidHeader`, :class:`AcceptEncodingNoHeader`, or :class:`AcceptEncodingInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptEncodingValidHeader` instance, then a new :class:`AcceptEncodingValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an invalid header value, or an :class:`AcceptEncodingInvalidHeader` instance, a new :class:`AcceptEncodingNoHeader` instance is returned. """ if isinstance(other, AcceptEncodingValidHeader): return AcceptEncodingValidHeader(header_value=other.header_value) if isinstance( other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader) ): return AcceptEncodingNoHeader() return self._add_instance_and_non_accept_encoding_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptEncodingValidHeader.__add__`. """ return self._add_instance_and_non_accept_encoding_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{}>'.format(self.__class__.__name__) # We do not display the header_value, as it is untrusted input. The # header_value could always be easily obtained from the .header_value # property. def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_encoding_type( self, instance, other, instance_on_the_right=False, ): if other is None: return AcceptEncodingNoHeader() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptEncodingValidHeader(header_value=other_header_value) except ValueError: # invalid header value return AcceptEncodingNoHeader() def create_accept_encoding_header(header_value): """ Create an object representing the ``Accept-Encoding`` header in a request. :param header_value: (``str``) header value :return: If `header_value` is ``None``, an :class:`AcceptEncodingNoHeader` instance. | If `header_value` is a valid ``Accept-Encoding`` header, an :class:`AcceptEncodingValidHeader` instance. | If `header_value` is an invalid ``Accept-Encoding`` header, an :class:`AcceptEncodingInvalidHeader` instance. """ if header_value is None: return AcceptEncodingNoHeader() if isinstance(header_value, AcceptEncoding): return header_value.copy() try: return AcceptEncodingValidHeader(header_value=header_value) except ValueError: return AcceptEncodingInvalidHeader(header_value=header_value) def accept_encoding_property(): doc = """ Property representing the ``Accept-Encoding`` header. (:rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`) The header value in the request environ is parsed and a new object representing the header is created every time we *get* the value of the property. (*set* and *del* change the header value in the request environ, and do not involve parsing.) """ ENVIRON_KEY = 'HTTP_ACCEPT_ENCODING' def fget(request): """Get an object representing the header in the request.""" return create_accept_encoding_header( header_value=request.environ.get(ENVIRON_KEY) ) def fset(request, value): """ Set the corresponding key in the request environ. `value` can be: * ``None`` * a ``str`` header value * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as keys, and qvalue ``float``'s as values * a ``tuple`` or ``list``, where each item is either a header element ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple`` or ``list`` * an :class:`AcceptEncodingValidHeader`, :class:`AcceptEncodingNoHeader`, or :class:`AcceptEncodingInvalidHeader` instance * object of any other type that returns a value for ``__str__`` """ if value is None or isinstance(value, AcceptEncodingNoHeader): fdel(request=request) else: if isinstance( value, (AcceptEncodingValidHeader, AcceptEncodingInvalidHeader) ): header_value = value.header_value else: header_value = AcceptEncoding._python_value_to_header_str( value=value, ) request.environ[ENVIRON_KEY] = header_value def fdel(request): """Delete the corresponding key from the request environ.""" try: del request.environ[ENVIRON_KEY] except KeyError: pass return property(fget, fset, fdel, textwrap.dedent(doc)) class AcceptLanguage(object): """ Represent an ``Accept-Language`` header. Base class for :class:`AcceptLanguageValidHeader`, :class:`AcceptLanguageNoHeader`, and :class:`AcceptLanguageInvalidHeader`. """ # RFC 7231 Section 5.3.5 "Accept-Language": # Accept-Language = 1#( language-range [ weight ] ) # language-range = # # RFC 4647 Section 2.1 "Basic Language Range": # language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" # alphanum = ALPHA / DIGIT lang_range_re = ( r'\*|' '(?:' '[A-Za-z]{1,8}' '(?:-[A-Za-z0-9]{1,8})*' ')' ) lang_range_n_weight_re = _item_n_weight_re(item_re=lang_range_re) lang_range_n_weight_compiled_re = re.compile(lang_range_n_weight_re) accept_language_compiled_re = _list_1_or_more__compiled_re( element_re=lang_range_n_weight_re, ) @classmethod def _python_value_to_header_str(cls, value): if isinstance(value, str): header_str = value else: if hasattr(value, 'items'): value = sorted( value.items(), key=lambda item: item[1], reverse=True, ) if isinstance(value, (tuple, list)): result = [] for element in value: if isinstance(element, (tuple, list)): element = _item_qvalue_pair_to_header_element( pair=element ) result.append(element) header_str = ', '.join(result) else: header_str = str(value) return header_str @classmethod def parse(cls, value): """ Parse an ``Accept-Language`` header. :param value: (``str``) header value :return: If `value` is a valid ``Accept-Language`` header, returns an iterator of (language range, quality value) tuples, as parsed from the header from left to right. :raises ValueError: if `value` is an invalid header """ # Check if header is valid # Using Python stdlib's `re` module, there is currently no way to check # the match *and* get all the groups using the same regex, so we have # to use one regex to check the match, and another to get the groups. if cls.accept_language_compiled_re.match(value) is None: raise ValueError('Invalid value for an Accept-Language header.') def generator(value): for match in ( cls.lang_range_n_weight_compiled_re.finditer(value) ): lang_range = match.group(1) qvalue = match.group(2) qvalue = float(qvalue) if qvalue else 1.0 yield (lang_range, qvalue) return generator(value=value) class AcceptLanguageValidHeader(AcceptLanguage): """ Represent a valid ``Accept-Language`` header. A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`. We take the reference from the ``language-range`` syntax rule in :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` to :rfc:`RFC 4647, section 2.1 <4647#section-2.1>` to mean that only basic language ranges (and not extended language ranges) are expected in the ``Accept-Language`` header. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptLanguageValidHeader.__add__`). """ def __init__(self, header_value): """ Create an :class:`AcceptLanguageValidHeader` instance. :param header_value: (``str``) header value. :raises ValueError: if `header_value` is an invalid value for an ``Accept-Language`` header. """ self._header_value = header_value self._parsed = list(self.parse(header_value)) self._parsed_nonzero = [item for item in self.parsed if item[1]] # item[1] is the qvalue def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. A list of (language range, quality value) tuples. """ return self._parsed def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` * a ``dict``, with language ranges as keys and qvalues as values * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple`` or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptLanguageValidHeader`, :class:`AcceptLanguageNoHeader`, or :class:`AcceptLanguageInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or another :class:`AcceptLanguageValidHeader` instance, the two header values are joined with ``', '``, and a new :class:`AcceptLanguageValidHeader` instance with the new header value is returned. If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an invalid header value, or an :class:`AcceptLanguageInvalidHeader` instance, a new :class:`AcceptLanguageValidHeader` instance with the same header value as ``self`` is returned. """ if isinstance(other, AcceptLanguageValidHeader): return create_accept_language_header( header_value=self.header_value + ', ' + other.header_value, ) if isinstance( other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader) ): return self.__class__(header_value=self.header_value) return self._add_instance_and_non_accept_language_type( instance=self, other=other, ) def __nonzero__(self): """ Return whether ``self`` represents a valid ``Accept-Language`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``True``. """ return True __bool__ = __nonzero__ # Python 3 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of :meth:`AcceptLanguageValidHeader.__contains__` is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. What is 'acceptable' depends on the needs of your application. :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests three matching schemes from :rfc:`RFC 4647 <4647>`, two of which WebOb supports with :meth:`AcceptLanguageValidHeader.basic_filtering` and :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to mean that Extended Filtering cannot apply for the ``Accept-Language`` header, as the header only accepts basic language ranges.) If these are not suitable for the needs of your application, you may need to write your own matching using :attr:`AcceptLanguageValidHeader.parsed`. :param offer: (``str``) language tag offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. This uses the old criterion of a match in :meth:`AcceptLanguageValidHeader._old_match`, which does not conform to :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` or any of the matching schemes suggested there. It also does not properly take into account ranges with ``q=0`` in the header:: >>> 'en-gb' in AcceptLanguageValidHeader('en, en-gb;q=0') True >>> 'en' in AcceptLanguageValidHeader('en;q=0, *') True (See the docstring for :meth:`AcceptLanguageValidHeader._old_match` for other problems with the old criterion for a match.) """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.__contains__ is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) for mask, quality in self._parsed_nonzero: if self._old_match(mask, offer): return True return False def __iter__(self): """ Return all the ranges with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the language ranges in the header with non-0 qvalues, in descending order of qvalue. If two ranges have the same qvalue, they are returned in the order of their positions in the header, from left to right. Please note that this is a simple filter for the ranges in the header with non-0 qvalues, and is not necessarily the same as what the client prefers, e.g. ``'en-gb;q=0, *'`` means 'everything but British English', but ``list(instance)`` would return only ``['*']``. """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) for m, q in sorted( self._parsed_nonzero, key=lambda i: i[1], reverse=True ): yield m def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptLanguageValidHeader.__add__`. """ return self._add_instance_and_non_accept_language_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{} ({!r})>'.format(self.__class__.__name__, str(self)) def __str__(self): r""" Return a tidied up version of the header value. e.g. If the ``header_value`` is ``', \t,de;q=0.000 \t, es;q=1.000, zh, jp;q=0.210 ,'``, ``str(instance)`` returns ``'de;q=0, es, zh, jp;q=0.21'``. """ return ', '.join( _item_qvalue_pair_to_header_element(pair=tuple_) for tuple_ in self.parsed ) def _add_instance_and_non_accept_language_type( self, instance, other, instance_on_the_right=False, ): if not other: return self.__class__(header_value=instance.header_value) other_header_value = self._python_value_to_header_str(value=other) try: self.parse(value=other_header_value) except ValueError: # invalid header value return self.__class__(header_value=instance.header_value) new_header_value = ( (other_header_value + ', ' + instance.header_value) if instance_on_the_right else (instance.header_value + ', ' + other_header_value) ) return self.__class__(header_value=new_header_value) def _old_match(self, mask, item): """ Return whether a language tag matches a language range. .. warning:: This is maintained for backward compatibility, and will be deprecated in the future. This method was WebOb's old criterion for deciding whether a language tag matches a language range, used in - :meth:`AcceptLanguageValidHeader.__contains__` - :meth:`AcceptLanguageValidHeader.best_match` - :meth:`AcceptLanguageValidHeader.quality` It does not conform to :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, or any of the matching schemes suggested there. :param mask: (``str``) | language range :param item: (``str``) | language tag. Subtags in language tags are separated by ``-`` (hyphen). If there are underscores (``_``) in this argument, they will be converted to hyphens before checking the match. :return: (``bool``) whether the tag in `item` matches the range in `mask`. `mask` and `item` are a match if: - ``mask == *``. - ``mask == item``. - If the first subtag of `item` equals `mask`, or if the first subtag of `mask` equals `item`. This means that:: >>> instance._old_match(mask='en-gb', item='en') True >>> instance._old_match(mask='en', item='en-gb') True Which is different from any of the matching schemes suggested in :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, in that none of those schemes match both more *and* less specific tags. However, this method appears to be only designed for language tags and ranges with at most two subtags. So with an `item`/language tag with more than two subtags like ``zh-Hans-CN``:: >>> instance._old_match(mask='zh', item='zh-Hans-CN') True >>> instance._old_match(mask='zh-Hans', item='zh-Hans-CN') False From commit history, this does not appear to have been from a decision to match only the first subtag, but rather because only language ranges and tags with at most two subtags were expected. """ item = item.replace('_', '-').lower() mask = mask.lower() return (mask == '*' or item == mask or item.split('-')[0] == mask or item == mask.split('-')[0] ) def basic_filtering(self, language_tags): """ Return the tags that match the header, using Basic Filtering. This is an implementation of the Basic Filtering matching scheme, suggested as a matching scheme for the ``Accept-Language`` header in :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and defined in :rfc:`RFC 4647, section 3.3.1 <4647#section-3.3.1>`. It filters the tags in the `language_tags` argument and returns the ones that match the header according to the matching scheme. :param language_tags: (``iterable``) language tags :return: A list of tuples of the form (language tag, qvalue), in descending order of qvalue. If two or more tags have the same qvalue, they are returned in the same order as that in the header of the ranges they matched. If the matched range is the same for two or more tags (i.e. their matched ranges have the same qvalue and the same position in the header), then they are returned in the same order as that in the `language_tags` argument. If `language_tags` is unordered, e.g. if it is a set or a dict, then that order may not be reliable. For each tag in `language_tags`: 1. If the tag matches a non-``*`` language range in the header with ``q=0`` (meaning "not acceptable", see :rfc:`RFC 7231, section 5.3.1 <7231#section-5.3.1>`), the tag is filtered out. 2. The non-``*`` language ranges in the header that do not have ``q=0`` are considered in descending order of qvalue; where two or more language ranges have the same qvalue, they are considered in the order in which they appear in the header. 3. A language range 'matches a particular language tag if, in a case-insensitive comparison, it exactly equals the tag, or if it exactly equals a prefix of the tag such that the first character following the prefix is "-".' (:rfc:`RFC 4647, section 3.3.1 <4647#section-3.3.1>`) 4. If the tag does not match any of the non-``*`` language ranges, and there is a ``*`` language range in the header, then if the ``*`` language range has ``q=0``, the language tag is filtered out, otherwise the tag is considered a match. (If a range (``*`` or non-``*``) appears in the header more than once -- this would not make sense, but is nonetheless a valid header according to the RFC -- the first in the header is used for matching, and the others are ignored.) """ # The Basic Filtering matching scheme as applied to the Accept-Language # header is very under-specified by RFCs 7231 and 4647. This # implementation combines the description of the matching scheme in RFC # 4647 and the rules of the Accept-Language header in RFC 7231 to # arrive at an algorithm for Basic Filtering as applied to the # Accept-Language header. lowercased_parsed = [ (range_.lower(), qvalue) for (range_, qvalue) in self.parsed ] lowercased_tags = [tag.lower() for tag in language_tags] not_acceptable_ranges = set() acceptable_ranges = dict() asterisk_qvalue = None for position_in_header, (range_, qvalue) in enumerate( lowercased_parsed ): if range_ == '*': if asterisk_qvalue is None: asterisk_qvalue = qvalue asterisk_position = position_in_header elif ( range_ not in acceptable_ranges and range_ not in not_acceptable_ranges # if we have not already encountered this range in the header ): if qvalue == 0.0: not_acceptable_ranges.add(range_) else: acceptable_ranges[range_] = (qvalue, position_in_header) acceptable_ranges = [ (range_, qvalue, position_in_header) for range_, (qvalue, position_in_header) in acceptable_ranges.items() ] # Sort acceptable_ranges by position_in_header, ascending order acceptable_ranges.sort(key=lambda tuple_: tuple_[2]) # Sort acceptable_ranges by qvalue, descending order acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True) # Sort guaranteed to be stable with Python >= 2.2, so position in # header is tiebreaker when two ranges have the same qvalue def match(tag, range_): # RFC 4647, section 2.1: 'A language range matches a particular # language tag if, in a case-insensitive comparison, it exactly # equals the tag, or if it exactly equals a prefix of the tag such # that the first character following the prefix is "-".' return (tag == range_) or tag.startswith(range_ + '-') # We can assume here that the language tags are valid tags, so we # do not have to worry about them being malformed and ending with # '-'. filtered_tags = [] for index, tag in enumerate(lowercased_tags): # If tag matches a non-* range with q=0, it is filtered out if any(( match(tag=tag, range_=range_) for range_ in not_acceptable_ranges )): continue matched_range_qvalue = None for range_, qvalue, position_in_header in acceptable_ranges: # acceptable_ranges is in descending order of qvalue, and tied # ranges are in ascending order of position_in_header, so the # first range_ that matches the tag is the best match if match(tag=tag, range_=range_): matched_range_qvalue = qvalue matched_range_position = position_in_header break else: if asterisk_qvalue: # From RFC 4647, section 3.3.1: '...HTTP/1.1 [RFC2616] # specifies that the range "*" matches only languages not # matched by any other range within an "Accept-Language" # header.' (Though RFC 2616 is obsolete, and there is no # mention of the meaning of "*" in RFC 7231, as the # ``language-range`` syntax rule in RFC 7231 section 5.3.1 # directs us to RFC 4647, we can only assume that the # meaning of "*" in the Accept-Language header remains the # same). matched_range_qvalue = asterisk_qvalue matched_range_position = asterisk_position if matched_range_qvalue is not None: # if there was a match filtered_tags.append(( language_tags[index], matched_range_qvalue, matched_range_position )) # sort by matched_range_position, ascending filtered_tags.sort(key=lambda tuple_: tuple_[2]) # When qvalues are tied, matched range position in the header is the # tiebreaker. # sort by qvalue, descending filtered_tags.sort(key=lambda tuple_: tuple_[1], reverse=True) return [(item[0], item[1]) for item in filtered_tags] # (tag, qvalue), dropping the matched_range_position # We return a list of tuples with qvalues, instead of just a set or # a list of language tags, because # RFC 4647 section 3.3: "If the language priority list contains more # than one range, the content returned is typically ordered in # descending level of preference, but it MAY be unordered, according to # the needs of the application or protocol." # We return the filtered tags in order of preference, each paired with # the qvalue of the range that was their best match, as the ordering # and the qvalues may well be needed in some applications, and a simple # set or list of language tags can always be easily obtained from the # returned list if the qvalues are not required. One use for qvalues, # for example, would be to indicate that two tags are equally preferred # (same qvalue), which we would not be able to do easily with a set or # a list without e.g. making a member of the set or list a sequence. def best_match(self, offers, default_match=None): """ Return the best match from the sequence of language tag `offers`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :meth:`AcceptLanguageValidHeader.best_match` uses its own algorithm (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a best match. The algorithm has many issues, and does not conform to :rfc:`RFC 7231 <7231>`. :meth:`AcceptLanguageValidHeader.lookup` is a possible alternative for finding a best match -- it conforms to, and is suggested as a matching scheme for the ``Accept-Language`` header in, :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` -- but please be aware that there are differences in how it determines what is a best match. If that is not suitable for the needs of your application, you may need to write your own matching using :attr:`AcceptLanguageValidHeader.parsed`. Each language tag in `offers` is checked against each non-0 range in the header. If the two are a match according to WebOb's old criterion for a match, the quality value of the match is the qvalue of the language range from the header multiplied by the server quality value of the offer (if the server quality value is not supplied, it is 1). The offer in the match with the highest quality value is the best match. If there is more than one match with the highest qvalue, the match where the language range has a lower number of '*'s is the best match. If the two have the same number of '*'s, the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` language tag, or a (language tag, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if there is no match :return: (``str``, or the type of `default_match`) | The language tag that is the best match. If there is no match, the value of `default_match` is returned. **Issues**: - Incorrect tiebreaking when quality values of two matches are the same (https://github.com/Pylons/webob/issues/256):: >>> header = AcceptLanguageValidHeader( ... header_value='en-gb;q=1, en;q=0.8' ... ) >>> header.best_match(offers=['en', 'en-GB']) 'en' >>> header.best_match(offers=['en-GB', 'en']) 'en-GB' >>> header = AcceptLanguageValidHeader(header_value='en-gb, en') >>> header.best_match(offers=['en', 'en-gb']) 'en' >>> header.best_match(offers=['en-gb', 'en']) 'en-gb' - Incorrect handling of ``q=0``:: >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *') >>> header.best_match(offers=['en']) 'en' >>> header = AcceptLanguageValidHeader(header_value='fr, en;q=0') >>> header.best_match(offers=['en-gb'], default_match='en') 'en' - Matching only takes into account the first subtag when matching a range with more specific or less specific tags:: >>> header = AcceptLanguageValidHeader(header_value='zh') >>> header.best_match(offers=['zh-Hans-CN']) 'zh-Hans-CN' >>> header = AcceptLanguageValidHeader(header_value='zh-Hans') >>> header.best_match(offers=['zh-Hans-CN']) >>> header.best_match(offers=['zh-Hans-CN']) is None True >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN') >>> header.best_match(offers=['zh']) 'zh' >>> header.best_match(offers=['zh-Hans']) >>> header.best_match(offers=['zh-Hans']) is None True """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.best_match is ' 'currently being maintained for backward compatibility, but it ' 'will be deprecated in the future as it does not conform to the ' 'RFC.', DeprecationWarning, ) best_quality = -1 best_offer = default_match matched_by = '*/*' # [We can see that this was written for the ``Accept`` header and not # the ``Accept-Language`` header, as there are no '/'s in a valid # ``Accept-Language`` header.] for offer in offers: if isinstance(offer, (tuple, list)): offer, server_quality = offer else: server_quality = 1 for mask, quality in self._parsed_nonzero: possible_quality = server_quality * quality if possible_quality < best_quality: continue elif possible_quality == best_quality: # 'text/plain' overrides 'message/*' overrides '*/*' # (if all match w/ the same q=) if matched_by.count('*') <= mask.count('*'): continue # [This tiebreaking was written for the `Accept` header. A # basic language range in a valid ``Accept-Language`` # header can only be either '*' or a range with no '*' in # it. This happens to work here, but is not sufficient as a # tiebreaker. # # A best match here, given this algorithm uses # self._old_match() which matches both more *and* less # specific tags, should be the match where the absolute # value of the difference between the subtag counts of # `mask` and `offer` is the lowest.] if self._old_match(mask, offer): best_quality = possible_quality best_offer = offer matched_by = mask return best_offer def lookup( self, language_tags, default_range=None, default_tag=None, default=None, ): """ Return the language tag that best matches the header, using Lookup. This is an implementation of the Lookup matching scheme, suggested as a matching scheme for the ``Accept-Language`` header in :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and described in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`. Each language range in the header is considered in turn, by descending order of qvalue; where qvalues are tied, ranges are considered from left to right. Each language range in the header represents the most specific tag that is an acceptable match: Lookup progressively truncates subtags from the end of the range until a matching language tag is found. An example is given in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, under "Example of a Lookup Fallback Pattern": :: Range to match: zh-Hant-CN-x-private1-private2 1. zh-Hant-CN-x-private1-private2 2. zh-Hant-CN-x-private1 3. zh-Hant-CN 4. zh-Hant 5. zh 6. (default) :param language_tags: (``iterable``) language tags :param default_range: (optional, ``None`` or ``str``) | If Lookup finds no match using the ranges in the header, and this argument is not None, Lookup will next attempt to match the range in this argument, using the same subtag truncation. | `default_range` cannot be '*', as '*' is skipped in Lookup. See :ref:`note `. | This parameter corresponds to the functionality described in :rfc:`RFC 4647, section 3.4.1 <4647#section-3.4.1>`, in the paragraph starting with "One common way to provide for a default is to allow a specific language range to be set as the default..." :param default_tag: (optional, ``None`` or ``str``) | At least one of `default_tag` or `default` must be supplied as an argument to the method, to define the defaulting behaviour. | If Lookup finds no match using the ranges in the header and `default_range`, this argument is not ``None``, and it does not match any range in the header with ``q=0`` (exactly, with no subtag truncation), then this value is returned. | This parameter corresponds to "return a particular language tag designated for the operation", one of the examples of "defaulting behavior" described in :rfc:`RFC 4647, section 3.4.1 <4647#section-3.4.1>`. :param default: (optional, ``None`` or any type, including a callable) | At least one of `default_tag` or `default` must be supplied as an argument to the method, to define the defaulting behaviour. | If Lookup finds no match using the ranges in the header and `default_range`, and `default_tag` is ``None`` or not acceptable because it matches a ``q=0`` range in the header, then Lookup will next examine the `default` argument. | If `default` is a callable, it will be called, and the callable's return value will be returned. | If `default` is not a callable, the value itself will be returned. | The difference between supplying a ``str`` to `default_tag` and `default` is that `default_tag` is checked against ``q=0`` ranges in the header to see if it matches one of the ranges specified as not acceptable, whereas a ``str`` for the `default` argument is simply returned. | This parameter corresponds to the "defaulting behavior" described in :rfc:`RFC 4647, section 3.4.1 <4647#section-3.4.1>` :return: (``str``, ``None``, or any type) | The best match according to the Lookup matching scheme, or a return value from one of the default arguments. **Notes**: .. _acceptparse-lookup-asterisk-note: - Lookup's behaviour with '*' language ranges in the header may be surprising. From :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`: In the lookup scheme, this range does not convey enough information by itself to determine which language tag is most appropriate, since it matches everything. If the language range "*" is followed by other language ranges, it is skipped. If the language range "*" is the only one in the language priority list or if no other language range follows, the default value is computed and returned. So :: >>> header = AcceptLanguageValidHeader('de, zh, *') >>> header.lookup(language_tags=['ja', 'en'], default='default') 'default' - Any tags in `language_tags` and `default_tag` and any tag matched during the subtag truncation search for `default_range`, that are an exact match for a non-``*`` range with ``q=0`` in the header, are considered not acceptable and ruled out. - If there is a ``*;q=0`` in the header, then `default_range` and `default_tag` have no effect, as ``*;q=0`` means that all languages not already matched by other ranges within the header are unacceptable. """ if default_tag is None and default is None: raise TypeError( '`default_tag` and `default` arguments cannot both be None.' ) # We need separate `default_tag` and `default` arguments because if we # only had the `default` argument, there would be no way to tell # whether a str is a language tag (in which case we have to check # whether it has been specified as not acceptable with a q=0 range in # the header) or not (in which case we can just return the value). if default_range == '*': raise ValueError('default_range cannot be *.') parsed = list(self.parsed) tags = language_tags not_acceptable_ranges = [] acceptable_ranges = [] asterisk_non0_found = False # Whether there is a '*' range in the header with q={not 0} asterisk_q0_found = False # Whether there is a '*' range in the header with q=0 # While '*' is skipped in Lookup because it "does not convey enough # information by itself to determine which language tag is most # appropriate" (RFC 4647, section 3.4), '*;q=0' is clear in meaning: # languages not matched by any other range within the header are not # acceptable. for range_, qvalue in parsed: if qvalue == 0.0: if range_ == '*': # *;q=0 asterisk_q0_found = True else: # {non-* range};q=0 not_acceptable_ranges.append(range_.lower()) elif not asterisk_q0_found and range_ == '*': # *;q={not 0} asterisk_non0_found = True # if asterisk_q0_found, then it does not matter whether # asterisk_non0_found else: # {non-* range};q={not 0} acceptable_ranges.append((range_, qvalue)) # Sort acceptable_ranges by qvalue, descending order acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True) # Sort guaranteed to be stable with Python >= 2.2, so position in # header is tiebreaker when two ranges have the same qvalue acceptable_ranges = [tuple_[0] for tuple_ in acceptable_ranges] lowered_tags = [tag.lower() for tag in tags] def best_match(range_): subtags = range_.split('-') while True: for index, tag in enumerate(lowered_tags): if tag in not_acceptable_ranges: continue # We think a non-'*' range with q=0 represents only # itself as a tag, and there should be no falling back # with subtag truncation. For example, with # 'en-gb;q=0', it should not mean 'en;q=0': the client # is unlikely to expect that specifying 'en-gb' as not # acceptable would mean that 'en' is also not # acceptable. There is no guidance on this at all in # the RFCs, so it is left to us to decide how it should # work. if tag == range_: return tags[index] # return the pre-lowered tag try: subtag_before_this = subtags[-2] except IndexError: # len(subtags) == 1 break # len(subtags) >= 2 if len(subtag_before_this) == 1 and ( subtag_before_this.isdigit() or subtag_before_this.isalpha() ): # if subtag_before_this is a single-letter or -digit subtag subtags.pop(-1) # pop twice instead of once subtags.pop(-1) range_ = '-'.join(subtags) for range_ in acceptable_ranges: match = best_match(range_=range_.lower()) if match is not None: return match if not asterisk_q0_found: if default_range is not None: lowered_default_range = default_range.lower() match = best_match(range_=lowered_default_range) if match is not None: return match if default_tag is not None: lowered_default_tag = default_tag.lower() if lowered_default_tag not in not_acceptable_ranges: return default_tag try: return default() except TypeError: # default is not a callable return default def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future. :meth:`AcceptLanguageValidHeader.quality` uses its own algorithm (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a best match. The algorithm has many issues, and does not conform to :rfc:`RFC 7231 <7231>`. What should be considered a match depends on the needs of your application (for example, should a language range in the header match a more specific language tag offer, or a less specific tag offer?) :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests three matching schemes from :rfc:`RFC 4647 <4647>`, two of which WebOb supports with :meth:`AcceptLanguageValidHeader.basic_filtering` and :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to mean that Extended Filtering cannot apply for the ``Accept-Language`` header, as the header only accepts basic language ranges.) :meth:`AcceptLanguageValidHeader.basic_filtering` returns quality values with the matched language tags. :meth:`AcceptLanguageValidHeader.lookup` returns a language tag without the quality value, but the quality value is less likely to be useful when we are looking for a best match. If these are not suitable or sufficient for the needs of your application, you may need to write your own matching using :attr:`AcceptLanguageValidHeader.parsed`. :param offer: (``str``) language tag offer :return: (``float`` or ``None``) | The highest quality value from the language range(s) that match the `offer`, or ``None`` if there is no match. **Issues**: - Incorrect handling of ``q=0`` and ``*``:: >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *') >>> header.quality(offer='en') 1.0 - Matching only takes into account the first subtag when matching a range with more specific or less specific tags:: >>> header = AcceptLanguageValidHeader(header_value='zh') >>> header.quality(offer='zh-Hans-CN') 1.0 >>> header = AcceptLanguageValidHeader(header_value='zh-Hans') >>> header.quality(offer='zh-Hans-CN') >>> header.quality(offer='zh-Hans-CN') is None True >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN') >>> header.quality(offer='zh') 1.0 >>> header.quality(offer='zh-Hans') >>> header.quality(offer='zh-Hans') is None True """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.quality is' 'currently being maintained for backward compatibility, but it ' 'will be deprecated in the future as it does not conform to the ' 'RFC.', DeprecationWarning, ) bestq = 0 for mask, q in self.parsed: if self._old_match(mask, offer): bestq = max(bestq, q) return bestq or None class _AcceptLanguageInvalidOrNoHeader(AcceptLanguage): """ Represent when an ``Accept-Language`` header is invalid or not in request. This is the base class for the behaviour that :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader` have in common. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept-Language`` header has an invalid value. This implementation disregards the header when the header is invalid, so :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader` have much behaviour in common. """ def __nonzero__(self): """ Return whether ``self`` represents a valid ``Accept-Language`` header. Return ``True`` if ``self`` represents a valid header, and ``False`` if it represents an invalid header, or the header not being in the request. For this class, it always returns ``False``. """ return False __bool__ = __nonzero__ # Python 3 def __contains__(self, offer): """ Return ``bool`` indicating whether `offer` is acceptable. .. warning:: The behavior of ``.__contains__`` for the ``AcceptLanguage`` classes is currently being maintained for backward compatibility, but it will change in the future to better conform to the RFC. :param offer: (``str``) language tag offer :return: (``bool``) Whether ``offer`` is acceptable according to the header. For this class, either there is no ``Accept-Language`` header in the request, or the header is invalid, so any language tag is acceptable, and this always returns ``True``. """ warnings.warn( 'The behavior of .__contains__ for the AcceptLanguage classes is ' 'currently being maintained for backward compatibility, but it ' 'will change in the future to better conform to the RFC.', DeprecationWarning, ) return True def __iter__(self): """ Return all the ranges with non-0 qvalues, in order of preference. .. warning:: The behavior of this method is currently maintained for backward compatibility, but will change in the future. :return: iterator of all the language ranges in the header with non-0 qvalues, in descending order of qvalue. If two ranges have the same qvalue, they are returned in the order of their positions in the header, from left to right. For this class, either there is no ``Accept-Language`` header in the request, or the header is invalid, so there are no language ranges, and this always returns an empty iterator. """ warnings.warn( 'The behavior of AcceptLanguageValidHeader.__iter__ is currently ' 'maintained for backward compatibility, but will change in the ' 'future.', DeprecationWarning, ) return iter(()) def basic_filtering(self, language_tags): """ Return the tags that match the header, using Basic Filtering. :param language_tags: (``iterable``) language tags :return: A list of tuples of the form (language tag, qvalue), in descending order of preference. When the header is invalid and when the header is not in the request, there are no matches, so this method always returns an empty list. """ return [] def best_match(self, offers, default_match=None): """ Return the best match from the sequence of language tag `offers`. This is the ``.best_match()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptLanguageValidHeader.best_match`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptLanguageValidHeader.best_match`). When the header is invalid, or there is no `Accept-Language` header in the request, any of the language tags in `offers` are considered acceptable, so the best match is the tag in `offers` with the highest server quality value (if the server quality value is not supplied, it is 1). If more than one language tags in `offers` have the same highest server quality value, then the one that shows up first in `offers` is the best match. :param offers: (iterable) | Each item in the iterable may be a ``str`` language tag, or a (language tag, server quality value) ``tuple`` or ``list``. (The two may be mixed in the iterable.) :param default_match: (optional, any type) the value to be returned if `offers` is empty. :return: (``str``, or the type of `default_match`) | The language tag that has the highest server quality value. If `offers` is empty, the value of `default_match` is returned. """ warnings.warn( 'The behavior of .best_match for the AcceptLanguage classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior is not ' 'specified in (and currently does not conform to) RFC 7231.', DeprecationWarning, ) best_quality = -1 best_offer = default_match for offer in offers: if isinstance(offer, (list, tuple)): offer, quality = offer else: quality = 1 if quality > best_quality: best_offer = offer best_quality = quality return best_offer def lookup( self, language_tags=None, default_range=None, default_tag=None, default=None, ): """ Return the language tag that best matches the header, using Lookup. When the header is invalid, or there is no ``Accept-Language`` header in the request, all language tags are considered acceptable, so it is as if the header is '*'. As specified for the Lookup matching scheme in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, when the header is '*', the default value is to be computed and returned. So this method will ignore the `language_tags` and `default_range` arguments, and proceed to `default_tag`, then `default`. :param language_tags: (optional, any type) | This argument is ignored, and is only used as a placeholder so that the method signature corresponds to that of :meth:`AcceptLanguageValidHeader.lookup`. :param default_range: (optional, any type) | This argument is ignored, and is only used as a placeholder so that the method signature corresponds to that of :meth:`AcceptLanguageValidHeader.lookup`. :param default_tag: (optional, ``None`` or ``str``) | At least one of `default_tag` or `default` must be supplied as an argument to the method, to define the defaulting behaviour. | If this argument is not ``None``, then it is returned. | This parameter corresponds to "return a particular language tag designated for the operation", one of the examples of "defaulting behavior" described in :rfc:`RFC 4647, section 3.4.1 <4647#section-3.4.1>`. :param default: (optional, ``None`` or any type, including a callable) | At least one of `default_tag` or `default` must be supplied as an argument to the method, to define the defaulting behaviour. | If `default_tag` is ``None``, then Lookup will next examine the `default` argument. | If `default` is a callable, it will be called, and the callable's return value will be returned. | If `default` is not a callable, the value itself will be returned. | This parameter corresponds to the "defaulting behavior" described in :rfc:`RFC 4647, section 3.4.1 <4647#section-3.4.1>` :return: (``str``, or any type) | the return value from `default_tag` or `default`. """ if default_tag is None and default is None: raise TypeError( '`default_tag` and `default` arguments cannot both be None.' ) if default_tag is not None: return default_tag try: return default() except TypeError: # default is not a callable return default def quality(self, offer): """ Return quality value of given offer, or ``None`` if there is no match. This is the ``.quality()`` method for when the header is invalid or not found in the request, corresponding to :meth:`AcceptLanguageValidHeader.quality`. .. warning:: This is currently maintained for backward compatibility, and will be deprecated in the future (see the documentation for :meth:`AcceptLanguageValidHeader.quality`). :param offer: (``str``) language tag offer :return: (``float``) ``1.0``. When the ``Accept-Language`` header is invalid or not in the request, all offers are equally acceptable, so 1.0 is always returned. """ warnings.warn( 'The behavior of .quality for the AcceptLanguage classes is ' 'currently being maintained for backward compatibility, but the ' 'method will be deprecated in the future, as its behavior is not ' 'specified in (and currently does not conform to) RFC 7231.', DeprecationWarning, ) return 1.0 class AcceptLanguageNoHeader(_AcceptLanguageInvalidOrNoHeader): """ Represent when there is no ``Accept-Language`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptLanguageNoHeader.__add__`). """ def __init__(self): """ Create an :class:`AcceptLanguageNoHeader` instance. """ self._header_value = None self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__() @property def header_value(self): """ (``str`` or ``None``) The header value. As there is no header in the request, this is ``None``. """ return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As there is no header in the request, this is ``None``. """ return self._parsed def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` * a ``dict``, with language ranges as keys and qvalues as values * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple`` or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptLanguageValidHeader`, :class:`AcceptLanguageNoHeader`, or :class:`AcceptLanguageInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptLanguageValidHeader` instance, a new :class:`AcceptLanguageValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an invalid header value, or an :class:`AcceptLanguageInvalidHeader` instance, a new :class:`AcceptLanguageNoHeader` instance is returned. """ if isinstance(other, AcceptLanguageValidHeader): return AcceptLanguageValidHeader(header_value=other.header_value) if isinstance( other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader) ): return self.__class__() return self._add_instance_and_non_accept_language_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptLanguageNoHeader.__add__`. """ return self.__add__(other=other) def __repr__(self): return '<{}>'.format(self.__class__.__name__) def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_language_type(self, instance, other): if not other: return self.__class__() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptLanguageValidHeader(header_value=other_header_value) except ValueError: # invalid header value return self.__class__() class AcceptLanguageInvalidHeader(_AcceptLanguageInvalidOrNoHeader): """ Represent an invalid ``Accept-Language`` header. An invalid header is one that does not conform to :rfc:`7231#section-5.3.5`. As specified in the RFC, an empty header is an invalid ``Accept-Language`` header. :rfc:`7231` does not provide any guidance on what should happen if the ``Accept-Language`` header has an invalid value. This implementation disregards the header, and treats it as if there is no ``Accept-Language`` header in the request. This object should not be modified. To add to the header, we can use the addition operators (``+`` and ``+=``), which return a new object (see the docstring for :meth:`AcceptLanguageInvalidHeader.__add__`). """ def __init__(self, header_value): """ Create an :class:`AcceptLanguageInvalidHeader` instance. """ self._header_value = header_value self._parsed = None self._parsed_nonzero = None def copy(self): """ Create a copy of the header object. """ return self.__class__(self._header_value) @property def header_value(self): """(``str`` or ``None``) The header value.""" return self._header_value @property def parsed(self): """ (``list`` or ``None``) Parsed form of the header. As the header is invalid and cannot be parsed, this is ``None``. """ return self._parsed def __add__(self, other): """ Add to header, creating a new header object. `other` can be: * ``None`` * a ``str`` * a ``dict``, with language ranges as keys and qvalues as values * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple`` or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptLanguageValidHeader`, :class:`AcceptLanguageNoHeader`, or :class:`AcceptLanguageInvalidHeader` instance * object of any other type that returns a value for ``__str__`` If `other` is a valid header value or an :class:`AcceptLanguageValidHeader` instance, a new :class:`AcceptLanguageValidHeader` instance with the valid header value is returned. If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an invalid header value, or an :class:`AcceptLanguageInvalidHeader` instance, a new :class:`AcceptLanguageNoHeader` instance is returned. """ if isinstance(other, AcceptLanguageValidHeader): return AcceptLanguageValidHeader(header_value=other.header_value) if isinstance( other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader) ): return AcceptLanguageNoHeader() return self._add_instance_and_non_accept_language_type( instance=self, other=other, ) def __radd__(self, other): """ Add to header, creating a new header object. See the docstring for :meth:`AcceptLanguageValidHeader.__add__`. """ return self._add_instance_and_non_accept_language_type( instance=self, other=other, instance_on_the_right=True, ) def __repr__(self): return '<{}>'.format(self.__class__.__name__) # We do not display the header_value, as it is untrusted input. The # header_value could always be easily obtained from the .header_value # property. def __str__(self): """Return the ``str`` ``''``.""" return '' def _add_instance_and_non_accept_language_type( self, instance, other, instance_on_the_right=False, ): if not other: return AcceptLanguageNoHeader() other_header_value = self._python_value_to_header_str(value=other) try: return AcceptLanguageValidHeader(header_value=other_header_value) except ValueError: # invalid header value return AcceptLanguageNoHeader() def create_accept_language_header(header_value): """ Create an object representing the ``Accept-Language`` header in a request. :param header_value: (``str``) header value :return: If `header_value` is ``None``, an :class:`AcceptLanguageNoHeader` instance. | If `header_value` is a valid ``Accept-Language`` header, an :class:`AcceptLanguageValidHeader` instance. | If `header_value` is an invalid ``Accept-Language`` header, an :class:`AcceptLanguageInvalidHeader` instance. """ if header_value is None: return AcceptLanguageNoHeader() if isinstance(header_value, AcceptLanguage): return header_value.copy() try: return AcceptLanguageValidHeader(header_value=header_value) except ValueError: return AcceptLanguageInvalidHeader(header_value=header_value) def accept_language_property(): doc = """ Property representing the ``Accept-Language`` header. (:rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`) The header value in the request environ is parsed and a new object representing the header is created every time we *get* the value of the property. (*set* and *del* change the header value in the request environ, and do not involve parsing.) """ ENVIRON_KEY = 'HTTP_ACCEPT_LANGUAGE' def fget(request): """Get an object representing the header in the request.""" return create_accept_language_header( header_value=request.environ.get(ENVIRON_KEY) ) def fset(request, value): """ Set the corresponding key in the request environ. `value` can be: * ``None`` * a ``str`` * a ``dict``, with language ranges as keys and qvalues as values * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple`` or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be mixed within the ``tuple`` or ``list``) * an :class:`AcceptLanguageValidHeader`, :class:`AcceptLanguageNoHeader`, or :class:`AcceptLanguageInvalidHeader` instance * object of any other type that returns a value for ``__str__`` """ if value is None or isinstance(value, AcceptLanguageNoHeader): fdel(request=request) else: if isinstance( value, (AcceptLanguageValidHeader, AcceptLanguageInvalidHeader) ): header_value = value.header_value else: header_value = AcceptLanguage._python_value_to_header_str( value=value, ) request.environ[ENVIRON_KEY] = header_value def fdel(request): """Delete the corresponding key from the request environ.""" try: del request.environ[ENVIRON_KEY] except KeyError: pass return property(fget, fset, fdel, textwrap.dedent(doc)) WebOb-1.8.6/src/webob/byterange.py0000644000076600000240000001121013611751037017710 0ustar xistencestaff00000000000000import re __all__ = ['Range', 'ContentRange'] _rx_range = re.compile(r'bytes *= *(\d*) *- *(\d*)', flags=re.I) _rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])') class Range(object): """ Represents the Range header. """ def __init__(self, start, end): assert end is None or end >= 0, "Bad range end: %r" % end self.start = start self.end = end # non-inclusive def range_for_length(self, length): """ *If* there is only one range, and *if* it is satisfiable by the given length, then return a (start, end) non-inclusive range of bytes to serve. Otherwise return None """ if length is None: return None start, end = self.start, self.end if end is None: end = length if start < 0: start += length if _is_content_range_valid(start, end, length): stop = min(end, length) return (start, stop) else: return None def content_range(self, length): """ Works like range_for_length; returns None or a ContentRange object You can use it like:: response.content_range = req.range.content_range(response.content_length) Though it's still up to you to actually serve that content range! """ range = self.range_for_length(length) if range is None: return None return ContentRange(range[0], range[1], length) def __str__(self): s,e = self.start, self.end if e is None: r = 'bytes=%s' % s if s >= 0: r += '-' return r return 'bytes=%s-%s' % (s, e-1) def __repr__(self): return '<%s bytes %r-%r>' % ( self.__class__.__name__, self.start, self.end) def __iter__(self): return iter((self.start, self.end)) @classmethod def parse(cls, header): """ Parse the header; may return None if header is invalid """ m = _rx_range.match(header or '') if not m: return None start, end = m.groups() if not start: return cls(-int(end), None) start = int(start) if not end: return cls(start, None) end = int(end) + 1 # return val is non-inclusive if start >= end: return None return cls(start, end) class ContentRange(object): """ Represents the Content-Range header This header is ``start-stop/length``, where start-stop and length can be ``*`` (represented as None in the attributes). """ def __init__(self, start, stop, length): if not _is_content_range_valid(start, stop, length): raise ValueError( "Bad start:stop/length: %r-%r/%r" % (start, stop, length)) self.start = start self.stop = stop # this is python-style range end (non-inclusive) self.length = length def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self) def __str__(self): if self.length is None: length = '*' else: length = self.length if self.start is None: assert self.stop is None return 'bytes */%s' % length stop = self.stop - 1 # from non-inclusive to HTTP-style return 'bytes %s-%s/%s' % (self.start, stop, length) def __iter__(self): """ Mostly so you can unpack this, like: start, stop, length = res.content_range """ return iter([self.start, self.stop, self.length]) @classmethod def parse(cls, value): """ Parse the header. May return None if it cannot parse. """ m = _rx_content_range.match(value or '') if not m: return None s, e, l = m.groups() if s: s = int(s) e = int(e) + 1 l = l and int(l) if not _is_content_range_valid(s, e, l, response=True): return None return cls(s, e, l) def _is_content_range_valid(start, stop, length, response=False): if (start is None) != (stop is None): return False elif start is None: return length is None or length >= 0 elif length is None: return 0 <= start < stop elif start >= stop: return False elif response and stop > length: # "content-range: bytes 0-50/10" is invalid for a response # "range: bytes 0-50" is valid for a request to a 10-bytes entity return False else: return 0 <= start < length WebOb-1.8.6/src/webob/cachecontrol.py0000644000076600000240000001516413611751037020410 0ustar xistencestaff00000000000000""" Represents the Cache-Control header """ import re class UpdateDict(dict): """ Dict that has a callback on all updates """ # these are declared as class attributes so that # we don't need to override constructor just to # set some defaults updated = None updated_args = None def _updated(self): """ Assign to new_dict.updated to track updates """ updated = self.updated if updated is not None: args = self.updated_args if args is None: args = (self,) updated(*args) def __setitem__(self, key, item): dict.__setitem__(self, key, item) self._updated() def __delitem__(self, key): dict.__delitem__(self, key) self._updated() def clear(self): dict.clear(self) self._updated() def update(self, *args, **kw): dict.update(self, *args, **kw) self._updated() def setdefault(self, key, value=None): val = dict.setdefault(self, key, value) if val is value: self._updated() return val def pop(self, *args): v = dict.pop(self, *args) self._updated() return v def popitem(self): v = dict.popitem(self) self._updated() return v token_re = re.compile( r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?') need_quote_re = re.compile(r'[^a-zA-Z0-9._-]') class exists_property(object): """ Represents a property that either is listed in the Cache-Control header, or is not listed (has no value) """ def __init__(self, prop, type=None): self.prop = prop self.type = type def __get__(self, obj, type=None): if obj is None: return self return self.prop in obj.properties def __set__(self, obj, value): if (self.type is not None and self.type != obj.type): raise AttributeError( "The property %s only applies to %s Cache-Control" % ( self.prop, self.type)) if value: obj.properties[self.prop] = None else: if self.prop in obj.properties: del obj.properties[self.prop] def __delete__(self, obj): self.__set__(obj, False) class value_property(object): """ Represents a property that has a value in the Cache-Control header. When no value is actually given, the value of self.none is returned. """ def __init__(self, prop, default=None, none=None, type=None): self.prop = prop self.default = default self.none = none self.type = type def __get__(self, obj, type=None): if obj is None: return self if self.prop in obj.properties: value = obj.properties[self.prop] if value is None: return self.none else: return value else: return self.default def __set__(self, obj, value): if (self.type is not None and self.type != obj.type): raise AttributeError( "The property %s only applies to %s Cache-Control" % ( self.prop, self.type)) if value == self.default: if self.prop in obj.properties: del obj.properties[self.prop] elif value is True: obj.properties[self.prop] = None # Empty value, but present else: obj.properties[self.prop] = value def __delete__(self, obj): if self.prop in obj.properties: del obj.properties[self.prop] class CacheControl(object): """ Represents the Cache-Control header. By giving a type of ``'request'`` or ``'response'`` you can control what attributes are allowed (some Cache-Control values only apply to requests or responses). """ update_dict = UpdateDict def __init__(self, properties, type): self.properties = properties self.type = type @classmethod def parse(cls, header, updates_to=None, type=None): """ Parse the header, returning a CacheControl object. The object is bound to the request or response object ``updates_to``, if that is given. """ if updates_to: props = cls.update_dict() props.updated = updates_to else: props = {} for match in token_re.finditer(header): name = match.group(1) value = match.group(2) or match.group(3) or None if value: try: value = int(value) except ValueError: pass props[name] = value obj = cls(props, type=type) if updates_to: props.updated_args = (obj,) return obj def __repr__(self): return '' % str(self) # Request values: # no-cache shared (below) # no-store shared (below) # max-age shared (below) max_stale = value_property('max-stale', none='*', type='request') min_fresh = value_property('min-fresh', type='request') # no-transform shared (below) only_if_cached = exists_property('only-if-cached', type='request') # Response values: public = exists_property('public', type='response') private = value_property('private', none='*', type='response') no_cache = value_property('no-cache', none='*') no_store = exists_property('no-store') no_transform = exists_property('no-transform') must_revalidate = exists_property('must-revalidate', type='response') proxy_revalidate = exists_property('proxy-revalidate', type='response') max_age = value_property('max-age', none=-1) s_maxage = value_property('s-maxage', type='response') s_max_age = s_maxage stale_while_revalidate = value_property( 'stale-while-revalidate', type='response') stale_if_error = value_property('stale-if-error', type='response') def __str__(self): return serialize_cache_control(self.properties) def copy(self): """ Returns a copy of this object. """ return self.__class__(self.properties.copy(), type=self.type) def serialize_cache_control(properties): if isinstance(properties, CacheControl): properties = properties.properties parts = [] for name, value in sorted(properties.items()): if value is None: parts.append(name) continue value = str(value) if need_quote_re.search(value): value = '"%s"' % value parts.append('%s=%s' % (name, value)) return ', '.join(parts) WebOb-1.8.6/src/webob/client.py0000644000076600000240000001530113611751037017213 0ustar xistencestaff00000000000000import errno import sys import re try: import httplib except ImportError: import http.client as httplib from webob.compat import url_quote import socket from webob import exc from webob.compat import PY2 __all__ = ['send_request_app', 'SendRequest'] class SendRequest: """ Sends the request, as described by the environ, over actual HTTP. All controls about how it is sent are contained in the request environ itself. This connects to the server given in SERVER_NAME:SERVER_PORT, and sends the Host header in HTTP_HOST -- they do not have to match. You can send requests to servers despite what DNS says. Set ``environ['webob.client.timeout'] = 10`` to set the timeout on the request (to, for example, 10 seconds). Does not add X-Forwarded-For or other standard headers If you use ``send_request_app`` then simple ``httplib`` connections will be used. """ def __init__(self, HTTPConnection=httplib.HTTPConnection, HTTPSConnection=httplib.HTTPSConnection): self.HTTPConnection = HTTPConnection self.HTTPSConnection = HTTPSConnection def __call__(self, environ, start_response): scheme = environ['wsgi.url_scheme'] if scheme == 'http': ConnClass = self.HTTPConnection elif scheme == 'https': ConnClass = self.HTTPSConnection else: raise ValueError( "Unknown scheme: %r" % scheme) if 'SERVER_NAME' not in environ: host = environ.get('HTTP_HOST') if not host: raise ValueError( "environ contains neither SERVER_NAME nor HTTP_HOST") if ':' in host: host, port = host.split(':', 1) else: if scheme == 'http': port = '80' else: port = '443' environ['SERVER_NAME'] = host environ['SERVER_PORT'] = port kw = {} if ('webob.client.timeout' in environ and self._timeout_supported(ConnClass) ): kw['timeout'] = environ['webob.client.timeout'] conn = ConnClass('%(SERVER_NAME)s:%(SERVER_PORT)s' % environ, **kw) headers = {} for key, value in environ.items(): if key.startswith('HTTP_'): key = key[5:].replace('_', '-').title() headers[key] = value path = (url_quote(environ.get('SCRIPT_NAME', '')) + url_quote(environ.get('PATH_INFO', ''))) if environ.get('QUERY_STRING'): path += '?' + environ['QUERY_STRING'] try: content_length = int(environ.get('CONTENT_LENGTH', '0')) except ValueError: content_length = 0 ## FIXME: there is no streaming of the body, and that might be useful ## in some cases if content_length: body = environ['wsgi.input'].read(content_length) else: body = '' headers['Content-Length'] = content_length if environ.get('CONTENT_TYPE'): headers['Content-Type'] = environ['CONTENT_TYPE'] if not path.startswith("/"): path = "/" + path try: conn.request(environ['REQUEST_METHOD'], path, body, headers) res = conn.getresponse() except socket.timeout: resp = exc.HTTPGatewayTimeout() return resp(environ, start_response) except (socket.error, socket.gaierror) as e: if ((isinstance(e, socket.error) and e.args[0] == -2) or (isinstance(e, socket.gaierror) and e.args[0] == 8)): # Name or service not known resp = exc.HTTPBadGateway( "Name or service not known (bad domain name: %s)" % environ['SERVER_NAME']) return resp(environ, start_response) elif e.args[0] in _e_refused: # pragma: no cover # Connection refused resp = exc.HTTPBadGateway("Connection refused") return resp(environ, start_response) raise headers_out = self.parse_headers(res.msg) status = '%s %s' % (res.status, res.reason) start_response(status, headers_out) length = res.getheader('content-length') # FIXME: This shouldn't really read in all the content at once if length is not None: body = res.read(int(length)) else: body = res.read() conn.close() return [body] # Remove these headers from response (specify lower case header # names): filtered_headers = ( 'transfer-encoding', ) MULTILINE_RE = re.compile(r'\r?\n\s*') def parse_headers(self, message): """ Turn a Message object into a list of WSGI-style headers. """ headers_out = [] if not PY2: headers = message._headers else: headers = message.headers for full_header in headers: if not full_header: # pragma: no cover # Shouldn't happen, but we'll just ignore continue if full_header[0].isspace(): # pragma: no cover # Continuation line, add to the last header if not headers_out: raise ValueError( "First header starts with a space (%r)" % full_header) last_header, last_value = headers_out.pop() value = last_value + ', ' + full_header.strip() headers_out.append((last_header, value)) continue if isinstance(full_header, tuple): # pragma: no cover header, value = full_header else: # pragma: no cover try: header, value = full_header.split(':', 1) except: raise ValueError("Invalid header: %r" % (full_header,)) value = value.strip() if '\n' in value or '\r\n' in value: # pragma: no cover # Python 3 has multiline values for continuations, Python 2 # has two items in headers value = self.MULTILINE_RE.sub(', ', value) if header.lower() not in self.filtered_headers: headers_out.append((header, value)) return headers_out def _timeout_supported(self, ConnClass): if sys.version_info < (2, 7) and ConnClass in ( httplib.HTTPConnection, httplib.HTTPSConnection): # pragma: no cover return False return True send_request_app = SendRequest() _e_refused = (errno.ECONNREFUSED,) if hasattr(errno, 'ENODATA'): # pragma: no cover _e_refused += (errno.ENODATA,) WebOb-1.8.6/src/webob/compat.py0000644000076600000240000001613513611751037017226 0ustar xistencestaff00000000000000# code stolen from "six" import sys import types from cgi import parse_header # True if we are running on Python 3. PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 if PY3: string_types = str, integer_types = int, class_types = type, text_type = str long = int else: string_types = basestring, integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode long = long # TODO check if errors is ever used def text_(s, encoding='latin-1', errors='strict'): if isinstance(s, bytes): return s.decode(encoding, errors) return s def bytes_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s.encode(encoding, errors) return s if PY3: def native_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s return str(s, encoding, errors) else: def native_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s.encode(encoding, errors) return str(s) try: from queue import Queue, Empty except ImportError: from Queue import Queue, Empty try: from collections.abc import MutableMapping from collections.abc import Iterable except ImportError: from collections import MutableMapping from collections import Iterable if PY3: from urllib import parse urlparse = parse from urllib.parse import quote as url_quote from urllib.parse import urlencode as url_encode, quote_plus from urllib.request import urlopen as url_open else: import urlparse from urllib import quote_plus from urllib import quote as url_quote from urllib import unquote as url_unquote from urllib import urlencode as url_encode from urllib2 import urlopen as url_open if PY3: # pragma: no cover def reraise(exc_info): etype, exc, tb = exc_info if exc.__traceback__ is not tb: raise exc.with_traceback(tb) raise exc else: exec("def reraise(exc): raise exc[0], exc[1], exc[2]") if PY3: def iteritems_(d): return d.items() def itervalues_(d): return d.values() else: def iteritems_(d): return d.iteritems() def itervalues_(d): return d.itervalues() if PY3: # pragma: no cover def unquote(string): if not string: return b'' res = string.split(b'%') if len(res) != 1: string = res[0] for item in res[1:]: try: string += bytes([int(item[:2], 16)]) + item[2:] except ValueError: string += b'%' + item return string def url_unquote(s): return unquote(s.encode('ascii')).decode('latin-1') def parse_qsl_text(qs, encoding='utf-8'): qs = qs.encode('latin-1') qs = qs.replace(b'+', b' ') pairs = [s2 for s1 in qs.split(b'&') for s2 in s1.split(b';') if s2] for name_value in pairs: nv = name_value.split(b'=', 1) if len(nv) != 2: nv.append('') name = unquote(nv[0]) value = unquote(nv[1]) yield (name.decode(encoding), value.decode(encoding)) else: from urlparse import parse_qsl def parse_qsl_text(qs, encoding='utf-8'): qsl = parse_qsl( qs, keep_blank_values=True, strict_parsing=False ) for (x, y) in qsl: yield (x.decode(encoding), y.decode(encoding)) if PY3: from html import escape else: from cgi import escape if PY3: import cgi import tempfile from cgi import FieldStorage as _cgi_FieldStorage # Various different FieldStorage work-arounds required on Python 3.x class cgi_FieldStorage(_cgi_FieldStorage): # pragma: no cover # Work around https://bugs.python.org/issue27777 def make_file(self): if self._binary_file or self.length >= 0: return tempfile.TemporaryFile("wb+") else: return tempfile.TemporaryFile( "w+", encoding=self.encoding, newline='\n' ) # Work around http://bugs.python.org/issue23801 # This is taken exactly from Python 3.5's cgi.py module def read_multi(self, environ, keep_blank_values, strict_parsing): """Internal: read a part that is itself multipart.""" ib = self.innerboundary if not cgi.valid_boundary(ib): raise ValueError( 'Invalid boundary in multipart form: %r' % (ib,)) self.list = [] if self.qs_on_post: query = cgi.urllib.parse.parse_qsl( self.qs_on_post, self.keep_blank_values, self.strict_parsing, encoding=self.encoding, errors=self.errors) for key, value in query: self.list.append(cgi.MiniFieldStorage(key, value)) klass = self.FieldStorageClass or self.__class__ first_line = self.fp.readline() # bytes if not isinstance(first_line, bytes): raise ValueError("%s should return bytes, got %s" % (self.fp, type(first_line).__name__)) self.bytes_read += len(first_line) # Ensure that we consume the file until we've hit our innerboundary while (first_line.strip() != (b"--" + self.innerboundary) and first_line): first_line = self.fp.readline() self.bytes_read += len(first_line) while True: parser = cgi.FeedParser() hdr_text = b"" while True: data = self.fp.readline() hdr_text += data if not data.strip(): break if not hdr_text: break # parser takes strings, not bytes self.bytes_read += len(hdr_text) parser.feed(hdr_text.decode(self.encoding, self.errors)) headers = parser.close() # Some clients add Content-Length for part headers, ignore them if 'content-length' in headers: filename = None if 'content-disposition' in self.headers: cdisp, pdict = parse_header(self.headers['content-disposition']) if 'filename' in pdict: filename = pdict['filename'] if filename is None: del headers['content-length'] part = klass(self.fp, headers, ib, environ, keep_blank_values, strict_parsing, self.limit-self.bytes_read, self.encoding, self.errors) self.bytes_read += part.bytes_read self.list.append(part) if part.done or self.bytes_read >= self.length > 0: break self.skip_lines() else: from cgi import FieldStorage as cgi_FieldStorage WebOb-1.8.6/src/webob/cookies.py0000644000076600000240000010011713611751611017367 0ustar xistencestaff00000000000000import base64 import binascii import hashlib import hmac import json from datetime import ( date, datetime, timedelta, ) import re import string import time import warnings from webob.compat import ( MutableMapping, PY2, text_type, bytes_, text_, native_, string_types, ) from webob.util import strings_differ __all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer', 'JSONSerializer', 'Base64Serializer', 'make_cookie'] _marker = object() # Module flag to handle validation of SameSite attributes # See the documentation for ``make_cookie`` for more information. SAMESITE_VALIDATION = True class RequestCookies(MutableMapping): _cache_key = 'webob._parsed_cookies' def __init__(self, environ): self._environ = environ @property def _cache(self): env = self._environ header = env.get('HTTP_COOKIE', '') cache, cache_header = env.get(self._cache_key, ({}, None)) if cache_header == header: return cache d = lambda b: b.decode('utf8') cache = dict((d(k), d(v)) for k,v in parse_cookie(header)) env[self._cache_key] = (cache, header) return cache def _mutate_header(self, name, value): header = self._environ.get('HTTP_COOKIE') had_header = header is not None header = header or '' if not PY2: header = header.encode('latin-1') bytes_name = bytes_(name, 'ascii') if value is None: replacement = None else: bytes_val = _value_quote(bytes_(value, 'utf-8')) replacement = bytes_name + b'=' + bytes_val matches = _rx_cookie.finditer(header) found = False for match in matches: start, end = match.span() match_name = match.group(1) if match_name == bytes_name: found = True if replacement is None: # remove value header = header[:start].rstrip(b' ;') + header[end:] else: # replace value header = header[:start] + replacement + header[end:] break else: if replacement is not None: if header: header += b'; ' + replacement else: header = replacement if header: self._environ['HTTP_COOKIE'] = native_(header, 'latin-1') elif had_header: self._environ['HTTP_COOKIE'] = '' return found def _valid_cookie_name(self, name): if not isinstance(name, string_types): raise TypeError(name, 'cookie name must be a string') if not isinstance(name, text_type): name = text_(name, 'utf-8') try: bytes_cookie_name = bytes_(name, 'ascii') except UnicodeEncodeError: raise TypeError('cookie name must be encodable to ascii') if not _valid_cookie_name(bytes_cookie_name): raise TypeError('cookie name must be valid according to RFC 6265') return name def __setitem__(self, name, value): name = self._valid_cookie_name(name) if not isinstance(value, string_types): raise ValueError(value, 'cookie value must be a string') if not isinstance(value, text_type): try: value = text_(value, 'utf-8') except UnicodeDecodeError: raise ValueError( value, 'cookie value must be utf-8 binary or unicode') self._mutate_header(name, value) def __getitem__(self, name): return self._cache[name] def get(self, name, default=None): return self._cache.get(name, default) def __delitem__(self, name): name = self._valid_cookie_name(name) found = self._mutate_header(name, None) if not found: raise KeyError(name) def keys(self): return self._cache.keys() def values(self): return self._cache.values() def items(self): return self._cache.items() if PY2: def iterkeys(self): return self._cache.iterkeys() def itervalues(self): return self._cache.itervalues() def iteritems(self): return self._cache.iteritems() def __contains__(self, name): return name in self._cache def __iter__(self): return self._cache.__iter__() def __len__(self): return len(self._cache) def clear(self): self._environ['HTTP_COOKIE'] = '' def __repr__(self): return '' % (self._cache,) class Cookie(dict): def __init__(self, input=None): if input: self.load(input) def load(self, data): morsel = {} for key, val in _parse_cookie(data): if key.lower() in _c_keys: morsel[key] = val else: morsel = self.add(key, val) def add(self, key, val): if not isinstance(key, bytes): key = key.encode('ascii', 'replace') if not _valid_cookie_name(key): return {} r = Morsel(key, val) dict.__setitem__(self, key, r) return r __setitem__ = add def serialize(self, full=True): return '; '.join(m.serialize(full) for m in self.values()) def values(self): return [m for _, m in sorted(self.items())] __str__ = serialize def __repr__(self): return '<%s: [%s]>' % (self.__class__.__name__, ', '.join(map(repr, self.values()))) def _parse_cookie(data): if not PY2: data = data.encode('latin-1') for key, val in _rx_cookie.findall(data): yield key, _unquote(val) def parse_cookie(data): """ Parse cookies ignoring anything except names and values """ return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k)) def cookie_property(key, serialize=lambda v: v): def fset(self, v): self[key] = serialize(v) return property(lambda self: self[key], fset) def serialize_max_age(v): if isinstance(v, timedelta): v = str(v.seconds + v.days*24*60*60) elif isinstance(v, int): v = str(v) return bytes_(v) def serialize_cookie_date(v): if v is None: return None elif isinstance(v, bytes): return v elif isinstance(v, text_type): return v.encode('ascii') elif isinstance(v, int): v = timedelta(seconds=v) if isinstance(v, timedelta): v = datetime.utcnow() + v if isinstance(v, (datetime, date)): v = v.timetuple() r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v) return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii') def serialize_samesite(v): v = bytes_(v) if SAMESITE_VALIDATION: if v.lower() not in (b"strict", b"lax", b"none"): raise ValueError("SameSite must be 'strict', 'lax', or 'none'") return v class Morsel(dict): __slots__ = ('name', 'value') def __init__(self, name, value): self.name = bytes_(name, encoding='ascii') self.value = bytes_(value, encoding='ascii') assert _valid_cookie_name(self.name) self.update(dict.fromkeys(_c_keys, None)) path = cookie_property(b'path') domain = cookie_property(b'domain') comment = cookie_property(b'comment') expires = cookie_property(b'expires', serialize_cookie_date) max_age = cookie_property(b'max-age', serialize_max_age) httponly = cookie_property(b'httponly', bool) secure = cookie_property(b'secure', bool) samesite = cookie_property(b'samesite', serialize_samesite) def __setitem__(self, k, v): k = bytes_(k.lower(), 'ascii') if k in _c_keys: dict.__setitem__(self, k, v) def serialize(self, full=True): result = [] add = result.append add(self.name + b'=' + _value_quote(self.value)) if full: for k in _c_valkeys: v = self[k] if v: info = _c_renames[k] name = info['name'] quoter = info['quoter'] add(name + b'=' + quoter(v)) expires = self[b'expires'] if expires: add(b'expires=' + expires) if self.secure: add(b'secure') if self.httponly: add(b'HttpOnly') if self.samesite: if not self.secure and self.samesite.lower() == b"none": raise ValueError( "Incompatible cookie attributes: " "when the samesite equals 'none', then the secure must be True" ) add(b"SameSite=" + self.samesite) return native_(b"; ".join(result), "ascii") __str__ = serialize def __repr__(self): return '<%s: %s=%r>' % (self.__class__.__name__, native_(self.name), native_(self.value) ) # # parsing # _re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string _legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'" _re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars) _re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" _re_cookie_str_key = r"(%s+?)" % _re_legal_char _re_cookie_str_equal = r"\s*=\s*" _re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char _re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val, _re_unquoted_val) _re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val _rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii')) _rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii')) _bchr = chr if PY2 else (lambda i: bytes([i])) _ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i)) for i in range(256) ) _ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values())) _b_dollar_sign = '$' if PY2 else ord('$') _b_quote_mark = '"' if PY2 else ord('"') def _unquote(v): #assert isinstance(v, bytes) if v and v[0] == v[-1] == _b_quote_mark: v = v[1:-1] return _rx_unquote.sub(_ch_unquote, v) def _ch_unquote(m): return _ch_unquote_map[m.group(1)] # # serializing # # these chars can be in cookie value see # http://tools.ietf.org/html/rfc6265#section-4.1.1 and # https://github.com/Pylons/webob/pull/104#issuecomment-28044314 # # ! (0x21), "#$%&'()*+" (0x25-0x2B), "-./0123456789:" (0x2D-0x3A), # "<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" (0x3C-0x5B), # "]^_`abcdefghijklmnopqrstuvwxyz{|}~" (0x5D-0x7E) _allowed_special_chars = "!#$%&'()*+-./:<=>?@[]^_`{|}~" _allowed_cookie_chars = (string.ascii_letters + string.digits + _allowed_special_chars) _allowed_cookie_bytes = bytes_(_allowed_cookie_chars) # these are the characters accepted in cookie *names* # From http://tools.ietf.org/html/rfc2616#section-2.2: # token = 1* # separators = "(" | ")" | "<" | ">" | "@" # | "," | ";" | ":" | "\" | <"> # | "/" | "[" | "]" | "?" | "=" # | "{" | "}" | SP | HT # # CTL = # _valid_token_chars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~" _valid_token_bytes = bytes_(_valid_token_chars) # this is a map used to escape the values _escape_noop_chars = _allowed_cookie_chars + ' ' _escape_map = dict((chr(i), '\\%03o' % i) for i in range(256)) _escape_map.update(zip(_escape_noop_chars, _escape_noop_chars)) if not PY2: # convert to {int -> bytes} _escape_map = dict( (ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items() ) _escape_char = _escape_map.__getitem__ weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') # This is temporary, until we can remove this from _value_quote _should_raise = None def __warn_or_raise(text, warn_class, to_raise, raise_reason): if _should_raise: raise to_raise(raise_reason) else: warnings.warn(text, warn_class, stacklevel=2) def _value_quote(v): # This looks scary, but is simple. We remove all valid characters from the # string, if we end up with leftovers (string is longer than 0, we have # invalid characters in our value) leftovers = v.translate(None, _allowed_cookie_bytes) if leftovers: __warn_or_raise( "Cookie value contains invalid bytes: (%r). Future versions " "will raise ValueError upon encountering invalid bytes." % (leftovers,), RuntimeWarning, ValueError, 'Invalid characters in cookie value' ) #raise ValueError('Invalid characters in cookie value') return b'"' + b''.join(map(_escape_char, v)) + b'"' return v def _valid_cookie_name(key): return isinstance(key, bytes) and not ( key.translate(None, _valid_token_bytes) # Not explicitly required by RFC6265, may consider removing later: or key[0] == _b_dollar_sign or key.lower() in _c_keys ) def _path_quote(v): return b''.join(map(_escape_char, v)) _domain_quote = _path_quote _max_age_quote = _path_quote _c_renames = { b"path" : {'name':b"Path", 'quoter':_path_quote}, b"comment" : {'name':b"Comment", 'quoter':_value_quote}, b"domain" : {'name':b"Domain", 'quoter':_domain_quote}, b"max-age" : {'name':b"Max-Age", 'quoter':_max_age_quote}, } _c_valkeys = sorted(_c_renames) _c_keys = set(_c_renames) _c_keys.update([b'expires', b'secure', b'httponly', b'samesite']) def make_cookie(name, value, max_age=None, path='/', domain=None, secure=False, httponly=False, comment=None, samesite=None): """ Generate a cookie value. ``name`` The name of the cookie. ``value`` The ``value`` of the cookie. If it is ``None``, it will generate a cookie value with an expiration date in the past. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``path`` The path used for the session cookie. Default: ``/``. ``domain`` The domain used for the session cookie. Default: ``None`` (no domain). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from JavaScript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``comment`` Set a comment on the cookie. Default: ``None`` ``samesite`` The 'SameSite' attribute of the cookie, can be either ``"strict"``, ``"lax"``, ``"none"``, or ``None``. By default, WebOb will validate the value to ensure it conforms to the allowable options in the various draft RFC's that exist. To disable this check and send headers that are experimental or introduced in a future RFC, set the module flag ``SAMESITE_VALIDATION`` to a false value like: .. code:: import webob.cookies webob.cookies.SAMESITE_VALIDATION = False ck = webob.cookies.make_cookie(cookie_name, value, samesite='future') .. danger:: This feature has known compatibility issues with various user agents, and is not yet an accepted RFC. It is therefore considered experimental and subject to change. For more information please see :ref:`Experimental: SameSite Cookies ` """ # We are deleting the cookie, override max_age and expires if value is None: value = b'' # Note that the max-age value of zero is technically contraspec; # RFC6265 says that max-age cannot be zero. However, all browsers # appear to support this to mean "delete immediately". # http://www.timwilson.id.au/news-three-critical-problems-with-rfc6265.html max_age = 0 expires = 'Wed, 31-Dec-97 23:59:59 GMT' # Convert max_age to seconds elif isinstance(max_age, timedelta): max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds expires = max_age elif max_age is not None: try: max_age = int(max_age) except ValueError: raise ValueError('max_age should be an integer. Amount of seconds until expiration.') expires = max_age else: expires = None morsel = Morsel(name, value) if domain is not None: morsel.domain = bytes_(domain) if path is not None: morsel.path = bytes_(path) if httponly: morsel.httponly = True if secure: morsel.secure = True if max_age is not None: morsel.max_age = max_age if expires is not None: morsel.expires = expires if comment is not None: morsel.comment = bytes_(comment) if samesite is not None: morsel.samesite = samesite return morsel.serialize() class JSONSerializer(object): """ A serializer which uses `json.dumps`` and ``json.loads``""" def dumps(self, appstruct): return bytes_(json.dumps(appstruct), encoding='utf-8') def loads(self, bstruct): # NB: json.loads raises ValueError if no json object can be decoded # so we don't have to do it explicitly here. return json.loads(text_(bstruct, encoding='utf-8')) class Base64Serializer(object): """ A serializer which uses base64 to encode/decode data""" def __init__(self, serializer=None): if serializer is None: serializer = JSONSerializer() self.serializer = serializer def dumps(self, appstruct): """ Given an ``appstruct``, serialize and sign the data. Returns a bytestring. """ cstruct = self.serializer.dumps(appstruct) # will be bytes return base64.urlsafe_b64encode(cstruct) def loads(self, bstruct): """ Given a ``bstruct`` (a bytestring), verify the signature and then deserialize and return the deserialized value. A ``ValueError`` will be raised if the signature fails to validate. """ try: cstruct = base64.urlsafe_b64decode(bytes_(bstruct)) except (binascii.Error, TypeError) as e: raise ValueError('Badly formed base64 data: %s' % e) return self.serializer.loads(cstruct) class SignedSerializer(object): """ A helper to cryptographically sign arbitrary content using HMAC. The serializer accepts arbitrary functions for performing the actual serialization and deserialization. ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 512 bit (64 character) secret. ``salt`` A namespace to avoid collisions between different uses of a shared secret. ``hashalg`` The HMAC digest algorithm to use for signing. The algorithm must be supported by the :mod:`hashlib` library. Default: ``'sha512'``. ``serializer`` An object with two methods: `loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None`, which will use a derivation of :func:`json.dumps` and ``json.loads``. """ def __init__(self, secret, salt, hashalg='sha512', serializer=None, ): self.salt = salt self.secret = secret self.hashalg = hashalg try: # bwcompat with webob <= 1.3.1, leave latin-1 as the default self.salted_secret = bytes_(salt or '') + bytes_(secret) except UnicodeEncodeError: self.salted_secret = ( bytes_(salt or '', 'utf-8') + bytes_(secret, 'utf-8')) self.digestmod = lambda string=b'': hashlib.new(self.hashalg, string) self.digest_size = self.digestmod().digest_size if serializer is None: serializer = JSONSerializer() self.serializer = serializer def dumps(self, appstruct): """ Given an ``appstruct``, serialize and sign the data. Returns a bytestring. """ cstruct = self.serializer.dumps(appstruct) # will be bytes sig = hmac.new(self.salted_secret, cstruct, self.digestmod).digest() return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') def loads(self, bstruct): """ Given a ``bstruct`` (a bytestring), verify the signature and then deserialize and return the deserialized value. A ``ValueError`` will be raised if the signature fails to validate. """ try: b64padding = b'=' * (-len(bstruct) % 4) fstruct = base64.urlsafe_b64decode(bytes_(bstruct) + b64padding) except (binascii.Error, TypeError) as e: raise ValueError('Badly formed base64 data: %s' % e) cstruct = fstruct[self.digest_size:] expected_sig = fstruct[:self.digest_size] sig = hmac.new( self.salted_secret, bytes_(cstruct), self.digestmod).digest() if strings_differ(sig, expected_sig): raise ValueError('Invalid signature') return self.serializer.loads(cstruct) _default = object() class CookieProfile(object): """ A helper class that helps bring some sanity to the insanity that is cookie handling. The helper is capable of generating multiple cookies if necessary to support subdomains and parent domains. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``samesite`` The 'SameSite' attribute of the cookie, can be either ``b"strict"``, ``b"lax"``, ``b"none"``, or ``None``. For more information please see the ``samesite`` documentation in :meth:`webob.cookies.make_cookie` ``path`` The path used for the session cookie. Default: ``'/'``. ``domains`` The domain(s) used for the session cookie. Default: ``None`` (no domain). Can be passed an iterable containing multiple domains, this will set multiple cookies one for each domain. ``serializer`` An object with two methods: ``loads`` and ``dumps``. The ``loads`` method should accept a bytestring and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None``, which will use a derivation of :func:`json.dumps` and :func:`json.loads`. """ def __init__(self, cookie_name, secure=False, max_age=None, httponly=None, samesite=None, path='/', domains=None, serializer=None ): self.cookie_name = cookie_name self.secure = secure self.max_age = max_age self.httponly = httponly self.samesite = samesite self.path = path self.domains = domains if serializer is None: serializer = Base64Serializer() self.serializer = serializer self.request = None def __call__(self, request): """ Bind a request to a copy of this instance and return it""" return self.bind(request) def bind(self, request): """ Bind a request to a copy of this instance and return it""" selfish = CookieProfile( self.cookie_name, self.secure, self.max_age, self.httponly, self.samesite, self.path, self.domains, self.serializer, ) selfish.request = request return selfish def get_value(self): """ Looks for a cookie by name in the currently bound request, and returns its value. If the cookie profile is not bound to a request, this method will raise a :exc:`ValueError`. Looks for the cookie in the cookies jar, and if it can find it it will attempt to deserialize it. Returns ``None`` if there is no cookie or if the value in the cookie cannot be successfully deserialized. """ if not self.request: raise ValueError('No request bound to cookie profile') cookie = self.request.cookies.get(self.cookie_name) if cookie is not None: try: return self.serializer.loads(bytes_(cookie)) except ValueError: return None def set_cookies(self, response, value, domains=_default, max_age=_default, path=_default, secure=_default, httponly=_default, samesite=_default): """ Set the cookies on a response.""" cookies = self.get_headers( value, domains=domains, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, ) response.headerlist.extend(cookies) return response def get_headers(self, value, domains=_default, max_age=_default, path=_default, secure=_default, httponly=_default, samesite=_default): """ Retrieve raw headers for setting cookies. Returns a list of headers that should be set for the cookies to be correctly tracked. """ if value is None: max_age = 0 bstruct = None else: bstruct = self.serializer.dumps(value) return self._get_cookies( bstruct, domains=domains, max_age=max_age, path=path, secure=secure, httponly=httponly, samesite=samesite, ) def _get_cookies(self, value, domains, max_age, path, secure, httponly, samesite): """Internal function This returns a list of cookies that are valid HTTP Headers. :environ: The request environment :value: The value to store in the cookie :domains: The domains, overrides any set in the CookieProfile :max_age: The max_age, overrides any set in the CookieProfile :path: The path, overrides any set in the CookieProfile :secure: Set this cookie to secure, overrides any set in CookieProfile :httponly: Set this cookie to HttpOnly, overrides any set in CookieProfile :samesite: Set this cookie to be for only the same site, overrides any set in CookieProfile. """ # If the user doesn't provide values, grab the defaults if domains is _default: domains = self.domains if max_age is _default: max_age = self.max_age if path is _default: path = self.path if secure is _default: secure = self.secure if httponly is _default: httponly = self.httponly if samesite is _default: samesite = self.samesite # Length selected based upon http://browsercookielimits.x64.me if value is not None and len(value) > 4093: raise ValueError( 'Cookie value is too long to store (%s bytes)' % len(value) ) cookies = [] if not domains: cookievalue = make_cookie( self.cookie_name, value, path=path, max_age=max_age, httponly=httponly, samesite=samesite, secure=secure ) cookies.append(('Set-Cookie', cookievalue)) else: for domain in domains: cookievalue = make_cookie( self.cookie_name, value, path=path, domain=domain, max_age=max_age, httponly=httponly, samesite=samesite, secure=secure, ) cookies.append(('Set-Cookie', cookievalue)) return cookies class SignedCookieProfile(CookieProfile): """ A helper for generating cookies that are signed to prevent tampering. By default this will create a single cookie, given a value it will serialize it, then use HMAC to cryptographically sign the data. Finally the result is base64-encoded for transport. This way a remote user can not tamper with the value without uncovering the secret/salt used. ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 512 bit (64 character) secret. ``salt`` A namespace to avoid collisions between different uses of a shared secret. ``hashalg`` The HMAC digest algorithm to use for signing. The algorithm must be supported by the :mod:`hashlib` library. Default: ``'sha512'``. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``samesite`` The 'SameSite' attribute of the cookie, can be either ``b"strict"``, ``b"lax"``, ``b"none"``, or ``None``. ``path`` The path used for the session cookie. Default: ``'/'``. ``domains`` The domain(s) used for the session cookie. Default: ``None`` (no domain). Can be passed an iterable containing multiple domains, this will set multiple cookies one for each domain. ``serializer`` An object with two methods: `loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None`, which will use a derivation of :func:`json.dumps` and ``json.loads``. """ def __init__(self, secret, salt, cookie_name, secure=False, max_age=None, httponly=False, samesite=None, path="/", domains=None, hashalg='sha512', serializer=None, ): self.secret = secret self.salt = salt self.hashalg = hashalg self.original_serializer = serializer signed_serializer = SignedSerializer( secret, salt, hashalg, serializer=self.original_serializer, ) CookieProfile.__init__( self, cookie_name, secure=secure, max_age=max_age, httponly=httponly, samesite=samesite, path=path, domains=domains, serializer=signed_serializer, ) def bind(self, request): """ Bind a request to a copy of this instance and return it""" selfish = SignedCookieProfile( self.secret, self.salt, self.cookie_name, self.secure, self.max_age, self.httponly, self.samesite, self.path, self.domains, self.hashalg, self.original_serializer, ) selfish.request = request return selfish WebOb-1.8.6/src/webob/datetime_utils.py0000644000076600000240000000477013611751037020761 0ustar xistencestaff00000000000000import calendar from datetime import ( date, datetime, timedelta, tzinfo, ) from email.utils import ( formatdate, mktime_tz, parsedate_tz, ) import time from webob.compat import ( integer_types, long, native_, text_type, ) __all__ = [ 'UTC', 'timedelta_to_seconds', 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'parse_date', 'serialize_date', 'parse_date_delta', 'serialize_date_delta', ] _now = datetime.now # hook point for unit tests class _UTC(tzinfo): def dst(self, dt): return timedelta(0) def utcoffset(self, dt): return timedelta(0) def tzname(self, dt): return 'UTC' def __repr__(self): return 'UTC' UTC = _UTC() def timedelta_to_seconds(td): """ Converts a timedelta instance to seconds. """ return td.seconds + (td.days*24*60*60) day = timedelta(days=1) week = timedelta(weeks=1) hour = timedelta(hours=1) minute = timedelta(minutes=1) second = timedelta(seconds=1) # Estimate, I know; good enough for expirations month = timedelta(days=30) year = timedelta(days=365) def parse_date(value): if not value: return None try: value = native_(value) except: return None t = parsedate_tz(value) if t is None: # Could not parse return None if t[-1] is None: # No timezone given. None would mean local time, but we'll force UTC t = t[:9] + (0,) t = mktime_tz(t) return datetime.fromtimestamp(t, UTC) def serialize_date(dt): if isinstance(dt, (bytes, text_type)): return native_(dt) if isinstance(dt, timedelta): dt = _now() + dt if isinstance(dt, (datetime, date)): dt = dt.timetuple() if isinstance(dt, (tuple, time.struct_time)): dt = calendar.timegm(dt) if not (isinstance(dt, float) or isinstance(dt, integer_types)): raise ValueError( "You must pass in a datetime, date, time tuple, or integer object, " "not %r" % dt) return formatdate(dt, usegmt=True) def parse_date_delta(value): """ like parse_date, but also handle delta seconds """ if not value: return None try: value = int(value) except ValueError: return parse_date(value) else: return _now() + timedelta(seconds=value) def serialize_date_delta(value): if isinstance(value, (float, int, long)): return str(int(value)) else: return serialize_date(value) WebOb-1.8.6/src/webob/dec.py0000644000076600000240000002436513611751037016502 0ustar xistencestaff00000000000000""" Decorators to wrap functions to make them WSGI applications. The main decorator :class:`wsgify` turns a function into a WSGI application (while also allowing normal calling of the method with an instantiated request). """ from webob.compat import ( bytes_, text_type, ) from webob.request import Request from webob.exc import HTTPException __all__ = ['wsgify'] class wsgify(object): """Turns a request-taking, response-returning function into a WSGI app You can use this like:: @wsgify def myfunc(req): return webob.Response('hey there') With that ``myfunc`` will be a WSGI application, callable like ``app_iter = myfunc(environ, start_response)``. You can also call it like normal, e.g., ``resp = myfunc(req)``. (You can also wrap methods, like ``def myfunc(self, req)``.) If you raise exceptions from :mod:`webob.exc` they will be turned into WSGI responses. There are also several parameters you can use to customize the decorator. Most notably, you can use a :class:`webob.Request` subclass, like:: class MyRequest(webob.Request): @property def is_local(self): return self.remote_addr == '127.0.0.1' @wsgify(RequestClass=MyRequest) def myfunc(req): if req.is_local: return Response('hi!') else: raise webob.exc.HTTPForbidden Another customization you can add is to add `args` (positional arguments) or `kwargs` (of course, keyword arguments). While generally not that useful, you can use this to create multiple WSGI apps from one function, like:: import simplejson def serve_json(req, json_obj): return Response(json.dumps(json_obj), content_type='application/json') serve_ob1 = wsgify(serve_json, args=(ob1,)) serve_ob2 = wsgify(serve_json, args=(ob2,)) You can return several things from a function: * A :class:`webob.Response` object (or subclass) * *Any* WSGI application * None, and then ``req.response`` will be used (a pre-instantiated Response object) * A string, which will be written to ``req.response`` and then that response will be used. * Raise an exception from :mod:`webob.exc` Also see :func:`wsgify.middleware` for a way to make middleware. You can also subclass this decorator; the most useful things to do in a subclass would be to change `RequestClass` or override `call_func` (e.g., to add ``req.urlvars`` as keyword arguments to the function). """ RequestClass = Request def __init__(self, func=None, RequestClass=None, args=(), kwargs=None, middleware_wraps=None): self.func = func if (RequestClass is not None and RequestClass is not self.RequestClass): self.RequestClass = RequestClass self.args = tuple(args) if kwargs is None: kwargs = {} self.kwargs = kwargs self.middleware_wraps = middleware_wraps def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.func) def __get__(self, obj, type=None): # This handles wrapping methods if hasattr(self.func, '__get__'): return self.clone(self.func.__get__(obj, type)) else: return self def __call__(self, req, *args, **kw): """Call this as a WSGI application or with a request""" func = self.func if func is None: if args or kw: raise TypeError( "Unbound %s can only be called with the function it " "will wrap" % self.__class__.__name__) func = req return self.clone(func) if isinstance(req, dict): if len(args) != 1 or kw: raise TypeError( "Calling %r as a WSGI app with the wrong signature" % self.func) environ = req start_response = args[0] req = self.RequestClass(environ) req.response = req.ResponseClass() try: args, kw = self._prepare_args(None, None) resp = self.call_func(req, *args, **kw) except HTTPException as exc: resp = exc if resp is None: ## FIXME: I'm not sure what this should be? resp = req.response if isinstance(resp, text_type): resp = bytes_(resp, req.charset) if isinstance(resp, bytes): body = resp resp = req.response resp.write(body) if resp is not req.response: resp = req.response.merge_cookies(resp) return resp(environ, start_response) else: args, kw = self._prepare_args(args, kw) return self.call_func(req, *args, **kw) def get(self, url, **kw): """Run a GET request on this application, returning a Response. This creates a request object using the given URL, and any other keyword arguments are set on the request object (e.g., ``last_modified=datetime.now()``). :: resp = myapp.get('/article?id=10') """ kw.setdefault('method', 'GET') req = self.RequestClass.blank(url, **kw) return self(req) def post(self, url, POST=None, **kw): """Run a POST request on this application, returning a Response. The second argument (`POST`) can be the request body (a string), or a dictionary or list of two-tuples, that give the POST body. :: resp = myapp.post('/article/new', dict(title='My Day', content='I ate a sandwich')) """ kw.setdefault('method', 'POST') req = self.RequestClass.blank(url, POST=POST, **kw) return self(req) def request(self, url, **kw): """Run a request on this application, returning a Response. This can be used for DELETE, PUT, etc requests. E.g.:: resp = myapp.request('/article/1', method='PUT', body='New article') """ req = self.RequestClass.blank(url, **kw) return self(req) def call_func(self, req, *args, **kwargs): """Call the wrapped function; override this in a subclass to change how the function is called.""" return self.func(req, *args, **kwargs) def clone(self, func=None, **kw): """Creates a copy/clone of this object, but with some parameters rebound """ kwargs = {} if func is not None: kwargs['func'] = func if self.RequestClass is not self.__class__.RequestClass: kwargs['RequestClass'] = self.RequestClass if self.args: kwargs['args'] = self.args if self.kwargs: kwargs['kwargs'] = self.kwargs kwargs.update(kw) return self.__class__(**kwargs) # To match @decorator: @property def undecorated(self): return self.func @classmethod def middleware(cls, middle_func=None, app=None, **kw): """Creates middleware Use this like:: @wsgify.middleware def restrict_ip(req, app, ips): if req.remote_addr not in ips: raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr) return app @wsgify def app(req): return 'hi' wrapped = restrict_ip(app, ips=['127.0.0.1']) Or as a decorator:: @restrict_ip(ips=['127.0.0.1']) @wsgify def wrapped_app(req): return 'hi' Or if you want to write output-rewriting middleware:: @wsgify.middleware def all_caps(req, app): resp = req.get_response(app) resp.body = resp.body.upper() return resp wrapped = all_caps(app) Note that you must call ``req.get_response(app)`` to get a WebOb response object. If you are not modifying the output, you can just return the app. As you can see, this method doesn't actually create an application, but creates "middleware" that can be bound to an application, along with "configuration" (that is, any other keyword arguments you pass when binding the application). """ if middle_func is None: return _UnboundMiddleware(cls, app, kw) if app is None: return _MiddlewareFactory(cls, middle_func, kw) return cls(middle_func, middleware_wraps=app, kwargs=kw) def _prepare_args(self, args, kwargs): args = args or self.args kwargs = kwargs or self.kwargs if self.middleware_wraps: args = (self.middleware_wraps,) + args return args, kwargs class _UnboundMiddleware(object): """A `wsgify.middleware` invocation that has not yet wrapped a middleware function; the intermediate object when you do something like ``@wsgify.middleware(RequestClass=Foo)`` """ def __init__(self, wrapper_class, app, kw): self.wrapper_class = wrapper_class self.app = app self.kw = kw def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.app) def __call__(self, func, app=None): if app is None: app = self.app return self.wrapper_class.middleware(func, app=app, **self.kw) class _MiddlewareFactory(object): """A middleware that has not yet been bound to an application or configured. """ def __init__(self, wrapper_class, middleware, kw): self.wrapper_class = wrapper_class self.middleware = middleware self.kw = kw def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.middleware) def __call__(self, app=None, **config): kw = self.kw.copy() kw.update(config) return self.wrapper_class.middleware(self.middleware, app, **kw) WebOb-1.8.6/src/webob/descriptors.py0000644000076600000240000002177413611751037020311 0ustar xistencestaff00000000000000import re from datetime import ( date, datetime, ) from collections import namedtuple from webob.byterange import ( ContentRange, Range, ) from webob.compat import ( PY2, text_type, ) from webob.datetime_utils import ( parse_date, serialize_date, ) from webob.util import ( header_docstring, warn_deprecation, ) CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I) SCHEME_RE = re.compile(r'^[a-z]+:', re.I) _not_given = object() def environ_getter(key, default=_not_given, rfc_section=None): if rfc_section: doc = header_docstring(key, rfc_section) else: doc = "Gets and sets the ``%s`` key in the environment." % key if default is _not_given: def fget(req): return req.environ[key] def fset(req, val): req.environ[key] = val fdel = None else: def fget(req): return req.environ.get(key, default) def fset(req, val): if val is None: if key in req.environ: del req.environ[key] else: req.environ[key] = val def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) def environ_decoder(key, default=_not_given, rfc_section=None, encattr=None): if rfc_section: doc = header_docstring(key, rfc_section) else: doc = "Gets and sets the ``%s`` key in the environment." % key if default is _not_given: def fget(req): return req.encget(key, encattr=encattr) def fset(req, val): return req.encset(key, val, encattr=encattr) fdel = None else: def fget(req): return req.encget(key, default, encattr=encattr) def fset(req, val): if val is None: if key in req.environ: del req.environ[key] else: return req.encset(key, val, encattr=encattr) def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) def upath_property(key): if PY2: def fget(req): encoding = req.url_encoding return req.environ.get(key, '').decode(encoding) def fset(req, val): encoding = req.url_encoding if isinstance(val, text_type): val = val.encode(encoding) req.environ[key] = val else: def fget(req): encoding = req.url_encoding return req.environ.get(key, '').encode('latin-1').decode(encoding) def fset(req, val): encoding = req.url_encoding req.environ[key] = val.encode(encoding).decode('latin-1') return property(fget, fset, doc='upath_property(%r)' % key) def deprecated_property(attr, name, text, version): # pragma: no cover """ Wraps a descriptor, with a deprecation warning or error """ def warn(): warn_deprecation('The attribute %s is deprecated: %s' % (name, text), version, 3 ) def fget(self): warn() return attr.__get__(self, type(self)) def fset(self, val): warn() attr.__set__(self, val) def fdel(self): warn() attr.__delete__(self) return property(fget, fset, fdel, '' % name ) def header_getter(header, rfc_section): doc = header_docstring(header, rfc_section) key = header.lower() def fget(r): for k, v in r._headerlist: if k.lower() == key: return v def fset(r, value): fdel(r) if value is not None: if '\n' in value or '\r' in value: raise ValueError('Header value may not contain control characters') if isinstance(value, text_type) and PY2: value = value.encode('latin-1') r._headerlist.append((header, value)) def fdel(r): r._headerlist[:] = [(k, v) for (k, v) in r._headerlist if k.lower() != key] return property(fget, fset, fdel, doc) def converter(prop, parse, serialize, convert_name=None): assert isinstance(prop, property) convert_name = convert_name or "``%s`` and ``%s``" % (parse.__name__, serialize.__name__) doc = prop.__doc__ or '' doc += " Converts it using %s." % convert_name hget, hset = prop.fget, prop.fset def fget(r): return parse(hget(r)) def fset(r, val): if val is not None: val = serialize(val) hset(r, val) return property(fget, fset, prop.fdel, doc) def list_header(header, rfc_section): prop = header_getter(header, rfc_section) return converter(prop, parse_list, serialize_list, 'list') def parse_list(value): if not value: return None return tuple(filter(None, [v.strip() for v in value.split(',')])) def serialize_list(value): if isinstance(value, (text_type, bytes)): return str(value) else: return ', '.join(map(str, value)) def converter_date(prop): return converter(prop, parse_date, serialize_date, 'HTTP date') def date_header(header, rfc_section): return converter_date(header_getter(header, rfc_section)) ######################## ## Converter functions ######################## _rx_etag = re.compile(r'(?:^|\s)(W/)?"((?:\\"|.)*?)"') def parse_etag_response(value, strong=False): """ Parse a response ETag. See: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11 """ if not value: return None m = _rx_etag.match(value) if not m: # this etag is invalid, but we'll just return it anyway return value elif strong and m.group(1): # this is a weak etag and we want only strong ones return None else: return m.group(2).replace('\\"', '"') def serialize_etag_response(value): #return '"%s"' % value.replace('"', '\\"') strong = True if isinstance(value, tuple): value, strong = value elif _rx_etag.match(value): # this is a valid etag already return value # let's quote the value r = '"%s"' % value.replace('"', '\\"') if not strong: r = 'W/' + r return r def serialize_if_range(value): if isinstance(value, (datetime, date)): return serialize_date(value) value = str(value) return value or None def parse_range(value): if not value: return None # Might return None too: return Range.parse(value) def serialize_range(value): if not value: return None elif isinstance(value, (list, tuple)): return str(Range(*value)) else: assert isinstance(value, str) return value def parse_int(value): if value is None or value == '': return None return int(value) def parse_int_safe(value): if value is None or value == '': return None try: return int(value) except ValueError: return None serialize_int = str def parse_content_range(value): if not value or not value.strip(): return None # May still return None return ContentRange.parse(value) def serialize_content_range(value): if isinstance(value, (tuple, list)): if len(value) not in (2, 3): raise ValueError( "When setting content_range to a list/tuple, it must " "be length 2 or 3 (not %r)" % value) if len(value) == 2: begin, end = value length = None else: begin, end, length = value value = ContentRange(begin, end, length) value = str(value).strip() if not value: return None return value _rx_auth_param = re.compile(r'([a-z]+)[ \t]*=[ \t]*(".*?"|[^,]*?)[ \t]*(?:\Z|, *)') def parse_auth_params(params): r = {} for k, v in _rx_auth_param.findall(params): r[k] = v.strip('"') return r # see http://lists.w3.org/Archives/Public/ietf-http-wg/2009OctDec/0297.html known_auth_schemes = ['Basic', 'Digest', 'WSSE', 'HMACDigest', 'GoogleLogin', 'Cookie', 'OpenID'] known_auth_schemes = dict.fromkeys(known_auth_schemes, None) _authorization = namedtuple('Authorization', ['authtype', 'params']) def parse_auth(val): if val is not None: authtype, sep, params = val.partition(' ') if authtype in known_auth_schemes: if authtype == 'Basic' and '"' not in params: # this is the "Authentication: Basic XXXXX==" case pass else: params = parse_auth_params(params) return _authorization(authtype, params) return val def serialize_auth(val): if isinstance(val, (tuple, list)): authtype, params = val if isinstance(params, dict): params = ', '.join(map('%s="%s"'.__mod__, params.items())) assert isinstance(params, str) return '%s %s' % (authtype, params) return val WebOb-1.8.6/src/webob/etag.py0000644000076600000240000000725113611751037016662 0ustar xistencestaff00000000000000""" Does parsing of ETag-related headers: If-None-Matches, If-Matches Also If-Range parsing """ from webob.datetime_utils import ( parse_date, serialize_date, ) from webob.descriptors import _rx_etag from webob.util import header_docstring __all__ = ['AnyETag', 'NoETag', 'ETagMatcher', 'IfRange', 'etag_property'] def etag_property(key, default, rfc_section, strong=True): doc = header_docstring(key, rfc_section) doc += " Converts it as a Etag." def fget(req): value = req.environ.get(key) if not value: return default else: return ETagMatcher.parse(value, strong=strong) def fset(req, val): if val is None: req.environ[key] = None else: req.environ[key] = str(val) def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) class _AnyETag(object): """ Represents an ETag of *, or a missing ETag when matching is 'safe' """ def __repr__(self): return '' def __nonzero__(self): return False __bool__ = __nonzero__ # python 3 def __contains__(self, other): return True def __str__(self): return '*' AnyETag = _AnyETag() class _NoETag(object): """ Represents a missing ETag when matching is unsafe """ def __repr__(self): return '' def __nonzero__(self): return False __bool__ = __nonzero__ # python 3 def __contains__(self, other): return False def __str__(self): return '' NoETag = _NoETag() # TODO: convert into a simple tuple class ETagMatcher(object): def __init__(self, etags): self.etags = etags def __contains__(self, other): return other in self.etags def __repr__(self): return '' % (' or '.join(self.etags)) @classmethod def parse(cls, value, strong=True): """ Parse this from a header value """ if value == '*': return AnyETag if not value: return cls([]) matches = _rx_etag.findall(value) if not matches: return cls([value]) elif strong: return cls([t for w, t in matches if not w]) else: return cls([t for w, t in matches]) def __str__(self): return ', '.join(map('"%s"'.__mod__, self.etags)) class IfRange(object): def __init__(self, etag): self.etag = etag @classmethod def parse(cls, value): """ Parse this from a header value. """ if not value: return cls(AnyETag) elif value.endswith(' GMT'): # Must be a date return IfRangeDate(parse_date(value)) else: return cls(ETagMatcher.parse(value)) def __contains__(self, resp): """ Return True if the If-Range header matches the given etag or last_modified """ return resp.etag_strong in self.etag def __nonzero__(self): return bool(self.etag) def __repr__(self): return '%s(%r)' % ( self.__class__.__name__, self.etag ) def __str__(self): return str(self.etag) if self.etag else '' __bool__ = __nonzero__ # python 3 class IfRangeDate(object): def __init__(self, date): self.date = date def __contains__(self, resp): last_modified = resp.last_modified return last_modified and (last_modified <= self.date) def __repr__(self): return '%s(%r)' % ( self.__class__.__name__, self.date ) def __str__(self): return serialize_date(self.date) WebOb-1.8.6/src/webob/exc.py0000644000076600000240000011245413611751037016523 0ustar xistencestaff00000000000000""" This module processes Python exceptions that relate to HTTP exceptions by defining a set of exceptions, all subclasses of HTTPException. Each exception, in addition to being a Python exception that can be raised and caught, is also a WSGI application and ``webob.Response`` object. This module defines exceptions according to RFC 2068 [1]_ : codes with 100-300 are not really errors; 400's are client errors, and 500's are server errors. According to the WSGI specification [2]_ , the application can call ``start_response`` more then once only under two conditions: (a) the response has not yet been sent, or (b) if the second and subsequent invocations of ``start_response`` have a valid ``exc_info`` argument obtained from ``sys.exc_info()``. The WSGI specification then requires the server or gateway to handle the case where content has been sent and then an exception was encountered. Exception HTTPException HTTPOk * 200 - :class:`HTTPOk` * 201 - :class:`HTTPCreated` * 202 - :class:`HTTPAccepted` * 203 - :class:`HTTPNonAuthoritativeInformation` * 204 - :class:`HTTPNoContent` * 205 - :class:`HTTPResetContent` * 206 - :class:`HTTPPartialContent` HTTPRedirection * 300 - :class:`HTTPMultipleChoices` * 301 - :class:`HTTPMovedPermanently` * 302 - :class:`HTTPFound` * 303 - :class:`HTTPSeeOther` * 304 - :class:`HTTPNotModified` * 305 - :class:`HTTPUseProxy` * 307 - :class:`HTTPTemporaryRedirect` * 308 - :class:`HTTPPermanentRedirect` HTTPError HTTPClientError * 400 - :class:`HTTPBadRequest` * 401 - :class:`HTTPUnauthorized` * 402 - :class:`HTTPPaymentRequired` * 403 - :class:`HTTPForbidden` * 404 - :class:`HTTPNotFound` * 405 - :class:`HTTPMethodNotAllowed` * 406 - :class:`HTTPNotAcceptable` * 407 - :class:`HTTPProxyAuthenticationRequired` * 408 - :class:`HTTPRequestTimeout` * 409 - :class:`HTTPConflict` * 410 - :class:`HTTPGone` * 411 - :class:`HTTPLengthRequired` * 412 - :class:`HTTPPreconditionFailed` * 413 - :class:`HTTPRequestEntityTooLarge` * 414 - :class:`HTTPRequestURITooLong` * 415 - :class:`HTTPUnsupportedMediaType` * 416 - :class:`HTTPRequestRangeNotSatisfiable` * 417 - :class:`HTTPExpectationFailed` * 422 - :class:`HTTPUnprocessableEntity` * 423 - :class:`HTTPLocked` * 424 - :class:`HTTPFailedDependency` * 428 - :class:`HTTPPreconditionRequired` * 429 - :class:`HTTPTooManyRequests` * 431 - :class:`HTTPRequestHeaderFieldsTooLarge` * 451 - :class:`HTTPUnavailableForLegalReasons` HTTPServerError * 500 - :class:`HTTPInternalServerError` * 501 - :class:`HTTPNotImplemented` * 502 - :class:`HTTPBadGateway` * 503 - :class:`HTTPServiceUnavailable` * 504 - :class:`HTTPGatewayTimeout` * 505 - :class:`HTTPVersionNotSupported` * 511 - :class:`HTTPNetworkAuthenticationRequired` Usage notes ----------- The HTTPException class is complicated by 4 factors: 1. The content given to the exception may either be plain-text or as html-text. 2. The template may want to have string-substitutions taken from the current ``environ`` or values from incoming headers. This is especially troublesome due to case sensitivity. 3. The final output may either be text/plain or text/html mime-type as requested by the client application. 4. Each exception has a default explanation, but those who raise exceptions may want to provide additional detail. Subclass attributes and call parameters are designed to provide an easier path through the complications. Attributes: ``code`` the HTTP status code for the exception ``title`` remainder of the status line (stuff after the code) ``explanation`` a plain-text explanation of the error message that is not subject to environment or header substitutions; it is accessible in the template via %(explanation)s ``detail`` a plain-text message customization that is not subject to environment or header substitutions; accessible in the template via %(detail)s ``body_template`` a content fragment (in HTML) used for environment and header substitution; the default template includes both the explanation and further detail provided in the message Parameters: ``detail`` a plain-text override of the default ``detail`` ``headers`` a list of (k,v) header pairs ``comment`` a plain-text additional information which is usually stripped/hidden for end-users ``body_template`` a string.Template object containing a content fragment in HTML that frames the explanation and further detail To override the template (which is HTML content) or the plain-text explanation, one must subclass the given exception; or customize it after it has been created. This particular breakdown of a message into explanation, detail and template allows both the creation of plain-text and html messages for various clients as well as error-free substitution of environment variables and headers. The subclasses of :class:`~_HTTPMove` (:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, :class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and :class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` field. Reflecting this, these subclasses have two additional keyword arguments: ``location`` and ``add_slash``. Parameters: ``location`` to set the location immediately ``add_slash`` set to True to redirect to the same URL as the request, except with a ``/`` appended Relative URLs in the location will be resolved to absolute. References: .. [1] https://www.python.org/dev/peps/pep-0333/#error-handling .. [2] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5 """ import json from string import Template import re import sys from webob.acceptparse import create_accept_header from webob.compat import ( class_types, text_, text_type, urlparse, ) from webob.request import Request from webob.response import Response from webob.util import html_escape tag_re = re.compile(r'<.*?>', re.S) br_re = re.compile(r'', re.I | re.S) comment_re = re.compile(r'') class _lazified(object): def __init__(self, func, value): self.func = func self.value = value def __str__(self): return self.func(self.value) def lazify(func): def wrapper(value): return _lazified(func, value) return wrapper def no_escape(value): if value is None: return '' if not isinstance(value, text_type): if hasattr(value, '__unicode__'): value = value.__unicode__() if isinstance(value, bytes): value = text_(value, 'utf-8') else: value = text_type(value) return value def strip_tags(value): value = value.replace('\n', ' ') value = value.replace('\r', '') value = br_re.sub('\n', value) value = comment_re.sub('', value) value = tag_re.sub('', value) return value class HTTPException(Exception): def __init__(self, message, wsgi_response): Exception.__init__(self, message) self.wsgi_response = wsgi_response def __call__(self, environ, start_response): return self.wsgi_response(environ, start_response) class WSGIHTTPException(Response, HTTPException): ## You should set in subclasses: # code = 200 # title = 'OK' # explanation = 'why this happens' # body_template_obj = Template('response template') code = 500 title = 'Internal Server Error' explanation = '' body_template_obj = Template('''\ ${explanation}

${detail} ${html_comment} ''') plain_template_obj = Template('''\ ${status} ${body}''') html_template_obj = Template('''\ ${status}

${status}

${body} ''') ## Set this to True for responses that should have no request body empty_body = False def __init__(self, detail=None, headers=None, comment=None, body_template=None, json_formatter=None, **kw): Response.__init__(self, status='%s %s' % (self.code, self.title), **kw) Exception.__init__(self, detail) if headers: self.headers.extend(headers) self.detail = detail self.comment = comment if body_template is not None: self.body_template = body_template self.body_template_obj = Template(body_template) if self.empty_body: del self.content_type del self.content_length if json_formatter is not None: self.json_formatter = json_formatter def __str__(self): return self.detail or self.explanation def _make_body(self, environ, escape): escape = lazify(escape) args = { 'explanation': escape(self.explanation), 'detail': escape(self.detail or ''), 'comment': escape(self.comment or ''), } if self.comment: args['html_comment'] = '' % escape(self.comment) else: args['html_comment'] = '' if WSGIHTTPException.body_template_obj is not self.body_template_obj: # Custom template; add headers to args for k, v in environ.items(): args[k] = escape(v) for k, v in self.headers.items(): args[k.lower()] = escape(v) t_obj = self.body_template_obj return t_obj.safe_substitute(args) def plain_body(self, environ): body = self._make_body(environ, no_escape) body = strip_tags(body) return self.plain_template_obj.substitute(status=self.status, title=self.title, body=body) def html_body(self, environ): body = self._make_body(environ, html_escape) return self.html_template_obj.substitute(status=self.status, body=body) def json_formatter(self, body, status, title, environ): return {'message': body, 'code': status, 'title': title} def json_body(self, environ): body = self._make_body(environ, no_escape) jsonbody = self.json_formatter(body=body, status=self.status, title=self.title, environ=environ) return json.dumps(jsonbody) def generate_response(self, environ, start_response): if self.content_length is not None: del self.content_length headerlist = list(self.headerlist) accept_value = environ.get('HTTP_ACCEPT', '') accept_header = create_accept_header(header_value=accept_value) acceptable_offers = accept_header.acceptable_offers( offers=['text/html', 'application/json'], ) match = acceptable_offers[0][0] if acceptable_offers else None if match == 'text/html': content_type = 'text/html' body = self.html_body(environ) elif match == 'application/json': content_type = 'application/json' body = self.json_body(environ) else: content_type = 'text/plain' body = self.plain_body(environ) resp = Response(body, status=self.status, headerlist=headerlist, content_type=content_type, ) resp.content_type = content_type return resp(environ, start_response) def __call__(self, environ, start_response): is_head = environ['REQUEST_METHOD'] == 'HEAD' if self.has_body or self.empty_body or is_head: app_iter = Response.__call__(self, environ, start_response) else: app_iter = self.generate_response(environ, start_response) if is_head: app_iter = [] return app_iter @property def wsgi_response(self): return self class HTTPError(WSGIHTTPException): """ base class for status codes in the 400's and 500's This is an exception which indicates that an error has occurred, and that any work in progress should not be committed. These are typically results in the 400's and 500's. """ class HTTPRedirection(WSGIHTTPException): """ base class for 300's status code (redirections) This is an abstract base class for 3xx redirection. It indicates that further action needs to be taken by the user agent in order to fulfill the request. It does not necessarly signal an error condition. """ class HTTPOk(WSGIHTTPException): """ Base class for the 200's status code (successful responses) code: 200, title: OK """ code = 200 title = 'OK' ############################################################ ## 2xx success ############################################################ class HTTPCreated(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that request has been fulfilled and resulted in a new resource being created. code: 201, title: Created """ code = 201 title = 'Created' class HTTPAccepted(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the request has been accepted for processing, but the processing has not been completed. code: 202, title: Accepted """ code = 202 title = 'Accepted' explanation = 'The request is accepted for processing.' class HTTPNonAuthoritativeInformation(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the returned metainformation in the entity-header is not the definitive set as available from the origin server, but is gathered from a local or a third-party copy. code: 203, title: Non-Authoritative Information """ code = 203 title = 'Non-Authoritative Information' class HTTPNoContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the server has fulfilled the request but does not need to return an entity-body, and might want to return updated metainformation. code: 204, title: No Content """ code = 204 title = 'No Content' empty_body = True class HTTPResetContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the the server has fulfilled the request and the user agent SHOULD reset the document view which caused the request to be sent. code: 205, title: Reset Content """ code = 205 title = 'Reset Content' empty_body = True class HTTPPartialContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the server has fulfilled the partial GET request for the resource. code: 206, title: Partial Content """ code = 206 title = 'Partial Content' ############################################################ ## 3xx redirection ############################################################ class _HTTPMove(HTTPRedirection): """ redirections which require a Location field Since a 'Location' header is a required attribute of 301, 302, 303, 305, 307 and 308 (but not 304), this base class provides the mechanics to make this easy. You can provide a location keyword argument to set the location immediately. You may also give ``add_slash=True`` if you want to redirect to the same URL as the request, except with a ``/`` added to the end. Relative URLs in the location will be resolved to absolute. """ explanation = 'The resource has been moved to' body_template_obj = Template('''\ ${explanation} ${location}; you should be redirected automatically. ${detail} ${html_comment}''') def __init__(self, detail=None, headers=None, comment=None, body_template=None, location=None, add_slash=False): super(_HTTPMove, self).__init__( detail=detail, headers=headers, comment=comment, body_template=body_template) if location is not None: if '\n' in location or '\r' in location: raise ValueError('Control characters are not allowed in location') self.location = location if add_slash: raise TypeError( "You can only provide one of the arguments location " "and add_slash") self.add_slash = add_slash def __call__(self, environ, start_response): req = Request(environ) if self.add_slash: url = req.path_url url += '/' if req.environ.get('QUERY_STRING'): url += '?' + req.environ['QUERY_STRING'] self.location = url self.location = urlparse.urljoin(req.path_url, self.location) return super(_HTTPMove, self).__call__( environ, start_response) class HTTPMultipleChoices(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource corresponds to any one of a set of representations, each with its own specific location, and agent-driven negotiation information is being provided so that the user can select a preferred representation and redirect its request to that location. code: 300, title: Multiple Choices """ code = 300 title = 'Multiple Choices' class HTTPMovedPermanently(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource has been assigned a new permanent URI and any future references to this resource SHOULD use one of the returned URIs. code: 301, title: Moved Permanently """ code = 301 title = 'Moved Permanently' class HTTPFound(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource resides temporarily under a different URI. code: 302, title: Found """ code = 302 title = 'Found' explanation = 'The resource was found at' # This one is safe after a POST (the redirected location will be # retrieved with GET): class HTTPSeeOther(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the response to the request can be found under a different URI and SHOULD be retrieved using a GET method on that resource. code: 303, title: See Other """ code = 303 title = 'See Other' class HTTPNotModified(HTTPRedirection): """ subclass of :class:`~HTTPRedirection` This indicates that if the client has performed a conditional GET request and access is allowed, but the document has not been modified, the server SHOULD respond with this status code. code: 304, title: Not Modified """ # TODO: this should include a date or etag header code = 304 title = 'Not Modified' empty_body = True class HTTPUseProxy(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource MUST be accessed through the proxy given by the Location field. code: 305, title: Use Proxy """ # Not a move, but looks a little like one code = 305 title = 'Use Proxy' explanation = ( 'The resource must be accessed through a proxy located at') class HTTPTemporaryRedirect(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource resides temporarily under a different URI. code: 307, title: Temporary Redirect """ code = 307 title = 'Temporary Redirect' class HTTPPermanentRedirect(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource resides permanently under a different URI. code: 308, title: Permanent Redirect """ code = 308 title = 'Permanent Redirect' ############################################################ ## 4xx client error ############################################################ class HTTPClientError(HTTPError): """ base class for the 400's, where the client is in error This is an error condition in which the client is presumed to be in-error. This is an expected problem, and thus is not considered a bug. A server-side traceback is not warranted. Unless specialized, this is a '400 Bad Request' code: 400, title: Bad Request """ code = 400 title = 'Bad Request' explanation = ('The server could not comply with the request since\r\n' 'it is either malformed or otherwise incorrect.\r\n') class HTTPBadRequest(HTTPClientError): pass class HTTPUnauthorized(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the request requires user authentication. code: 401, title: Unauthorized """ code = 401 title = 'Unauthorized' explanation = ( 'This server could not verify that you are authorized to\r\n' 'access the document you requested. Either you supplied the\r\n' 'wrong credentials (e.g., bad password), or your browser\r\n' 'does not understand how to supply the credentials required.\r\n') class HTTPPaymentRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` code: 402, title: Payment Required """ code = 402 title = 'Payment Required' explanation = ('Access was denied for financial reasons.') class HTTPForbidden(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server understood the request, but is refusing to fulfill it. code: 403, title: Forbidden """ code = 403 title = 'Forbidden' explanation = ('Access was denied to this resource.') class HTTPNotFound(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server did not find anything matching the Request-URI. code: 404, title: Not Found """ code = 404 title = 'Not Found' explanation = ('The resource could not be found.') class HTTPMethodNotAllowed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the method specified in the Request-Line is not allowed for the resource identified by the Request-URI. code: 405, title: Method Not Allowed """ code = 405 title = 'Method Not Allowed' # override template since we need an environment variable body_template_obj = Template('''\ The method ${REQUEST_METHOD} is not allowed for this resource.

${detail}''') class HTTPNotAcceptable(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates the resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request. code: 406, title: Not Acceptable """ code = 406 title = 'Not Acceptable' # override template since we need an environment variable body_template_obj = Template('''\ The resource could not be generated that was acceptable to your browser (content of type ${HTTP_ACCEPT}.

${detail}''') class HTTPProxyAuthenticationRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This is similar to 401, but indicates that the client must first authenticate itself with the proxy. code: 407, title: Proxy Authentication Required """ code = 407 title = 'Proxy Authentication Required' explanation = ('Authentication with a local proxy is needed.') class HTTPRequestTimeout(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the client did not produce a request within the time that the server was prepared to wait. code: 408, title: Request Timeout """ code = 408 title = 'Request Timeout' explanation = ('The server has waited too long for the request to ' 'be sent by the client.') class HTTPConflict(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the request could not be completed due to a conflict with the current state of the resource. code: 409, title: Conflict """ code = 409 title = 'Conflict' explanation = ('There was a conflict when trying to complete ' 'your request.') class HTTPGone(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the requested resource is no longer available at the server and no forwarding address is known. code: 410, title: Gone """ code = 410 title = 'Gone' explanation = ('This resource is no longer available. No forwarding ' 'address is given.') class HTTPLengthRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the the server refuses to accept the request without a defined Content-Length. code: 411, title: Length Required """ code = 411 title = 'Length Required' explanation = ('Content-Length header required.') class HTTPPreconditionFailed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the precondition given in one or more of the request-header fields evaluated to false when it was tested on the server. code: 412, title: Precondition Failed """ code = 412 title = 'Precondition Failed' explanation = ('Request precondition failed.') class HTTPRequestEntityTooLarge(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to process a request because the request entity is larger than the server is willing or able to process. code: 413, title: Request Entity Too Large """ code = 413 title = 'Request Entity Too Large' explanation = ('The body of your request was too large for this server.') class HTTPRequestURITooLong(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to service the request because the Request-URI is longer than the server is willing to interpret. code: 414, title: Request-URI Too Long """ code = 414 title = 'Request-URI Too Long' explanation = ('The request URI was too long for this server.') class HTTPUnsupportedMediaType(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method. code: 415, title: Unsupported Media Type """ code = 415 title = 'Unsupported Media Type' # override template since we need an environment variable body_template_obj = Template('''\ The request media type ${CONTENT_TYPE} is not supported by this server.

${detail}''') class HTTPRequestRangeNotSatisfiable(HTTPClientError): """ subclass of :class:`~HTTPClientError` The server SHOULD return a response with this status code if a request included a Range request-header field, and none of the range-specifier values in this field overlap the current extent of the selected resource, and the request did not include an If-Range request-header field. code: 416, title: Request Range Not Satisfiable """ code = 416 title = 'Request Range Not Satisfiable' explanation = ('The Range requested is not available.') class HTTPExpectationFailed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indidcates that the expectation given in an Expect request-header field could not be met by this server. code: 417, title: Expectation Failed """ code = 417 title = 'Expectation Failed' explanation = ('Expectation failed.') class HTTPUnprocessableEntity(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unable to process the contained instructions. code: 422, title: Unprocessable Entity """ ## Note: from WebDAV code = 422 title = 'Unprocessable Entity' explanation = 'Unable to process the contained instructions' class HTTPLocked(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the resource is locked. code: 423, title: Locked """ ## Note: from WebDAV code = 423 title = 'Locked' explanation = ('The resource is locked') class HTTPFailedDependency(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the method could not be performed because the requested action depended on another action and that action failed. code: 424, title: Failed Dependency """ ## Note: from WebDAV code = 424 title = 'Failed Dependency' explanation = ( 'The method could not be performed because the requested ' 'action dependended on another action and that action failed') class HTTPPreconditionRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the origin server requires the request to be conditional. From RFC 6585, "Additional HTTP Status Codes". code: 428, title: Precondition Required """ code = 428 title = 'Precondition Required' explanation = ('This request is required to be conditional') class HTTPTooManyRequests(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the client has sent too many requests in a given amount of time. Useful for rate limiting. From RFC 6585, "Additional HTTP Status Codes". code: 429, title: Too Many Requests """ code = 429 title = 'Too Many Requests' explanation = ( 'The client has sent too many requests in a given amount of time') class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unwilling to process the request because its header fields are too large. The request may be resubmitted after reducing the size of the request header fields. From RFC 6585, "Additional HTTP Status Codes". code: 431, title: Request Header Fields Too Large """ code = 431 title = 'Request Header Fields Too Large' explanation = ( 'The request header fields were too large') class HTTPUnavailableForLegalReasons(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unable to process the request because of legal reasons, e.g. censorship or government-mandated blocked access. From the draft "A New HTTP Status Code for Legally-restricted Resources" by Tim Bray: https://tools.ietf.org/html/draft-tbray-http-legally-restricted-status-00 code: 451, title: Unavailable For Legal Reasons """ code = 451 title = 'Unavailable For Legal Reasons' explanation = ('The resource is not available due to legal reasons.') ############################################################ ## 5xx Server Error ############################################################ # Response status codes beginning with the digit "5" indicate cases in # which the server is aware that it has erred or is incapable of # performing the request. Except when responding to a HEAD request, the # server SHOULD include an entity containing an explanation of the error # situation, and whether it is a temporary or permanent condition. User # agents SHOULD display any included entity to the user. These response # codes are applicable to any request method. class HTTPServerError(HTTPError): """ base class for the 500's, where the server is in-error This is an error condition in which the server is presumed to be in-error. This is usually unexpected, and thus requires a traceback; ideally, opening a support ticket for the customer. Unless specialized, this is a '500 Internal Server Error' """ code = 500 title = 'Internal Server Error' explanation = ( 'The server has either erred or is incapable of performing\r\n' 'the requested operation.\r\n') class HTTPInternalServerError(HTTPServerError): pass class HTTPNotImplemented(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not support the functionality required to fulfill the request. code: 501, title: Not Implemented """ code = 501 title = 'Not Implemented' body_template_obj = Template(''' The request method ${REQUEST_METHOD} is not implemented for this server.

${detail}''') class HTTPBadGateway(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server it accessed in attempting to fulfill the request. code: 502, title: Bad Gateway """ code = 502 title = 'Bad Gateway' explanation = ('Bad gateway.') class HTTPServiceUnavailable(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server is currently unable to handle the request due to a temporary overloading or maintenance of the server. code: 503, title: Service Unavailable """ code = 503 title = 'Service Unavailable' explanation = ('The server is currently unavailable. ' 'Please try again at a later time.') class HTTPGatewayTimeout(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server specified by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server (e.g. DNS) it needed to access in attempting to complete the request. code: 504, title: Gateway Timeout """ code = 504 title = 'Gateway Timeout' explanation = ('The gateway has timed out.') class HTTPVersionNotSupported(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not support, or refuses to support, the HTTP protocol version that was used in the request message. code: 505, title: HTTP Version Not Supported """ code = 505 title = 'HTTP Version Not Supported' explanation = ('The HTTP version is not supported.') class HTTPInsufficientStorage(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not have enough space to save the resource. code: 507, title: Insufficient Storage """ code = 507 title = 'Insufficient Storage' explanation = ('There was not enough space to save the resource') class HTTPNetworkAuthenticationRequired(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the client needs to authenticate to gain network access. From RFC 6585, "Additional HTTP Status Codes". code: 511, title: Network Authentication Required """ code = 511 title = 'Network Authentication Required' explanation = ('Network authentication is required') class HTTPExceptionMiddleware(object): """ Middleware that catches exceptions in the sub-application. This does not catch exceptions in the app_iter; only during the initial calling of the application. This should be put *very close* to applications that might raise these exceptions. This should not be applied globally; letting *expected* exceptions raise through the WSGI stack is dangerous. """ def __init__(self, application): self.application = application def __call__(self, environ, start_response): try: return self.application(environ, start_response) except HTTPException: parent_exc_info = sys.exc_info() def repl_start_response(status, headers, exc_info=None): if exc_info is None: exc_info = parent_exc_info return start_response(status, headers, exc_info) return parent_exc_info[1](environ, repl_start_response) try: from paste import httpexceptions except ImportError: # pragma: no cover # Without Paste we don't need to do this fixup pass else: # pragma: no cover for name in dir(httpexceptions): obj = globals().get(name) if (obj and isinstance(obj, type) and issubclass(obj, HTTPException) and obj is not HTTPException and obj is not WSGIHTTPException): obj.__bases__ = obj.__bases__ + (getattr(httpexceptions, name),) del name, obj, httpexceptions __all__ = ['HTTPExceptionMiddleware', 'status_map'] status_map={} for name, value in list(globals().items()): if (isinstance(value, (type, class_types)) and issubclass(value, HTTPException) and not name.startswith('_')): __all__.append(name) if all(( getattr(value, 'code', None), value not in (HTTPRedirection, HTTPClientError, HTTPServerError), issubclass( value, (HTTPOk, HTTPRedirection, HTTPClientError, HTTPServerError) ) )): status_map[value.code]=value if hasattr(value, 'explanation'): value.explanation = ' '.join(value.explanation.strip().split()) del name, value WebOb-1.8.6/src/webob/headers.py0000644000076600000240000000776613611751037017370 0ustar xistencestaff00000000000000from webob.compat import ( MutableMapping, iteritems_, string_types, ) from webob.multidict import MultiDict __all__ = ['ResponseHeaders', 'EnvironHeaders'] class ResponseHeaders(MultiDict): """ Dictionary view on the response headerlist. Keys are normalized for case and whitespace. """ def __getitem__(self, key): key = key.lower() for k, v in reversed(self._items): if k.lower() == key: return v raise KeyError(key) def getall(self, key): key = key.lower() return [v for (k, v) in self._items if k.lower() == key] def mixed(self): r = self.dict_of_lists() for key, val in iteritems_(r): if len(val) == 1: r[key] = val[0] return r def dict_of_lists(self): r = {} for key, val in iteritems_(self): r.setdefault(key.lower(), []).append(val) return r def __setitem__(self, key, value): norm_key = key.lower() self._items[:] = [(k, v) for (k, v) in self._items if k.lower() != norm_key] self._items.append((key, value)) def __delitem__(self, key): key = key.lower() items = self._items found = False for i in range(len(items)-1, -1, -1): if items[i][0].lower() == key: del items[i] found = True if not found: raise KeyError(key) def __contains__(self, key): key = key.lower() for k, v in self._items: if k.lower() == key: return True return False has_key = __contains__ def setdefault(self, key, default=None): c_key = key.lower() for k, v in self._items: if k.lower() == c_key: return v self._items.append((key, default)) return default def pop(self, key, *args): if len(args) > 1: raise TypeError("pop expected at most 2 arguments, got %s" % repr(1 + len(args))) key = key.lower() for i in range(len(self._items)): if self._items[i][0].lower() == key: v = self._items[i][1] del self._items[i] return v if args: return args[0] else: raise KeyError(key) key2header = { 'CONTENT_TYPE': 'Content-Type', 'CONTENT_LENGTH': 'Content-Length', 'HTTP_CONTENT_TYPE': 'Content_Type', 'HTTP_CONTENT_LENGTH': 'Content_Length', } header2key = dict([(v.upper(),k) for (k,v) in key2header.items()]) def _trans_key(key): if not isinstance(key, string_types): return None elif key in key2header: return key2header[key] elif key.startswith('HTTP_'): return key[5:].replace('_', '-').title() else: return None def _trans_name(name): name = name.upper() if name in header2key: return header2key[name] return 'HTTP_'+name.replace('-', '_') class EnvironHeaders(MutableMapping): """An object that represents the headers as present in a WSGI environment. This object is a wrapper (with no internal state) for a WSGI request object, representing the CGI-style HTTP_* keys as a dictionary. Because a CGI environment can only hold one value for each key, this dictionary is single-valued (unlike outgoing headers). """ def __init__(self, environ): self.environ = environ def __getitem__(self, hname): return self.environ[_trans_name(hname)] def __setitem__(self, hname, value): self.environ[_trans_name(hname)] = value def __delitem__(self, hname): del self.environ[_trans_name(hname)] def keys(self): return filter(None, map(_trans_key, self.environ)) def __contains__(self, hname): return _trans_name(hname) in self.environ def __len__(self): return len(list(self.keys())) def __iter__(self): for k in self.keys(): yield k WebOb-1.8.6/src/webob/multidict.py0000644000076600000240000003317113611751037017740 0ustar xistencestaff00000000000000# (c) 2005 Ian Bicking and contributors; written for Paste # (http://pythonpaste.org) Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php """ Gives a multi-value dictionary object (MultiDict) plus several wrappers """ import binascii import warnings from webob.compat import ( MutableMapping, PY2, iteritems_, itervalues_, url_encode, ) __all__ = ['MultiDict', 'NestedMultiDict', 'NoVars', 'GetDict'] class MultiDict(MutableMapping): """ An ordered dictionary that can have multiple values for each key. Adds the methods getall, getone, mixed and extend and add to the normal dictionary interface. """ def __init__(self, *args, **kw): if len(args) > 1: raise TypeError("MultiDict can only be called with one positional " "argument") if args: if hasattr(args[0], 'iteritems'): items = list(args[0].iteritems()) elif hasattr(args[0], 'items'): items = list(args[0].items()) else: items = list(args[0]) self._items = items else: self._items = [] if kw: self._items.extend(kw.items()) @classmethod def view_list(cls, lst): """ Create a dict that is a view on the given list """ if not isinstance(lst, list): raise TypeError( "%s.view_list(obj) takes only actual list objects, not %r" % (cls.__name__, lst)) obj = cls() obj._items = lst return obj @classmethod def from_fieldstorage(cls, fs): """ Create a dict from a cgi.FieldStorage instance """ obj = cls() # fs.list can be None when there's nothing to parse for field in fs.list or (): charset = field.type_options.get('charset', 'utf8') transfer_encoding = field.headers.get('Content-Transfer-Encoding', None) supported_transfer_encoding = { 'base64' : binascii.a2b_base64, 'quoted-printable' : binascii.a2b_qp } if not PY2: if charset == 'utf8': decode = lambda b: b else: decode = lambda b: b.encode('utf8').decode(charset) else: decode = lambda b: b.decode(charset) if field.filename: field.filename = decode(field.filename) obj.add(field.name, field) else: value = field.value if transfer_encoding in supported_transfer_encoding: if not PY2: # binascii accepts bytes value = value.encode('utf8') value = supported_transfer_encoding[transfer_encoding](value) if not PY2: # binascii returns bytes value = value.decode('utf8') obj.add(field.name, decode(value)) return obj def __getitem__(self, key): for k, v in reversed(self._items): if k == key: return v raise KeyError(key) def __setitem__(self, key, value): try: del self[key] except KeyError: pass self._items.append((key, value)) def add(self, key, value): """ Add the key and value, not overwriting any previous value. """ self._items.append((key, value)) def getall(self, key): """ Return a list of all values matching the key (may be an empty list) """ return [v for k, v in self._items if k == key] def getone(self, key): """ Get one value matching the key, raising a KeyError if multiple values were found. """ v = self.getall(key) if not v: raise KeyError('Key not found: %r' % key) if len(v) > 1: raise KeyError('Multiple values match %r: %r' % (key, v)) return v[0] def mixed(self): """ Returns a dictionary where the values are either single values, or a list of values when a key/value appears more than once in this dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ result = {} multi = {} for key, value in self.items(): if key in result: # We do this to not clobber any lists that are # *actual* values in this dictionary: if key in multi: result[key].append(value) else: result[key] = [result[key], value] multi[key] = None else: result[key] = value return result def dict_of_lists(self): """ Returns a dictionary where each key is associated with a list of values. """ r = {} for key, val in self.items(): r.setdefault(key, []).append(val) return r def __delitem__(self, key): items = self._items found = False for i in range(len(items)-1, -1, -1): if items[i][0] == key: del items[i] found = True if not found: raise KeyError(key) def __contains__(self, key): for k, v in self._items: if k == key: return True return False has_key = __contains__ def clear(self): del self._items[:] def copy(self): return self.__class__(self) def setdefault(self, key, default=None): for k, v in self._items: if key == k: return v self._items.append((key, default)) return default def pop(self, key, *args): if len(args) > 1: raise TypeError("pop expected at most 2 arguments, got %s" % repr(1 + len(args))) for i in range(len(self._items)): if self._items[i][0] == key: v = self._items[i][1] del self._items[i] return v if args: return args[0] else: raise KeyError(key) def popitem(self): return self._items.pop() def update(self, *args, **kw): if args: lst = args[0] if len(lst) != len(dict(lst)): # this does not catch the cases where we overwrite existing # keys, but those would produce too many warning msg = ("Behavior of MultiDict.update() has changed " "and overwrites duplicate keys. Consider using .extend()" ) warnings.warn(msg, UserWarning, stacklevel=2) MutableMapping.update(self, *args, **kw) def extend(self, other=None, **kwargs): if other is None: pass elif hasattr(other, 'items'): self._items.extend(other.items()) elif hasattr(other, 'keys'): for k in other.keys(): self._items.append((k, other[k])) else: for k, v in other: self._items.append((k, v)) if kwargs: self.update(kwargs) def __repr__(self): items = map('(%r, %r)'.__mod__, _hide_passwd(self.items())) return '%s([%s])' % (self.__class__.__name__, ', '.join(items)) def __len__(self): return len(self._items) ## ## All the iteration: ## def iterkeys(self): for k, v in self._items: yield k if PY2: def keys(self): return [k for k, v in self._items] else: keys = iterkeys __iter__ = iterkeys def iteritems(self): return iter(self._items) if PY2: def items(self): return self._items[:] else: items = iteritems def itervalues(self): for k, v in self._items: yield v if PY2: def values(self): return [v for k, v in self._items] else: values = itervalues _dummy = object() class GetDict(MultiDict): # def __init__(self, data, tracker, encoding, errors): # d = lambda b: b.decode(encoding, errors) # data = [(d(k), d(v)) for k,v in data] def __init__(self, data, env): self.env = env MultiDict.__init__(self, data) def on_change(self): e = lambda t: t.encode('utf8') data = [(e(k), e(v)) for k,v in self.items()] qs = url_encode(data) self.env['QUERY_STRING'] = qs self.env['webob._parsed_query_vars'] = (self, qs) def __setitem__(self, key, value): MultiDict.__setitem__(self, key, value) self.on_change() def add(self, key, value): MultiDict.add(self, key, value) self.on_change() def __delitem__(self, key): MultiDict.__delitem__(self, key) self.on_change() def clear(self): MultiDict.clear(self) self.on_change() def setdefault(self, key, default=None): result = MultiDict.setdefault(self, key, default) self.on_change() return result def pop(self, key, *args): result = MultiDict.pop(self, key, *args) self.on_change() return result def popitem(self): result = MultiDict.popitem(self) self.on_change() return result def update(self, *args, **kwargs): MultiDict.update(self, *args, **kwargs) self.on_change() def extend(self, *args, **kwargs): MultiDict.extend(self, *args, **kwargs) self.on_change() def __repr__(self): items = map('(%r, %r)'.__mod__, _hide_passwd(self.items())) # TODO: GET -> GetDict return 'GET([%s])' % (', '.join(items)) def copy(self): # Copies shouldn't be tracked return MultiDict(self) class NestedMultiDict(MultiDict): """ Wraps several MultiDict objects, treating it as one large MultiDict """ def __init__(self, *dicts): self.dicts = dicts def __getitem__(self, key): for d in self.dicts: value = d.get(key, _dummy) if value is not _dummy: return value raise KeyError(key) def _readonly(self, *args, **kw): raise KeyError("NestedMultiDict objects are read-only") __setitem__ = _readonly add = _readonly __delitem__ = _readonly clear = _readonly setdefault = _readonly pop = _readonly popitem = _readonly update = _readonly def getall(self, key): result = [] for d in self.dicts: result.extend(d.getall(key)) return result # Inherited: # getone # mixed # dict_of_lists def copy(self): return MultiDict(self) def __contains__(self, key): for d in self.dicts: if key in d: return True return False has_key = __contains__ def __len__(self): v = 0 for d in self.dicts: v += len(d) return v def __nonzero__(self): for d in self.dicts: if d: return True return False def iteritems(self): for d in self.dicts: for item in iteritems_(d): yield item if PY2: def items(self): return list(self.iteritems()) else: items = iteritems def itervalues(self): for d in self.dicts: for value in itervalues_(d): yield value if PY2: def values(self): return list(self.itervalues()) else: values = itervalues def __iter__(self): for d in self.dicts: for key in d: yield key iterkeys = __iter__ if PY2: def keys(self): return list(self.iterkeys()) else: keys = iterkeys class NoVars(object): """ Represents no variables; used when no variables are applicable. This is read-only """ def __init__(self, reason=None): self.reason = reason or 'N/A' def __getitem__(self, key): raise KeyError("No key %r: %s" % (key, self.reason)) def __setitem__(self, *args, **kw): raise KeyError("Cannot add variables: %s" % self.reason) add = __setitem__ setdefault = __setitem__ update = __setitem__ def __delitem__(self, *args, **kw): raise KeyError("No keys to delete: %s" % self.reason) clear = __delitem__ pop = __delitem__ popitem = __delitem__ def get(self, key, default=None): return default def getall(self, key): return [] def getone(self, key): return self[key] def mixed(self): return {} dict_of_lists = mixed def __contains__(self, key): return False has_key = __contains__ def copy(self): return self def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.reason) def __len__(self): return 0 def iterkeys(self): return iter([]) if PY2: def __cmp__(self, other): return cmp({}, other) def keys(self): return [] items = keys values = keys itervalues = iterkeys iteritems = iterkeys else: keys = iterkeys items = iterkeys values = iterkeys __iter__ = iterkeys def _hide_passwd(items): for k, v in items: if ('password' in k or 'passwd' in k or 'pwd' in k ): yield k, '******' else: yield k, v WebOb-1.8.6/src/webob/request.py0000644000076600000240000016312213611751723017434 0ustar xistencestaff00000000000000import binascii import io import os import re import sys import tempfile import mimetypes try: import simplejson as json except ImportError: import json import warnings from webob.acceptparse import ( accept_charset_property, accept_encoding_property, accept_language_property, accept_property, ) from webob.cachecontrol import ( CacheControl, serialize_cache_control, ) from webob.compat import ( PY2, bytes_, native_, parse_qsl_text, reraise, text_type, url_encode, url_quote, url_unquote, quote_plus, urlparse, cgi_FieldStorage ) from webob.cookies import RequestCookies from webob.descriptors import ( CHARSET_RE, SCHEME_RE, converter, converter_date, environ_getter, environ_decoder, parse_auth, parse_int, parse_int_safe, parse_range, serialize_auth, serialize_if_range, serialize_int, serialize_range, upath_property, ) from webob.etag import ( IfRange, AnyETag, NoETag, etag_property, ) from webob.headers import EnvironHeaders from webob.multidict import ( NestedMultiDict, MultiDict, NoVars, GetDict, ) __all__ = ['BaseRequest', 'Request', 'LegacyRequest'] class _NoDefault: def __repr__(self): return '(No Default)' NoDefault = _NoDefault() PATH_SAFE = "/~!$&'()*+,;=:@" _LATIN_ENCODINGS = ( 'ascii', 'latin-1', 'latin', 'latin_1', 'l1', 'latin1', 'iso-8859-1', 'iso8859_1', 'iso_8859_1', 'iso8859', '8859', ) class BaseRequest(object): # The limit after which request bodies should be stored on disk # if they are read in (under this, and the request body is stored # in memory): request_body_tempfile_limit = 10 * 1024 _charset = None def __init__(self, environ, charset=None, unicode_errors=None, decode_param_names=None, **kw): if type(environ) is not dict: raise TypeError( "WSGI environ must be a dict; you passed %r" % (environ,)) if unicode_errors is not None: warnings.warn( "You unicode_errors=%r to the Request constructor. Passing a " "``unicode_errors`` value to the Request is no longer " "supported in WebOb 1.2+. This value has been ignored " % ( unicode_errors,), DeprecationWarning ) if decode_param_names is not None: warnings.warn( "You passed decode_param_names=%r to the Request constructor. " "Passing a ``decode_param_names`` value to the Request " "is no longer supported in WebOb 1.2+. This value has " "been ignored " % (decode_param_names,), DeprecationWarning ) if not _is_utf8(charset): raise DeprecationWarning( "You passed charset=%r to the Request constructor. As of " "WebOb 1.2, if your application needs a non-UTF-8 request " "charset, please construct the request without a charset or " "with a charset of 'None', then use ``req = " "req.decode(charset)``" % charset ) d = self.__dict__ d['environ'] = environ if kw: cls = self.__class__ if 'method' in kw: # set method first, because .body setters # depend on it for checks self.method = kw.pop('method') for name, value in kw.items(): if not hasattr(cls, name): raise TypeError( "Unexpected keyword: %s=%r" % (name, value)) setattr(self, name, value) def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) if val is NoDefault: raise KeyError(key) if val is default: return default if not encattr: return val encoding = getattr(self, encattr) if PY2: return val.decode(encoding) if encoding in _LATIN_ENCODINGS: # shortcut return val return bytes_(val, 'latin-1').decode(encoding) def encset(self, key, val, encattr=None): if encattr: encoding = getattr(self, encattr) else: encoding = 'ascii' if PY2: # pragma: no cover self.environ[key] = bytes_(val, encoding) else: self.environ[key] = bytes_(val, encoding).decode('latin-1') @property def charset(self): if self._charset is None: charset = detect_charset(self._content_type_raw) if _is_utf8(charset): charset = 'UTF-8' self._charset = charset return self._charset @charset.setter def charset(self, charset): if _is_utf8(charset): charset = 'UTF-8' if charset != self.charset: raise DeprecationWarning("Use req = req.decode(%r)" % charset) def decode(self, charset=None, errors='strict'): charset = charset or self.charset if charset == 'UTF-8': return self # cookies and path are always utf-8 t = Transcoder(charset, errors) new_content_type = CHARSET_RE.sub('; charset="UTF-8"', self._content_type_raw) content_type = self.content_type r = self.__class__( self.environ.copy(), query_string=t.transcode_query(self.query_string), content_type=new_content_type, ) if content_type == 'application/x-www-form-urlencoded': r.body = bytes_(t.transcode_query(native_(self.body))) return r elif content_type != 'multipart/form-data': return r fs_environ = self.environ.copy() fs_environ.setdefault('CONTENT_LENGTH', '0') fs_environ['QUERY_STRING'] = '' if PY2: fs = cgi_FieldStorage(fp=self.body_file, environ=fs_environ, keep_blank_values=True) else: fs = cgi_FieldStorage(fp=self.body_file, environ=fs_environ, keep_blank_values=True, encoding=charset, errors=errors) fout = t.transcode_fs(fs, r._content_type_raw) # this order is important, because setting body_file # resets content_length r.body_file = fout r.content_length = fout.tell() fout.seek(0) return r # this is necessary for correct warnings depth for both # BaseRequest and Request (due to AdhocAttrMixin.__setattr__) _setattr_stacklevel = 2 @property def body_file(self): """ Input stream of the request (wsgi.input). Setting this property resets the content_length and seekable flag (unlike setting req.body_file_raw). """ if not self.is_body_readable: return io.BytesIO() r = self.body_file_raw clen = self.content_length if not self.is_body_seekable and clen is not None: # we need to wrap input in LimitedLengthFile # but we have to cache the instance as well # otherwise this would stop working # (.remaining counter would reset between calls): # req.body_file.read(100) # req.body_file.read(100) env = self.environ wrapped, raw = env.get('webob._body_file', (0, 0)) if raw is not r: wrapped = LimitedLengthFile(r, clen) wrapped = io.BufferedReader(wrapped) env['webob._body_file'] = wrapped, r r = wrapped return r @body_file.setter def body_file(self, value): if isinstance(value, bytes): raise ValueError('Excepted fileobj but received bytes.') self.content_length = None self.body_file_raw = value self.is_body_seekable = False self.is_body_readable = True @body_file.deleter def body_file(self): self.body = b'' body_file_raw = environ_getter('wsgi.input') @property def body_file_seekable(self): """ Get the body of the request (wsgi.input) as a seekable file-like object. Middleware and routing applications should use this attribute over .body_file. If you access this value, CONTENT_LENGTH will also be updated. """ if not self.is_body_seekable: self.make_body_seekable() return self.body_file_raw url_encoding = environ_getter('webob.url_encoding', 'UTF-8') scheme = environ_getter('wsgi.url_scheme') method = environ_getter('REQUEST_METHOD', 'GET') http_version = environ_getter('SERVER_PROTOCOL') content_length = converter( environ_getter('CONTENT_LENGTH', None, '14.13'), parse_int_safe, serialize_int, 'int') remote_user = environ_getter('REMOTE_USER', None) remote_host = environ_getter('REMOTE_HOST', None) remote_addr = environ_getter('REMOTE_ADDR', None) query_string = environ_getter('QUERY_STRING', '') server_name = environ_getter('SERVER_NAME') server_port = converter( environ_getter('SERVER_PORT'), parse_int, serialize_int, 'int') script_name = environ_decoder('SCRIPT_NAME', '', encattr='url_encoding') path_info = environ_decoder('PATH_INFO', encattr='url_encoding') # bw compat uscript_name = script_name upath_info = path_info _content_type_raw = environ_getter('CONTENT_TYPE', '') def _content_type__get(self): """Return the content type, but leaving off any parameters (like charset, but also things like the type in ``application/atom+xml; type=entry``) If you set this property, you can include parameters, or if you don't include any parameters in the value then existing parameters will be preserved. """ return self._content_type_raw.split(';', 1)[0] def _content_type__set(self, value=None): if value is not None: value = str(value) if ';' not in value: content_type = self._content_type_raw if ';' in content_type: value += ';' + content_type.split(';', 1)[1] self._content_type_raw = value content_type = property(_content_type__get, _content_type__set, _content_type__set, _content_type__get.__doc__) _headers = None def _headers__get(self): """ All the request headers as a case-insensitive dictionary-like object. """ if self._headers is None: self._headers = EnvironHeaders(self.environ) return self._headers def _headers__set(self, value): self.headers.clear() self.headers.update(value) headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) @property def client_addr(self): """ The effective client IP address as a string. If the ``HTTP_X_FORWARDED_FOR`` header exists in the WSGI environ, this attribute returns the client IP address present in that header (e.g. if the header value is ``192.168.1.1, 192.168.1.2``, the value will be ``192.168.1.1``). If no ``HTTP_X_FORWARDED_FOR`` header is present in the environ at all, this attribute will return the value of the ``REMOTE_ADDR`` header. If the ``REMOTE_ADDR`` header is unset, this attribute will return the value ``None``. .. warning:: It is possible for user agents to put someone else's IP or just any string in ``HTTP_X_FORWARDED_FOR`` as it is a normal HTTP header. Forward proxies can also provide incorrect values (private IP addresses etc). You cannot "blindly" trust the result of this method to provide you with valid data unless you're certain that ``HTTP_X_FORWARDED_FOR`` has the correct values. The WSGI server must be behind a trusted proxy for this to be true. """ e = self.environ xff = e.get('HTTP_X_FORWARDED_FOR') if xff is not None: addr = xff.split(',')[0].strip() else: addr = e.get('REMOTE_ADDR') return addr @property def host_port(self): """ The effective server port number as a string. If the ``HTTP_HOST`` header exists in the WSGI environ, this attribute returns the port number present in that header. If the ``HTTP_HOST`` header exists but contains no explicit port number: if the WSGI url scheme is "https" , this attribute returns "443", if the WSGI url scheme is "http", this attribute returns "80" . If no ``HTTP_HOST`` header is present in the environ at all, this attribute will return the value of the ``SERVER_PORT`` header (which is guaranteed to be present). """ e = self.environ host = e.get('HTTP_HOST') if host is not None: if ':' in host and host[-1] != ']': host, port = host.rsplit(':', 1) else: url_scheme = e['wsgi.url_scheme'] if url_scheme == 'https': port = '443' else: port = '80' else: port = e['SERVER_PORT'] return port @property def host_url(self): """ The URL through the host (no path) """ e = self.environ scheme = e.get('wsgi.url_scheme') url = scheme + '://' host = e.get('HTTP_HOST') if host is not None: if ':' in host and host[-1] != ']': host, port = host.rsplit(':', 1) else: port = None else: host = e.get('SERVER_NAME') port = e.get('SERVER_PORT') if scheme == 'https': if port == '443': port = None elif scheme == 'http': if port == '80': port = None url += host if port: url += ':%s' % port return url @property def application_url(self): """ The URL including SCRIPT_NAME (no PATH_INFO or query string) """ bscript_name = bytes_(self.script_name, self.url_encoding) return self.host_url + url_quote(bscript_name, PATH_SAFE) @property def path_url(self): """ The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING """ bpath_info = bytes_(self.path_info, self.url_encoding) return self.application_url + url_quote(bpath_info, PATH_SAFE) @property def path(self): """ The path of the request, without host or query string """ bscript = bytes_(self.script_name, self.url_encoding) bpath = bytes_(self.path_info, self.url_encoding) return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE) @property def path_qs(self): """ The path of the request, without host but with query string """ path = self.path qs = self.environ.get('QUERY_STRING') if qs: path += '?' + qs return path @property def url(self): """ The full request URL, including QUERY_STRING """ url = self.path_url qs = self.environ.get('QUERY_STRING') if qs: url += '?' + qs return url def relative_url(self, other_url, to_application=False): """ Resolve other_url relative to the request URL. If ``to_application`` is True, then resolve it relative to the URL with only SCRIPT_NAME """ if to_application: url = self.application_url if not url.endswith('/'): url += '/' else: url = self.path_url return urlparse.urljoin(url, other_url) def path_info_pop(self, pattern=None): """ 'Pops' off the next segment of PATH_INFO, pushing it onto SCRIPT_NAME, and returning the popped segment. Returns None if there is nothing left on PATH_INFO. Does not return ``''`` when there's an empty segment (like ``/path//path``); these segments are just ignored. Optional ``pattern`` argument is a regexp to match the return value before returning. If there is no match, no changes are made to the request and None is returned. """ path = self.path_info if not path: return None slashes = '' while path.startswith('/'): slashes += '/' path = path[1:] idx = path.find('/') if idx == -1: idx = len(path) r = path[:idx] if pattern is None or re.match(pattern, r): self.script_name += slashes + r self.path_info = path[idx:] return r def path_info_peek(self): """ Returns the next segment on PATH_INFO, or None if there is no next segment. Doesn't modify the environment. """ path = self.path_info if not path: return None path = path.lstrip('/') return path.split('/', 1)[0] def _urlvars__get(self): """ Return any *named* variables matched in the URL. Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ if 'paste.urlvars' in self.environ: return self.environ['paste.urlvars'] elif 'wsgiorg.routing_args' in self.environ: return self.environ['wsgiorg.routing_args'][1] else: result = {} self.environ['wsgiorg.routing_args'] = ((), result) return result def _urlvars__set(self, value): environ = self.environ if 'wsgiorg.routing_args' in environ: environ['wsgiorg.routing_args'] = ( environ['wsgiorg.routing_args'][0], value) if 'paste.urlvars' in environ: del environ['paste.urlvars'] elif 'paste.urlvars' in environ: environ['paste.urlvars'] = value else: environ['wsgiorg.routing_args'] = ((), value) def _urlvars__del(self): if 'paste.urlvars' in self.environ: del self.environ['paste.urlvars'] if 'wsgiorg.routing_args' in self.environ: if not self.environ['wsgiorg.routing_args'][0]: del self.environ['wsgiorg.routing_args'] else: self.environ['wsgiorg.routing_args'] = ( self.environ['wsgiorg.routing_args'][0], {}) urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__) def _urlargs__get(self): """ Return any *positional* variables matched in the URL. Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ if 'wsgiorg.routing_args' in self.environ: return self.environ['wsgiorg.routing_args'][0] else: # Since you can't update this value in-place, we don't need # to set the key in the environment return () def _urlargs__set(self, value): environ = self.environ if 'paste.urlvars' in environ: # Some overlap between this and wsgiorg.routing_args; we need # wsgiorg.routing_args to make this work routing_args = (value, environ.pop('paste.urlvars')) elif 'wsgiorg.routing_args' in environ: routing_args = (value, environ['wsgiorg.routing_args'][1]) else: routing_args = (value, {}) environ['wsgiorg.routing_args'] = routing_args def _urlargs__del(self): if 'wsgiorg.routing_args' in self.environ: if not self.environ['wsgiorg.routing_args'][1]: del self.environ['wsgiorg.routing_args'] else: self.environ['wsgiorg.routing_args'] = ( (), self.environ['wsgiorg.routing_args'][1]) urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__) @property def is_xhr(self): """Is X-Requested-With header present and equal to ``XMLHttpRequest``? Note: this isn't set by every XMLHttpRequest request, it is only set if you are using a Javascript library that sets it (or you set the header yourself manually). Currently Prototype and jQuery are known to set this header.""" return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest' def _host__get(self): """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME""" if 'HTTP_HOST' in self.environ: return self.environ['HTTP_HOST'] else: return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ def _host__set(self, value): self.environ['HTTP_HOST'] = value def _host__del(self): if 'HTTP_HOST' in self.environ: del self.environ['HTTP_HOST'] host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__) @property def domain(self): """ Returns the domain portion of the host value. Equivalent to: .. code-block:: python domain = request.host if ':' in domain and domain[-1] != ']': # Check for ] because of IPv6 domain = domain.rsplit(':', 1)[0] This will be equivalent to the domain portion of the ``HTTP_HOST`` value in the environment if it exists, or the ``SERVER_NAME`` value in the environment if it doesn't. For example, if the environment contains an ``HTTP_HOST`` value of ``foo.example.com:8000``, ``request.domain`` will return ``foo.example.com``. Note that this value cannot be *set* on the request. To set the host value use :meth:`webob.request.Request.host` instead. """ domain = self.host if ':' in domain and domain[-1] != ']': domain = domain.rsplit(':', 1)[0] return domain @property def body(self): """ Return the content of the request body. """ if not self.is_body_readable: return b'' self.make_body_seekable() # we need this to have content_length r = self.body_file.read(self.content_length) self.body_file_raw.seek(0) return r @body.setter def body(self, value): if value is None: value = b'' if not isinstance(value, bytes): raise TypeError("You can only set Request.body to bytes (not %r)" % type(value)) self.content_length = len(value) self.body_file_raw = io.BytesIO(value) self.is_body_seekable = True @body.deleter def body(self): self.body = b'' def _json_body__get(self): """Access the body of the request as JSON""" return json.loads(self.body.decode(self.charset)) def _json_body__set(self, value): self.body = json.dumps(value, separators=(',', ':')).encode(self.charset) def _json_body__del(self): del self.body json = json_body = property(_json_body__get, _json_body__set, _json_body__del) def _text__get(self): """ Get/set the text value of the body """ if not self.charset: raise AttributeError( "You cannot access Request.text unless charset is set") body = self.body return body.decode(self.charset) def _text__set(self, value): if not self.charset: raise AttributeError( "You cannot access Response.text unless charset is set") if not isinstance(value, text_type): raise TypeError( "You can only set Request.text to a unicode string " "(not %s)" % type(value)) self.body = value.encode(self.charset) def _text__del(self): del self.body text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) @property def POST(self): """ Return a MultiDict containing all the variables from a form request. Returns an empty dict-like object for non-form requests. Form requests are typically POST requests, however any other requests with an appropriate Content-Type are also supported. """ env = self.environ if 'webob._parsed_post_vars' in env: vars, body_file = env['webob._parsed_post_vars'] if body_file is self.body_file_raw: return vars content_type = self.content_type if ((self.method != 'POST' and not content_type) or content_type not in ('', 'application/x-www-form-urlencoded', 'multipart/form-data') ): # Not an HTML form submission return NoVars('Not an HTML form submission (Content-Type: %s)' % content_type) self._check_charset() self.make_body_seekable() self.body_file_raw.seek(0) fs_environ = env.copy() # FieldStorage assumes a missing CONTENT_LENGTH, but a # default of 0 is better: fs_environ.setdefault('CONTENT_LENGTH', '0') fs_environ['QUERY_STRING'] = '' if PY2: fs = cgi_FieldStorage( fp=self.body_file, environ=fs_environ, keep_blank_values=True) else: fs = cgi_FieldStorage( fp=self.body_file, environ=fs_environ, keep_blank_values=True, encoding='utf8') vars = MultiDict.from_fieldstorage(fs) env['webob._parsed_post_vars'] = (vars, self.body_file_raw) return vars @property def GET(self): """ Return a MultiDict containing all the variables from the QUERY_STRING. """ env = self.environ source = env.get('QUERY_STRING', '') if 'webob._parsed_query_vars' in env: vars, qs = env['webob._parsed_query_vars'] if qs == source: return vars data = [] if source: # this is disabled because we want to access req.GET # for text/plain; charset=ascii uploads for example #self._check_charset() data = parse_qsl_text(source) #d = lambda b: b.decode('utf8') #data = [(d(k), d(v)) for k,v in data] vars = GetDict(data, env) env['webob._parsed_query_vars'] = (vars, source) return vars def _check_charset(self): if self.charset != 'UTF-8': raise DeprecationWarning( "Requests are expected to be submitted in UTF-8, not %s. " "You can fix this by doing req = req.decode('%s')" % ( self.charset, self.charset) ) @property def params(self): """ A dictionary-like object containing both the parameters from the query string and request body. """ params = NestedMultiDict(self.GET, self.POST) return params @property def cookies(self): """ Return a dictionary of cookies as found in the request. """ return RequestCookies(self.environ) @cookies.setter def cookies(self, val): self.environ.pop('HTTP_COOKIE', None) r = RequestCookies(self.environ) r.update(val) def copy(self): """ Copy the request and environment object. This only does a shallow copy, except of wsgi.input """ self.make_body_seekable() env = self.environ.copy() new_req = self.__class__(env) new_req.copy_body() return new_req def copy_get(self): """ Copies the request and environment object, but turning this request into a GET along the way. If this was a POST request (or any other verb) then it becomes GET, and the request body is thrown away. """ env = self.environ.copy() return self.__class__(env, method='GET', content_type=None, body=b'') # webob.is_body_seekable marks input streams that are seekable # this way we can have seekable input without testing the .seek() method is_body_seekable = environ_getter('webob.is_body_seekable', False) @property def is_body_readable(self): """ webob.is_body_readable is a flag that tells us that we can read the input stream even though CONTENT_LENGTH is missing. """ clen = self.content_length if clen is not None and clen != 0: return True elif clen is None: # Rely on the special flag that signifies that either Chunked # Encoding is allowed (and works) or we have replaced # self.body_file with something that is readable and EOF's # correctly. return self.environ.get( 'wsgi.input_terminated', # For backwards compatibility, we fall back to checking if # webob.is_body_readable is set in the environ self.environ.get( 'webob.is_body_readable', False ) ) return False @is_body_readable.setter def is_body_readable(self, flag): self.environ['wsgi.input_terminated'] = bool(flag) def make_body_seekable(self): """ This forces ``environ['wsgi.input']`` to be seekable. That means that, the content is copied into a BytesIO or temporary file and flagged as seekable, so that it will not be unnecessarily copied again. After calling this method the .body_file is always seeked to the start of file and .content_length is not None. The choice to copy to BytesIO is made from ``self.request_body_tempfile_limit`` """ if self.is_body_seekable: self.body_file_raw.seek(0) else: self.copy_body() def copy_body(self): """ Copies the body, in cases where it might be shared with another request object and that is not desired. This copies the body either into a BytesIO object (through setting req.body) or a temporary file. """ if self.is_body_readable: # Before we copy, if we can, rewind the body file if self.is_body_seekable: self.body_file_raw.seek(0) tempfile_limit = self.request_body_tempfile_limit todo = self.content_length if self.content_length is not None else 65535 newbody = b'' fileobj = None input = self.body_file while todo > 0: data = input.read(min(todo, 65535)) if not data and self.content_length is None: # We attempted to read more data, but got none, break. # This can happen if for instance we are reading as much as # we can because we don't have a Content-Length... break elif not data: # We have a Content-Length and we attempted to read, but # there was nothing more to read. Oh the humanity! This # should rarely if never happen because self.body_file # should be a LimitedLengthFile which should already have # raised if there was less data than expected. raise DisconnectionError( "Client disconnected (%s more bytes were expected)" % todo ) if fileobj: fileobj.write(data) else: newbody += data # When we have enough data that we need a tempfile, let's # create one, then clear the temporary variable we were # using if len(newbody) > tempfile_limit: fileobj = self.make_tempfile() fileobj.write(newbody) newbody = b'' # Only decrement todo if Content-Length is set if self.content_length is not None: todo -= len(data) if fileobj: # We apparently had enough data to need a file # Set the Content-Length to the amount of data that was just # written. self.content_length = fileobj.tell() # Seek it back to the beginning fileobj.seek(0) self.body_file_raw = fileobj # Allow it to be seeked in the future, so we don't need to copy # for things like .body self.is_body_seekable = True # Not strictly required since Content-Length is set self.is_body_readable = True else: # No file created, set the body and let it deal with creating # Content-Length and other vars. self.body = newbody else: # Always leave the request with a valid body, and this is pretty # cheap. self.body = b'' def make_tempfile(self): """ Create a tempfile to store big request body. This API is not stable yet. A 'size' argument might be added. """ return tempfile.TemporaryFile() def remove_conditional_headers(self, remove_encoding=True, remove_range=True, remove_match=True, remove_modified=True): """ Remove headers that make the request conditional. These headers can cause the response to be 304 Not Modified, which in some cases you may not want to be possible. This does not remove headers like If-Match, which are used for conflict detection. """ check_keys = [] if remove_range: check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE'] if remove_match: check_keys.append('HTTP_IF_NONE_MATCH') if remove_modified: check_keys.append('HTTP_IF_MODIFIED_SINCE') if remove_encoding: check_keys.append('HTTP_ACCEPT_ENCODING') for key in check_keys: if key in self.environ: del self.environ[key] accept = accept_property() accept_charset = accept_charset_property() accept_encoding = accept_encoding_property() accept_language = accept_language_property() authorization = converter( environ_getter('HTTP_AUTHORIZATION', None, '14.8'), parse_auth, serialize_auth, ) def _cache_control__get(self): """ Get/set/modify the Cache-Control header (`HTTP spec section 14.9 `_) """ env = self.environ value = env.get('HTTP_CACHE_CONTROL', '') cache_header, cache_obj = env.get('webob._cache_control', (None, None)) if cache_obj is not None and cache_header == value: return cache_obj cache_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='request') env['webob._cache_control'] = (value, cache_obj) return cache_obj def _cache_control__set(self, value): env = self.environ value = value or '' if isinstance(value, dict): value = CacheControl(value, type='request') if isinstance(value, CacheControl): str_value = str(value) env['HTTP_CACHE_CONTROL'] = str_value env['webob._cache_control'] = (str_value, value) else: env['HTTP_CACHE_CONTROL'] = str(value) env['webob._cache_control'] = (None, None) def _cache_control__del(self): env = self.environ if 'HTTP_CACHE_CONTROL' in env: del env['HTTP_CACHE_CONTROL'] if 'webob._cache_control' in env: del env['webob._cache_control'] def _update_cache_control(self, prop_dict): self.environ['HTTP_CACHE_CONTROL'] = serialize_cache_control(prop_dict) cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__) if_match = etag_property('HTTP_IF_MATCH', AnyETag, '14.24') if_none_match = etag_property('HTTP_IF_NONE_MATCH', NoETag, '14.26', strong=False) date = converter_date(environ_getter('HTTP_DATE', None, '14.8')) if_modified_since = converter_date( environ_getter('HTTP_IF_MODIFIED_SINCE', None, '14.25')) if_unmodified_since = converter_date( environ_getter('HTTP_IF_UNMODIFIED_SINCE', None, '14.28')) if_range = converter( environ_getter('HTTP_IF_RANGE', None, '14.27'), IfRange.parse, serialize_if_range, 'IfRange object') max_forwards = converter( environ_getter('HTTP_MAX_FORWARDS', None, '14.31'), parse_int, serialize_int, 'int') pragma = environ_getter('HTTP_PRAGMA', None, '14.32') range = converter( environ_getter('HTTP_RANGE', None, '14.35'), parse_range, serialize_range, 'Range object') referer = environ_getter('HTTP_REFERER', None, '14.36') referrer = referer user_agent = environ_getter('HTTP_USER_AGENT', None, '14.43') def __repr__(self): try: name = '%s %s' % (self.method, self.url) except KeyError: name = '(invalid WSGI environ)' msg = '<%s at 0x%x %s>' % ( self.__class__.__name__, abs(id(self)), name) return msg def as_bytes(self, skip_body=False): """ Return HTTP bytes representing this request. If skip_body is True, exclude the body. If skip_body is an integer larger than one, skip body only if its length is bigger than that number. """ url = self.url host = self.host_url assert url.startswith(host) url = url[len(host):] parts = [bytes_('%s %s %s' % (self.method, url, self.http_version))] # acquire body before we handle headers so that # content-length will be set body = None if self.is_body_readable: if skip_body > 1: if len(self.body) > skip_body: body = bytes_('' % len(self.body)) else: skip_body = False if not skip_body: body = self.body for k, v in sorted(self.headers.items()): header = bytes_('%s: %s' % (k, v)) parts.append(header) if body: parts.extend([b'', body]) # HTTP clearly specifies CRLF return b'\r\n'.join(parts) def as_text(self): bytes = self.as_bytes() return bytes.decode(self.charset) __str__ = as_text @classmethod def from_bytes(cls, b): """ Create a request from HTTP bytes data. If the bytes contain extra data after the request, raise a ValueError. """ f = io.BytesIO(b) r = cls.from_file(f) if f.tell() != len(b): raise ValueError("The string contains more data than expected") return r @classmethod def from_text(cls, s): b = bytes_(s, 'utf-8') return cls.from_bytes(b) @classmethod def from_file(cls, fp): """Read a request from a file-like object (it must implement ``.read(size)`` and ``.readline()``). It will read up to the end of the request, not the end of the file (unless the request is a POST or PUT and has no Content-Length, in that case, the entire file is read). This reads the request as represented by ``str(req)``; it may not read every valid HTTP request properly. """ start_line = fp.readline() is_text = isinstance(start_line, text_type) if is_text: crlf = '\r\n' colon = ':' else: crlf = b'\r\n' colon = b':' try: header = start_line.rstrip(crlf) method, resource, http_version = header.split(None, 2) method = native_(method, 'utf-8') resource = native_(resource, 'utf-8') http_version = native_(http_version, 'utf-8') except ValueError: raise ValueError('Bad HTTP request line: %r' % start_line) r = cls(environ_from_url(resource), http_version=http_version, method=method.upper() ) del r.environ['HTTP_HOST'] while 1: line = fp.readline() if not line.strip(): # end of headers break hname, hval = line.split(colon, 1) hname = native_(hname, 'utf-8') hval = native_(hval, 'utf-8').strip() if hname in r.headers: hval = r.headers[hname] + ', ' + hval r.headers[hname] = hval clen = r.content_length if clen is None: body = fp.read() else: body = fp.read(clen) if is_text: body = bytes_(body, 'utf-8') r.body = body return r def call_application(self, application, catch_exc_info=False): """ Call the given WSGI application, returning ``(status_string, headerlist, app_iter)`` Be sure to call ``app_iter.close()`` if it's there. If catch_exc_info is true, then returns ``(status_string, headerlist, app_iter, exc_info)``, where the fourth item may be None, but won't be if there was an exception. If you don't do this and there was an exception, the exception will be raised directly. """ if self.is_body_seekable: self.body_file_raw.seek(0) captured = [] output = [] def start_response(status, headers, exc_info=None): if exc_info is not None and not catch_exc_info: reraise(exc_info) captured[:] = [status, headers, exc_info] return output.append app_iter = application(self.environ, start_response) if output or not captured: try: output.extend(app_iter) finally: if hasattr(app_iter, 'close'): app_iter.close() app_iter = output if catch_exc_info: return (captured[0], captured[1], app_iter, captured[2]) else: return (captured[0], captured[1], app_iter) # Will be filled in later: ResponseClass = None def send(self, application=None, catch_exc_info=False): """ Like ``.call_application(application)``, except returns a response object with ``.status``, ``.headers``, and ``.body`` attributes. This will use ``self.ResponseClass`` to figure out the class of the response object to return. If ``application`` is not given, this will send the request to ``self.make_default_send_app()`` """ if application is None: application = self.make_default_send_app() if catch_exc_info: status, headers, app_iter, exc_info = self.call_application( application, catch_exc_info=True) del exc_info else: status, headers, app_iter = self.call_application( application, catch_exc_info=False) return self.ResponseClass( status=status, headerlist=list(headers), app_iter=app_iter) get_response = send def make_default_send_app(self): global _client try: client = _client except NameError: from webob import client _client = client return client.send_request_app @classmethod def blank(cls, path, environ=None, base_url=None, headers=None, POST=None, **kw): """ Create a blank request environ (and Request wrapper) with the given path (path should be urlencoded), and any keys from environ. The path will become path_info, with any query string split off and used. All necessary keys will be added to the environ, but the values you pass in will take precedence. If you pass in base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will be filled in from that value. Any extra keyword will be passed to ``__init__``. """ env = environ_from_url(path) if base_url: scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url) if query or fragment: raise ValueError( "base_url (%r) cannot have a query or fragment" % base_url) if scheme: env['wsgi.url_scheme'] = scheme if netloc: if ':' not in netloc: if scheme == 'http': netloc += ':80' elif scheme == 'https': netloc += ':443' else: raise ValueError( "Unknown scheme: %r" % scheme) host, port = netloc.split(':', 1) env['SERVER_PORT'] = port env['SERVER_NAME'] = host env['HTTP_HOST'] = netloc if path: env['SCRIPT_NAME'] = url_unquote(path) if environ: env.update(environ) content_type = kw.get('content_type', env.get('CONTENT_TYPE')) if headers and 'Content-Type' in headers: content_type = headers['Content-Type'] if content_type is not None: kw['content_type'] = content_type environ_add_POST(env, POST, content_type=content_type) obj = cls(env, **kw) if headers is not None: obj.headers.update(headers) return obj class LegacyRequest(BaseRequest): uscript_name = upath_property('SCRIPT_NAME') upath_info = upath_property('PATH_INFO') def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) if val is NoDefault: raise KeyError(key) if val is default: return default return val class AdhocAttrMixin(object): _setattr_stacklevel = 3 def __setattr__(self, attr, value, DEFAULT=object()): if (getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_')): object.__setattr__(self, attr, value) else: self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value def __getattr__(self, attr, DEFAULT=object()): try: return self.environ['webob.adhoc_attrs'][attr] except KeyError: raise AttributeError(attr) def __delattr__(self, attr, DEFAULT=object()): if getattr(self.__class__, attr, DEFAULT) is not DEFAULT: return object.__delattr__(self, attr) try: del self.environ['webob.adhoc_attrs'][attr] except KeyError: raise AttributeError(attr) class Request(AdhocAttrMixin, BaseRequest): """ The default request implementation """ def environ_from_url(path): if SCHEME_RE.search(path): scheme, netloc, path, qs, fragment = urlparse.urlsplit(path) if fragment: raise TypeError("Path cannot contain a fragment (%r)" % fragment) if qs: path += '?' + qs if ':' not in netloc: if scheme == 'http': netloc += ':80' elif scheme == 'https': netloc += ':443' else: raise TypeError("Unknown scheme: %r" % scheme) else: scheme = 'http' netloc = 'localhost:80' if path and '?' in path: path_info, query_string = path.split('?', 1) path_info = url_unquote(path_info) else: path_info = url_unquote(path) query_string = '' env = { 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': path_info or '', 'QUERY_STRING': query_string, 'SERVER_NAME': netloc.split(':')[0], 'SERVER_PORT': netloc.split(':')[1], 'HTTP_HOST': netloc, 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.version': (1, 0), 'wsgi.url_scheme': scheme, 'wsgi.input': io.BytesIO(), 'wsgi.errors': sys.stderr, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, #'webob.is_body_seekable': True, } return env def environ_add_POST(env, data, content_type=None): if data is None: return elif isinstance(data, text_type): data = data.encode('ascii') if env['REQUEST_METHOD'] not in ('POST', 'PUT'): env['REQUEST_METHOD'] = 'POST' has_files = False if hasattr(data, 'items'): data = list(data.items()) for k, v in data: if isinstance(v, (tuple, list)): has_files = True break if content_type is None: if has_files: content_type = 'multipart/form-data' else: content_type = 'application/x-www-form-urlencoded' if content_type.startswith('multipart/form-data'): if not isinstance(data, bytes): content_type, data = _encode_multipart(data, content_type) elif content_type.startswith('application/x-www-form-urlencoded'): if has_files: raise ValueError('Submiting files is not allowed for' ' content type `%s`' % content_type) if not isinstance(data, bytes): data = url_encode(data) else: if not isinstance(data, bytes): raise ValueError('Please provide `POST` data as bytes' ' for content type `%s`' % content_type) data = bytes_(data, 'utf8') env['wsgi.input'] = io.BytesIO(data) env['webob.is_body_seekable'] = True env['CONTENT_LENGTH'] = str(len(data)) env['CONTENT_TYPE'] = content_type # # Helper classes and monkeypatching # class DisconnectionError(IOError): pass class LimitedLengthFile(io.RawIOBase): def __init__(self, file, maxlen): self.file = file self.maxlen = maxlen self.remaining = maxlen def __repr__(self): return '<%s(%r, maxlen=%s)>' % ( self.__class__.__name__, self.file, self.maxlen ) def fileno(self): return self.file.fileno() @staticmethod def readable(): return True def readinto(self, buff): if not self.remaining: return 0 sz0 = min(len(buff), self.remaining) data = self.file.read(sz0) sz = len(data) self.remaining -= sz if sz < sz0 and self.remaining: raise DisconnectionError( "The client disconnected while sending the body " "(%d more bytes were expected)" % (self.remaining,) ) buff[:sz] = data return sz def _cgi_FieldStorage__repr__patch(self): """ monkey patch for FieldStorage.__repr__ Unbelievably, the default __repr__ on FieldStorage reads the entire file content instead of being sane about it. This is a simple replacement that doesn't do that """ if self.file: return "FieldStorage(%r, %r)" % (self.name, self.filename) return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value) cgi_FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch class FakeCGIBody(io.RawIOBase): def __init__(self, vars, content_type): warnings.warn( "FakeCGIBody is no longer used by WebOb and will be removed from a future " "version of WebOb. If you require FakeCGIBody please make a copy into " "you own project", DeprecationWarning ) if content_type.startswith('multipart/form-data'): if not _get_multipart_boundary(content_type): raise ValueError('Content-type: %r does not contain boundary' % content_type) self.vars = vars self.content_type = content_type self.file = None def __repr__(self): inner = repr(self.vars) if len(inner) > 20: inner = inner[:15] + '...' + inner[-5:] return '<%s at 0x%x viewing %s>' % ( self.__class__.__name__, abs(id(self)), inner) def fileno(self): return None @staticmethod def readable(): return True def readinto(self, buff): if self.file is None: if self.content_type.startswith('application/x-www-form-urlencoded'): data = '&'.join( '%s=%s' % ( quote_plus(bytes_(k, 'utf8')), quote_plus(bytes_(v, 'utf8')) ) for k, v in self.vars.items() ) self.file = io.BytesIO(bytes_(data)) elif self.content_type.startswith('multipart/form-data'): self.file = _encode_multipart( self.vars.items(), self.content_type, fout=io.BytesIO() )[1] self.file.seek(0) else: assert 0, ('Bad content type: %r' % self.content_type) return self.file.readinto(buff) def _get_multipart_boundary(ctype): m = re.search(r'boundary=([^ ]+)', ctype, re.I) if m: return native_(m.group(1).strip('"')) def _encode_multipart(vars, content_type, fout=None): """Encode a multipart request body into a string""" f = fout or io.BytesIO() w = f.write def wt(t): w(t.encode('utf8')) CRLF = b'\r\n' boundary = _get_multipart_boundary(content_type) if not boundary: boundary = native_(binascii.hexlify(os.urandom(10))) content_type += ('; boundary=%s' % boundary) for name, value in vars: w(b'--') wt(boundary) w(CRLF) wt('Content-Disposition: form-data') if name is not None: wt('; name="%s"' % name) filename = None if getattr(value, 'filename', None): filename = value.filename elif isinstance(value, (list, tuple)): filename, value = value if hasattr(value, 'read'): value = value.read() if filename is not None: wt('; filename="%s"' % filename) mime_type = mimetypes.guess_type(filename)[0] else: mime_type = None w(CRLF) # TODO: should handle value.disposition_options if getattr(value, 'type', None): wt('Content-type: %s' % value.type) if value.type_options: for ct_name, ct_value in sorted(value.type_options.items()): wt('; %s="%s"' % (ct_name, ct_value)) w(CRLF) elif mime_type: wt('Content-type: %s' % mime_type) w(CRLF) w(CRLF) if hasattr(value, 'value'): value = value.value if isinstance(value, bytes): w(value) else: wt(value) w(CRLF) wt('--%s--' % boundary) if fout: return content_type, fout else: return content_type, f.getvalue() def detect_charset(ctype): m = CHARSET_RE.search(ctype) if m: return m.group(1).strip('"').strip() def _is_utf8(charset): if not charset: return True else: return charset.lower().replace('-', '') == 'utf8' class Transcoder(object): def __init__(self, charset, errors='strict'): self.charset = charset # source charset self.errors = errors # unicode errors self._trans = lambda b: b.decode(charset, errors).encode('utf8') def transcode_query(self, q): q_orig = q if '=' not in q: # this doesn't look like a form submission return q_orig if PY2: q = urlparse.parse_qsl(q, self.charset) t = self._trans q = [(t(k), t(v)) for k, v in q] else: q = list(parse_qsl_text(q, self.charset)) return url_encode(q) def transcode_fs(self, fs, content_type): # transcode FieldStorage if PY2: def decode(b): if b is not None: return b.decode(self.charset, self.errors) else: return b else: def decode(b): return b data = [] for field in fs.list or (): field.name = decode(field.name) if field.filename: field.filename = decode(field.filename) data.append((field.name, field)) else: data.append((field.name, decode(field.value))) # TODO: transcode big requests to temp file content_type, fout = _encode_multipart( data, content_type, fout=io.BytesIO() ) return fout WebOb-1.8.6/src/webob/response.py0000644000076600000240000015400013611751063017572 0ustar xistencestaff00000000000000import re import struct import zlib from base64 import b64encode from datetime import datetime, timedelta from hashlib import md5 from webob.byterange import ContentRange from webob.cachecontrol import CacheControl, serialize_cache_control from webob.compat import ( PY2, bytes_, native_, string_types, text_type, url_quote, urlparse, ) from webob.cookies import Cookie, make_cookie from webob.datetime_utils import ( parse_date_delta, serialize_date_delta, timedelta_to_seconds, ) from webob.descriptors import ( CHARSET_RE, SCHEME_RE, converter, date_header, header_getter, list_header, parse_auth, parse_content_range, parse_etag_response, parse_int, parse_int_safe, serialize_auth, serialize_content_range, serialize_etag_response, serialize_int, ) from webob.headers import ResponseHeaders from webob.request import BaseRequest from webob.util import status_generic_reasons, status_reasons, warn_deprecation try: import simplejson as json except ImportError: import json __all__ = ['Response'] _PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I) _OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I) _gzip_header = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff' _marker = object() class Response(object): """ Represents a WSGI response. If no arguments are passed, creates a :class:`~Response` that uses a variety of defaults. The defaults may be changed by sub-classing the :class:`~Response`. See the :ref:`sub-classing notes `. :cvar ~Response.body: If ``body`` is a ``text_type``, then it will be encoded using either ``charset`` when provided or ``default_encoding`` when ``charset`` is not provided if the ``content_type`` allows for a ``charset``. This argument is mutually exclusive with ``app_iter``. :vartype ~Response.body: bytes or text_type :cvar ~Response.status: Either an :class:`int` or a string that is an integer followed by the status text. If it is an integer, it will be converted to a proper status that also includes the status text. Any existing status text will be kept. Non-standard values are allowed. :vartype ~Response.status: int or str :cvar ~Response.headerlist: A list of HTTP headers for the response. :vartype ~Response.headerlist: list :cvar ~Response.app_iter: An iterator that is used as the body of the response. Should conform to the WSGI requirements and should provide bytes. This argument is mutually exclusive with ``body``. :vartype ~Response.app_iter: iterable :cvar ~Response.content_type: Sets the ``Content-Type`` header. If no ``content_type`` is provided, and there is no ``headerlist``, the ``default_content_type`` will be automatically set. If ``headerlist`` is provided then this value is ignored. :vartype ~Response.content_type: str or None :cvar conditional_response: Used to change the behavior of the :class:`~Response` to check the original request for conditional response headers. See :meth:`~Response.conditional_response_app` for more information. :vartype conditional_response: bool :cvar ~Response.charset: Adds a ``charset`` ``Content-Type`` parameter. If no ``charset`` is provided and the ``Content-Type`` is text, then the ``default_charset`` will automatically be added. Currently the only ``Content-Type``'s that allow for a ``charset`` are defined to be ``text/*``, ``application/xml``, and ``*/*+xml``. Any other ``Content-Type``'s will not have a ``charset`` added. If a ``headerlist`` is provided this value is ignored. :vartype ~Response.charset: str or None All other response attributes may be set on the response by providing them as keyword arguments. A :exc:`TypeError` will be raised for any unexpected keywords. .. _response_subclassing_notes: **Sub-classing notes:** * The ``default_content_type`` is used as the default for the ``Content-Type`` header that is returned on the response. It is ``text/html``. * The ``default_charset`` is used as the default character set to return on the ``Content-Type`` header, if the ``Content-Type`` allows for a ``charset`` parameter. Currently the only ``Content-Type``'s that allow for a ``charset`` are defined to be: ``text/*``, ``application/xml``, and ``*/*+xml``. Any other ``Content-Type``'s will not have a ``charset`` added. * The ``unicode_errors`` is set to ``strict``, and access on a :attr:`~Response.text` will raise an error if it fails to decode the :attr:`~Response.body`. * ``default_conditional_response`` is set to ``False``. This flag may be set to ``True`` so that all ``Response`` objects will attempt to check the original request for conditional response headers. See :meth:`~Response.conditional_response_app` for more information. * ``default_body_encoding`` is set to 'UTF-8' by default. It exists to allow users to get/set the ``Response`` object using ``.text``, even if no ``charset`` has been set for the ``Content-Type``. """ default_content_type = 'text/html' default_charset = 'UTF-8' unicode_errors = 'strict' default_conditional_response = False default_body_encoding = 'UTF-8' # These two are only around so that when people pass them into the # constructor they correctly get saved and set, however they are not used # by any part of the Response. See commit # 627593bbcd4ab52adc7ee569001cdda91c670d5d for rationale. request = None environ = None # # __init__, from_file, copy # def __init__(self, body=None, status=None, headerlist=None, app_iter=None, content_type=None, conditional_response=None, charset=_marker, **kw): # Do some sanity checking, and turn json_body into an actual body if app_iter is None and body is None and ('json_body' in kw or 'json' in kw): if 'json_body' in kw: json_body = kw.pop('json_body') else: json_body = kw.pop('json') body = json.dumps(json_body, separators=(',', ':')).encode('UTF-8') if content_type is None: content_type = 'application/json' if app_iter is None: if body is None: body = b'' elif body is not None: raise TypeError( "You may only give one of the body and app_iter arguments") # Set up Response.status if status is None: self._status = '200 OK' else: self.status = status # Initialize headers self._headers = None if headerlist is None: self._headerlist = [] else: self._headerlist = headerlist # Set the encoding for the Response to charset, so if a charset is # passed but the Content-Type does not allow for a charset, we can # still encode text_type body's. # r = Response( # content_type='application/foo', # charset='UTF-8', # body=u'somebody') # Should work without issues, and the header will be correctly set to # Content-Type: application/foo with no charset on it. encoding = None if charset is not _marker: encoding = charset # Does the status code have a body or not? code_has_body = ( self._status[0] != '1' and self._status[:3] not in ('204', '205', '304') ) # We only set the content_type to the one passed to the constructor or # the default content type if there is none that exists AND there was # no headerlist passed. If a headerlist was provided then most likely # the ommission of the Content-Type is on purpose and we shouldn't try # to be smart about it. # # Also allow creation of a empty Response with just the status set to a # Response with empty body, such as Response(status='204 No Content') # without the default content_type being set (since empty bodies have # no Content-Type) # # Check if content_type is set because default_content_type could be # None, in which case there is no content_type, and thus we don't need # to anything content_type = content_type or self.default_content_type if headerlist is None and code_has_body and content_type: # Set up the charset, if the content_type doesn't already have one has_charset = 'charset=' in content_type # If the Content-Type already has a charset, we don't set the user # provided charset on the Content-Type, so we shouldn't use it as # the encoding for text_type based body's. if has_charset: encoding = None # Do not use the default_charset for the encoding because we # want things like # Response(content_type='image/jpeg',body=u'foo') to raise when # trying to encode the body. new_charset = encoding if ( not has_charset and charset is _marker and self.default_charset ): new_charset = self.default_charset # Optimize for the default_content_type as shipped by # WebOb, becuase we know that 'text/html' has a charset, # otherwise add a charset if the content_type has a charset. # # Even if the user supplied charset explicitly, we do not add # it to the Content-Type unless it has has a charset, instead # the user supplied charset is solely used for encoding the # body if it is a text_type if ( new_charset and ( content_type == 'text/html' or _content_type_has_charset(content_type) ) ): content_type += '; charset=' + new_charset self._headerlist.append(('Content-Type', content_type)) # Set up conditional response if conditional_response is None: self.conditional_response = self.default_conditional_response else: self.conditional_response = bool(conditional_response) # Set up app_iter if the HTTP Status code has a body if app_iter is None and code_has_body: if isinstance(body, text_type): # Fall back to trying self.charset if encoding is not set. In # most cases encoding will be set to the default value. encoding = encoding or self.charset if encoding is None: raise TypeError( "You cannot set the body to a text value without a " "charset") body = body.encode(encoding) app_iter = [body] if headerlist is not None: self._headerlist[:] = [ (k, v) for (k, v) in self._headerlist if k.lower() != 'content-length' ] self._headerlist.append(('Content-Length', str(len(body)))) elif app_iter is None and not code_has_body: app_iter = [b''] self._app_iter = app_iter # Loop through all the remaining keyword arguments for name, value in kw.items(): if not hasattr(self.__class__, name): # Not a basic attribute raise TypeError( "Unexpected keyword: %s=%r" % (name, value)) setattr(self, name, value) @classmethod def from_file(cls, fp): """Reads a response from a file-like object (it must implement ``.read(size)`` and ``.readline()``). It will read up to the end of the response, not the end of the file. This reads the response as represented by ``str(resp)``; it may not read every valid HTTP response properly. Responses must have a ``Content-Length``.""" headerlist = [] status = fp.readline().strip() is_text = isinstance(status, text_type) if is_text: _colon = ':' _http = 'HTTP/' else: _colon = b':' _http = b'HTTP/' if status.startswith(_http): (http_ver, status_num, status_text) = status.split(None, 2) status = '%s %s' % (native_(status_num), native_(status_text)) while 1: line = fp.readline().strip() if not line: # end of headers break try: header_name, value = line.split(_colon, 1) except ValueError: raise ValueError('Bad header line: %r' % line) value = value.strip() headerlist.append(( native_(header_name, 'latin-1'), native_(value, 'latin-1') )) r = cls( status=status, headerlist=headerlist, app_iter=(), ) body = fp.read(r.content_length or 0) if is_text: r.text = body else: r.body = body return r def copy(self): """Makes a copy of the response.""" # we need to do this for app_iter to be reusable app_iter = list(self._app_iter) iter_close(self._app_iter) # and this to make sure app_iter instances are different self._app_iter = list(app_iter) return self.__class__( status=self._status, headerlist=self._headerlist[:], app_iter=app_iter, conditional_response=self.conditional_response) # # __repr__, __str__ # def __repr__(self): return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)), self.status) def __str__(self, skip_body=False): parts = [self.status] if not skip_body: # Force enumeration of the body (to set content-length) self.body parts += map('%s: %s'.__mod__, self.headerlist) if not skip_body and self.body: parts += ['', self.body if PY2 else self.text] return '\r\n'.join(parts) # # status, status_code/status_int # def _status__get(self): """ The status string. """ return self._status def _status__set(self, value): try: code = int(value) except (ValueError, TypeError): pass else: self.status_code = code return if not PY2: if isinstance(value, bytes): value = value.decode('ascii') elif isinstance(value, text_type): value = value.encode('ascii') if not isinstance(value, str): raise TypeError( "You must set status to a string or integer (not %s)" % type(value)) # Attempt to get the status code itself, if this fails we should fail try: # We don't need this value anywhere, we just want to validate it's # an integer. So we are using the side-effect of int() raises a # ValueError as a test int(value.split()[0]) except ValueError: raise ValueError('Invalid status code, integer required.') self._status = value status = property(_status__get, _status__set, doc=_status__get.__doc__) def _status_code__get(self): """ The status as an integer. """ return int(self._status.split()[0]) def _status_code__set(self, code): try: self._status = '%d %s' % (code, status_reasons[code]) except KeyError: self._status = '%d %s' % (code, status_generic_reasons[code // 100]) status_code = status_int = property(_status_code__get, _status_code__set, doc=_status_code__get.__doc__) # # headerslist, headers # def _headerlist__get(self): """ The list of response headers. """ return self._headerlist def _headerlist__set(self, value): self._headers = None if not isinstance(value, list): if hasattr(value, 'items'): value = value.items() value = list(value) self._headerlist = value def _headerlist__del(self): self.headerlist = [] headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__) def _headers__get(self): """ The headers in a dictionary-like object. """ if self._headers is None: self._headers = ResponseHeaders.view_list(self._headerlist) return self._headers def _headers__set(self, value): if hasattr(value, 'items'): value = value.items() self.headerlist = value self._headers = None headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) # # body # def _body__get(self): """ The body of the response, as a :class:`bytes`. This will read in the entire app_iter if necessary. """ app_iter = self._app_iter # try: # if len(app_iter) == 1: # return app_iter[0] # except: # pass if isinstance(app_iter, list) and len(app_iter) == 1: return app_iter[0] if app_iter is None: raise AttributeError("No body has been set") try: body = b''.join(app_iter) finally: iter_close(app_iter) if isinstance(body, text_type): raise _error_unicode_in_app_iter(app_iter, body) self._app_iter = [body] if len(body) == 0: # if body-length is zero, we assume it's a HEAD response and # leave content_length alone pass elif self.content_length is None: self.content_length = len(body) elif self.content_length != len(body): raise AssertionError( "Content-Length is different from actual app_iter length " "(%r!=%r)" % (self.content_length, len(body)) ) return body def _body__set(self, value=b''): if not isinstance(value, bytes): if isinstance(value, text_type): msg = ("You cannot set Response.body to a text object " "(use Response.text)") else: msg = ("You can only set the body to a binary type (not %s)" % type(value)) raise TypeError(msg) if self._app_iter is not None: self.content_md5 = None self._app_iter = [value] self.content_length = len(value) # def _body__del(self): # self.body = '' # #self.content_length = None body = property(_body__get, _body__set, _body__set) def _json_body__get(self): """ Set/get the body of the response as JSON. .. note:: This will automatically :meth:`~bytes.decode` the :attr:`~Response.body` as ``UTF-8`` on get, and :meth:`~str.encode` the :meth:`json.dumps` as ``UTF-8`` before assigning to :attr:`~Response.body`. """ # Note: UTF-8 is a content-type specific default for JSON return json.loads(self.body.decode('UTF-8')) def _json_body__set(self, value): self.body = json.dumps(value, separators=(',', ':')).encode('UTF-8') def _json_body__del(self): del self.body json = json_body = property(_json_body__get, _json_body__set, _json_body__del) def _has_body__get(self): """ Determine if the the response has a :attr:`~Response.body`. In contrast to simply accessing :attr:`~Response.body`, this method will **not** read the underlying :attr:`~Response.app_iter`. """ app_iter = self._app_iter if isinstance(app_iter, list) and len(app_iter) == 1: if app_iter[0] != b'': return True else: return False if app_iter is None: # pragma: no cover return False return True has_body = property(_has_body__get) # # text, unicode_body, ubody # def _text__get(self): """ Get/set the text value of the body using the ``charset`` of the ``Content-Type`` or the ``default_body_encoding``. """ if not self.charset and not self.default_body_encoding: raise AttributeError( "You cannot access Response.text unless charset or default_body_encoding" " is set" ) decoding = self.charset or self.default_body_encoding body = self.body return body.decode(decoding, self.unicode_errors) def _text__set(self, value): if not self.charset and not self.default_body_encoding: raise AttributeError( "You cannot access Response.text unless charset or default_body_encoding" " is set" ) if not isinstance(value, text_type): raise TypeError( "You can only set Response.text to a unicode string " "(not %s)" % type(value)) encoding = self.charset or self.default_body_encoding self.body = value.encode(encoding) def _text__del(self): del self.body text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) unicode_body = ubody = property(_text__get, _text__set, _text__del, "Deprecated alias for .text") # # body_file, write(text) # def _body_file__get(self): """ A file-like object that can be used to write to the body. If you passed in a list ``app_iter``, that ``app_iter`` will be modified by writes. """ return ResponseBodyFile(self) def _body_file__set(self, file): self.app_iter = iter_file(file) def _body_file__del(self): del self.body body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__) def write(self, text): if not isinstance(text, bytes): if not isinstance(text, text_type): msg = "You can only write str to a Response.body_file, not %s" raise TypeError(msg % type(text)) if not self.charset: msg = ("You can only write text to Response if charset has " "been set") raise TypeError(msg) text = text.encode(self.charset) app_iter = self._app_iter if not isinstance(app_iter, list): try: new_app_iter = self._app_iter = list(app_iter) finally: iter_close(app_iter) app_iter = new_app_iter self.content_length = sum(len(chunk) for chunk in app_iter) app_iter.append(text) if self.content_length is not None: self.content_length += len(text) # # app_iter # def _app_iter__get(self): """ Returns the ``app_iter`` of the response. If ``body`` was set, this will create an ``app_iter`` from that ``body`` (a single-item list). """ return self._app_iter def _app_iter__set(self, value): if self._app_iter is not None: # Undo the automatically-set content-length self.content_length = None self._app_iter = value def _app_iter__del(self): self._app_iter = [] self.content_length = None app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__) # # headers attrs # allow = list_header('Allow', '14.7') # TODO: (maybe) support response.vary += 'something' # TODO: same thing for all listy headers vary = list_header('Vary', '14.44') content_length = converter( header_getter('Content-Length', '14.17'), parse_int, serialize_int, 'int') content_encoding = header_getter('Content-Encoding', '14.11') content_language = list_header('Content-Language', '14.12') content_location = header_getter('Content-Location', '14.14') content_md5 = header_getter('Content-MD5', '14.14') content_disposition = header_getter('Content-Disposition', '19.5.1') accept_ranges = header_getter('Accept-Ranges', '14.5') content_range = converter( header_getter('Content-Range', '14.16'), parse_content_range, serialize_content_range, 'ContentRange object') date = date_header('Date', '14.18') expires = date_header('Expires', '14.21') last_modified = date_header('Last-Modified', '14.29') _etag_raw = header_getter('ETag', '14.19') etag = converter( _etag_raw, parse_etag_response, serialize_etag_response, 'Entity tag' ) @property def etag_strong(self): return parse_etag_response(self._etag_raw, strong=True) location = header_getter('Location', '14.30') pragma = header_getter('Pragma', '14.32') age = converter( header_getter('Age', '14.6'), parse_int_safe, serialize_int, 'int') retry_after = converter( header_getter('Retry-After', '14.37'), parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds') server = header_getter('Server', '14.38') # TODO: the standard allows this to be a list of challenges www_authenticate = converter( header_getter('WWW-Authenticate', '14.47'), parse_auth, serialize_auth, ) # # charset # def _charset__get(self): """ Get/set the ``charset`` specified in ``Content-Type``. There is no checking to validate that a ``content_type`` actually allows for a ``charset`` parameter. """ header = self.headers.get('Content-Type') if not header: return None match = CHARSET_RE.search(header) if match: return match.group(1) return None def _charset__set(self, charset): if charset is None: self._charset__del() return header = self.headers.get('Content-Type', None) if header is None: raise AttributeError("You cannot set the charset when no " "content-type is defined") match = CHARSET_RE.search(header) if match: header = header[:match.start()] + header[match.end():] header += '; charset=%s' % charset self.headers['Content-Type'] = header def _charset__del(self): header = self.headers.pop('Content-Type', None) if header is None: # Don't need to remove anything return match = CHARSET_RE.search(header) if match: header = header[:match.start()] + header[match.end():] self.headers['Content-Type'] = header charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__) # # content_type # def _content_type__get(self): """ Get/set the ``Content-Type`` header. If no ``Content-Type`` header is set, this will return ``None``. .. versionchanged:: 1.7 Setting a new ``Content-Type`` will remove all ``Content-Type`` parameters and reset the ``charset`` to the default if the ``Content-Type`` is ``text/*`` or XML (``application/xml`` or ``*/*+xml``). To preserve all ``Content-Type`` parameters, you may use the following code: .. code-block:: python resp = Response() params = resp.content_type_params resp.content_type = 'application/something' resp.content_type_params = params """ header = self.headers.get('Content-Type') if not header: return None return header.split(';', 1)[0] def _content_type__set(self, value): if not value: self._content_type__del() return else: if PY2 and isinstance(value, text_type): value = value.encode("latin-1") if not isinstance(value, string_types): raise TypeError("content_type requires value to be of string_types") content_type = value # Set up the charset if the content-type doesn't have one has_charset = 'charset=' in content_type new_charset = None if ( not has_charset and self.default_charset ): new_charset = self.default_charset # Optimize for the default_content_type as shipped by # WebOb, becuase we know that 'text/html' has a charset, # otherwise add a charset if the content_type has a charset. # # We add the default charset if the content-type is "texty". if ( new_charset and ( content_type == 'text/html' or _content_type_has_charset(content_type) ) ): content_type += '; charset=' + new_charset self.headers['Content-Type'] = content_type def _content_type__del(self): self.headers.pop('Content-Type', None) content_type = property(_content_type__get, _content_type__set, _content_type__del, doc=_content_type__get.__doc__) # # content_type_params # def _content_type_params__get(self): """ A dictionary of all the parameters in the content type. (This is not a view, set to change, modifications of the dict will not be applied otherwise.) """ params = self.headers.get('Content-Type', '') if ';' not in params: return {} params = params.split(';', 1)[1] result = {} for match in _PARAM_RE.finditer(params): result[match.group(1)] = match.group(2) or match.group(3) or '' return result def _content_type_params__set(self, value_dict): if not value_dict: self._content_type_params__del() return params = [] for k, v in sorted(value_dict.items()): if not _OK_PARAM_RE.search(v): v = '"%s"' % v.replace('"', '\\"') params.append('; %s=%s' % (k, v)) ct = self.headers.pop('Content-Type', '').split(';', 1)[0] ct += ''.join(params) self.headers['Content-Type'] = ct def _content_type_params__del(self): self.headers['Content-Type'] = self.headers.get( 'Content-Type', '').split(';', 1)[0] content_type_params = property( _content_type_params__get, _content_type_params__set, _content_type_params__del, _content_type_params__get.__doc__ ) # # set_cookie, unset_cookie, delete_cookie, merge_cookies # def set_cookie(self, name, value='', max_age=None, path='/', domain=None, secure=False, httponly=False, comment=None, expires=None, overwrite=False, samesite=None): """ Set (add) a cookie for the response. Arguments are: ``name`` The cookie name. ``value`` The cookie value, which should be a string or ``None``. If ``value`` is ``None``, it's equivalent to calling the :meth:`webob.response.Response.unset_cookie` method for this cookie key (it effectively deletes the cookie on the client). ``max_age`` An integer representing a number of seconds, ``datetime.timedelta``, or ``None``. This value is used as the ``Max-Age`` of the generated cookie. If ``expires`` is not passed and this value is not ``None``, the ``max_age`` value will also influence the ``Expires`` value of the cookie (``Expires`` will be set to ``now`` + ``max_age``). If this value is ``None``, the cookie will not have a ``Max-Age`` value (unless ``expires`` is set). If both ``max_age`` and ``expires`` are set, this value takes precedence. ``path`` A string representing the cookie ``Path`` value. It defaults to ``/``. ``domain`` A string representing the cookie ``Domain``, or ``None``. If domain is ``None``, no ``Domain`` value will be sent in the cookie. ``secure`` A boolean. If it's ``True``, the ``secure`` flag will be sent in the cookie, if it's ``False``, the ``secure`` flag will not be sent in the cookie. ``httponly`` A boolean. If it's ``True``, the ``HttpOnly`` flag will be sent in the cookie, if it's ``False``, the ``HttpOnly`` flag will not be sent in the cookie. ``samesite`` A string representing the ``SameSite`` attribute of the cookie or ``None``. If samesite is ``None`` no ``SameSite`` value will be sent in the cookie. Should only be ``"strict"``, ``"lax"``, or ``"none"``. ``comment`` A string representing the cookie ``Comment`` value, or ``None``. If ``comment`` is ``None``, no ``Comment`` value will be sent in the cookie. ``expires`` A ``datetime.timedelta`` object representing an amount of time, ``datetime.datetime`` or ``None``. A non-``None`` value is used to generate the ``Expires`` value of the generated cookie. If ``max_age`` is not passed, but this value is not ``None``, it will influence the ``Max-Age`` header. If this value is ``None``, the ``Expires`` cookie value will be unset (unless ``max_age`` is set). If ``max_age`` is set, it will be used to generate the ``expires`` and this value is ignored. If a ``datetime.datetime`` is provided it has to either be timezone aware or be based on UTC. ``datetime.datetime`` objects that are local time are not supported. Timezone aware ``datetime.datetime`` objects are converted to UTC. This argument will be removed in future versions of WebOb (version 1.9). ``overwrite`` If this key is ``True``, before setting the cookie, unset any existing cookie. """ # Remove in WebOb 1.10 if expires: warn_deprecation('Argument "expires" will be removed in a future ' 'version of WebOb, please use "max_age".', 1.10, 1) if overwrite: self.unset_cookie(name, strict=False) # If expires is set, but not max_age we set max_age to expires if not max_age and isinstance(expires, timedelta): max_age = expires # expires can also be a datetime if not max_age and isinstance(expires, datetime): # If expires has a timezone attached, convert it to UTC if expires.tzinfo and expires.utcoffset(): expires = (expires - expires.utcoffset()).replace(tzinfo=None) max_age = expires - datetime.utcnow() value = bytes_(value, 'utf-8') cookie = make_cookie(name, value, max_age=max_age, path=path, domain=domain, secure=secure, httponly=httponly, comment=comment, samesite=samesite) self.headerlist.append(('Set-Cookie', cookie)) def delete_cookie(self, name, path='/', domain=None): """ Delete a cookie from the client. Note that ``path`` and ``domain`` must match how the cookie was originally set. This sets the cookie to the empty string, and ``max_age=0`` so that it should expire immediately. """ self.set_cookie(name, None, path=path, domain=domain) def unset_cookie(self, name, strict=True): """ Unset a cookie with the given name (remove it from the response). """ existing = self.headers.getall('Set-Cookie') if not existing and not strict: return cookies = Cookie() for header in existing: cookies.load(header) if isinstance(name, text_type): name = name.encode('utf8') if name in cookies: del cookies[name] del self.headers['Set-Cookie'] for m in cookies.values(): self.headerlist.append(('Set-Cookie', m.serialize())) elif strict: raise KeyError("No cookie has been set with the name %r" % name) def merge_cookies(self, resp): """Merge the cookies that were set on this response with the given ``resp`` object (which can be any WSGI application). If the ``resp`` is a :class:`webob.Response` object, then the other object will be modified in-place. """ if not self.headers.get('Set-Cookie'): return resp if isinstance(resp, Response): for header in self.headers.getall('Set-Cookie'): resp.headers.add('Set-Cookie', header) return resp else: c_headers = [h for h in self.headerlist if h[0].lower() == 'set-cookie'] def repl_app(environ, start_response): def repl_start_response(status, headers, exc_info=None): return start_response(status, headers + c_headers, exc_info=exc_info) return resp(environ, repl_start_response) return repl_app # # cache_control # _cache_control_obj = None def _cache_control__get(self): """ Get/set/modify the Cache-Control header (`HTTP spec section 14.9 `_). """ value = self.headers.get('cache-control', '') if self._cache_control_obj is None: self._cache_control_obj = CacheControl.parse( value, updates_to=self._update_cache_control, type='response') self._cache_control_obj.header_value = value if self._cache_control_obj.header_value != value: new_obj = CacheControl.parse(value, type='response') self._cache_control_obj.properties.clear() self._cache_control_obj.properties.update(new_obj.properties) self._cache_control_obj.header_value = value return self._cache_control_obj def _cache_control__set(self, value): # This actually becomes a copy if not value: value = "" if isinstance(value, dict): value = CacheControl(value, 'response') if isinstance(value, text_type): value = str(value) if isinstance(value, str): if self._cache_control_obj is None: self.headers['Cache-Control'] = value return value = CacheControl.parse(value, 'response') cache = self.cache_control cache.properties.clear() cache.properties.update(value.properties) def _cache_control__del(self): self.cache_control = {} def _update_cache_control(self, prop_dict): value = serialize_cache_control(prop_dict) if not value: if 'Cache-Control' in self.headers: del self.headers['Cache-Control'] else: self.headers['Cache-Control'] = value cache_control = property( _cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__) # # cache_expires # def _cache_expires(self, seconds=0, **kw): """ Set expiration on this request. This sets the response to expire in the given seconds, and any other attributes are used for ``cache_control`` (e.g., ``private=True``). """ if seconds is True: seconds = 0 elif isinstance(seconds, timedelta): seconds = timedelta_to_seconds(seconds) cache_control = self.cache_control if seconds is None: pass elif not seconds: # To really expire something, you have to force a # bunch of these cache control attributes, and IE may # not pay attention to those still so we also set # Expires. cache_control.no_store = True cache_control.no_cache = True cache_control.must_revalidate = True cache_control.max_age = 0 cache_control.post_check = 0 cache_control.pre_check = 0 self.expires = datetime.utcnow() if 'last-modified' not in self.headers: self.last_modified = datetime.utcnow() self.pragma = 'no-cache' else: cache_control.properties.clear() cache_control.max_age = seconds self.expires = datetime.utcnow() + timedelta(seconds=seconds) self.pragma = None for name, value in kw.items(): setattr(cache_control, name, value) cache_expires = property(lambda self: self._cache_expires, _cache_expires) # # encode_content, decode_content, md5_etag # def encode_content(self, encoding='gzip', lazy=False): """ Encode the content with the given encoding (only ``gzip`` and ``identity`` are supported). """ assert encoding in ('identity', 'gzip'), \ "Unknown encoding: %r" % encoding if encoding == 'identity': self.decode_content() return if self.content_encoding == 'gzip': return if lazy: self.app_iter = gzip_app_iter(self._app_iter) self.content_length = None else: self.app_iter = list(gzip_app_iter(self._app_iter)) self.content_length = sum(map(len, self._app_iter)) self.content_encoding = 'gzip' def decode_content(self): content_encoding = self.content_encoding or 'identity' if content_encoding == 'identity': return if content_encoding not in ('gzip', 'deflate'): raise ValueError( "I don't know how to decode the content %s" % content_encoding) if content_encoding == 'gzip': from gzip import GzipFile from io import BytesIO gzip_f = GzipFile(filename='', mode='r', fileobj=BytesIO(self.body)) self.body = gzip_f.read() self.content_encoding = None gzip_f.close() else: # Weird feature: http://bugs.python.org/issue5784 self.body = zlib.decompress(self.body, -15) self.content_encoding = None def md5_etag(self, body=None, set_content_md5=False): """ Generate an etag for the response object using an MD5 hash of the body (the ``body`` parameter, or ``self.body`` if not given). Sets ``self.etag``. If ``set_content_md5`` is ``True``, sets ``self.content_md5`` as well. """ if body is None: body = self.body md5_digest = md5(body).digest() md5_digest = b64encode(md5_digest) md5_digest = md5_digest.replace(b'\n', b'') md5_digest = native_(md5_digest) self.etag = md5_digest.strip('=') if set_content_md5: self.content_md5 = md5_digest @staticmethod def _make_location_absolute(environ, value): if SCHEME_RE.search(value): return value new_location = urlparse.urljoin(_request_uri(environ), value) return new_location def _abs_headerlist(self, environ): # Build the headerlist, if we have a Location header, make it absolute return [ (k, v) if k.lower() != 'location' else (k, self._make_location_absolute(environ, v)) for (k, v) in self._headerlist ] # # __call__, conditional_response_app # def __call__(self, environ, start_response): """ WSGI application interface """ if self.conditional_response: return self.conditional_response_app(environ, start_response) headerlist = self._abs_headerlist(environ) start_response(self.status, headerlist) if environ['REQUEST_METHOD'] == 'HEAD': # Special case here... return EmptyResponse(self._app_iter) return self._app_iter _safe_methods = ('GET', 'HEAD') def conditional_response_app(self, environ, start_response): """ Like the normal ``__call__`` interface, but checks conditional headers: * ``If-Modified-Since`` (``304 Not Modified``; only on ``GET``, ``HEAD``) * ``If-None-Match`` (``304 Not Modified``; only on ``GET``, ``HEAD``) * ``Range`` (``406 Partial Content``; only on ``GET``, ``HEAD``) """ req = BaseRequest(environ) headerlist = self._abs_headerlist(environ) method = environ.get('REQUEST_METHOD', 'GET') if method in self._safe_methods: status304 = False if req.if_none_match and self.etag: status304 = self.etag in req.if_none_match elif req.if_modified_since and self.last_modified: status304 = self.last_modified <= req.if_modified_since if status304: start_response('304 Not Modified', filter_headers(headerlist)) return EmptyResponse(self._app_iter) if ( req.range and self in req.if_range and self.content_range is None and method in ('HEAD', 'GET') and self.status_code == 200 and self.content_length is not None ): content_range = req.range.content_range(self.content_length) if content_range is None: iter_close(self._app_iter) body = bytes_("Requested range not satisfiable: %s" % req.range) headerlist = [ ('Content-Length', str(len(body))), ('Content-Range', str(ContentRange(None, None, self.content_length))), ('Content-Type', 'text/plain'), ] + filter_headers(headerlist) start_response('416 Requested Range Not Satisfiable', headerlist) if method == 'HEAD': return () return [body] else: app_iter = self.app_iter_range(content_range.start, content_range.stop) if app_iter is not None: # the following should be guaranteed by # Range.range_for_length(length) assert content_range.start is not None headerlist = [ ('Content-Length', str(content_range.stop - content_range.start)), ('Content-Range', str(content_range)), ] + filter_headers(headerlist, ('content-length',)) start_response('206 Partial Content', headerlist) if method == 'HEAD': return EmptyResponse(app_iter) return app_iter start_response(self.status, headerlist) if method == 'HEAD': return EmptyResponse(self._app_iter) return self._app_iter def app_iter_range(self, start, stop): """ Return a new ``app_iter`` built from the response ``app_iter``, that serves up only the given ``start:stop`` range. """ app_iter = self._app_iter if hasattr(app_iter, 'app_iter_range'): return app_iter.app_iter_range(start, stop) return AppIterRange(app_iter, start, stop) def filter_headers(hlist, remove_headers=('content-length', 'content-type')): return [h for h in hlist if (h[0].lower() not in remove_headers)] def iter_file(file, block_size=1 << 18): # 256Kb while True: data = file.read(block_size) if not data: break yield data class ResponseBodyFile(object): mode = 'wb' closed = False def __init__(self, response): """ Represents a :class:`~Response` as a file like object. """ self.response = response self.write = response.write def __repr__(self): return '' % self.response encoding = property( lambda self: self.response.charset, doc="The encoding of the file (inherited from response.charset)" ) def writelines(self, seq): """ Write a sequence of lines to the response. """ for item in seq: self.write(item) def close(self): raise NotImplementedError("Response bodies cannot be closed") def flush(self): pass def tell(self): """ Provide the current location where we are going to start writing. """ if not self.response.has_body: return 0 return sum([len(chunk) for chunk in self.response.app_iter]) class AppIterRange(object): """ Wraps an ``app_iter``, returning just a range of bytes. """ def __init__(self, app_iter, start, stop): assert start >= 0, "Bad start: %r" % start assert stop is None or (stop >= 0 and stop >= start), ( "Bad stop: %r" % stop) self.app_iter = iter(app_iter) self._pos = 0 # position in app_iter self.start = start self.stop = stop def __iter__(self): return self def _skip_start(self): start, stop = self.start, self.stop for chunk in self.app_iter: self._pos += len(chunk) if self._pos < start: continue elif self._pos == start: return b'' else: chunk = chunk[start - self._pos:] if stop is not None and self._pos > stop: chunk = chunk[:stop - self._pos] assert len(chunk) == stop - start return chunk else: raise StopIteration() def next(self): if self._pos < self.start: # need to skip some leading bytes return self._skip_start() stop = self.stop if stop is not None and self._pos >= stop: raise StopIteration chunk = next(self.app_iter) self._pos += len(chunk) if stop is None or self._pos <= stop: return chunk else: return chunk[:stop - self._pos] __next__ = next # py3 def close(self): iter_close(self.app_iter) class EmptyResponse(object): """ An empty WSGI response. An iterator that immediately stops. Optionally provides a close method to close an underlying ``app_iter`` it replaces. """ def __init__(self, app_iter=None): if app_iter is not None and hasattr(app_iter, 'close'): self.close = app_iter.close def __iter__(self): return self def __len__(self): return 0 def next(self): raise StopIteration() __next__ = next # py3 def _is_xml(content_type): return ( content_type.startswith('application/xml') or ( content_type.startswith('application/') and content_type.endswith('+xml') ) or ( content_type.startswith('image/') and content_type.endswith('+xml') ) ) def _content_type_has_charset(content_type): return ( content_type.startswith('text/') or _is_xml(content_type) ) def _request_uri(environ): """Like ``wsgiref.url.request_uri``, except eliminates ``:80`` ports. Returns the full request URI.""" url = environ['wsgi.url_scheme'] + '://' if environ.get('HTTP_HOST'): url += environ['HTTP_HOST'] else: url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT'] if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http': url = url[:-3] elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https': url = url[:-4] if PY2: script_name = environ.get('SCRIPT_NAME', '/') path_info = environ.get('PATH_INFO', '') else: script_name = bytes_(environ.get('SCRIPT_NAME', '/'), 'latin-1') path_info = bytes_(environ.get('PATH_INFO', ''), 'latin-1') url += url_quote(script_name) qpath_info = url_quote(path_info) if 'SCRIPT_NAME' not in environ: url += qpath_info[1:] else: url += qpath_info return url def iter_close(iter): if hasattr(iter, 'close'): iter.close() def gzip_app_iter(app_iter): size = 0 crc = zlib.crc32(b"") & 0xffffffff compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 0) yield _gzip_header for item in app_iter: size += len(item) crc = zlib.crc32(item, crc) & 0xffffffff # The compress function may return zero length bytes if the input is # small enough; it buffers the input for the next iteration or for a # flush. result = compress.compress(item) if result: yield result # Similarly, flush may also not yield a value. result = compress.flush() if result: yield result yield struct.pack("<2L", crc, size & 0xffffffff) def _error_unicode_in_app_iter(app_iter, body): app_iter_repr = repr(app_iter) if len(app_iter_repr) > 50: app_iter_repr = ( app_iter_repr[:30] + '...' + app_iter_repr[-10:]) raise TypeError( 'An item of the app_iter (%s) was text, causing a ' 'text body: %r' % (app_iter_repr, body)) WebOb-1.8.6/src/webob/static.py0000644000076600000240000001301213611751037017221 0ustar xistencestaff00000000000000import mimetypes import os from webob import exc from webob.dec import wsgify from webob.response import Response __all__ = [ 'FileApp', 'DirectoryApp', ] mimetypes._winreg = None # do not load mimetypes from windows registry mimetypes.add_type('text/javascript', '.js') # stdlib default is application/x-javascript mimetypes.add_type('image/x-icon', '.ico') # not among defaults BLOCK_SIZE = 1<<16 class FileApp(object): """An application that will send the file at the given filename. Adds a mime type based on `mimetypes.guess_type()`. """ def __init__(self, filename, **kw): self.filename = filename content_type, content_encoding = mimetypes.guess_type(filename) kw.setdefault('content_type', content_type) kw.setdefault('content_encoding', content_encoding) kw.setdefault('accept_ranges', 'bytes') self.kw = kw # Used for testing purpose self._open = open @wsgify def __call__(self, req): if req.method not in ('GET', 'HEAD'): return exc.HTTPMethodNotAllowed("You cannot %s a file" % req.method) try: stat = os.stat(self.filename) except (IOError, OSError) as e: msg = "Can't open %r: %s" % (self.filename, e) return exc.HTTPNotFound(comment=msg) try: file = self._open(self.filename, 'rb') except (IOError, OSError) as e: msg = "You are not permitted to view this file (%s)" % e return exc.HTTPForbidden(msg) if 'wsgi.file_wrapper' in req.environ: app_iter = req.environ['wsgi.file_wrapper'](file, BLOCK_SIZE) else: app_iter = FileIter(file) return Response( app_iter = app_iter, content_length = stat.st_size, last_modified = stat.st_mtime, #@@ etag **self.kw ).conditional_response_app class FileIter(object): def __init__(self, file): self.file = file def app_iter_range(self, seek=None, limit=None, block_size=None): """Iter over the content of the file. You can set the `seek` parameter to read the file starting from a specific position. You can set the `limit` parameter to read the file up to specific position. Finally, you can change the number of bytes read at once by setting the `block_size` parameter. """ if block_size is None: block_size = BLOCK_SIZE if seek: self.file.seek(seek) if limit is not None: limit -= seek try: while True: data = self.file.read(min(block_size, limit) if limit is not None else block_size) if not data: return yield data if limit is not None: limit -= len(data) if limit <= 0: return finally: self.file.close() __iter__ = app_iter_range class DirectoryApp(object): """An application that serves up the files in a given directory. This will serve index files (by default ``index.html``), or set ``index_page=None`` to disable this. If you set ``hide_index_with_redirect=True`` (it defaults to False) then requests to, e.g., ``/index.html`` will be redirected to ``/``. To customize `FileApp` instances creation (which is what actually serves the responses), override the `make_fileapp` method. """ def __init__(self, path, index_page='index.html', hide_index_with_redirect=False, **kw): self.path = os.path.abspath(path) if not self.path.endswith(os.path.sep): self.path += os.path.sep if not os.path.isdir(self.path): raise IOError( "Path does not exist or is not directory: %r" % self.path) self.index_page = index_page self.hide_index_with_redirect = hide_index_with_redirect self.fileapp_kw = kw def make_fileapp(self, path): return FileApp(path, **self.fileapp_kw) @wsgify def __call__(self, req): path = os.path.abspath(os.path.join(self.path, req.path_info.lstrip('/'))) if os.path.isdir(path) and self.index_page: return self.index(req, path) if (self.index_page and self.hide_index_with_redirect and path.endswith(os.path.sep + self.index_page)): new_url = req.path_url.rsplit('/', 1)[0] new_url += '/' if req.query_string: new_url += '?' + req.query_string return Response( status=301, location=new_url) if not path.startswith(self.path): return exc.HTTPForbidden() elif not os.path.isfile(path): return exc.HTTPNotFound(comment=path) else: return self.make_fileapp(path) def index(self, req, path): index_path = os.path.join(path, self.index_page) if not os.path.isfile(index_path): return exc.HTTPNotFound(comment=index_path) if not req.path_info.endswith('/'): url = req.path_url + '/' if req.query_string: url += '?' + req.query_string return Response( status=301, location=url) return self.make_fileapp(index_path) WebOb-1.8.6/src/webob/util.py0000644000076600000240000001135313611751037016715 0ustar xistencestaff00000000000000import warnings from webob.compat import ( escape, string_types, text_, text_type, ) from webob.headers import _trans_key def html_escape(s): """HTML-escape a string or object This converts any non-string objects passed into it to strings (actually, using ``unicode()``). All values returned are non-unicode strings (using ``&#num;`` entities for all non-ASCII characters). None is treated specially, and returns the empty string. """ if s is None: return '' __html__ = getattr(s, '__html__', None) if __html__ is not None and callable(__html__): return s.__html__() if not isinstance(s, string_types): __unicode__ = getattr(s, '__unicode__', None) if __unicode__ is not None and callable(__unicode__): s = s.__unicode__() else: s = str(s) s = escape(s, True) if isinstance(s, text_type): s = s.encode('ascii', 'xmlcharrefreplace') return text_(s) def header_docstring(header, rfc_section): if header.isupper(): header = _trans_key(header) major_section = rfc_section.split('.')[0] link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % ( major_section, rfc_section) return "Gets and sets the ``%s`` header (`HTTP spec section %s <%s>`_)." % ( header, rfc_section, link) def warn_deprecation(text, version, stacklevel): # version specifies when to start raising exceptions instead of warnings if version in ('1.2', '1.3', '1.4', '1.5', '1.6', '1.7'): raise DeprecationWarning(text) else: cls = DeprecationWarning warnings.warn(text, cls, stacklevel=stacklevel + 1) status_reasons = { # Status Codes # Informational 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', # Successful 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi Status', 226: 'IM Used', # Redirection 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', # Client Error 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Request Entity Too Large', 414: 'Request URI Too Long', 415: 'Unsupported Media Type', 416: 'Requested Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a teapot", 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 451: 'Unavailable for Legal Reasons', 431: 'Request Header Fields Too Large', # Server Error 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 507: 'Insufficient Storage', 510: 'Not Extended', 511: 'Network Authentication Required', } # generic class responses as per RFC2616 status_generic_reasons = { 1: 'Continue', 2: 'Success', 3: 'Multiple Choices', 4: 'Unknown Client Error', 5: 'Unknown Server Error', } try: # py3.3+ have native comparison support from hmac import compare_digest except ImportError: # pragma: nocover (Python 2.7.7 backported this) compare_digest = None def strings_differ(string1, string2, compare_digest=compare_digest): """Check whether two strings differ while avoiding timing attacks. This function returns True if the given strings differ and False if they are equal. It's careful not to leak information about *where* they differ as a result of its running time, which can be very important to avoid certain timing-related crypto attacks: http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf .. versionchanged:: 1.5 Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+ and Python 3.3+). """ len_eq = len(string1) == len(string2) if len_eq: invalid_bits = 0 left = string1 else: invalid_bits = 1 left = string2 right = string2 if compare_digest is not None: invalid_bits += not compare_digest(left, right) else: for a, b in zip(left, right): invalid_bits += a != b return invalid_bits != 0 WebOb-1.8.6/tests/0000755000076600000240000000000013611756053014643 5ustar xistencestaff00000000000000WebOb-1.8.6/tests/conftest.py0000644000076600000240000000334213611751037017041 0ustar xistencestaff00000000000000import pytest import threading import random import logging from contextlib import contextmanager from wsgiref.simple_server import make_server from wsgiref.simple_server import WSGIRequestHandler from wsgiref.simple_server import WSGIServer from wsgiref.simple_server import ServerHandler log = logging.getLogger(__name__) ServerHandler.handle_error = lambda: None class QuietHandler(WSGIRequestHandler): def log_request(self, *args): pass class QuietServer(WSGIServer): def handle_error(self, req, addr): pass def _make_test_server(app): maxport = ((1 << 16) - 1) # we'll make 3 attempts to find a free port for i in range(3, 0, -1): try: port = random.randint(maxport // 2, maxport) server = make_server( 'localhost', port, app, server_class=QuietServer, handler_class=QuietHandler, ) server.timeout = 5 return server except: if i == 1: raise @pytest.fixture def serve(): @contextmanager def _serve(app): server = _make_test_server(app) try: worker = threading.Thread(target=server.serve_forever) worker.setDaemon(True) worker.start() server.url = "http://localhost:%d" % server.server_port log.debug("server started on %s", server.url) yield server finally: log.debug("shutting server down") server.shutdown() worker.join(1) if worker.isAlive(): log.warning('worker is hanged') else: log.debug("server stopped") return _serve WebOb-1.8.6/tests/performance_test.py0000644000076600000240000000315013611751037020551 0ustar xistencestaff00000000000000#!/usr/bin/env python from webob.response import Response def make_middleware(app): from repoze.profile.profiler import AccumulatingProfileMiddleware return AccumulatingProfileMiddleware( app, log_filename='/tmp/profile.log', discard_first_request=True, flush_at_shutdown=True, path='/__profile__') def simple_app(environ, start_response): resp = Response('Hello world!') return resp(environ, start_response) if __name__ == '__main__': import sys import os import signal if sys.argv[1:]: arg = sys.argv[1] else: arg = None if arg in ['open', 'run']: import subprocess import webbrowser import time os.environ['SHOW_OUTPUT'] = '0' proc = subprocess.Popen([sys.executable, __file__]) time.sleep(1) subprocess.call(['ab', '-n', '1000', 'http://localhost:8080/']) if arg == 'open': webbrowser.open('http://localhost:8080/__profile__') print('Hit ^C to end') try: while 1: raw_input() finally: os.kill(proc.pid, signal.SIGKILL) else: from paste.httpserver import serve if os.environ.get('SHOW_OUTPUT') != '0': print('Note you can also use:)') print(' %s %s open' % (sys.executable, __file__)) print('to run ab and open a browser (or "run" to just run ab)') print('Now do:') print('ab -n 1000 http://localhost:8080/') print('wget -O - http://localhost:8080/__profile__') serve(make_middleware(simple_app)) WebOb-1.8.6/tests/test_acceptparse.py0000644000076600000240000067550613611751037020566 0ustar xistencestaff00000000000000import re import warnings import pytest from webob.acceptparse import ( Accept, AcceptCharset, AcceptCharsetInvalidHeader, AcceptCharsetNoHeader, AcceptCharsetValidHeader, AcceptEncoding, AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, AcceptEncodingValidHeader, AcceptInvalidHeader, AcceptLanguage, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, AcceptLanguageValidHeader, AcceptNoHeader, AcceptValidHeader, MIMEAccept, _item_n_weight_re, _list_1_or_more__compiled_re, accept_charset_property, accept_encoding_property, accept_language_property, accept_property, create_accept_charset_header, create_accept_encoding_header, create_accept_header, create_accept_language_header, ) from webob.request import Request IGNORE_BEST_MATCH = 'ignore:.*best_match.*' IGNORE_QUALITY = 'ignore:.*quality.*' IGNORE_CONTAINS = 'ignore:.*__contains__.*' IGNORE_ITER = 'ignore:.*__iter__.*' IGNORE_MIMEACCEPT = 'ignore:.*MIMEAccept.*' class Test_ItemNWeightRe(object): @pytest.mark.parametrize('header_value', [ 'q=', 'q=1', ';q', ';q=', ';q=1', 'foo;', 'foo;q', 'foo;q1', 'foo;q=', 'foo;q=-1', 'foo;q=2', 'foo;q=1.001', 'foo;q=0.0001', 'foo;q=00', 'foo;q=01', 'foo;q=00.1', 'foo,q=0.1', 'foo;q =1', 'foo;q= 1', ]) def test_invalid(self, header_value): regex = _item_n_weight_re(item_re='foo') assert re.match('^' + regex + '$', header_value, re.VERBOSE) is None @pytest.mark.parametrize('header_value, groups', [ ('foo', ('foo', None)), ('foo;q=0', ('foo', '0')), ('foo;q=0.0', ('foo', '0.0')), ('foo;q=0.00', ('foo', '0.00')), ('foo;q=0.000', ('foo', '0.000')), ('foo;q=1', ('foo', '1')), ('foo;q=1.0', ('foo', '1.0')), ('foo;q=1.00', ('foo', '1.00')), ('foo;q=1.000', ('foo', '1.000')), ('foo;q=0.1', ('foo', '0.1')), ('foo;q=0.87', ('foo', '0.87')), ('foo;q=0.382', ('foo', '0.382')), ('foo;Q=0.382', ('foo', '0.382')), ('foo ;Q=0.382', ('foo', '0.382')), ('foo; Q=0.382', ('foo', '0.382')), ('foo ; Q=0.382', ('foo', '0.382')), ]) def test_valid(self, header_value, groups): regex = _item_n_weight_re(item_re='foo') assert re.match( '^' + regex + '$', header_value, re.VERBOSE, ).groups() == groups class Test_List1OrMoreCompiledRe(object): @pytest.mark.parametrize('header_value', [ # RFC 7230 Section 7 ',', ', ,', # RFC 7230 Errata ID: 4169 'foo , ,bar,charlie ', # Our tests ' foo , ,bar,charlie', ' ,foo , ,bar,charlie', ',foo , ,bar,charlie, ', '\tfoo , ,bar,charlie', '\t,foo , ,bar,charlie', ',foo , ,bar,charlie\t', ',foo , ,bar,charlie,\t', ]) def test_invalid(self, header_value): regex = _list_1_or_more__compiled_re(element_re='([a-z]+)') assert regex.match(header_value) is None @pytest.mark.parametrize('header_value', [ # RFC 7230 Section 7 'foo,bar', 'foo, bar,', # RFC 7230 Errata ID: 4169 'foo , ,bar,charlie', # Our tests 'foo , ,bar,charlie', ',foo , ,bar,charlie', ',foo , ,bar,charlie,', ',\t ,,, \t \t, ,\t\t\t,foo \t\t,, bar, ,\tcharlie \t,, ,', ]) def test_valid(self, header_value): regex = _list_1_or_more__compiled_re(element_re='([a-z]+)') assert regex.match(header_value) class TestAccept(object): @pytest.mark.parametrize('value', [ ', ', ', , ', 'noslash', '/', 'text/', '/html', 'text/html;', 'text/html;param', 'text/html;param=', 'text/html ;param=val;', 'text/html; param=val;', 'text/html;param=val;', 'text/html;param=\x19', 'text/html;param=\x22', 'text/html;param=\x5c', 'text/html;param=\x7f', r'text/html;param="\"', r'text/html;param="\\\"', r'text/html;param="\\""', 'text/html;param="\\\x19"', 'text/html;param="\\\x7f"', 'text/html;q', 'text/html;q=', 'text/html;q=-1', 'text/html;q=2', 'text/html;q=1.001', 'text/html;q=0.0001', 'text/html;q=00', 'text/html;q=01', 'text/html;q=00.1', 'text/html,q=0.1', 'text/html;q =1', 'text/html;q= 1', 'text/html;q=1;', 'text/html;param;q=1', 'text/html;q=1;extparam;', 'text/html;q=1;extparam=val;', 'text/html;q=1;extparam="val";', 'text/html;q=1;extparam="', 'text/html;q=1;extparam="val', 'text/html;q=1;extparam=val"', 'text/html;q=1;extparam=\x19', 'text/html;q=1;extparam=\x22', 'text/html;q=1;extparam=\x5c', 'text/html;q=1;extparam=\x7f', r'text/html;q=1;extparam="\"', r'text/html;q=1;extparam="\\\"', r'text/html;q=1;extparam="\\""', 'text/html;q=1;extparam="\\\x19"', 'text/html;q=1;extparam="\\\x7f"', 'text/html;param=\x19;q=1;extparam', 'text/html;param=val;q=1;extparam=\x19', ]) def test_parse__invalid_header(self, value): with pytest.raises(ValueError): AcceptValidHeader.parse(value=value) @pytest.mark.parametrize('value, expected_list', [ # Examples from RFC 7231, Section 5.3.2 "Accept": ( 'audio/*; q=0.2, audio/basic', [('audio/*', 0.2, [], []), ('audio/basic', 1.0, [], [])], ), ( 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c', [ ('text/plain', 0.5, [], []), ('text/html', 1.0, [], []), ('text/x-dvi', 0.8, [], []), ('text/x-c', 1.0, [], []), ], ), ( 'text/*, text/plain, text/plain;format=flowed, */*', [ ('text/*', 1.0, [], []), ('text/plain', 1.0, [], []), ('text/plain;format=flowed', 1.0, [('format', 'flowed')], []), ('*/*', 1.0, [], []), ], ), ( 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, ' 'text/html;level=2;q=0.4, */*;q=0.5', [ ('text/*', 0.3, [], []), ('text/html', 0.7, [], []), ('text/html;level=1', 1.0, [('level', '1')], []), ('text/html;level=2', 0.4, [('level', '2')], []), ('*/*', 0.5, [], []), ], ), # Our tests ('', []), (',', []), (', ,', []), ( '*/*, text/*, text/html', [ ('*/*', 1.0, [], []), ('text/*', 1.0, [], []), ('text/html', 1.0, [], []), ] ), # It does not seem from RFC 7231, section 5.3.2 "Accept" that the '*' # in a range like '*/html' was intended to have any special meaning # (the section lists '*/*', 'type/*' and 'type/subtype', but not # '*/subtype'). However, because type and subtype are tokens (section # 3.1.1.1), and a token may contain '*'s, '*/subtype' is valid. ('*/html', [('*/html', 1.0, [], [])]), ( 'text/html \t;\t param1=val1\t; param2="val2" ' + r'; param3="\"\\\\"', [( r'text/html;param1=val1;param2=val2;param3="\"\\\\"', 1.0, [('param1', 'val1'), ('param2', 'val2'), ('param3', r'"\\')], [], )], ), ( 'text/html;param=!#$%&\'*+-.^_`|~09AZaz', [( 'text/html;param=!#$%&\'*+-.^_`|~09AZaz', 1.0, [('param', '!#$%&\'*+-.^_`|~09AZaz')], [], )], ), ( 'text/html;param=""', [('text/html;param=""', 1.0, [('param', '')], [])], ), ( 'text/html;param="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', [( 'text/html;param="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', 1.0, [('param', '\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e')], [], )], ), ( 'text/html;param="\x80\x81\xfe\xff\\\x22\\\x5c"', [( 'text/html;param="\x80\x81\xfe\xff\\\x22\\\x5c"', 1.0, [('param', '\x80\x81\xfe\xff\x22\x5c')], [], )], ), ( 'text/html;param="\\\t\\ \\\x21\\\x7e\\\x80\\\xff"', [( 'text/html;param="\t \x21\x7e\x80\xff"', 1.0, [('param', '\t \x21\x7e\x80\xff')], [], )], ), ( "text/html;param='val'", # This makes it look like the media type parameter value could be # surrounded with single quotes instead of double quotes, but the # single quotes are actually part of the media type parameter value # token [("text/html;param='val'", 1.0, [('param', "'val'")], [])], ), ('text/html;q=0.9', [('text/html', 0.9, [], [])]), ('text/html;q=0', [('text/html', 0.0, [], [])]), ('text/html;q=0.0', [('text/html', 0.0, [], [])]), ('text/html;q=0.00', [('text/html', 0.0, [], [])]), ('text/html;q=0.000', [('text/html', 0.0, [], [])]), ('text/html;q=1', [('text/html', 1.0, [], [])]), ('text/html;q=1.0', [('text/html', 1.0, [], [])]), ('text/html;q=1.00', [('text/html', 1.0, [], [])]), ('text/html;q=1.000', [('text/html', 1.0, [], [])]), ('text/html;q=0.1', [('text/html', 0.1, [], [])]), ('text/html;q=0.87', [('text/html', 0.87, [], [])]), ('text/html;q=0.382', [('text/html', 0.382, [], [])]), ('text/html;Q=0.382', [('text/html', 0.382, [], [])]), ('text/html ;Q=0.382', [('text/html', 0.382, [], [])]), ('text/html; Q=0.382', [('text/html', 0.382, [], [])]), ('text/html ; Q=0.382', [('text/html', 0.382, [], [])]), ('text/html;q=0.9;q=0.8', [('text/html', 0.9, [], [('q', '0.8')])]), ( 'text/html;q=1;q=1;q=1', [('text/html', 1.0, [], [('q', '1'), ('q', '1')])], ), ( 'text/html;q=0.9;extparam1;extparam2=val2;extparam3="val3"', [( 'text/html', 0.9, [], ['extparam1', ('extparam2', 'val2'), ('extparam3', 'val3')] )], ), ( 'text/html;q=1;extparam=!#$%&\'*+-.^_`|~09AZaz', [('text/html', 1.0, [], [('extparam', '!#$%&\'*+-.^_`|~09AZaz')])], ), ( 'text/html;q=1;extparam=""', [('text/html', 1.0, [], [('extparam', '')])], ), ( 'text/html;q=1;extparam="\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e"', [( 'text/html', 1.0, [], [('extparam', '\t \x21\x23\x24\x5a\x5b\x5d\x5e\x7d\x7e')], )], ), ( 'text/html;q=1;extparam="\x80\x81\xfe\xff\\\x22\\\x5c"', [( 'text/html', 1.0, [], [('extparam', '\x80\x81\xfe\xff\x22\x5c')], )], ), ( 'text/html;q=1;extparam="\\\t\\ \\\x21\\\x7e\\\x80\\\xff"', [('text/html', 1.0, [], [('extparam', '\t \x21\x7e\x80\xff')])], ), ( "text/html;q=1;extparam='val'", # This makes it look like the extension parameter value could be # surrounded with single quotes instead of double quotes, but the # single quotes are actually part of the extension parameter value # token [('text/html', 1.0, [], [('extparam', "'val'")])], ), ( 'text/html;param1="val1";param2=val2;q=0.9;extparam1="val1"' ';extparam2;extparam3=val3', [( 'text/html;param1=val1;param2=val2', 0.9, [('param1', 'val1'), ('param2', 'val2')], [('extparam1', 'val1'), 'extparam2', ('extparam3', 'val3')], )], ), ( ', ,, a/b \t;\t p1=1 ;\t\tp2=2 ; q=0.6\t \t;\t\t e1\t; e2, ,', [('a/b;p1=1;p2=2', 0.6, [('p1', '1'), ('p2', '2')], ['e1', 'e2'])], ), ( ( ',\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, ' + 'g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t,' ), [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('e/f;p1=v1', 0.0, [('p1', 'v1')], ['e1']), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ], ), ]) def test_parse__valid_header(self, value, expected_list): returned = AcceptValidHeader.parse(value=value) list_of_returned = list(returned) assert list_of_returned == expected_list @pytest.mark.parametrize('offer, expected_return, expected_str', [ ['text/html', ('text', 'html', ()), 'text/html'], [ 'text/html;charset=utf8', ('text', 'html', (('charset', 'utf8'),)), 'text/html;charset=utf8', ], [ 'text/html;charset=utf8;x-version=1', ('text', 'html', (('charset', 'utf8'), ('x-version', '1'))), 'text/html;charset=utf8;x-version=1', ], [ 'text/HtMl;cHaRseT=UtF-8;X-Version=1', ('text', 'html', (('charset', 'UtF-8'), ('x-version', '1'))), 'text/html;charset=UtF-8;x-version=1', ], ]) def test_parse_offer__valid(self, offer, expected_return, expected_str): result = Accept.parse_offer(offer) assert result == expected_return assert str(result) == expected_str assert result is Accept.parse_offer(result) @pytest.mark.parametrize('offer', [ '', 'foo', 'foo/bar/baz', '*/plain', '*/plain;charset=utf8', '*/plain;charset=utf8;x-version=1', '*/*;charset=utf8', 'text/*;charset=utf8', 'text/*', '*/*', ]) def test_parse_offer__invalid(self, offer): with pytest.raises(ValueError): Accept.parse_offer(offer) class TestAcceptValidHeader(object): def test_parse__inherited(self): returned = AcceptValidHeader.parse( value=( ',\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, ' + 'g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t,' ), ) list_of_returned = list(returned) assert list_of_returned == [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('e/f;p1=v1', 0.0, [('p1', 'v1')], ['e1']), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ] @pytest.mark.parametrize('header_value', [ ', ', 'text/html;param=val;q=1;extparam=\x19', ]) def test___init___invalid_header(self, header_value): with pytest.raises(ValueError): AcceptValidHeader(header_value=header_value) def test___init___valid_header(self): header_value = ( ',\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, ' + 'g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t,' ) instance = AcceptValidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed == [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('e/f;p1=v1', 0.0, [('p1', 'v1')], ['e1']), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ] assert instance._parsed_nonzero == [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ] assert isinstance(instance, Accept) def test___add___None(self): left_operand = AcceptValidHeader(header_value='text/html') result = left_operand + None assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptValidHeader(header_value='text/html') result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptValidHeader(header_value='text/html') class Other(object): def __str__(self): return str_ right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptValidHeader(header_value=',\t ,i/j, k/l;q=0.333,') result = left_operand + value assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptValidHeader(header_value=',\t ,i/j, k/l;q=0.333,') class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): header = ',\t ,i/j, k/l;q=0.333,' result = AcceptValidHeader(header_value=header) + value assert isinstance(result, AcceptValidHeader) assert result.header_value == header + ', ' + value_as_header def test___add___other_type_with_valid___str___not_empty(self): header = ',\t ,i/j, k/l;q=0.333,' class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' right_operand = Other() result = AcceptValidHeader(header_value=header) + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == header + ', ' + str(right_operand) def test___add___AcceptValidHeader_header_value_empty(self): left_operand = AcceptValidHeader( header_value='a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ) right_operand = AcceptValidHeader(header_value='') result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand def test___add___AcceptValidHeader_header_value_not_empty(self): left_operand = AcceptValidHeader( header_value='a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ) right_operand = AcceptValidHeader( header_value=',\t ,i/j, k/l;q=0.333,', ) result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == left_operand.header_value + ', ' + \ right_operand.header_value def test___add___AcceptNoHeader(self): valid_header_instance = AcceptValidHeader(header_value='a/b') result = valid_header_instance + AcceptNoHeader() assert isinstance(result, AcceptValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance @pytest.mark.parametrize('header_value', [ ', ', 'a/b;p1=1;p2=2;q=0.8;e1;e2="', ]) def test___add___AcceptInvalidHeader(self, header_value): valid_header_instance = AcceptValidHeader(header_value='a/b') result = valid_header_instance + AcceptInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance def test___bool__(self): instance = AcceptValidHeader(header_value='type/subtype') returned = bool(instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): accept = AcceptValidHeader('A/a, B/b, C/c') assert 'A/a' in accept assert 'A/*' in accept assert '*/a' in accept assert 'A/b' not in accept assert 'B/a' not in accept for mask in ['*/*', 'text/html', 'TEXT/HTML']: assert 'text/html' in AcceptValidHeader(mask) @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptValidHeader( header_value=( 'text/plain; q=0.5, text/html; q=0, text/x-dvi; q=0.8, ' 'text/x-c' ), ) assert list(instance) == ['text/x-c', 'text/x-dvi', 'text/plain'] def test___radd___None(self): right_operand = AcceptValidHeader(header_value='a/b') result = None + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptValidHeader(header_value='a/b') result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptValidHeader(header_value='a/b') class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): right_operand = AcceptValidHeader(header_value='a/b') result = value + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___radd___other_type_with_valid___str___empty(self): right_operand = AcceptValidHeader( header_value=',\t ,i/j, k/l;q=0.333,', ) class Other(object): def __str__(self): return '' result = Other() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): header = ',\t ,i/j, k/l;q=0.333,' result = value + AcceptValidHeader(header_value=header) assert isinstance(result, AcceptValidHeader) assert result.header_value == value_as_header + ', ' + header def test___radd___other_type_with_valid___str___not_empty(self): header = ',\t ,i/j, k/l;q=0.333,' class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' left_operand = Other() result = left_operand + AcceptValidHeader(header_value=header) assert isinstance(result, AcceptValidHeader) assert result.header_value == str(left_operand) + ', ' + header @pytest.mark.parametrize('header_value, expected_returned', [ ('', ""), ( r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', r'''", ), ( ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' + 'e3=3, c/d ,,', r'''" ), ]) def test___repr__(self, header_value, expected_returned): instance = AcceptValidHeader(header_value=header_value) assert repr(instance) == expected_returned @pytest.mark.parametrize('header_value, expected_returned', [ ('', ''), ( r',,text/html ; p1="\"\1\"" ; q=0.50; e1=1 ;e2 , text/plain ,', r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain', ), ( ',\t, a/b ; p1=1 ; p2=2 ;\t q=0.20 ;\te1="\\"\\1\\""\t; e2 ; ' + 'e3=3, c/d ,,', 'a/b;p1=1;p2=2;q=0.2;e1="\\"1\\"";e2;e3=3, c/d' ), ]) def test___str__(self, header_value, expected_returned): instance = AcceptValidHeader(header_value=header_value) assert str(instance) == expected_returned def test__old_match(self): accept = AcceptValidHeader('image/jpg') assert accept._old_match('image/jpg', 'image/jpg') assert accept._old_match('image/*', 'image/jpg') assert accept._old_match('*/*', 'image/jpg') assert not accept._old_match('text/html', 'image/jpg') mismatches = [ ('B/b', 'A/a'), ('B/b', 'B/a'), ('B/b', 'A/b'), ('A/a', 'B/b'), ('B/a', 'B/b'), ('A/b', 'B/b') ] for mask, offer in mismatches: assert not accept._old_match(mask, offer) def test__old_match_wildcard_matching(self): """ Wildcard matching forces the match to take place against the type or subtype of the mask and offer (depending on where the wildcard matches) """ accept = AcceptValidHeader('type/subtype') matches = [ ('*/*', '*/*'), ('*/*', 'A/*'), ('*/*', '*/a'), ('*/*', 'A/a'), ('A/*', '*/*'), ('A/*', 'A/*'), ('A/*', '*/a'), ('A/*', 'A/a'), ('*/a', '*/*'), ('*/a', 'A/*'), ('*/a', '*/a'), ('*/a', 'A/a'), ('A/a', '*/*'), ('A/a', 'A/*'), ('A/a', '*/a'), ('A/a', 'A/a'), # Offers might not contain a subtype ('*/*', '*'), ('A/*', '*'), ('*/a', '*')] for mask, offer in matches: assert accept._old_match(mask, offer) # Test malformed mask and offer variants where either is missing a # type or subtype assert accept._old_match('A', offer) assert accept._old_match(mask, 'a') mismatches = [ ('B/b', 'A/*'), ('B/*', 'A/a'), ('B/*', 'A/*'), ('*/b', '*/a')] for mask, offer in mismatches: assert not accept._old_match(mask, offer) @pytest.mark.parametrize('header_value, returned', [ ('tExt/HtMl', True), ('APPlication/XHTML+xml', True), ('appliCATION/xMl', True), ('TeXt/XmL', True), ('image/jpg', False), ('TeXt/Plain', False), ('image/jpg, text/html', True), ]) def test_accept_html(self, header_value, returned): instance = AcceptValidHeader(header_value=header_value) assert instance.accept_html() is returned @pytest.mark.parametrize('header_value, returned', [ ('tExt/HtMl', True), ('APPlication/XHTML+xml', True), ('appliCATION/xMl', True), ('TeXt/XmL', True), ('image/jpg', False), ('TeXt/Plain', False), ('image/jpg, text/html', True), ]) def test_accepts_html(self, header_value, returned): instance = AcceptValidHeader(header_value=header_value) assert instance.accepts_html is returned @pytest.mark.parametrize('header, offers, expected_returned', [ (AcceptValidHeader('text/html'), ['text/html;p=1;q=0.5'], []), (AcceptValidHeader('text/html'), ['text/html;q=0.5'], []), (AcceptValidHeader('text/html'), ['text/html;q=0.5;e=1'], []), ( AcceptValidHeader('text/html'), ['text/html', 'text/plain;p=1;q=0.5;e=1', 'foo'], [('text/html', 1.0)], ), ( AcceptInvalidHeader('foo'), ['text/html', 'text/plain;p=1;q=0.5;e=1', 'foo'], [('text/html', 1.0)], ), ( AcceptNoHeader(), ['text/html', 'text/plain;p=1;q=0.5;e=1', 'foo'], [('text/html', 1.0)], ), ]) def test_acceptable_offers__invalid_offers( self, header, offers, expected_returned, ): assert header.acceptable_offers(offers=offers) == expected_returned @pytest.mark.parametrize('header_value, offers, expected_returned', [ # RFC 7231, section 5.3.2 ( 'audio/*; q=0.2, audio/basic', ['audio/mpeg', 'audio/basic'], [('audio/basic', 1.0), ('audio/mpeg', 0.2)], ), ( 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c', ['text/x-dvi', 'text/x-c', 'text/html', 'text/plain'], [ ('text/x-c', 1.0), ('text/html', 1.0), ('text/x-dvi', 0.8), ('text/plain', 0.5), ], ), ( 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, ' + 'text/html;level=2;q=0.4, */*;q=0.5', [ 'text/html;level=1', 'text/html', 'text/plain', 'image/jpeg', 'text/html;level=2', 'text/html;level=3', ], [ ('text/html;level=1', 1.0), ('text/html', 0.7), ('text/html;level=3', 0.7), ('image/jpeg', 0.5), ('text/html;level=2', 0.4), ('text/plain', 0.3), ], ), # Our tests ( 'teXT/*;Q=0.5, TeXt/hTmL;LeVeL=1', ['tExT/HtMl;lEvEl=1', 'TExt/PlAiN'], [('tExT/HtMl;lEvEl=1', 1.0), ('TExt/PlAiN', 0.5)], ), ( 'text/html, application/json', ['text/html', 'application/json'], [('text/html', 1.0), ('application/json', 1.0)], ), ( 'text/html ;\t level=1', ['text/html\t\t ; \tlevel=1'], [('text/html\t\t ; \tlevel=1', 1.0)], ), ('', ['text/html'], []), ('text/html, image/jpeg', ['audio/basic', 'text/plain'], []), ( r'text/html;p1=1;p2=2;p3="\""', [r'text/html;p1=1;p2="2";p3="\""'], [(r'text/html;p1=1;p2="2";p3="\""', 1.0)], ), ('text/html;p1=1', ['text/html;p1=2'], []), ('text/html', ['text/html;p1=1'], [('text/html;p1=1', 1.0)]), ('text/html;p1=1', ['text/html'], []), ('text/html', ['text/html'], [('text/html', 1.0)]), ('text/*', ['text/html;p=1'], [('text/html;p=1', 1.0)]), ('*/*', ['text/html;p=1'], [('text/html;p=1', 1.0)]), ('text/*', ['text/html'], [('text/html', 1.0)]), ('*/*', ['text/html'], [('text/html', 1.0)]), ('text/html;p1=1;q=0', ['text/html;p1=1'], []), ('text/html;q=0', ['text/html;p1=1', 'text/html'], []), ('text/*;q=0', ['text/html;p1=1', 'text/html', 'text/plain'], []), ( '*/*;q=0', ['text/html;p1=1', 'text/html', 'text/plain', 'image/jpeg'], [], ), ( '*/*;q=0, audio/mpeg', [ 'text/html;p1=1', 'audio/mpeg', 'text/html', 'text/plain', 'image/jpeg', ], [('audio/mpeg', 1.0)], ), ( 'text/html;p1=1, text/html;q=0', ['text/html;p1=1'], [('text/html;p1=1', 1.0)], ), ('text/html, text/*;q=0', ['text/html'], [('text/html', 1.0)]), ('text/*, */*;q=0', ['text/html'], [('text/html', 1.0)]), ('text/html;q=0, text/html', ['text/html'], []), ( 'text/html', ['text/html;level=1', 'text/html', 'text/html;level=2'], [ ('text/html;level=1', 1.0), ('text/html', 1.0), ('text/html;level=2', 1.0), ], ), ( 'text/*;q=0.3, text/html;q=0, image/png, text/html;level=1, ' + 'text/html;level=2;q=0.4, image/jpeg;q=0.5', [ 'text/html;level=1', 'text/html', 'text/plain', 'image/jpeg', 'text/html;level=2', 'text/html;level=3', 'audio/basic', ], [ ('text/html;level=1', 1.0), ('image/jpeg', 0.5), ('text/html;level=2', 0.4), ('text/plain', 0.3), ], ), ( 'text/*;q=0.3, text/html;q=0.5, text/html;level=1;q=0.7', ['text/*', '*/*', 'text/html', 'image/*'], [('text/html', 0.5)], ), ( 'text/html;level=1;q=0.7', ['text/*', '*/*', 'text/html', 'text/html;level=1', 'image/*'], [('text/html;level=1', 0.7)], ), ( '*/*', ['text/*'], [], ), ( '', ['text/*', '*/*', 'text/html', 'text/html;level=1', 'image/*'], [], ), ]) def test_acceptable_offers__valid_offers( self, header_value, offers, expected_returned, ): instance = AcceptValidHeader(header_value=header_value) returned = instance.acceptable_offers(offers=offers) assert returned == expected_returned def test_acceptable_offers_uses_AcceptOffer_objects(self): from webob.acceptparse import AcceptOffer offer = AcceptOffer('text', 'html', (('level', '1'),)) instance = AcceptValidHeader(header_value='text/*;q=0.5') result = instance.acceptable_offers([offer]) assert result == [(offer, 0.5)] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptValidHeader('text/html, foo/bar') assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' assert accept.best_match(['foo/bar', 'text/html']) == 'foo/bar' assert accept.best_match([('foo/bar', 0.5), 'text/html']) == 'text/html' assert accept.best_match([('foo/bar', 0.5), ('text/html', 0.4)]) == 'foo/bar' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_one_lower_q(self): accept = AcceptValidHeader('text/html, foo/bar;q=0.5') assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' accept = AcceptValidHeader('text/html;q=0.5, foo/bar') assert accept.best_match(['text/html', 'foo/bar']) == 'foo/bar' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_complex_q(self): accept = AcceptValidHeader( 'text/html, foo/bar;q=0.55, baz/gort;q=0.59' ) assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' accept = AcceptValidHeader( 'text/html;q=0.5, foo/bar;q=0.586, baz/gort;q=0.596' ) assert accept.best_match(['text/html', 'baz/gort']) == 'baz/gort' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_json(self): accept = AcceptValidHeader('text/html, */*; q=0.2') assert accept.best_match(['application/json']) == 'application/json' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_mixedcase(self): accept = AcceptValidHeader( 'image/jpg; q=0.2, Image/pNg; Q=0.4, image/*; q=0.05' ) assert accept.best_match(['Image/JpG']) == 'Image/JpG' assert accept.best_match(['image/Tiff']) == 'image/Tiff' assert accept.best_match(['image/Tiff', 'image/PnG', 'image/jpg']) == \ 'image/PnG' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_best_match_zero_quality(self): assert AcceptValidHeader('text/plain, */*;q=0').best_match( ['text/html'] ) is None assert 'audio/basic' not in AcceptValidHeader('*/*;q=0') @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): accept = AcceptValidHeader('text/html') assert accept.quality('text/html') == 1 accept = AcceptValidHeader('text/html;q=0.5') assert accept.quality('text/html') == 0.5 @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality_not_found(self): accept = AcceptValidHeader('text/html') assert accept.quality('foo/bar') is None class TestAcceptNoHeader(object): def test_parse__inherited(self): returned = AcceptNoHeader.parse( value=( ',\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, ' + 'g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t,' ), ) list_of_returned = list(returned) assert list_of_returned == [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('e/f;p1=v1', 0.0, [('p1', 'v1')], ['e1']), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ] def test___init__(self): instance = AcceptNoHeader() assert instance.header_value is None assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, Accept) def test___add___None(self): left_operand = AcceptNoHeader() result = left_operand + None assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptNoHeader() class Other(object): def __str__(self): return str_ right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptNoHeader() result = left_operand + value assert isinstance(result, AcceptValidHeader) assert result.header_value == '' def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptNoHeader() class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptNoHeader() + value assert isinstance(result, AcceptValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' right_operand = Other() result = AcceptNoHeader() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptValidHeader_header_value_empty(self): right_operand = AcceptValidHeader(header_value='') result = AcceptNoHeader() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptValidHeader_header_value_not_empty(self): right_operand = AcceptValidHeader( header_value=',\t ,i/j, k/l;q=0.333,', ) result = AcceptNoHeader() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value def test___add___AcceptNoHeader(self): left_operand = AcceptNoHeader() right_operand = AcceptNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) assert result is not left_operand assert result is not right_operand @pytest.mark.parametrize('header_value', [ ', ', 'a/b;p1=1;p2=2;q=0.8;e1;e2="', ]) def test___add___AcceptInvalidHeader(self, header_value): left_operand = AcceptNoHeader() result = left_operand + AcceptInvalidHeader(header_value=header_value) assert isinstance(result, AcceptNoHeader) assert result is not left_operand def test___bool__(self): instance = AcceptNoHeader() returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptNoHeader() returned = ('type/subtype' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptNoHeader() returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptNoHeader() result = None + right_operand assert isinstance(result, AcceptNoHeader) assert result is not right_operand @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) assert result is not right_operand @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptNoHeader() class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptNoHeader) assert result is not right_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): result = value + AcceptNoHeader() assert isinstance(result, AcceptValidHeader) assert result.header_value == '' def test___radd___other_type_with_valid___str___empty(self): class Other(object): def __str__(self): return '' result = Other() + AcceptNoHeader() assert isinstance(result, AcceptValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): result = value + AcceptNoHeader() assert isinstance(result, AcceptValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' left_operand = Other() result = left_operand + AcceptNoHeader() assert isinstance(result, AcceptValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptNoHeader() assert repr(instance) == '' def test___str__(self): instance = AcceptNoHeader() assert str(instance) == '' def test_accept_html(self): instance = AcceptNoHeader() assert instance.accept_html() is True def test_accepts_html(self): instance = AcceptNoHeader() assert instance.accepts_html is True def test_acceptable_offers(self): instance = AcceptNoHeader() returned = instance.acceptable_offers(offers=['a/b', 'c/d', 'e/f']) assert returned == [('a/b', 1.0), ('c/d', 1.0), ('e/f', 1.0)] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptNoHeader() assert accept.best_match(['text/html', 'audio/basic']) == 'text/html' assert accept.best_match([('text/html', 1), ('audio/basic', 0.5)]) == \ 'text/html' assert accept.best_match([('text/html', 0.5), ('audio/basic', 1)]) == \ 'audio/basic' assert accept.best_match([('text/html', 0.5), 'audio/basic']) == \ 'audio/basic' assert accept.best_match( [('text/html', 0.5), 'audio/basic'], default_match=True ) == 'audio/basic' assert accept.best_match( [('text/html', 0.5), 'audio/basic'], default_match=False ) == 'audio/basic' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptNoHeader() returned = instance.quality(offer='type/subtype') assert returned == 1.0 class TestAcceptInvalidHeader(object): def test_parse__inherited(self): returned = AcceptInvalidHeader.parse( value=( ',\t , a/b;q=1;e1;e2=v2 \t,\t\t c/d, e/f;p1=v1;q=0;e1, ' + 'g/h;p1=v1\t ;\t\tp2=v2;q=0.5 \t,' ), ) list_of_returned = list(returned) assert list_of_returned == [ ('a/b', 1.0, [], ['e1', ('e2', 'v2')]), ('c/d', 1.0, [], []), ('e/f;p1=v1', 0.0, [('p1', 'v1')], ['e1']), ('g/h;p1=v1;p2=v2', 0.5, [('p1', 'v1'), ('p2', 'v2')], []), ] def test___init__(self): header_value = ', ' instance = AcceptInvalidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, Accept) def test___add___None(self): left_operand = AcceptInvalidHeader(header_value=', ') result = left_operand + None assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptInvalidHeader(header_value='invalid header') result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptInvalidHeader(header_value='invalid header') class Other(object): def __str__(self): return str_ right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptInvalidHeader(header_value=', ') result = left_operand + value assert isinstance(result, AcceptValidHeader) assert result.header_value == '' def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptInvalidHeader(header_value=', ') class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptInvalidHeader(header_value=', ') + value assert isinstance(result, AcceptValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' right_operand = Other() result = AcceptInvalidHeader(header_value=', ') + \ right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptValidHeader_header_value_empty(self): left_operand = AcceptInvalidHeader(header_value=', ') right_operand = AcceptValidHeader(header_value='') result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptValidHeader_header_value_not_empty(self): left_operand = AcceptInvalidHeader(header_value=', ') right_operand = AcceptValidHeader( header_value=',\t ,i/j, k/l;q=0.333,', ) result = left_operand + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == right_operand.header_value def test___add___AcceptNoHeader(self): left_operand = AcceptInvalidHeader(header_value=', ') right_operand = AcceptNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) assert result is not right_operand @pytest.mark.parametrize('header_value', [ ', ', 'a/b;p1=1;p2=2;q=0.8;e1;e2="', ]) def test___add___AcceptInvalidHeader(self, header_value): result = AcceptInvalidHeader(header_value=', ') + \ AcceptInvalidHeader(header_value=header_value) assert isinstance(result, AcceptNoHeader) def test___bool__(self): instance = AcceptInvalidHeader(header_value=', ') returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptInvalidHeader(header_value=', ') returned = ('type/subtype' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptInvalidHeader(header_value=', ') returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptInvalidHeader(header_value=', ') result = None + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, {', ;level=1': (1.0, ';e1=1')}, 'a/b, c/d;q=1;e1;', ['a/b', 'c/d;q=1;e1;'], ('a/b', 'c/d;q=1;e1;',), {'a/b': 1.0, 'cd': 1.0}, {'a/b': (1.0, ';e1=1'), 'c/d': (1.0, ';e2=2;')}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptInvalidHeader(header_value=', ') result = left_operand + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('str_', [', ', 'a/b, c/d;q=1;e1;']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptInvalidHeader(header_value=', ') class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): right_operand = AcceptInvalidHeader(header_value='invalid header') result = value + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == '' def test___radd___other_type_with_valid___str___empty(self): right_operand = AcceptInvalidHeader(header_value='invalid header') class Other(object): def __str__(self): return '' result = Other() + right_operand assert isinstance(result, AcceptValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): result = value + AcceptInvalidHeader(header_value='invalid header') assert isinstance(result, AcceptValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' left_operand = Other() result = left_operand + AcceptInvalidHeader( header_value='invalid header', ) assert isinstance(result, AcceptValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptInvalidHeader(header_value='\x00') assert repr(instance) == '' def test___str__(self): instance = AcceptInvalidHeader(header_value=", ") assert str(instance) == '' def test_accept_html(self): instance = AcceptInvalidHeader(header_value=', ') assert instance.accept_html() is True def test_accepts_html(self): instance = AcceptInvalidHeader(header_value=', ') assert instance.accepts_html is True def test_acceptable_offers(self): instance = AcceptInvalidHeader(header_value=', ') returned = instance.acceptable_offers(offers=['a/b', 'c/d', 'e/f']) assert returned == [('a/b', 1.0), ('c/d', 1.0), ('e/f', 1.0)] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptInvalidHeader(header_value=', ') assert accept.best_match(['text/html', 'audio/basic']) == 'text/html' assert accept.best_match([('text/html', 1), ('audio/basic', 0.5)]) == \ 'text/html' assert accept.best_match([('text/html', 0.5), ('audio/basic', 1)]) == \ 'audio/basic' assert accept.best_match([('text/html', 0.5), 'audio/basic']) == \ 'audio/basic' assert accept.best_match( [('text/html', 0.5), 'audio/basic'], default_match=True ) == 'audio/basic' assert accept.best_match( [('text/html', 0.5), 'audio/basic'], default_match=False ) == 'audio/basic' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptInvalidHeader(header_value=', ') returned = instance.quality(offer='type/subtype') assert returned == 1.0 class TestCreateAcceptHeader(object): def test_header_value_is_None(self): header_value = None returned = create_accept_header(header_value=header_value) assert isinstance(returned, AcceptNoHeader) assert returned.header_value == header_value returned2 = create_accept_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value def test_header_value_is_valid(self): header_value = 'text/html, text/plain;q=0.9' returned = create_accept_header(header_value=header_value) assert isinstance(returned, AcceptValidHeader) assert returned.header_value == header_value returned2 = create_accept_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value @pytest.mark.parametrize('header_value', [', ', 'noslash']) def test_header_value_is_invalid(self, header_value): returned = create_accept_header(header_value=header_value) assert isinstance(returned, AcceptInvalidHeader) assert returned.header_value == header_value returned2 = create_accept_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value class TestAcceptProperty(object): def test_fget_header_is_valid(self): header_value = 'text/html;p1="1";p2=v2;q=0.9;e1="1";e2, audio/basic' request = Request.blank('/', environ={'HTTP_ACCEPT': header_value}) property_ = accept_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptValidHeader) assert returned.header_value == header_value def test_fget_header_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT': None}) property_ = accept_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptNoHeader) def test_fget_header_is_invalid(self): header_value = 'invalid' request = Request.blank('/', environ={'HTTP_ACCEPT': header_value}) property_ = accept_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptInvalidHeader) assert returned.header_value == header_value def test_fset_value_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) header_value = 'text/html;p1="1";p2=v2;q=0.9;e1="1";e2, audio/basic' property_ = accept_property() property_.fset(request=request, value=header_value) assert request.environ['HTTP_ACCEPT'] == header_value def test_fset_value_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) property_ = accept_property() property_.fset(request=request, value=None) assert 'HTTP_ACCEPT' not in request.environ def test_fset_value_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) header_value = 'invalid' property_ = accept_property() property_.fset(request=request, value=header_value) assert request.environ['HTTP_ACCEPT'] == header_value @pytest.mark.parametrize('value, value_as_header', [ ('', ''), ([], ''), ((), ''), ({}, ''), # str ( 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of strs ( ['a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 3-item tuples, with extension parameters ( [ ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # list of 2-item tuples, without extension parameters ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # list of a mixture of strs, 3-item tuples and 2-item tuples ( [ ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ], 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of strs ( ('a/b;q=0.5', 'c/d;p1=1;q=0', 'e/f', 'g/h;p1=1;q=1;e1=1'), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 3-item tuples, with extension parameters ( ( ('a/b', 0.5, ''), ('c/d;p1=1', 0.0, ''), ('e/f', 1.0, ''), ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # tuple of 2-item tuples, without extension parameters ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0), ('e/f', 1.0), ('g/h;p1=1', 1.0), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1', ), # tuple of a mixture of strs, 3-item tuples and 2-item tuples ( ( ('a/b', 0.5), ('c/d;p1=1', 0.0, ''), 'e/f', ('g/h;p1=1', 1.0, ';e1=1'), ), 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ), # dict ( { 'a/b': (0.5, ';e1=1'), 'c/d': 0.0, 'e/f;p1=1': (1.0, ';e1=1;e2=2') }, 'e/f;p1=1;q=1;e1=1;e2=2, a/b;q=0.5;e1=1, c/d;q=0', ), ]) def test_fset_value_types(self, value, value_as_header): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) property_ = accept_property() property_.fset(request=request, value=value) assert request.environ['HTTP_ACCEPT'] == value_as_header @pytest.mark.parametrize('header_value', [ '', 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1', ]) def test_fset_other_type_with___str__(self, header_value): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) property_ = accept_property() class Other(object): def __str__(self): return header_value value = Other() property_.fset(request=request, value=value) assert request.environ['HTTP_ACCEPT'] == str(value) def test_fset_AcceptValidHeader(self): request = Request.blank('/', environ={}) header_value = 'a/b;q=0.5, c/d;p1=1;q=0, e/f, g/h;p1=1;q=1;e1=1' header = AcceptValidHeader(header_value=header_value) property_ = accept_property() property_.fset(request=request, value=header) assert request.environ['HTTP_ACCEPT'] == header.header_value def test_fset_AcceptNoHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) property_ = accept_property() header = AcceptNoHeader() property_.fset(request=request, value=header) assert 'HTTP_ACCEPT' not in request.environ def test_fset_AcceptInvalidHeader(self): request = Request.blank('/', environ={}) header_value = 'invalid' header = AcceptInvalidHeader(header_value=header_value) property_ = accept_property() property_.fset(request=request, value=header) assert request.environ['HTTP_ACCEPT'] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank('/', environ={'HTTP_ACCEPT': 'text/html'}) property_ = accept_property() property_.fdel(request=request) assert 'HTTP_ACCEPT' not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank('/') property_ = accept_property() property_.fdel(request=request) assert 'HTTP_ACCEPT' not in request.environ class TestAcceptCharset(object): @pytest.mark.parametrize('value', [ '', '"', '(', ')', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}', 'foo, bar, baz;q= 0.001', 'foo , ,bar,charlie ', ]) def test_parse__invalid_header(self, value): with pytest.raises(ValueError): AcceptCharset.parse(value=value) @pytest.mark.parametrize('value, expected_list', [ ('*', [('*', 1.0)]), ("!#$%&'*+-.^_`|~;q=0.5", [("!#$%&'*+-.^_`|~", 0.5)]), ('0123456789', [('0123456789', 1.0)]), ( ',\t foo \t;\t q=0.345,, bar ; Q=0.456 \t, ,\tcharlie \t,, ,', [('foo', 0.345), ('bar', 0.456), ('charlie', 1.0)] ), ( 'iso-8859-5;q=0.372,unicode-1-1;q=0.977,UTF-8, *;q=0.000', [ ('iso-8859-5', 0.372), ('unicode-1-1', 0.977), ('UTF-8', 1.0), ('*', 0.0) ] ), # RFC 7230 Section 7 ('foo,bar', [('foo', 1.0), ('bar', 1.0)]), ('foo, bar,', [('foo', 1.0), ('bar', 1.0)]), # RFC 7230 Errata ID: 4169 ('foo , ,bar,charlie', [('foo', 1.0), ('bar', 1.0), ('charlie', 1.0)]), ]) def test_parse__valid_header(self, value, expected_list): returned = AcceptCharset.parse(value=value) list_of_returned = list(returned) assert list_of_returned == expected_list class TestAcceptCharsetValidHeader(object): def test_parse__inherited(self): returned = AcceptCharsetValidHeader.parse( value=',iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90,', ) list_of_returned = list(returned) assert list_of_returned == [ ('iso-8859-5', 0.333), ('utf-8', 1.0), ('unicode-1-1', 0.9), ] @pytest.mark.parametrize('header_value', [ '', ', iso-8859-5 ', ]) def test___init___invalid_header(self, header_value): with pytest.raises(ValueError): AcceptCharsetValidHeader(header_value=header_value) def test___init___valid_header(self): header_value = \ 'iso-8859-5;q=0.372,unicode-1-1;q=0.977,UTF-8, *;q=0.000' instance = AcceptCharsetValidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed == [ ('iso-8859-5', 0.372), ('unicode-1-1', 0.977), ('UTF-8', 1.0), ('*', 0.0) ] assert instance._parsed_nonzero == [ ('iso-8859-5', 0.372), ('unicode-1-1', 0.977), ('UTF-8', 1.0), ] assert isinstance(instance, AcceptCharset) def test___add___None(self): left_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') result = left_operand + None assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') class Other(object): def __str__(self): return str_ right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): left_operand = AcceptCharsetValidHeader( header_value=',\t ,iso-8859-5;q=0.333,', ) result = left_operand + value assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value + ', ' + \ value_as_header def test___add___other_type_with_valid___str__(self): left_operand = AcceptCharsetValidHeader( header_value=',\t ,iso-8859-5;q=0.333,', ) class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value + ', ' + \ str(right_operand) def test___add___AcceptCharsetValidHeader(self): left_operand = AcceptCharsetValidHeader( header_value=',\t ,iso-8859-5;q=0.333,', ) right_operand = AcceptCharsetValidHeader( header_value=', ,utf-7;q=0, \tutf-8;q=1,', ) result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == left_operand.header_value + ', ' + \ right_operand.header_value def test___add___AcceptCharsetNoHeader(self): valid_header_instance = AcceptCharsetValidHeader( header_value=', ,utf-7;q=0, \tutf-8;q=1,' ) result = valid_header_instance + AcceptCharsetNoHeader() assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance @pytest.mark.parametrize('header_value', ['', 'utf/8']) def test___add___AcceptCharsetInvalidHeader(self, header_value): valid_header_instance = AcceptCharsetValidHeader( header_value='header', ) result = valid_header_instance + AcceptCharsetInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance def test___bool__(self): instance = AcceptCharsetValidHeader(header_value='valid-header') returned = bool(instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): for mask in ['*', 'utf-8', 'UTF-8']: assert 'utf-8' in AcceptCharsetValidHeader(mask) assert 'utf-8' not in AcceptCharsetValidHeader('utf-7') @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains___not(self): accept = AcceptCharsetValidHeader('utf-8') assert 'utf-7' not in accept @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains___zero_quality(self): assert 'foo' not in AcceptCharsetValidHeader('*;q=0') @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptCharsetValidHeader( header_value=\ 'utf-8; q=0.5, utf-7; q=0, iso-8859-5; q=0.8, unicode-1-1', ) assert list(instance) == ['unicode-1-1', 'iso-8859-5', 'utf-8'] def test___radd___None(self): right_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') result = None + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptCharsetValidHeader(header_value='iso-8859-5') class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___radd___valid_value(self, value, value_as_header): right_operand = AcceptCharsetValidHeader( header_value=',\t ,iso-8859-5;q=0.333,', ) result = value + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == value_as_header + ', ' + \ right_operand.header_value def test___radd___other_type_with_valid___str__(self): right_operand = AcceptCharsetValidHeader( header_value=',\t ,iso-8859-5;q=0.333,', ) class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' left_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == str(left_operand) + ', ' + \ right_operand.header_value def test___repr__(self): instance = AcceptCharsetValidHeader( header_value=',utf-7;q=0.200,UTF-8;q=0.300', ) assert repr(instance) == \ "" def test___str__(self): header_value = ( ', \t,iso-8859-5;q=0.000 \t, utf-8;q=1.000, UTF-7, ' 'unicode-1-1;q=0.210 ,' ) instance = AcceptCharsetValidHeader(header_value=header_value) assert str(instance) == \ 'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21' @pytest.mark.parametrize('header_value, offers, returned', [ ('UTF-7, unicode-1-1', ['UTF-8', 'iso-8859-5'], []), ( 'utf-8, unicode-1-1, iSo-8859-5', ['UTF-8', 'iso-8859-5'], [('UTF-8', 1.0), ('iso-8859-5', 1.0)], ), ( 'utF-8;q=0.2, uniCode-1-1;q=0.9, iSo-8859-5;q=0.8', ['iso-8859-5', 'unicode-1-1', 'utf-8'], [('unicode-1-1', 0.9), ('iso-8859-5', 0.8), ('utf-8', 0.2)], ), ( 'utf-8, unicode-1-1;q=0.9, iSo-8859-5;q=0.9', ['iso-8859-5', 'utf-8', 'unicode-1-1'], [('utf-8', 1.0), ('iso-8859-5', 0.9), ('unicode-1-1', 0.9)], ), ('*', ['UTF-8', 'iso-8859-5'], [('UTF-8', 1.0), ('iso-8859-5', 1.0)]), ( '*;q=0.8', ['UTF-8', 'iso-8859-5'], [('UTF-8', 0.8), ('iso-8859-5', 0.8)], ), ('UTF-7, *', ['UTF-8', 'UTF-7'], [('UTF-8', 1.0), ('UTF-7', 1.0)]), ( 'UTF-7;q=0.5, *', ['UTF-7', 'UTF-8'], [('UTF-8', 1.0), ('UTF-7', 0.5)], ), ('UTF-8, *;q=0', ['UTF-7'], []), ('UTF-8, *;q=0', ['UTF-8'], [('UTF-8', 1.0)]), ('UTF-8;q=0, *', ['UTF-8'], []), ('UTF-8;q=0, *;q=0', ['UTF-8', 'UTF-7'], []), ('UTF-8, UTF-8;q=0', ['UTF-8'], [('UTF-8', 1.0)]), ( 'UTF-8, UTF-8;q=0, UTF-7', ['UTF-8', 'UTF-7'], [('UTF-8', 1.0), ('UTF-7', 1.0)] ), ( 'UTF-8;q=0.5, UTF-8;q=0.7, UTF-8;q=0.6, UTF-7', ['UTF-8', 'UTF-7'], [('UTF-7', 1.0), ('UTF-8', 0.5)], ), ( 'UTF-8;q=0.8, *;q=0.9, *;q=0', ['UTF-8', 'UTF-7'], [('UTF-7', 0.9), ('UTF-8', 0.8)] ), ( 'UTF-8;q=0.8, *;q=0, *;q=0.9', ['UTF-8', 'UTF-7'], [('UTF-8', 0.8)] ), ]) def test_acceptable_offers(self, header_value, offers, returned): instance = AcceptCharsetValidHeader(header_value=header_value) assert instance.acceptable_offers(offers=offers) == returned @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptCharsetValidHeader('utf-8, iso-8859-5') assert accept.best_match(['utf-8', 'iso-8859-5']) == 'utf-8' assert accept.best_match(['iso-8859-5', 'utf-8']) == 'iso-8859-5' assert accept.best_match([('iso-8859-5', 0.5), 'utf-8']) == 'utf-8' assert accept.best_match([('iso-8859-5', 0.5), ('utf-8', 0.4)]) == \ 'iso-8859-5' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_one_lower_q(self): accept = AcceptCharsetValidHeader('utf-8, iso-8859-5;q=0.5') assert accept.best_match(['utf-8', 'iso-8859-5']) == 'utf-8' accept = AcceptCharsetValidHeader('utf-8;q=0.5, iso-8859-5') assert accept.best_match(['utf-8', 'iso-8859-5']) == 'iso-8859-5' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_complex_q(self): accept = AcceptCharsetValidHeader( 'utf-8, iso-8859-5;q=0.55, utf-7;q=0.59' ) assert accept.best_match(['utf-8', 'iso-8859-5']) == 'utf-8' accept = AcceptCharsetValidHeader( 'utf-8;q=0.5, iso-8859-5;q=0.586, utf-7;q=0.596' ) assert accept.best_match(['utf-8', 'utf-7']) == 'utf-7' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_mixedcase(self): accept = AcceptCharsetValidHeader( 'uTf-8; q=0.2, UtF-7; Q=0.4, *; q=0.05' ) assert accept.best_match(['UtF-8']) == 'UtF-8' assert accept.best_match(['IsO-8859-5']) == 'IsO-8859-5' assert accept.best_match(['iSo-8859-5', 'uTF-7', 'UtF-8']) == 'uTF-7' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_best_match_zero_quality(self): assert AcceptCharsetValidHeader('utf-7, *;q=0').best_match( ['utf-8'] ) is None assert 'char-set' not in AcceptCharsetValidHeader('*;q=0') @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): accept = AcceptCharsetValidHeader('utf-8') assert accept.quality('utf-8') == 1.0 accept = AcceptCharsetValidHeader('utf-8;q=0.5') assert accept.quality('utf-8') == 0.5 @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality_not_found(self): accept = AcceptCharsetValidHeader('utf-8') assert accept.quality('iso-8859-5') is None class TestAcceptCharsetNoHeader(object): def test_parse__inherited(self): returned = AcceptCharsetNoHeader.parse( value=',iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90,', ) list_of_returned = list(returned) assert list_of_returned == [ ('iso-8859-5', 0.333), ('utf-8', 1.0), ('unicode-1-1', 0.9), ] def test___init__(self): instance = AcceptCharsetNoHeader() assert instance.header_value is None assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptCharset) def test___add___None(self): instance = AcceptCharsetNoHeader() result = instance + None assert isinstance(result, AcceptCharsetNoHeader) assert result is not instance @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptCharsetNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not left_operand @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptCharsetNoHeader() class Other(object): def __str__(self): return str_ result = left_operand + Other() assert isinstance(result, AcceptCharsetNoHeader) assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptCharsetNoHeader() + value assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str__(self): class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' right_operand = Other() result = AcceptCharsetNoHeader() + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptCharsetValidHeader(self): right_operand = AcceptCharsetValidHeader( header_value=', ,utf-7;q=0, \tutf-8;q=1,', ) result = AcceptCharsetNoHeader() + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptCharsetNoHeader(self): left_operand = AcceptCharsetNoHeader() right_operand = AcceptCharsetNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not left_operand assert result is not right_operand @pytest.mark.parametrize('header_value', ['', 'utf/8']) def test___add___AcceptCharsetInvalidHeader(self, header_value): left_operand = AcceptCharsetNoHeader() result = left_operand + AcceptCharsetInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptCharsetNoHeader) assert result is not left_operand def test___bool__(self): instance = AcceptCharsetNoHeader() returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptCharsetNoHeader() returned = ('char-set' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptCharsetNoHeader() returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptCharsetNoHeader() result = None + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not right_operand @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptCharsetNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not right_operand @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptCharsetNoHeader() class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___radd___valid_value(self, value, value_as_header): result = value + AcceptCharsetNoHeader() assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str__(self): class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' left_operand = Other() result = left_operand + AcceptCharsetNoHeader() assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptCharsetNoHeader() assert repr(instance) == '' def test___str__(self): instance = AcceptCharsetNoHeader() assert str(instance) == '' def test_acceptable_offers(self): instance = AcceptCharsetNoHeader() returned = instance.acceptable_offers( offers=['utf-8', 'utf-7', 'unicode-1-1'], ) assert returned == [ ('utf-8', 1.0), ('utf-7', 1.0), ('unicode-1-1', 1.0) ] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptCharsetNoHeader() assert accept.best_match(['utf-8', 'iso-8859-5']) == 'utf-8' assert accept.best_match([('utf-8', 1), ('iso-8859-5', 0.5)]) == \ 'utf-8' assert accept.best_match([('utf-8', 0.5), ('iso-8859-5', 1)]) == \ 'iso-8859-5' assert accept.best_match([('utf-8', 0.5), 'iso-8859-5']) == \ 'iso-8859-5' assert accept.best_match( [('utf-8', 0.5), 'iso-8859-5'], default_match=True ) == 'iso-8859-5' assert accept.best_match( [('utf-8', 0.5), 'iso-8859-5'], default_match=False ) == 'iso-8859-5' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptCharsetNoHeader() returned = instance.quality(offer='char-set') assert returned == 1.0 class TestAcceptCharsetInvalidHeader(object): def test_parse__inherited(self): returned = AcceptCharsetInvalidHeader.parse( value=',iso-8859-5 ; q=0.333 , ,utf-8,unicode-1-1 ;q=0.90,', ) list_of_returned = list(returned) assert list_of_returned == [ ('iso-8859-5', 0.333), ('utf-8', 1.0), ('unicode-1-1', 0.9), ] def test___init__(self): header_value = 'invalid header' instance = AcceptCharsetInvalidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptCharset) def test___add___None(self): instance = AcceptCharsetInvalidHeader(header_value='') result = instance + None assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___add___invalid_value(self, right_operand): result = AcceptCharsetInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___add___other_type_with_invalid___str__(self, str_): class Other(object): def __str__(self): return str_ result = AcceptCharsetInvalidHeader(header_value='') + Other() assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___add___valid_header_value(self, value, value_as_header): result = AcceptCharsetInvalidHeader(header_value='') + value assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == value_as_header def test___add___other_type_valid_header_value(self): class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' right_operand = Other() result = AcceptCharsetInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptCharsetValidHeader(self): right_operand = AcceptCharsetValidHeader( header_value=', ,utf-7;q=0, \tutf-8;q=1,', ) result = AcceptCharsetInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptCharsetNoHeader(self): right_operand = AcceptCharsetNoHeader() result = AcceptCharsetInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptCharsetNoHeader) assert result is not right_operand def test___add___AcceptCharsetInvalidHeader(self): result = AcceptCharsetInvalidHeader(header_value='') + \ AcceptCharsetInvalidHeader(header_value='utf/8') assert isinstance(result, AcceptCharsetNoHeader) def test___bool__(self): instance = AcceptCharsetInvalidHeader(header_value='') returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptCharsetInvalidHeader(header_value='') returned = ('char-set' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptCharsetInvalidHeader(header_value='') returned = list(instance) assert returned == [] def test___radd___None(self): result = None + AcceptCharsetInvalidHeader(header_value='') assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'UTF/8', ['UTF/8'], ('UTF/8',), {'UTF/8': 1.0}, ]) def test___radd___invalid_value(self, left_operand): result = left_operand + AcceptCharsetInvalidHeader(header_value='') assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('str_', ['', 'UTF/8']) def test___radd___other_type_with_invalid___str__(self, str_): class Other(object): def __str__(self): return str_ result = Other() + AcceptCharsetInvalidHeader(header_value='') assert isinstance(result, AcceptCharsetNoHeader) @pytest.mark.parametrize('value, value_as_header', [ ( 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( [('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'], 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( (('UTF-7', 0.5), ('unicode-1-1', 0.0), 'UTF-8'), 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8', ), ( {'UTF-7': 0.5, 'unicode-1-1': 0.0, 'UTF-8': 1.0}, 'UTF-8, UTF-7;q=0.5, unicode-1-1;q=0', ), ]) def test___radd___valid_header_value(self, value, value_as_header): result = value + AcceptCharsetInvalidHeader(header_value='') assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == value_as_header def test___radd___other_type_valid_header_value(self): class Other(object): def __str__(self): return 'UTF-7;q=0.5, unicode-1-1;q=0, UTF-8' left_operand = Other() result = left_operand + AcceptCharsetInvalidHeader(header_value='') assert isinstance(result, AcceptCharsetValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptCharsetInvalidHeader(header_value='\x00') assert repr(instance) == '' def test___str__(self): instance = AcceptCharsetInvalidHeader(header_value='') assert str(instance) == '' def test_acceptable_offers(self): instance = AcceptCharsetInvalidHeader(header_value='') returned = instance.acceptable_offers( offers=['utf-8', 'utf-7', 'unicode-1-1'], ) assert returned == [ ('utf-8', 1.0), ('utf-7', 1.0), ('unicode-1-1', 1.0) ] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptCharsetInvalidHeader(header_value='') assert accept.best_match(['utf-8', 'iso-8859-5']) == 'utf-8' assert accept.best_match([('utf-8', 1), ('iso-8859-5', 0.5)]) == \ 'utf-8' assert accept.best_match([('utf-8', 0.5), ('iso-8859-5', 1)]) == \ 'iso-8859-5' assert accept.best_match([('utf-8', 0.5), 'iso-8859-5']) == \ 'iso-8859-5' assert accept.best_match( [('utf-8', 0.5), 'iso-8859-5'], default_match=True ) == 'iso-8859-5' assert accept.best_match( [('utf-8', 0.5), 'iso-8859-5'], default_match=False ) == 'iso-8859-5' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptCharsetInvalidHeader(header_value='') returned = instance.quality(offer='char-set') assert returned == 1.0 class TestCreateAcceptCharsetHeader(object): def test_header_value_is_valid(self): header_value = 'iso-8859-5, unicode-1-1;q=0.8' returned = create_accept_charset_header(header_value=header_value) assert isinstance(returned, AcceptCharsetValidHeader) assert returned.header_value == header_value returned2 = create_accept_charset_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value def test_header_value_is_None(self): header_value = None returned = create_accept_charset_header(header_value=header_value) assert isinstance(returned, AcceptCharsetNoHeader) assert returned.header_value == header_value returned2 = create_accept_charset_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value @pytest.mark.parametrize('header_value', ['', 'iso-8859-5, unicode/1']) def test_header_value_is_invalid(self, header_value): returned = create_accept_charset_header(header_value=header_value) assert isinstance(returned, AcceptCharsetInvalidHeader) assert returned.header_value == header_value returned2 = create_accept_charset_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value class TestAcceptCharsetProperty(object): def test_fget_header_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': None}) property_ = accept_charset_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptCharsetNoHeader) def test_fget_header_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'UTF-8'}) property_ = accept_charset_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptCharsetValidHeader) def test_fget_header_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': ''}) property_ = accept_charset_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptCharsetInvalidHeader) def test_fset_value_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'UTF-8'}) property_ = accept_charset_property() property_.fset(request=request, value=None) assert isinstance(request.accept_charset, AcceptCharsetNoHeader) assert 'HTTP_ACCEPT_CHARSET' not in request.environ def test_fset_value_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'UTF-8'}) property_ = accept_charset_property() property_.fset(request=request, value='') assert isinstance(request.accept_charset, AcceptCharsetInvalidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == '' def test_fset_value_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'UTF-8'}) property_ = accept_charset_property() property_.fset(request=request, value='UTF-7') assert isinstance(request.accept_charset, AcceptCharsetValidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == 'UTF-7' @pytest.mark.parametrize('value, value_as_header', [ ( 'utf-8;q=0.5, iso-8859-5;q=0, utf-7', 'utf-8;q=0.5, iso-8859-5;q=0, utf-7', ), ( [('utf-8', 0.5), ('iso-8859-5', 0.0), 'utf-7'], 'utf-8;q=0.5, iso-8859-5;q=0, utf-7', ), ( (('utf-8', 0.5), ('iso-8859-5', 0.0), 'utf-7'), 'utf-8;q=0.5, iso-8859-5;q=0, utf-7', ), ( {'utf-8': 0.5, 'iso-8859-5': 0.0, 'utf-7': 1.0}, 'utf-7, utf-8;q=0.5, iso-8859-5;q=0', ), ]) def test_fset_value_types(self, value, value_as_header): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': ''}) property_ = accept_charset_property() property_.fset(request=request, value=value) assert isinstance(request.accept_charset, AcceptCharsetValidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': ''}) property_ = accept_charset_property() class Other(object): def __str__(self): return 'utf-8;q=0.5, iso-8859-5;q=0, utf-7' value = Other() property_.fset(request=request, value=value) assert isinstance(request.accept_charset, AcceptCharsetValidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == str(value) def test_fset_AcceptCharsetNoHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'utf-8'}) property_ = accept_charset_property() header = AcceptCharsetNoHeader() property_.fset(request=request, value=header) assert isinstance(request.accept_charset, AcceptCharsetNoHeader) assert 'HTTP_ACCEPT_CHARSET' not in request.environ def test_fset_AcceptCharsetValidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'utf-8'}) property_ = accept_charset_property() header = AcceptCharsetValidHeader('utf-7') property_.fset(request=request, value=header) assert isinstance(request.accept_charset, AcceptCharsetValidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == header.header_value def test_fset_AcceptCharsetInvalidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'utf-8'}) property_ = accept_charset_property() header = AcceptCharsetInvalidHeader('') property_.fset(request=request, value=header) assert isinstance(request.accept_charset, AcceptCharsetInvalidHeader) assert request.environ['HTTP_ACCEPT_CHARSET'] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank('/', environ={'HTTP_ACCEPT_CHARSET': 'utf-8'}) property_ = accept_charset_property() property_.fdel(request=request) assert isinstance(request.accept_charset, AcceptCharsetNoHeader) assert 'HTTP_ACCEPT_CHARSET' not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank('/') property_ = accept_charset_property() property_.fdel(request=request) assert isinstance(request.accept_charset, AcceptCharsetNoHeader) assert 'HTTP_ACCEPT_CHARSET' not in request.environ class TestAcceptEncoding(object): @pytest.mark.parametrize('value', [ '"', '(', ')', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}', ', ', ', , ', 'gzip;q=1.0, identity; q =0.5, *;q=0', ]) def test_parse__invalid_header(self, value): with pytest.raises(ValueError): AcceptEncoding.parse(value=value) @pytest.mark.parametrize('value, expected_list', [ (',', []), (', ,', []), ('*', [('*', 1.0)]), ("!#$%&'*+-.^_`|~;q=0.5", [("!#$%&'*+-.^_`|~", 0.5)]), ('0123456789', [('0123456789', 1.0)]), ( ',,\t foo \t;\t q=0.345,, bar ; Q=0.456 \t, ,\tCHARLIE \t,, ,', [('foo', 0.345), ('bar', 0.456), ('CHARLIE', 1.0)] ), # RFC 7231, section 5.3.4 ('compress, gzip', [('compress', 1.0), ('gzip', 1.0)]), ('', []), ('*', [('*', 1.0)]), ('compress;q=0.5, gzip;q=1.0', [('compress', 0.5), ('gzip', 1.0)]), ( 'gzip;q=1.0, identity; q=0.5, *;q=0', [('gzip', 1.0), ('identity', 0.5), ('*', 0.0)], ), ]) def test_parse__valid_header(self, value, expected_list): returned = AcceptEncoding.parse(value=value) list_of_returned = list(returned) assert list_of_returned == expected_list class TestAcceptEncodingValidHeader(object): def test_parse__inherited(self): returned = AcceptEncodingValidHeader.parse( value=',,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ,', ) list_of_returned = list(returned) assert list_of_returned == \ [('gzip', 1.0), ('identity', 0.5), ('*', 0.0)] @pytest.mark.parametrize('header_value', [ ', ', 'gzip;q=1.0, identity; q =0.5, *;q=0', ]) def test___init___invalid_header(self, header_value): with pytest.raises(ValueError): AcceptEncodingValidHeader(header_value=header_value) def test___init___valid_header(self): header_value = ',,\t gzip;q=1.0, identity; q=0, *;q=0.5 \t ,' instance = AcceptEncodingValidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed == [ ('gzip', 1.0), ('identity', 0.0), ('*', 0.5), ] assert instance._parsed_nonzero == [('gzip', 1.0), ('*', 0.5)] assert isinstance(instance, AcceptEncoding) def test___add___None(self): left_operand = AcceptEncodingValidHeader(header_value='gzip') result = left_operand + None assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptEncodingValidHeader(header_value='gzip') result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand def test___add___other_type_with_invalid___str__(self): left_operand = AcceptEncodingValidHeader(header_value='gzip') class Other(object): def __str__(self): return ', ' right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptEncodingValidHeader(header_value='gzip') result = left_operand + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptEncodingValidHeader(header_value='gzip') class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): header = ',\t ,gzip, identity;q=0.333,' result = AcceptEncodingValidHeader(header_value=header) + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == header + ', ' + value_as_header def test___add___other_type_with_valid___str___not_empty(self): header = ',\t ,gzip, identity;q=0.333,' class Other(object): def __str__(self): return 'compress;q=0.5, deflate;q=0, *' right_operand = Other() result = AcceptEncodingValidHeader(header_value=header) + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == header + ', ' + str(right_operand) def test___add___AcceptEncodingValidHeader_header_value_empty(self): left_operand = AcceptEncodingValidHeader( header_value=',\t ,gzip, identity;q=0.333,' ) right_operand = AcceptEncodingValidHeader(header_value='') result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): left_operand = AcceptEncodingValidHeader( header_value=',\t ,gzip, identity;q=0.333,', ) right_operand = AcceptEncodingValidHeader( header_value='compress;q=0.5, deflate;q=0, *', ) result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == left_operand.header_value + ', ' + \ right_operand.header_value def test___add___AcceptEncodingNoHeader(self): valid_header_instance = AcceptEncodingValidHeader(header_value='gzip') result = valid_header_instance + AcceptEncodingNoHeader() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance @pytest.mark.parametrize('header_value', [ ', ', 'compress;q=1.001', ]) def test___add___AcceptEncodingInvalidHeader(self, header_value): valid_header_instance = AcceptEncodingValidHeader(header_value='gzip') result = valid_header_instance + AcceptEncodingInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance def test___bool__(self): instance = AcceptEncodingValidHeader(header_value='gzip') returned = bool(instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): accept = AcceptEncodingValidHeader('gzip, compress') assert 'gzip' in accept assert 'deflate' not in accept for mask in ['*', 'gzip', 'gZIP']: assert 'gzip' in AcceptEncodingValidHeader(mask) @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptEncodingValidHeader( header_value='gzip; q=0.5, *; q=0, deflate; q=0.8, compress', ) assert list(instance) == ['compress', 'deflate', 'gzip'] def test___radd___None(self): right_operand = AcceptEncodingValidHeader(header_value='gzip') result = None + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptEncodingValidHeader(header_value='gzip') result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___radd___other_type_with_invalid___str__(self): right_operand = AcceptEncodingValidHeader(header_value='gzip') class Other(object): def __str__(self): return ', ' result = Other() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): right_operand = AcceptEncodingValidHeader(header_value='gzip') result = value + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___radd___other_type_with_valid___str___empty(self): right_operand = AcceptEncodingValidHeader(header_value='gzip') class Other(object): def __str__(self): return '' result = Other() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): header = ',\t ,gzip, identity;q=0.333,' result = value + AcceptEncodingValidHeader(header_value=header) assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == value_as_header + ', ' + header def test___radd___other_type_with_valid___str___not_empty(self): header = ',\t ,gzip, identity;q=0.333,' class Other(object): def __str__(self): return 'compress;q=0.5, deflate;q=0, *' left_operand = Other() result = left_operand + AcceptEncodingValidHeader(header_value=header) assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == str(left_operand) + ', ' + header @pytest.mark.parametrize('header_value, expected_returned', [ ('', ""), ( ",\t, a ;\t q=0.20 , b ,',", # single quote is valid character in token """""" ), ]) def test___repr__(self, header_value, expected_returned): instance = AcceptEncodingValidHeader(header_value=header_value) assert repr(instance) == expected_returned @pytest.mark.parametrize('header_value, expected_returned', [ ('', ''), (",\t, a ;\t q=0.20 , b ,',", "a;q=0.2, b, '"), ]) def test___str__(self, header_value, expected_returned): instance = AcceptEncodingValidHeader(header_value=header_value) assert str(instance) == expected_returned @pytest.mark.parametrize('header_value, offers, expected_returned', [ ('', [], []), ('gzip, compress', [], []), ('', ['gzip', 'deflate'], []), ('', ['gzip', 'identity'], [('identity', 1.0)]), ('compress, deflate, gzip', ['identity'], [('identity', 1.0)]), ('compress, identity;q=0, gzip', ['identity'], []), # *;q=0 does not make sense, but is valid ('*;q=0', ['identity'], []), ('*;q=0, deflate, gzip', ['identity'], []), ('*;q=0, deflate, identity;q=0, gzip', ['identity'], []), ( '*;q=0, deflate, identity;q=0.1, gzip', ['identity'], [('identity', 0.1)], ), ( 'compress, deflate, gzip', ['identity', 'gzip'], [('identity', 1.0), ('gzip', 1.0)], ), ( 'compress, deflate, gzip', ['gzip', 'identity'], [('gzip', 1.0), ('identity', 1.0)], ), ( 'IDentity;q=0.5, deflATE;q=0, gZIP;q=0, COMPress', ['GZip', 'DEFlate', 'IDENTity', 'comPRESS'], [('comPRESS', 1.0), ('IDENTity', 0.5)], ), ( 'compress;q=0, identity, *;q=0.5, identity;q=0, *;q=0, compress', # does not make sense, but is valid ['compress', 'identity', 'deflate', 'gzip'], [('identity', 1.0), ('deflate', 0.5), ('gzip', 0.5)], ), ]) def test_acceptable_offers( self, header_value, offers, expected_returned, ): instance = AcceptEncodingValidHeader(header_value=header_value) returned = instance.acceptable_offers(offers=offers) assert returned == expected_returned @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptEncodingValidHeader('gzip, iso-8859-5') assert accept.best_match(['gzip', 'iso-8859-5']) == 'gzip' assert accept.best_match(['iso-8859-5', 'gzip']) == 'iso-8859-5' assert accept.best_match([('iso-8859-5', 0.5), 'gzip']) == 'gzip' assert accept.best_match([('iso-8859-5', 0.5), ('gzip', 0.4)]) == \ 'iso-8859-5' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_one_lower_q(self): accept = AcceptEncodingValidHeader('gzip, compress;q=0.5') assert accept.best_match(['gzip', 'compress']) == 'gzip' accept = AcceptEncodingValidHeader('gzip;q=0.5, compress') assert accept.best_match(['gzip', 'compress']) == 'compress' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_with_complex_q(self): accept = AcceptEncodingValidHeader( 'gzip, compress;q=0.55, deflate;q=0.59' ) assert accept.best_match(['gzip', 'compress']) == 'gzip' accept = AcceptEncodingValidHeader( 'gzip;q=0.5, compress;q=0.586, deflate;q=0.596' ) assert accept.best_match(['gzip', 'deflate']) == 'deflate' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match_mixedcase(self): accept = AcceptEncodingValidHeader( 'gZiP; q=0.2, COMPress; Q=0.4, *; q=0.05' ) assert accept.best_match(['gzIP']) == 'gzIP' assert accept.best_match(['DeFlAte']) == 'DeFlAte' assert accept.best_match(['deflaTe', 'compRess', 'UtF-8']) == \ 'compRess' @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_best_match_zero_quality(self): assert AcceptEncodingValidHeader('deflate, *;q=0').best_match( ['gzip'] ) is None assert 'content-coding' not in AcceptEncodingValidHeader('*;q=0') @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): accept = AcceptEncodingValidHeader('gzip') assert accept.quality('gzip') == 1 accept = AcceptEncodingValidHeader('gzip;q=0.5') assert accept.quality('gzip') == 0.5 @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality_not_found(self): accept = AcceptEncodingValidHeader('gzip') assert accept.quality('compress') is None class TestAcceptEncodingNoHeader(object): def test_parse__inherited(self): returned = AcceptEncodingNoHeader.parse( value=',,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ,', ) list_of_returned = list(returned) assert list_of_returned == \ [('gzip', 1.0), ('identity', 0.5), ('*', 0.0)] def test___init__(self): instance = AcceptEncodingNoHeader() assert instance.header_value is None assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptEncoding) def test___add___None(self): left_operand = AcceptEncodingNoHeader() result = left_operand + None assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptEncodingNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) def test___add___other_type_with_invalid___str__(self): left_operand = AcceptEncodingNoHeader() class Other(object): def __str__(self): return ', ' right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptEncodingNoHeader() result = left_operand + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptEncodingNoHeader() class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptEncodingNoHeader() + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'compress;q=0.5, deflate;q=0, *' right_operand = Other() result = AcceptEncodingNoHeader() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptEncodingValidHeader_header_value_empty(self): right_operand = AcceptEncodingValidHeader(header_value='') result = AcceptEncodingNoHeader() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): right_operand = AcceptEncodingValidHeader( header_value='compress;q=0.5, deflate;q=0, *', ) result = AcceptEncodingNoHeader() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value def test___add___AcceptEncodingNoHeader(self): left_operand = AcceptEncodingNoHeader() right_operand = AcceptEncodingNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) assert result is not left_operand assert result is not right_operand @pytest.mark.parametrize('header_value', [ ', ', 'compress;q=1.001', ]) def test___add___AcceptEncodingInvalidHeader(self, header_value): left_operand = AcceptEncodingNoHeader() result = left_operand + AcceptEncodingInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptEncodingNoHeader) assert result is not left_operand def test___bool__(self): instance = AcceptEncodingNoHeader() returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptEncodingNoHeader() returned = ('content-coding' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptEncodingNoHeader() returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptEncodingNoHeader() result = None + right_operand assert isinstance(result, AcceptEncodingNoHeader) assert result is not right_operand @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptEncodingNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) assert result is not right_operand def test___radd___other_type_with_invalid___str__(self): right_operand = AcceptEncodingNoHeader() class Other(object): def __str__(self): return ', ' result = Other() + right_operand assert isinstance(result, AcceptEncodingNoHeader) assert result is not right_operand @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): result = value + AcceptEncodingNoHeader() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' def test___radd___other_type_with_valid___str___empty(self): class Other(object): def __str__(self): return '' result = Other() + AcceptEncodingNoHeader() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): result = value + AcceptEncodingNoHeader() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'compress;q=0.5, deflate;q=0, *' left_operand = Other() result = left_operand + AcceptEncodingNoHeader() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptEncodingNoHeader() assert repr(instance) == '' def test___str__(self): instance = AcceptEncodingNoHeader() assert str(instance) == '' def test_acceptable_offers(self): instance = AcceptEncodingNoHeader() returned = instance.acceptable_offers(offers=['a', 'b', 'c']) assert returned == [('a', 1.0), ('b', 1.0), ('c', 1.0)] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptEncodingNoHeader() assert accept.best_match(['gzip', 'compress']) == 'gzip' assert accept.best_match([('gzip', 1), ('compress', 0.5)]) == 'gzip' assert accept.best_match([('gzip', 0.5), ('compress', 1)]) == \ 'compress' assert accept.best_match([('gzip', 0.5), 'compress']) == 'compress' assert accept.best_match( [('gzip', 0.5), 'compress'], default_match=True ) == 'compress' assert accept.best_match( [('gzip', 0.5), 'compress'], default_match=False ) == 'compress' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptEncodingNoHeader() returned = instance.quality(offer='content-coding') assert returned == 1.0 class TestAcceptEncodingInvalidHeader(object): def test_parse__inherited(self): returned = AcceptEncodingInvalidHeader.parse( value=',,\t gzip;q=1.0, identity; q=0.5, *;q=0 \t ,', ) list_of_returned = list(returned) assert list_of_returned == \ [('gzip', 1.0), ('identity', 0.5), ('*', 0.0)] def test___init__(self): header_value = 'invalid header' instance = AcceptEncodingInvalidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptEncoding) def test___add___None(self): left_operand = AcceptEncodingInvalidHeader(header_value=', ') result = left_operand + None assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('right_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptEncodingInvalidHeader( header_value='invalid header', ) result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) def test___add___other_type_with_invalid___str__(self): left_operand = AcceptEncodingInvalidHeader( header_value='invalid header', ) class Other(object): def __str__(self): return ', ' right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___add___valid_empty_value(self, value): left_operand = AcceptEncodingInvalidHeader(header_value=', ') result = left_operand + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' def test___add___other_type_with_valid___str___empty(self): left_operand = AcceptEncodingInvalidHeader(header_value=', ') class Other(object): def __str__(self): return '' result = left_operand + Other() assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptEncodingInvalidHeader(header_value=', ') + value assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return '*, compress;q=0.5, deflate;q=0' right_operand = Other() result = AcceptEncodingInvalidHeader(header_value=', ') + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptEncodingValidHeader_header_value_empty(self): left_operand = AcceptEncodingInvalidHeader(header_value=', ') right_operand = AcceptEncodingValidHeader(header_value='') result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptEncodingValidHeader_header_value_not_empty(self): left_operand = AcceptEncodingInvalidHeader(header_value=', ') right_operand = AcceptEncodingValidHeader( header_value='compress;q=0.5, deflate;q=0, *', ) result = left_operand + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == right_operand.header_value def test___add___AcceptEncodingNoHeader(self): left_operand = AcceptEncodingInvalidHeader(header_value=', ') right_operand = AcceptEncodingNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) assert result is not right_operand @pytest.mark.parametrize('header_value', [ ', ', 'compress;q=1.001', ]) def test___add___AcceptEncodingInvalidHeader(self, header_value): result = AcceptEncodingInvalidHeader(header_value='gzip;;q=1') + \ AcceptEncodingInvalidHeader(header_value=header_value) assert isinstance(result, AcceptEncodingNoHeader) def test___bool__(self): instance = AcceptEncodingInvalidHeader(header_value=', ') returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptEncodingInvalidHeader(header_value=', ') returned = ('content-coding' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptEncodingInvalidHeader(header_value=', ') returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptEncodingInvalidHeader(header_value=', ') result = None + right_operand assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('left_operand', [ ', ', [', '], (', ',), {', ': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptEncodingInvalidHeader(header_value='gzip;q= 1') result = left_operand + right_operand assert isinstance(result, AcceptEncodingNoHeader) def test___radd___other_type_with_invalid___str__(self): right_operand = AcceptEncodingInvalidHeader(header_value='gzip;q= 1') class Other(object): def __str__(self): return ', ' result = Other() + right_operand assert isinstance(result, AcceptEncodingNoHeader) @pytest.mark.parametrize('value', [ '', [], (), {}, ]) def test___radd___valid_empty_value(self, value): right_operand = AcceptEncodingInvalidHeader(header_value=', ') result = value + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' def test___radd___other_type_with_valid___str___empty(self): right_operand = AcceptEncodingInvalidHeader(header_value=', ') class Other(object): def __str__(self): return '' result = Other() + right_operand assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == '' @pytest.mark.parametrize('value, value_as_header', [ ('compress;q=0.5, deflate;q=0, *', 'compress;q=0.5, deflate;q=0, *'), ( ['compress;q=0.5', 'deflate;q=0', '*'], 'compress;q=0.5, deflate;q=0, *', ), ( [('compress', 0.5), ('deflate', 0.0), ('*', 1.0)], 'compress;q=0.5, deflate;q=0, *', ), ( ('compress;q=0.5', 'deflate;q=0', '*'), 'compress;q=0.5, deflate;q=0, *', ), ( (('compress', 0.5), ('deflate', 0.0), ('*', 1.0)), 'compress;q=0.5, deflate;q=0, *', ), ( {'compress': 0.5, 'deflate': 0.0, '*': 1.0}, '*, compress;q=0.5, deflate;q=0', ), ]) def test___radd___valid_non_empty_value(self, value, value_as_header): result = value + AcceptEncodingInvalidHeader(header_value=', ') assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str___not_empty(self): class Other(object): def __str__(self): return 'compress;q=0.5, deflate;q=0, *' left_operand = Other() result = left_operand + AcceptEncodingInvalidHeader(header_value=', ') assert isinstance(result, AcceptEncodingValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptEncodingInvalidHeader(header_value='\x00') assert repr(instance) == '' def test___str__(self): instance = AcceptEncodingInvalidHeader(header_value=", ") assert str(instance) == '' def test_acceptable_offers(self): instance = AcceptEncodingInvalidHeader(header_value=', ') returned = instance.acceptable_offers(offers=['a', 'b', 'c']) assert returned == [('a', 1.0), ('b', 1.0), ('c', 1.0)] @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self): accept = AcceptEncodingInvalidHeader(header_value=', ') assert accept.best_match(['gzip', 'compress']) == 'gzip' assert accept.best_match([('gzip', 1), ('compress', 0.5)]) == 'gzip' assert accept.best_match([('gzip', 0.5), ('compress', 1)]) == \ 'compress' assert accept.best_match([('gzip', 0.5), 'compress']) == 'compress' assert accept.best_match( [('gzip', 0.5), 'compress'], default_match=True ) == 'compress' assert accept.best_match( [('gzip', 0.5), 'compress'], default_match=False ) == 'compress' assert accept.best_match([], default_match='fallback') == 'fallback' @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptEncodingInvalidHeader(header_value=', ') returned = instance.quality(offer='content-coding') assert returned == 1.0 class TestCreateAcceptEncodingHeader(object): def test_header_value_is_None(self): header_value = None returned = create_accept_encoding_header(header_value=header_value) assert isinstance(returned, AcceptEncodingNoHeader) assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value def test_header_value_is_valid(self): header_value = 'gzip, identity;q=0.9' returned = create_accept_encoding_header(header_value=header_value) assert isinstance(returned, AcceptEncodingValidHeader) assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value @pytest.mark.parametrize('header_value', [', ', 'gzip;q= 1']) def test_header_value_is_invalid(self, header_value): returned = create_accept_encoding_header(header_value=header_value) assert isinstance(returned, AcceptEncodingInvalidHeader) assert returned.header_value == header_value returned2 = create_accept_encoding_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value class TestAcceptEncodingProperty(object): def test_fget_header_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': None}) property_ = accept_encoding_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptEncodingNoHeader) def test_fget_header_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptEncodingValidHeader) def test_fget_header_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': ', '}) property_ = accept_encoding_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptEncodingInvalidHeader) def test_fset_value_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() property_.fset(request=request, value=None) assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) assert 'HTTP_ACCEPT_ENCODING' not in request.environ def test_fset_value_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() property_.fset(request=request, value=', ') assert isinstance(request.accept_encoding, AcceptEncodingInvalidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == ', ' def test_fset_value_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() property_.fset(request=request, value='compress') assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == 'compress' @pytest.mark.parametrize('value, value_as_header', [ ( 'gzip;q=0.5, compress;q=0, deflate', 'gzip;q=0.5, compress;q=0, deflate', ), ( [('gzip', 0.5), ('compress', 0.0), 'deflate'], 'gzip;q=0.5, compress;q=0, deflate', ), ( (('gzip', 0.5), ('compress', 0.0), 'deflate'), 'gzip;q=0.5, compress;q=0, deflate', ), ( {'gzip': 0.5, 'compress': 0.0, 'deflate': 1.0}, 'deflate, gzip;q=0.5, compress;q=0', ), ]) def test_fset_value_types(self, value, value_as_header): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': ''}) property_ = accept_encoding_property() property_.fset(request=request, value=value) assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': ''}) property_ = accept_encoding_property() class Other(object): def __str__(self): return 'gzip;q=0.5, compress;q=0, deflate' value = Other() property_.fset(request=request, value=value) assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == str(value) def test_fset_AcceptEncodingNoHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': ''}) property_ = accept_encoding_property() header = AcceptEncodingNoHeader() property_.fset(request=request, value=header) assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) assert 'HTTP_ACCEPT_ENCODING' not in request.environ def test_fset_AcceptEncodingValidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': ''}) property_ = accept_encoding_property() header = AcceptEncodingValidHeader('gzip') property_.fset(request=request, value=header) assert isinstance(request.accept_encoding, AcceptEncodingValidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == header.header_value def test_fset_AcceptEncodingInvalidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() header = AcceptEncodingInvalidHeader(', ') property_.fset(request=request, value=header) assert isinstance(request.accept_encoding, AcceptEncodingInvalidHeader) assert request.environ['HTTP_ACCEPT_ENCODING'] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank('/', environ={'HTTP_ACCEPT_ENCODING': 'gzip'}) property_ = accept_encoding_property() property_.fdel(request=request) assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) assert 'HTTP_ACCEPT_ENCODING' not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank('/') property_ = accept_encoding_property() property_.fdel(request=request) assert isinstance(request.accept_encoding, AcceptEncodingNoHeader) assert 'HTTP_ACCEPT_ENCODING' not in request.environ class TestAcceptLanguage(object): @pytest.mark.parametrize('value', [ '', '*s', '*-a', 'a-*', 'a' * 9, 'a-' + 'a' * 9, 'a-a-' + 'a' * 9, '-', 'a-', '-a', '---', '--a', '1-a', '1-a-a', 'en_gb', 'en/gb', 'foo, bar, baz;q= 0.001', 'foo , ,bar,charlie ', ]) def test_parse__invalid_header(self, value): with pytest.raises(ValueError): AcceptLanguage.parse(value=value) @pytest.mark.parametrize('value, expected_list', [ ('*', [('*', 1.0)]), ('fR;q=0.5', [('fR', 0.5)]), ('zh-Hant;q=0.500', [('zh-Hant', 0.5)]), ('zh-Hans-CN;q=1', [('zh-Hans-CN', 1.0)]), ('de-CH-x-phonebk;q=1.0', [('de-CH-x-phonebk', 1.0)]), ('az-Arab-x-AZE-derbend;q=1.00', [('az-Arab-x-AZE-derbend', 1.0)]), ('zh-CN-a-myExt-x-private;q=1.000', [('zh-CN-a-myExt-x-private', 1.0)]), ('aaaaaaaa', [('aaaaaaaa', 1.0)]), ('aaaaaaaa-a', [('aaaaaaaa-a', 1.0)]), ('aaaaaaaa-aaaaaaaa', [('aaaaaaaa-aaaaaaaa', 1.0)]), ('a-aaaaaaaa-aaaaaaaa', [('a-aaaaaaaa-aaaaaaaa', 1.0)]), ('aaaaaaaa-a-aaaaaaaa', [('aaaaaaaa-a-aaaaaaaa', 1.0)]), ( 'zh-Hant;q=0.372,zh-CN-a-myExt-x-private;q=0.977,de,*;q=0.000', [ ('zh-Hant', 0.372), ('zh-CN-a-myExt-x-private', 0.977), ('de', 1.0), ('*', 0.0) ] ), ( ',\t foo \t;\t q=0.345,, bar ; Q=0.456 \t, ,\tcharlie \t,, ,', [('foo', 0.345), ('bar', 0.456), ('charlie', 1.0)] ), # RFC 7230 Section 7 ('foo,bar', [('foo', 1.0), ('bar', 1.0)]), ('foo, bar,', [('foo', 1.0), ('bar', 1.0)]), # RFC 7230 Errata ID: 4169 ('foo , ,bar,charlie', [('foo', 1.0), ('bar', 1.0), ('charlie', 1.0)]), ]) def test_parse__valid_header(self, value, expected_list): returned = AcceptLanguage.parse(value=value) list_of_returned = list(returned) assert list_of_returned == expected_list class TestAcceptLanguageValidHeader(object): @pytest.mark.parametrize('header_value', [ '', ', da;q=0.2, en-gb;q=0.3 ', ]) def test___init___invalid_header(self, header_value): with pytest.raises(ValueError): AcceptLanguageValidHeader(header_value=header_value) def test___init___valid_header(self): header_value = \ 'zh-Hant;q=0.372,zh-CN-a-myExt-x-private;q=0.977,de,*;q=0.000' instance = AcceptLanguageValidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed == [ ('zh-Hant', 0.372), ('zh-CN-a-myExt-x-private', 0.977), ('de', 1.0), ('*', 0.0) ] assert instance._parsed_nonzero == [ ('zh-Hant', 0.372), ('zh-CN-a-myExt-x-private', 0.977), ('de', 1.0) ] assert isinstance(instance, AcceptLanguage) def test___add___None(self): left_operand = AcceptLanguageValidHeader(header_value='en') result = left_operand + None assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ',', [','], (',',), {',': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptLanguageValidHeader(header_value='en') result = left_operand + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('str_', ['', 'en_gb', ',']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptLanguageValidHeader(header_value='en') class Other(object): def __str__(self): return str_ right_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == left_operand.header_value assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ ('en-gb;q=0.5, fr;q=0, es', 'en-gb;q=0.5, fr;q=0, es'), ([('en-gb', 0.5), ('fr', 0.0), 'es'], 'en-gb;q=0.5, fr;q=0, es'), ((('en-gb', 0.5), ('fr', 0.0), 'es'), 'en-gb;q=0.5, fr;q=0, es'), ({'en-gb': 0.5, 'fr': 0.0, 'es': 1.0}, 'es, en-gb;q=0.5, fr;q=0'), ]) def test___add___valid_value(self, value, value_as_header): header = ',\t ,de, zh-Hans;q=0.333,' result = AcceptLanguageValidHeader(header_value=header) + value assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == header + ', ' + value_as_header def test___add___other_type_with_valid___str__(self): header = ',\t ,de, zh-Hans;q=0.333,' class Other(object): def __str__(self): return 'en-gb;q=0.5, fr;q=0, es' right_operand = Other() result = AcceptLanguageValidHeader(header_value=header) + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == header + ', ' + str(right_operand) def test___add___AcceptLanguageValidHeader(self): header1 = ',\t ,de, zh-Hans;q=0.333,' header2 = ', ,fr;q=0, \tes;q=1,' result = AcceptLanguageValidHeader(header_value=header1) + \ AcceptLanguageValidHeader(header_value=header2) assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == header1 + ', ' + header2 def test___add___AcceptLanguageNoHeader(self): valid_header_instance = AcceptLanguageValidHeader(header_value='es') result = valid_header_instance + AcceptLanguageNoHeader() assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance @pytest.mark.parametrize('header_value', ['', 'en_gb', ',']) def test___add___AcceptLanguageInvalidHeader(self, header_value): valid_header_instance = AcceptLanguageValidHeader( header_value='header', ) result = valid_header_instance + AcceptLanguageInvalidHeader( header_value=header_value, ) assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == valid_header_instance.header_value assert result is not valid_header_instance def test___bool__(self): instance = AcceptLanguageValidHeader(header_value='valid-header') returned = bool(instance) assert returned is True @pytest.mark.parametrize('header_value, offer', [ ('*', 'da'), ('da', 'DA'), ('en', 'en-gb'), ('en-gb', 'en-gb'), ('en-gb', 'en'), ('en-gb', 'en_GB'), ]) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains___in(self, header_value, offer): instance = AcceptLanguageValidHeader(header_value=header_value) assert offer in instance @pytest.mark.parametrize('header_value, offer', [ ('en-gb', 'en-us'), ('en-gb', 'fr-fr'), ('en-gb', 'fr'), ('en', 'fr-fr'), ]) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains___not_in(self, header_value, offer): instance = AcceptLanguageValidHeader(header_value=header_value) assert offer not in instance @pytest.mark.parametrize('header_value, expected_list', [ ('fr;q=0, jp;q=0', []), ('en-gb, da', ['en-gb', 'da']), ('en-gb;q=0.5, da;q=0.5', ['en-gb', 'da']), ( 'de;q=0.8, de-DE-1996;q=0.5, de-Deva;q=0, de-Latn-DE', ['de-Latn-DE', 'de', 'de-DE-1996'] ), # __iter__ is currently a simple filter for the ranges in the header # with non-0 qvalues, and does not attempt to account for the special # meanings of q=0 and *: ('en-gb;q=0, *', ['*']), ('de, de;q=0', ['de']), ]) @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self, header_value, expected_list): instance = AcceptLanguageValidHeader(header_value=header_value) assert list(instance) == expected_list def test___radd___None(self): right_operand = AcceptLanguageValidHeader(header_value='en') result = None + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ',', [','], (',',), {',': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptLanguageValidHeader(header_value='en') result = left_operand + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('str_', ['', 'en_gb', ',']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptLanguageValidHeader(header_value='en') class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ ('en-gb;q=0.5, fr;q=0, es', 'en-gb;q=0.5, fr;q=0, es'), ([('en-gb', 0.5), ('fr', 0.0), 'es'], 'en-gb;q=0.5, fr;q=0, es'), ((('en-gb', 0.5), ('fr', 0.0), 'es'), 'en-gb;q=0.5, fr;q=0, es'), ({'en-gb': 0.5, 'fr': 0.0, 'es': 1.0}, 'es, en-gb;q=0.5, fr;q=0'), ]) def test___radd___valid_value(self, value, value_as_header): right_operand = AcceptLanguageValidHeader( header_value=',\t ,de, zh-Hans;q=0.333,', ) result = value + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == value_as_header + ', ' + \ right_operand.header_value def test___radd___other_type_with_valid___str__(self): right_operand = AcceptLanguageValidHeader( header_value=',\t ,de, zh-Hans;q=0.333,', ) class Other(object): def __str__(self): return 'en-gb;q=0.5, fr;q=0, es' left_operand = Other() result = left_operand + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == str(left_operand) + ', ' + \ right_operand.header_value def test___repr__(self): instance = AcceptLanguageValidHeader( header_value=',da;q=0.200,en-gb;q=0.300', ) assert repr(instance) == \ "" def test___str__(self): header_value = \ ', \t,de;q=0.000 \t, es;q=1.000, zh, jp;q=0.210 ,' instance = AcceptLanguageValidHeader(header_value=header_value) assert str(instance) == 'de;q=0, es, zh, jp;q=0.21' @pytest.mark.parametrize( 'header_value, language_tags, expected_returned', [ # Example from RFC 4647, Section 3.4 ( 'de-de', ['de', 'de-DE-1996', 'de-Deva', 'de-Latn-DE'], [('de-DE-1996', 1.0)] ), # Empty `language_tags` ( 'a', [], [] ), # No matches ( 'a, b', ['c', 'd'], [] ), # Several ranges and tags, no matches ( 'a-b;q=0.9, c-d;q=0.5, e-f', ('a', 'b', 'c', 'd', 'e', 'f'), [] ), # Case-insensitive match ( 'foO, BaR', ['foo', 'bar'], [('foo', 1.0), ('bar', 1.0)] ), # If a tag matches a non-'*' range with q=0, tag is filtered out ( 'b-c, a, b;q=0, d;q=0', ['b-c', 'a', 'b-c-d', 'd-e-f'], [('a', 1.0)] ), # Match if a range exactly equals a tag ( 'd-e-f', ['a-b-c', 'd-e-f'], [('d-e-f', 1.0)] ), # Match if a range exactly equals a prefix of the tag such that the # first character following the prefix is '-' ( 'a-b-c-d, a-b-c-d-e, a-b-c-d-f-g-h', ['a-b-c-d-f-g'], [('a-b-c-d-f-g', 1.0)] ), # '*', when it is the only range in the header, matches everything ( '*', ['a', 'b'], [('a', 1.0), ('b', 1.0)] ), # '*' range matches only tags not matched by any other range ( '*;q=0.2, a;q=0.5, b', ['a-a', 'b-a', 'c-a', 'd-a'], [('b-a', 1.0), ('a-a', 0.5), ('c-a', 0.2), ('d-a', 0.2)] ), # '*' range without a qvalue gives a matched qvalue of 1.0 ( 'a;q=0.5, b, *', ['a-a', 'b-a', 'c-a', 'd-a'], [('b-a', 1.0), ('c-a', 1.0), ('d-a', 1.0), ('a-a', 0.5)] ), # The qvalue for the '*' range works the same way as qvalues for # non-'*' ranges. ( 'a;q=0.5, *;q=0.9', # (meaning: prefer anything other than 'a', with 'a' as a # fallback) ['a', 'b'], [('b', 0.9), ('a', 0.5)] ), # More than one range matching the same tag: range with the highest # qvalue is matched ( 'a-b-c;q=0.7, a;q=0.9, a-b;q=0.8', ['a-b-c'], [('a-b-c', 0.9)] ), # More than one range with the same qvalue matching the same tag: # the range in an earlier position in the header is matched ( 'a-b-c;q=0.7, a;q=0.9, b;q=0.9, a-b;q=0.9', ['a-b-c', 'b'], [('a-b-c', 0.9), ('b', 0.9)] ), # The returned list of tuples is sorted in descending order of qvalue ( 'a;q=0.7, b;q=0.3, c, d;q=0.5', ['d', 'c', 'b', 'a'], [('c', 1.0), ('a', 0.7), ('d', 0.5), ('b', 0.3)] ), # When qvalues are the same, the tag whose matched range appears # earlier in the header comes first ( 'a, c, b', ['b', 'a', 'c'], [('a', 1.0), ('c', 1.0), ('b', 1.0)] ), # When many tags match the same range (so same qvalue and same # matched range position in header), they are returned in order of # their position in the `language_tags` argument ( 'a', ['a-b', 'a', 'a-b-c'], [('a-b', 1.0), ('a', 1.0), ('a-b-c', 1.0)] ), # When a non-'*' range appears in the header more than once, we use # the first one for matching and ignore the others ( 'a;q=0.5, c;q=0.6, b;q=0.7, c;q=0.9', ['a', 'b', 'c'], [('b', 0.7), ('c', 0.6), ('a', 0.5)] ), ( 'a, b, c;q=0.5, c;q=0', ['a-a', 'b-a', 'c-a'], [('a-a', 1.0), ('b-a', 1.0), ('c-a', 0.5)] ), ( 'a;q=0.5, c;q=0.9, b;q=0.9, c;q=0.9', ['a', 'b', 'c'], [('c', 0.9), ('b', 0.9), ('a', 0.5)] ), # When the '*' range appears in the header more than once, we use # the first one for matching and ignore the others ( 'a;q=0.5, *;q=0.6, b;q=0.7, *;q=0.9', ['a', 'b', 'c'], [('b', 0.7), ('c', 0.6), ('a', 0.5)] ), ( 'a, b, *;q=0.5, *;q=0', ['a-a', 'b-a', 'c-a'], [('a-a', 1.0), ('b-a', 1.0), ('c-a', 0.5)] ), ( 'a;q=0.5, *;q=0.9, b;q=0.9, *;q=0.9', ['a', 'b', 'c'], [('c', 0.9), ('b', 0.9), ('a', 0.5)] ), # Both '*' and non-'*' ranges appearing more than once ( 'a-b;q=0.5, c-d, *, a-b, c-d;q=0.3, *;q=0', ['a-b-c', 'c-d-e', 'e-f-g'], [('c-d-e', 1.0), ('e-f-g', 1.0), ('a-b-c', 0.5)] ), ] ) def test_basic_filtering( self, header_value, language_tags, expected_returned, ): instance = AcceptLanguageValidHeader(header_value=header_value) returned = instance.basic_filtering(language_tags=language_tags) assert returned == expected_returned @pytest.mark.parametrize( 'header_value, offers, default_match, expected_returned', [ ('bar, *;q=0', ['foo'], None, None), ('en-gb, sr-Cyrl', ['sr-Cyrl', 'en-gb'], None, 'sr-Cyrl'), ('en-gb, sr-Cyrl', ['en-gb', 'sr-Cyrl'], None, 'en-gb'), ('en-gb, sr-Cyrl', [('sr-Cyrl', 0.5), 'en-gb'], None, 'en-gb'), ( 'en-gb, sr-Cyrl', [('sr-Cyrl', 0.5), ('en-gb', 0.4)], None, 'sr-Cyrl', ), ('en-gb, sr-Cyrl;q=0.5', ['en-gb', 'sr-Cyrl'], None, 'en-gb'), ('en-gb;q=0.5, sr-Cyrl', ['en-gb', 'sr-Cyrl'], None, 'sr-Cyrl'), ( 'en-gb, sr-Cyrl;q=0.55, es;q=0.59', ['en-gb', 'sr-Cyrl'], None, 'en-gb', ), ( 'en-gb;q=0.5, sr-Cyrl;q=0.586, es-419;q=0.597', ['en-gb', 'es-419'], None, 'es-419', ), ] ) @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match( self, header_value, offers, default_match, expected_returned, ): instance = AcceptLanguageValidHeader(header_value=header_value) returned = instance.best_match( offers=offers, default_match=default_match, ) assert returned == expected_returned def test_lookup_default_tag_and_default_cannot_both_be_None(self): instance = AcceptLanguageValidHeader(header_value='valid-header') with pytest.raises(TypeError): instance.lookup( language_tags=['tag'], default_range='language-range', default_tag=None, default=None, ) def test_lookup_default_range_cannot_be_asterisk(self): instance = AcceptLanguageValidHeader(header_value='valid-header') with pytest.raises(ValueError): instance.lookup( language_tags=['tag'], default_range='*', default_tag='default-tag', default=None, ) @pytest.mark.parametrize( ( 'header_value, language_tags, default_range, default_tag, default' ', expected' ), [ # Each language range in the header is considered in turn, in # descending order of qvalue ( 'aA;q=0.3, Bb, cC;q=0.7', ['Aa', 'bB', 'Cc'], None, 'default-tag', None, 'bB', ), # For ranges with the same qvalue, position in header is the # tiebreaker. ( 'bB-Cc;q=0.8, aA;q=0.9, Bb;q=0.9', ['bb', 'aa'], None, 'default-tag', None, 'aa', ), # Each language range represents the most specific tag that is an # acceptable match. Examples from RFC 4647, section 3.4, first # paragraph: ( 'de-ch', ['de-CH-1996', 'de-CH', 'de'], None, 'default-tag', None, 'de-CH', ), ( 'de-ch', ['de-CH-1996', 'de'], None, 'default-tag', None, 'de', ), # The language range is progressively truncated from the end until # a matching language tag is located. From the example of a Lookup # Fallback Pattern in RFC 4647, section 3.4: ( 'zh-Hant-CN-x-private1-private2', [ 'zh-Hant-CN-x-private1-private2', 'zh-Hant-CN-x-private1', 'zh-Hant-CN-x', 'zh-Hant-CN', 'zh-Hant', 'zh', ], None, 'default-tag', None, 'zh-Hant-CN-x-private1-private2', ), ( 'zh-Hant-CN-x-private1-private2', [ 'zh-Hant-CN-x-private1', 'zh-Hant-CN-x', 'zh-Hant-CN', 'zh-Hant', 'zh', ], None, 'default-tag', None, 'zh-Hant-CN-x-private1', ), ( 'zh-Hant-CN-x-private1-private2', [ 'zh-Hant-CN-x', 'zh-Hant-CN', 'zh-Hant', 'zh', ], None, 'default-tag', None, 'zh-Hant-CN', ), ( 'zh-Hant-CN-x-private1-private2', [ 'zh-Hant-CN', 'zh-Hant', 'zh', ], None, 'default-tag', None, 'zh-Hant-CN', ), ( 'zh-Hant-CN-x-private1-private2', [ 'zh-Hant', 'zh', ], None, 'default-tag', None, 'zh-Hant', ), ( 'zh-Hant-CN-x-private1-private2', ['zh'], None, 'default-tag', None, 'zh', ), ( 'zh-Hant-CN-x-private1-private2', ['some-other-tag-1', 'some-other-tag-2'], None, 'default-tag', None, 'default-tag', ), # Further tests to check that single-letter or -digit subtags are # removed at the same time as their closest trailing subtag: ( 'AA-T-subtag', ['Aa-t', 'aA'], None, 'default-tag', None, 'aA', ), ( 'AA-1-subtag', ['aa-1', 'aA'], None, 'default-tag', None, 'aA', ), ( 'Aa-P-subtag-8-subtag', ['Aa-p-subtag-8', 'Aa-p', 'aA'], None, 'default-tag', None, 'aA', ), ( 'aA-3-subTag-C-subtag', ['aA-3-subtag-c', 'aA-3', 'aA'], None, 'default-tag', None, 'aA', ), # Test that single-letter or -digit subtag in first position works # as expected ( 'T-subtag', ['t-SubTag', 'another'], None, 'default-tag', None, 't-SubTag', ), ( 'T-subtag', ['another'], None, 'default-tag', None, 'default-tag', ), # If the language range "*" is followed by other language ranges, # it is skipped. ( '*, Aa-aA-AA', ['bb', 'aA'], None, 'default-tag', None, 'aA', ), # If the language range "*" is the only one in the header, lookup # proceeds to the default arguments. ( '*', ['bb', 'aa'], None, 'default-tag', None, 'default-tag', ), # If no other language range follows the "*" in the header, lookup # proceeds to the default arguments. ( 'dd, cc, *', ['bb', 'aa'], None, 'default-tag', None, 'default-tag', ), # If a non-'*' range has q=0, any tag that matches the range # exactly (without subtag truncation) is not acceptable. ( 'aa, bB-Cc-DD;q=0, bB-Cc, cc', ['bb', 'bb-Cc-DD', 'bb-cC-dd', 'Bb-cc', 'bb-cC-dd'], None, 'default-tag', None, 'Bb-cc', ), # ;q=0 and ;q={not 0} both in header: q=0 takes precedence and # makes the exact match not acceptable, but the q={not 0} means # that tags can still match after subtag truncation. ( 'aa, bB-Cc-DD;q=0.9, cc, Bb-cC-dD;q=0', ['bb', 'Bb-Cc', 'Bb-cC-dD'], None, 'default-tag', None, 'Bb-Cc', ), # If none of the ranges in the header match any of the language # tags, and the `default_range` argument is not None and does not # match any q=0 range in the header, we search through it by # progressively truncating from the end, as we do with the ranges # in the header. Example from RFC 4647, section 3.4.1: ( 'fr-FR, zh-Hant', [ 'fr-FR', 'fr', 'zh-Hant', 'zh', 'ja-JP', 'ja', ], 'ja-JP', 'default-tag', None, 'fr-FR', ), ( 'fr-FR, zh-Hant', [ 'fr', 'zh-Hant', 'zh', 'ja-JP', 'ja', ], 'ja-JP', 'default-tag', None, 'fr', ), ( 'fr-FR, zh-Hant', [ 'zh-Hant', 'zh', 'ja-JP', 'ja', ], 'ja-JP', 'default-tag', None, 'zh-Hant', ), ( 'fr-FR, zh-Hant', [ 'zh', 'ja-JP', 'ja', ], 'ja-JP', 'default-tag', None, 'zh', ), ( 'fr-FR, zh-Hant', [ 'ja-JP', 'ja', ], 'ja-JP', 'default-tag', None, 'ja-JP', ), ( 'fr-FR, zh-Hant', ['ja'], 'ja-JP', 'default-tag', None, 'ja', ), ( 'fr-FR, zh-Hant', ['some-other-tag-1', 'some-other-tag-2'], 'ja-JP', 'default-tag', None, 'default-tag', ), # If none of the ranges in the header match the language tags, the # `default_range` argument is not None, and there is a '*;q=0' # range in the header, then the `default_range` and its substrings # from subtag truncation are not acceptable. ( 'aa-bb, cc-dd, *;q=0', ['ee-ff', 'ee'], 'ee-ff', None, 'default', 'default', ), # If none of the ranges in the header match the language tags, the # `default_range` argument is not None, and the argument exactly # matches a non-'*' range in the header with q=0 (without fallback # subtag truncation), then the `default_range` itself is not # acceptable... ( 'aa-bb, cc-dd, eE-Ff;q=0', ['Ee-fF'], 'EE-FF', 'default-tag', None, 'default-tag', ), # ...but it should still be searched with subtag truncation, # because its substrings other than itself are still acceptable: ( 'aa-bb, cc-dd, eE-Ff-Gg;q=0', ['Ee', 'Ee-fF-gG', 'Ee-fF'], 'EE-FF-GG', 'default-tag', None, 'Ee-fF', ), ( 'aa-bb, cc-dd, eE-Ff-Gg;q=0', ['Ee-fF-gG', 'Ee'], 'EE-FF-GG', 'default-tag', None, 'Ee', ), # If `default_range` only has one subtag, then no subtag truncation # is possible, and we proceed to `default-tag`: ( 'aa-bb, cc-dd, eE;q=0', ['Ee'], 'EE', 'default-tag', None, 'default-tag', ), # If the `default_range` argument would only match a non-'*' range # in the header with q=0 exactly if the `default_range` had subtags # from the end truncated, then it is acceptable, and we attempt to # match it with the language tags using subtag truncation. However, # the tag equivalent of the range with q=0 would be considered not # acceptable and ruled out, if we reach it during the subtag # truncation search. ( 'aa-bb, cc-dd, eE-Ff;q=0', ['Ee-fF', 'Ee-fF-33', 'ee'], 'EE-FF-33', 'default-tag', None, 'Ee-fF-33', ), ( 'aa-bb, cc-dd, eE-Ff;q=0', ['Ee-fF', 'eE'], 'EE-FF-33', 'default-tag', None, 'eE', ), # If none of the ranges in the header match, the `default_range` # argument is None or does not match, and the `default_tag` # argument is not None and does not match any range in the header # with q=0, then the `default_tag` argument is returned. ( 'aa-bb, cc-dd', ['ee-ff', 'ee'], None, 'default-tag', None, 'default-tag', ), ( 'aa-bb, cc-dd', ['ee-ff', 'ee'], 'gg-hh', 'default-tag', None, 'default-tag', ), # If none of the ranges in the header match, the `default_range` # argument is None or does not match, the `default_tag` argument is # not None, and there is a '*' range in the header with q=0, then # the `default_tag` argument is not acceptable. ( 'aa-bb, cc-dd, *;q=0', ['ee-ff', 'ee'], 'gg-hh', 'ii-jj', 'default', 'default', ), # If none of the ranges in the header match, the `default_range` # argument is None or does not match, the `default_tag` argument is # not None and matches a non-'*' range in the header with q=0 # exactly, then the `default_tag` argument is not acceptable. ( 'aa-bb, cc-dd, iI-jJ;q=0', ['ee-ff', 'ee'], 'gg-hh', 'Ii-Jj', 'default', 'default', ), # If none of the ranges in the header match, the `default_range` # argument is None or does not match, and the `default_tag` # argument is None, then we proceed to the `default` argument. ( 'aa-bb, cc-dd', ['ee-ff', 'ee'], None, None, 'default', 'default', ), ( 'aa-bb, cc-dd', ['ee-ff', 'ee'], 'gg-hh', None, 'default', 'default', ), # If we fall back to the `default` argument, and it is not a # callable, the argument itself is returned. ( 'aa', ['bb'], None, None, 0, 0, ), ( 'Aa, cC;q=0', ['bb'], 'aA-Cc', 'Cc', ['non-callable object'], ['non-callable object'], ), # If we fall back to the `default` argument, and it is a callable, # it is called, and the callable's return value is returned by the # method. ( 'aa', ['bb'], None, None, lambda: 'callable called', 'callable called', ), ( 'Aa, cc;q=0', ['bb'], 'aA-cC', 'cc', lambda: 'callable called', 'callable called', ), # Even if the 'default' argument is a str that matches a q=0 range # in the header, it is still returned. ( 'aa, *;q=0', ['bb'], None, None, 'cc', 'cc', ), ( 'aa, cc;q=0', ['bb'], None, None, 'cc', 'cc', ), # If the `default_tag` argument is not acceptable because of a q=0 # range in the header, and the `default` argument is None, then # None is returned. ( 'aa, Bb;q=0', ['cc'], None, 'bB', None, None, ), ( 'aa, *;q=0', ['cc'], None, 'bb', None, None, ), # Test that method works with empty `language_tags`: ( 'range', [], None, 'default-tag', None, 'default-tag', ), # Test that method works with empty `default_range`: ( 'range', [], '', 'default-tag', None, 'default-tag', ), ( 'range', ['tag'], '', 'default-tag', None, 'default-tag', ), # Test that method works with empty `default_tag`: ( 'range', [], '', '', None, '', ), ( 'range', ['tag'], 'default-range', '', None, '', ), ] ) def test_lookup( self, header_value, language_tags, default_range, default_tag, default, expected, ): instance = AcceptLanguageValidHeader(header_value=header_value) returned = instance.lookup( language_tags=language_tags, default_range=default_range, default_tag=default_tag, default=default, ) assert returned == expected @pytest.mark.parametrize('header_value, offer, expected_returned', [ ('en-gb', 'en-gb', 1), ('en-gb;q=0.5', 'en-gb', 0.5), ('en-gb', 'sr-Cyrl', None), ]) @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self, header_value, offer, expected_returned): instance = AcceptLanguageValidHeader(header_value=header_value) returned = instance.quality(offer=offer) assert returned == expected_returned class TestAcceptLanguageNoHeader(object): def test___init__(self): instance = AcceptLanguageNoHeader() assert instance.header_value is None assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptLanguage) def test___add___None(self): instance = AcceptLanguageNoHeader() result = instance + None assert isinstance(result, AcceptLanguageNoHeader) assert result is not instance @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ]) def test___add___invalid_value(self, right_operand): left_operand = AcceptLanguageNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not left_operand @pytest.mark.parametrize('str_', ['', 'en_gb']) def test___add___other_type_with_invalid___str__(self, str_,): left_operand = AcceptLanguageNoHeader() class Other(object): def __str__(self): return str_ result = left_operand + Other() assert isinstance(result, AcceptLanguageNoHeader) assert result is not left_operand @pytest.mark.parametrize('value, value_as_header', [ ('en-gb;q=0.5, fr;q=0, es', 'en-gb;q=0.5, fr;q=0, es'), ([('en-gb', 0.5), ('fr', 0.0), 'es'], 'en-gb;q=0.5, fr;q=0, es'), ((('en-gb', 0.5), ('fr', 0.0), 'es'), 'en-gb;q=0.5, fr;q=0, es'), ({'en-gb': 0.5, 'fr': 0.0, 'es': 1.0}, 'es, en-gb;q=0.5, fr;q=0'), ]) def test___add___valid_value(self, value, value_as_header): result = AcceptLanguageNoHeader() + value assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == value_as_header def test___add___other_type_with_valid___str__(self): class Other(object): def __str__(self): return 'en-gb;q=0.5, fr;q=0, es' right_operand = Other() result = AcceptLanguageNoHeader() + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == str(right_operand) def test___add___AcceptLanguageValidHeader(self): right_operand = AcceptLanguageValidHeader( header_value=', ,fr;q=0, \tes;q=1,', ) result = AcceptLanguageNoHeader() + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == right_operand.header_value def test___add___AcceptLanguageNoHeader(self): left_operand = AcceptLanguageNoHeader() right_operand = AcceptLanguageNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not left_operand assert result is not right_operand @pytest.mark.parametrize('invalid_header_value', ['', 'en_gb']) def test___add___AcceptLanguageInvalidHeader(self, invalid_header_value): left_operand = AcceptLanguageNoHeader() result = left_operand + AcceptLanguageInvalidHeader( header_value=invalid_header_value, ) assert isinstance(result, AcceptLanguageNoHeader) assert result is not left_operand def test___bool__(self): instance = AcceptLanguageNoHeader() returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptLanguageNoHeader() returned = ('any-tag' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptLanguageNoHeader() returned = list(instance) assert returned == [] def test___radd___None(self): right_operand = AcceptLanguageNoHeader() result = None + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not right_operand @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ]) def test___radd___invalid_value(self, left_operand): right_operand = AcceptLanguageNoHeader() result = left_operand + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not right_operand @pytest.mark.parametrize('str_', ['', 'en_gb', ',']) def test___radd___other_type_with_invalid___str__(self, str_,): right_operand = AcceptLanguageNoHeader() class Other(object): def __str__(self): return str_ result = Other() + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not right_operand @pytest.mark.parametrize('value, value_as_header', [ ('en-gb;q=0.5, fr;q=0, es', 'en-gb;q=0.5, fr;q=0, es'), ([('en-gb', 0.5), ('fr', 0.0), 'es'], 'en-gb;q=0.5, fr;q=0, es'), ((('en-gb', 0.5), ('fr', 0.0), 'es'), 'en-gb;q=0.5, fr;q=0, es'), ({'en-gb': 0.5, 'fr': 0.0, 'es': 1.0}, 'es, en-gb;q=0.5, fr;q=0'), ]) def test___radd___valid_value(self, value, value_as_header): result = value + AcceptLanguageNoHeader() assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == value_as_header def test___radd___other_type_with_valid___str__(self): class Other(object): def __str__(self): return 'en-gb;q=0.5, fr;q=0, es' left_operand = Other() result = left_operand + AcceptLanguageNoHeader() assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == str(left_operand) def test___repr__(self): instance = AcceptLanguageNoHeader() assert repr(instance) == '' def test___str__(self): instance = AcceptLanguageNoHeader() assert str(instance) == '' def test_basic_filtering(self): instance = AcceptLanguageNoHeader() returned = instance.basic_filtering(language_tags=['tag1', 'tag2']) assert returned == [] @pytest.mark.parametrize('offers, default_match, expected_returned', [ (['foo', 'bar'], None, 'foo'), ([('foo', 1), ('bar', 0.5)], None, 'foo'), ([('foo', 0.5), ('bar', 1)], None, 'bar'), ([('foo', 0.5), 'bar'], None, 'bar'), ([('foo', 0.5), 'bar'], object(), 'bar'), ([], 'fallback', 'fallback'), ]) @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self, offers, default_match, expected_returned): instance = AcceptLanguageNoHeader() returned = instance.best_match( offers=offers, default_match=default_match, ) assert returned == expected_returned def test_lookup_default_tag_and_default_cannot_both_be_None(self): instance = AcceptLanguageNoHeader() with pytest.raises(TypeError): instance.lookup(default_tag=None, default=None) @pytest.mark.parametrize('default_tag, default, expected', [ # If `default_tag` is not None, it is returned. ('default-tag', 'default', 'default-tag'), # If `default_tag` is None, we proceed to the `default` argument. If # `default` is not a callable, the argument itself is returned. (None, 0, 0), # If `default` is a callable, it is called, and the callable's return # value is returned by the method. (None, lambda: 'callable called', 'callable called'), ]) def test_lookup(self, default_tag, default, expected): instance = AcceptLanguageNoHeader() returned = instance.lookup( default_tag=default_tag, default=default, ) assert returned == expected @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptLanguageNoHeader() returned = instance.quality(offer='any-tag') assert returned == 1.0 class TestAcceptLanguageInvalidHeader(object): def test___init__(self): header_value = 'invalid header' instance = AcceptLanguageInvalidHeader(header_value=header_value) assert instance.header_value == header_value assert instance.parsed is None assert instance._parsed_nonzero is None assert isinstance(instance, AcceptLanguage) def test___add___None(self): instance = AcceptLanguageInvalidHeader(header_value='') result = instance + None assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('right_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ]) def test___add___invalid_value(self, right_operand): result = AcceptLanguageInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('str_', ['', 'en_gb']) def test___add___other_type_with_invalid___str__(self, str_): class Other(object): def __str__(self): return str_ result = AcceptLanguageInvalidHeader(header_value='') + Other() assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('value', [ 'en', ['en'], ('en',), {'en': 1.0}, ]) def test___add___valid_header_value(self, value): result = AcceptLanguageInvalidHeader(header_value='') + value assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == 'en' def test___add___other_type_valid_header_value(self): class Other(object): def __str__(self): return 'en' result = AcceptLanguageInvalidHeader(header_value='') + Other() assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == 'en' def test___add___AcceptLanguageValidHeader(self): right_operand = AcceptLanguageValidHeader(header_value='en') result = AcceptLanguageInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == right_operand.header_value assert result is not right_operand def test___add___AcceptLanguageNoHeader(self): right_operand = AcceptLanguageNoHeader() result = AcceptLanguageInvalidHeader(header_value='') + right_operand assert isinstance(result, AcceptLanguageNoHeader) assert result is not right_operand def test___add___AcceptLanguageInvalidHeader(self): result = AcceptLanguageInvalidHeader(header_value='') + \ AcceptLanguageInvalidHeader(header_value='') assert isinstance(result, AcceptLanguageNoHeader) def test___bool__(self): instance = AcceptLanguageInvalidHeader(header_value='') returned = bool(instance) assert returned is False @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test___contains__(self): instance = AcceptLanguageInvalidHeader(header_value='') returned = ('any-tag' in instance) assert returned is True @pytest.mark.filterwarnings(IGNORE_ITER) def test___iter__(self): instance = AcceptLanguageInvalidHeader(header_value='') returned = list(instance) assert returned == [] def test___radd___None(self): instance = AcceptLanguageInvalidHeader(header_value='') result = None + instance assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('left_operand', [ '', [], (), {}, 'en_gb', ['en_gb'], ('en_gb',), {'en_gb': 1.0}, ]) def test___radd___invalid_value(self, left_operand): result = left_operand + AcceptLanguageInvalidHeader(header_value='') assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('str_', ['', 'en_gb']) def test___radd___other_type_with_invalid___str__(self, str_): class Other(object): def __str__(self): return str_ result = Other() + AcceptLanguageInvalidHeader(header_value='') assert isinstance(result, AcceptLanguageNoHeader) @pytest.mark.parametrize('value', [ 'en', ['en'], ('en',), {'en': 1.0}, ]) def test___radd___valid_header_value(self, value): result = value + AcceptLanguageInvalidHeader(header_value='') assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == 'en' def test___radd___other_type_valid_header_value(self): class Other(object): def __str__(self): return 'en' result = Other() + AcceptLanguageInvalidHeader(header_value='') assert isinstance(result, AcceptLanguageValidHeader) assert result.header_value == 'en' def test___repr__(self): instance = AcceptLanguageInvalidHeader(header_value='\x00') assert repr(instance) == '' def test___str__(self): instance = AcceptLanguageInvalidHeader(header_value="invalid header") assert str(instance) == '' def test_basic_filtering(self): instance = AcceptLanguageInvalidHeader(header_value='') returned = instance.basic_filtering(language_tags=['tag1', 'tag2']) assert returned == [] @pytest.mark.parametrize('offers, default_match, expected_returned', [ (['foo', 'bar'], None, 'foo'), ([('foo', 1), ('bar', 0.5)], None, 'foo'), ([('foo', 0.5), ('bar', 1)], None, 'bar'), ([('foo', 0.5), 'bar'], None, 'bar'), ([('foo', 0.5), 'bar'], object(), 'bar'), ([], 'fallback', 'fallback'), ]) @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_best_match(self, offers, default_match, expected_returned): instance = AcceptLanguageInvalidHeader(header_value='') returned = instance.best_match( offers=offers, default_match=default_match, ) assert returned == expected_returned def test_lookup_default_tag_and_default_cannot_both_be_None(self): instance = AcceptLanguageInvalidHeader(header_value='') with pytest.raises(TypeError): instance.lookup(default_tag=None, default=None) @pytest.mark.parametrize('default_tag, default, expected', [ # If `default_tag` is not None, it is returned. ('default-tag', 'default', 'default-tag'), # If `default_tag` is None, we proceed to the `default` argument. If # `default` is not a callable, the argument itself is returned. (None, 0, 0), # If `default` is a callable, it is called, and the callable's return # value is returned by the method. (None, lambda: 'callable called', 'callable called'), ]) def test_lookup(self, default_tag, default, expected): instance = AcceptLanguageInvalidHeader(header_value='') returned = instance.lookup( default_tag=default_tag, default=default, ) assert returned == expected @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_quality(self): instance = AcceptLanguageInvalidHeader(header_value='') returned = instance.quality(offer='any-tag') assert returned == 1.0 class TestCreateAcceptLanguageHeader(object): def test_header_value_is_None(self): header_value = None returned = create_accept_language_header(header_value=header_value) assert isinstance(returned, AcceptLanguageNoHeader) assert returned.header_value == header_value returned2 = create_accept_language_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value def test_header_value_is_valid(self): header_value = 'es, ja' returned = create_accept_language_header(header_value=header_value) assert isinstance(returned, AcceptLanguageValidHeader) assert returned.header_value == header_value returned2 = create_accept_language_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value @pytest.mark.parametrize('header_value', ['', 'en_gb']) def test_header_value_is_invalid(self, header_value): returned = create_accept_language_header(header_value=header_value) assert isinstance(returned, AcceptLanguageInvalidHeader) assert returned.header_value == header_value returned2 = create_accept_language_header(returned) assert returned2 is not returned assert returned2._header_value == returned._header_value class TestAcceptLanguageProperty(object): def test_fget_header_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': None}) property_ = accept_language_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptLanguageNoHeader) def test_fget_header_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'es'}) property_ = accept_language_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptLanguageValidHeader) def test_fget_header_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'en_gb'}) property_ = accept_language_property() returned = property_.fget(request=request) assert isinstance(returned, AcceptLanguageInvalidHeader) def test_fset_value_is_None(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'es'}) property_ = accept_language_property() property_.fset(request=request, value=None) assert isinstance(request.accept_language, AcceptLanguageNoHeader) assert 'HTTP_ACCEPT_LANGUAGE' not in request.environ def test_fset_value_is_invalid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'es'}) property_ = accept_language_property() property_.fset(request=request, value='en_GB') assert isinstance(request.accept_language, AcceptLanguageInvalidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == 'en_GB' def test_fset_value_is_valid(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'es'}) property_ = accept_language_property() property_.fset(request=request, value='en-GB') assert isinstance(request.accept_language, AcceptLanguageValidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == 'en-GB' @pytest.mark.parametrize('value, value_as_header', [ ('en-gb;q=0.5, fr;q=0, es', 'en-gb;q=0.5, fr;q=0, es'), ([('en-gb', 0.5), ('fr', 0.0), 'es'], 'en-gb;q=0.5, fr;q=0, es'), ((('en-gb', 0.5), ('fr', 0.0), 'es'), 'en-gb;q=0.5, fr;q=0, es'), ({'en-gb': 0.5, 'fr': 0.0, 'es': 1.0}, 'es, en-gb;q=0.5, fr;q=0'), ]) def test_fset_value_types(self, value, value_as_header): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': ''}) property_ = accept_language_property() property_.fset(request=request, value=value) assert isinstance(request.accept_language, AcceptLanguageValidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == value_as_header def test_fset_other_type_with_valid___str__(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': ''}) property_ = accept_language_property() class Other(object): def __str__(self): return 'en-gb;q=0.5, fr;q=0, es' value = Other() property_.fset(request=request, value=value) assert isinstance(request.accept_language, AcceptLanguageValidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == str(value) def test_fset_AcceptLanguageNoHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'en'}) property_ = accept_language_property() header = AcceptLanguageNoHeader() property_.fset(request=request, value=header) assert isinstance(request.accept_language, AcceptLanguageNoHeader) assert 'HTTP_ACCEPT_LANGUAGE' not in request.environ def test_fset_AcceptLanguageValidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': ''}) property_ = accept_language_property() header = AcceptLanguageValidHeader('es') property_.fset(request=request, value=header) assert isinstance(request.accept_language, AcceptLanguageValidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == header.header_value def test_fset_AcceptLanguageInvalidHeader(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': ''}) property_ = accept_language_property() header = AcceptLanguageInvalidHeader('en_gb') property_.fset(request=request, value=header) assert isinstance(request.accept_language, AcceptLanguageInvalidHeader) assert request.environ['HTTP_ACCEPT_LANGUAGE'] == header.header_value def test_fdel_header_key_in_environ(self): request = Request.blank('/', environ={'HTTP_ACCEPT_LANGUAGE': 'es'}) property_ = accept_language_property() property_.fdel(request=request) assert isinstance(request.accept_language, AcceptLanguageNoHeader) assert 'HTTP_ACCEPT_LANGUAGE' not in request.environ def test_fdel_header_key_not_in_environ(self): request = Request.blank('/') property_ = accept_language_property() property_.fdel(request=request) assert isinstance(request.accept_language, AcceptLanguageNoHeader) assert 'HTTP_ACCEPT_LANGUAGE' not in request.environ # Deprecated tests: @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_init_warns(): with warnings.catch_warnings(record=True) as warning: warnings.simplefilter("always") MIMEAccept('image/jpg') assert len(warning) == 1 @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_init(): mimeaccept = MIMEAccept('image/jpg') assert mimeaccept._parsed == [('image/jpg', 1)] mimeaccept = MIMEAccept('image/png, image/jpg;q=0.5') assert mimeaccept._parsed == [('image/png', 1), ('image/jpg', 0.5)] mimeaccept = MIMEAccept('image, image/jpg;q=0.5') assert mimeaccept._parsed == [] mimeaccept = MIMEAccept('*/*') assert mimeaccept._parsed == [('*/*', 1)] mimeaccept = MIMEAccept('*/png') assert mimeaccept._parsed == [('*/png', 1)] mimeaccept = MIMEAccept('image/pn*') assert mimeaccept._parsed == [('image/pn*', 1.0)] mimeaccept = MIMEAccept('image/*') assert mimeaccept._parsed == [('image/*', 1)] @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_MIMEAccept_parse(): assert list(MIMEAccept.parse('image/jpg')) == [('image/jpg', 1)] assert list(MIMEAccept.parse('invalid')) == [] @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_accept_html(): mimeaccept = MIMEAccept('image/jpg') assert not mimeaccept.accept_html() mimeaccept = MIMEAccept('image/jpg, text/html') assert mimeaccept.accept_html() @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_MIMEAccept_contains(): mimeaccept = MIMEAccept('A/a, B/b, C/c') assert 'A/a' in mimeaccept assert 'A/*' in mimeaccept assert '*/a' in mimeaccept assert 'A/b' not in mimeaccept assert 'B/a' not in mimeaccept @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_BEST_MATCH) def test_MIMEAccept_json(): mimeaccept = MIMEAccept('text/html, */*; q=.2') assert mimeaccept.best_match(['application/json']) == 'application/json' @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_no_raise_invalid(): assert MIMEAccept('invalid') @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_ITER) def test_MIMEAccept_iter(): assert list(iter(MIMEAccept('text/html, other/whatever'))) == [ 'text/html', 'other/whatever', ] @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_str(): assert str(MIMEAccept('image/jpg')) == 'image/jpg' @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_add(): assert str(MIMEAccept('image/jpg') + 'image/png') == 'image/jpg, image/png' assert str(MIMEAccept('image/jpg') + MIMEAccept('image/png')) == 'image/jpg, image/png' assert isinstance(MIMEAccept('image/jpg') + 'image/png', MIMEAccept) assert isinstance(MIMEAccept('image/jpg') + MIMEAccept('image/png'), MIMEAccept) @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) def test_MIMEAccept_radd(): assert str('image/png' + MIMEAccept('image/jpg')) == 'image/png, image/jpg' assert isinstance('image/png' + MIMEAccept('image/jpg'), MIMEAccept) @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_CONTAINS) def test_MIMEAccept_repr(): assert 'image/jpg' in repr(MIMEAccept('image/jpg')) @pytest.mark.filterwarnings(IGNORE_MIMEACCEPT) @pytest.mark.filterwarnings(IGNORE_QUALITY) def test_MIMEAccept_quality(): assert MIMEAccept('image/jpg;q=0.9').quality('image/jpg') == 0.9 assert MIMEAccept('image/png;q=0.9').quality('image/jpg') is None WebOb-1.8.6/tests/test_byterange.py0000644000076600000240000000677113611751037020244 0ustar xistencestaff00000000000000import pytest from webob.byterange import Range from webob.byterange import ContentRange from webob.byterange import _is_content_range_valid from webob.compat import Iterable # Range class def test_not_satisfiable(): range = Range.parse('bytes=-100') assert range.range_for_length(50) is None range = Range.parse('bytes=100-') assert range.range_for_length(50) is None def test_range_parse(): assert isinstance(Range.parse('bytes=0-99'), Range) assert isinstance(Range.parse('BYTES=0-99'), Range) assert isinstance(Range.parse('bytes = 0-99'), Range) assert isinstance(Range.parse('bytes=0 - 102'), Range) assert Range.parse('bytes=10-5') is None assert Range.parse('bytes 5-10') is None assert Range.parse('words=10-5') is None def test_range_content_range_length_none(): range = Range(0, 100) assert range.content_range(None) is None assert isinstance(range.content_range(1), ContentRange) assert tuple(range.content_range(1)) == (0, 1, 1) assert tuple(range.content_range(200)) == (0, 100, 200) def test_range_for_length_end_is_none(): # End is None range = Range(0, None) assert range.range_for_length(100) == (0, 100) def test_range_for_length_end_is_none_negative_start(): # End is None and start is negative range = Range(-5, None) assert range.range_for_length(100) == (95, 100) def test_range_start_none(): # Start is None range = Range(None, 99) assert range.range_for_length(100) is None def test_range_str_end_none(): range = Range(0, None) assert str(range) == 'bytes=0-' def test_range_str_end_none_negative_start(): range = Range(-5, None) assert str(range) == 'bytes=-5' def test_range_str_1(): range = Range(0, 100) assert str(range) == 'bytes=0-99' def test_range_repr(): range = Range(0, 99) assert repr(range) == '' # ContentRange class def test_contentrange_bad_input(): with pytest.raises(ValueError): ContentRange(None, 99, None) def test_contentrange_repr(): contentrange = ContentRange(0, 99, 100) assert repr(contentrange) == '' def test_contentrange_str(): contentrange = ContentRange(0, 99, None) assert str(contentrange) == 'bytes 0-98/*' contentrange = ContentRange(None, None, 100) assert str(contentrange) == 'bytes */100' def test_contentrange_iter(): contentrange = ContentRange(0, 99, 100) assert isinstance(contentrange, Iterable) assert ContentRange.parse('bytes 0-99/100').__class__ == ContentRange assert ContentRange.parse(None) is None assert ContentRange.parse('0-99 100') is None assert ContentRange.parse('bytes 0-99 100') is None assert ContentRange.parse('bytes 0-99/xxx') is None assert ContentRange.parse('bytes 0 99/100') is None assert ContentRange.parse('bytes */100').__class__ == ContentRange assert ContentRange.parse('bytes A-99/100') is None assert ContentRange.parse('bytes 0-B/100') is None assert ContentRange.parse('bytes 99-0/100') is None assert ContentRange.parse('bytes 0 99/*') is None # _is_content_range_valid function def test_is_content_range_valid(): assert not _is_content_range_valid(None, 99, 90) assert not _is_content_range_valid(99, None, 90) assert _is_content_range_valid(None, None, 90) assert not _is_content_range_valid(None, 99, 90) assert _is_content_range_valid(0, 99, None) assert not _is_content_range_valid(0, 99, 90, response=True) assert _is_content_range_valid(0, 99, 90) WebOb-1.8.6/tests/test_cachecontrol.py0000644000076600000240000001757613611751037020735 0ustar xistencestaff00000000000000import pytest def test_cache_control_object_max_age_None(): from webob.cachecontrol import CacheControl cc = CacheControl({}, 'a') cc.properties['max-age'] = None assert cc.max_age == -1 class TestUpdateDict(object): def setup_method(self, method): self.call_queue = [] def callback(args): self.call_queue.append("Called with: %s" % repr(args)) self.callback = callback def make_one(self, callback): from webob.cachecontrol import UpdateDict ud = UpdateDict() ud.updated = callback return ud def test_clear(self): newone = self.make_one(self.callback) newone['first'] = 1 assert len(newone) == 1 newone.clear() assert len(newone) == 0 def test_update(self): newone = self.make_one(self.callback) d = {'one': 1} newone.update(d) assert newone == d def test_set_delete(self): newone = self.make_one(self.callback) newone['first'] = 1 assert len(self.call_queue) == 1 assert self.call_queue[-1] == "Called with: {'first': 1}" del newone['first'] assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}' def test_setdefault(self): newone = self.make_one(self.callback) assert newone.setdefault('haters', 'gonna-hate') == 'gonna-hate' assert len(self.call_queue) == 1 assert self.call_queue[-1] == "Called with: {'haters': 'gonna-hate'}" # no effect if failobj is not set assert newone.setdefault('haters', 'gonna-love') == 'gonna-hate' assert len(self.call_queue) == 1 def test_pop(self): newone = self.make_one(self.callback) newone['first'] = 1 newone.pop('first') assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1] def test_popitem(self): newone = self.make_one(self.callback) newone['first'] = 1 assert newone.popitem() == ('first', 1) assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1] class TestExistProp(object): """ Test webob.cachecontrol.exists_property """ def setUp(self): pass def make_one(self): from webob.cachecontrol import exists_property class Dummy(object): properties = dict(prop=1) type = 'dummy' prop = exists_property('prop', 'dummy') badprop = exists_property('badprop', 'big_dummy') return Dummy def test_get_on_class(self): from webob.cachecontrol import exists_property Dummy = self.make_one() assert isinstance(Dummy.prop, exists_property), Dummy.prop def test_get_on_instance(self): obj = self.make_one()() assert obj.prop is True def test_type_mismatch_raise(self): with pytest.raises(AttributeError): obj = self.make_one()() obj.badprop = True def test_set_w_value(self): obj = self.make_one()() obj.prop = True assert obj.prop is True assert obj.properties['prop'] is None def test_del_value(self): obj = self.make_one()() del obj.prop assert 'prop' not in obj.properties class TestValueProp(object): """ Test webob.cachecontrol.exists_property """ def setUp(self): pass def make_one(self): from webob.cachecontrol import value_property class Dummy(object): properties = dict(prop=1) type = 'dummy' prop = value_property('prop', 'dummy') badprop = value_property('badprop', 'big_dummy') return Dummy def test_get_on_class(self): from webob.cachecontrol import value_property Dummy = self.make_one() assert isinstance(Dummy.prop, value_property), Dummy.prop def test_get_on_instance(self): dummy = self.make_one()() assert dummy.prop, dummy.prop def test_set_on_instance(self): dummy = self.make_one()() dummy.prop = "new" assert dummy.prop == "new", dummy.prop assert dummy.properties['prop'] == "new", dict(dummy.properties) def test_set_on_instance_bad_attribute(self): dummy = self.make_one()() dummy.prop = "new" assert dummy.prop == "new", dummy.prop assert dummy.properties['prop'] == "new", dict(dummy.properties) def test_set_wrong_type(self): from webob.cachecontrol import value_property class Dummy(object): properties = dict(prop=1, type='fail') type = 'dummy' prop = value_property('prop', 'dummy', type='failingtype') dummy = Dummy() def assign(): dummy.prop = 'foo' with pytest.raises(AttributeError): assign() def test_set_type_true(self): dummy = self.make_one()() dummy.prop = True assert dummy.prop is None def test_set_on_instance_w_default(self): dummy = self.make_one()() dummy.prop = "dummy" assert dummy.prop == "dummy" # TODO: this probably needs more tests def test_del(self): dummy = self.make_one()() dummy.prop = 'Ian Bicking likes to skip' del dummy.prop assert dummy.prop == "dummy" def test_copy_cc(): from webob.cachecontrol import CacheControl cc = CacheControl({'header': '%', "msg": 'arewerichyet?'}, 'request') cc2 = cc.copy() assert cc.properties is not cc2.properties assert cc.type is cc2.type def test_serialize_cache_control_emptydict(): from webob.cachecontrol import serialize_cache_control result = serialize_cache_control(dict()) assert result == '' def test_serialize_cache_control_cache_control_object(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({}, 'request')) assert result == '' def test_serialize_cache_control_object_with_headers(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header': 'a'}, 'request')) assert result == 'header=a' def test_serialize_cache_control_value_is_None(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header': None}, 'request')) assert result == 'header' def test_serialize_cache_control_value_needs_quote(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header': '""'}, 'request')) assert result == 'header=""""' class TestCacheControl(object): def make_one(self, props, typ): from webob.cachecontrol import CacheControl return CacheControl(props, typ) def test_ctor(self): cc = self.make_one({'a': 1}, 'typ') assert cc.properties == {'a': 1} assert cc.type == 'typ' def test_parse(self): from webob.cachecontrol import CacheControl cc = CacheControl.parse("public, max-age=315360000") assert type(cc) == CacheControl assert cc.max_age == 315360000 assert cc.public is True def test_parse_updates_to(self): from webob.cachecontrol import CacheControl def foo(arg): return {'a': 1} cc = CacheControl.parse("public, max-age=315360000", updates_to=foo) assert type(cc) == CacheControl assert cc.max_age == 315360000 def test_parse_valueerror_int(self): from webob.cachecontrol import CacheControl def foo(arg): return {'a': 1} cc = CacheControl.parse("public, max-age=abc") assert type(cc) == CacheControl assert cc.max_age == 'abc' def test_repr(self): cc = self.make_one({'a': '1'}, 'typ') assert repr(cc) == "" WebOb-1.8.6/tests/test_client.py0000644000076600000240000002232213611751037017530 0ustar xistencestaff00000000000000import unittest import io import socket class TestSendRequest(unittest.TestCase): def _getTargetClass(self): from webob.client import SendRequest return SendRequest def _makeOne(self, **kw): cls = self._getTargetClass() return cls(**kw) def _makeEnviron(self, extra=None): environ = { 'wsgi.url_scheme':'http', 'SERVER_NAME':'localhost', 'HTTP_HOST':'localhost:80', 'SERVER_PORT':'80', 'wsgi.input':io.BytesIO(), 'CONTENT_LENGTH':0, 'REQUEST_METHOD':'GET', } if extra is not None: environ.update(extra) return environ def test___call___unknown_scheme(self): environ = self._makeEnviron({'wsgi.url_scheme':'abc'}) inst = self._makeOne() self.assertRaises(ValueError, inst, environ, None) def test___call___gardenpath(self): environ = self._makeEnviron() response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___no_servername_no_http_host(self): environ = self._makeEnviron() del environ['SERVER_NAME'] del environ['HTTP_HOST'] response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) self.assertRaises(ValueError, inst, environ, None) def test___call___no_servername_colon_not_in_host_http(self): environ = self._makeEnviron() del environ['SERVER_NAME'] environ['HTTP_HOST'] = 'localhost' response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(environ['SERVER_NAME'], 'localhost') self.assertEqual(environ['SERVER_PORT'], '80') def test___call___no_servername_colon_not_in_host_https(self): environ = self._makeEnviron() del environ['SERVER_NAME'] environ['HTTP_HOST'] = 'localhost' environ['wsgi.url_scheme'] = 'https' response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPSConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(environ['SERVER_NAME'], 'localhost') self.assertEqual(environ['SERVER_PORT'], '443') def test___call___no_content_length(self): environ = self._makeEnviron() del environ['CONTENT_LENGTH'] response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___with_webob_client_timeout_and_timeout_supported(self): environ = self._makeEnviron() environ['webob.client.timeout'] = 10 response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(conn_factory.kw, {'timeout':10}) def test___call___bad_content_length(self): environ = self._makeEnviron({'CONTENT_LENGTH':'abc'}) response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___with_socket_timeout(self): environ = self._makeEnviron() response = socket.timeout() response.msg = 'msg' conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '504 Gateway Timeout') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'504')) def test___call___with_socket_error_neg2(self): environ = self._makeEnviron() response = socket.error(-2) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'502')) def test___call___with_socket_error_ENODATA(self): import errno environ = self._makeEnviron() if not hasattr(errno, 'ENODATA'): # no ENODATA on win return response = socket.error(errno.ENODATA) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'502')) def test___call___with_socket_error_unknown(self): environ = self._makeEnviron() response = socket.error('nope') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True self.assertRaises(socket.error, inst, environ, start_response) def test___call___nolength(self): environ = self._makeEnviron() response = DummyResponse('msg', None) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(response.length, None) class DummyMessage(object): def __init__(self, msg): self.msg = msg self.headers = self._headers = {} class DummyResponse(object): def __init__(self, msg, headerval='10'): self.msg = DummyMessage(msg) self.status = '200' self.reason = 'OK' self.headerval = headerval def getheader(self, name): return self.headerval def read(self, length=None): self.length = length return b'foo' class DummyConnectionFactory(object): def __init__(self, result=None): self.result = result self.closed = False def __call__(self, hostport, **kw): self.hostport = hostport self.kw = kw self.request = DummyRequestFactory(hostport, **kw) return self def getresponse(self): if isinstance(self.result, Exception): raise self.result return self.result def close(self): self.closed = True class DummyRequestFactory(object): def __init__(self, hostport, **kw): self.hostport = hostport self.kw = kw def __call__(self, method, path, body, headers): return self WebOb-1.8.6/tests/test_client_functional.py0000644000076600000240000000651413611751037021757 0ustar xistencestaff00000000000000import time import pytest from webob import Request, Response from webob.dec import wsgify from webob.client import SendRequest @wsgify def simple_app(req): data = {'headers': dict(req.headers), 'body': req.text, 'method': req.method, } return Response(json=data) @pytest.mark.usefixtures("serve") def test_client(serve, client_app=None): with serve(simple_app) as server: req = Request.blank(server.url, method='POST', content_type='application/json', json={'test': 1}) resp = req.send(client_app) assert resp.status_code == 200, resp.status assert resp.json['headers']['Content-Type'] == 'application/json' assert resp.json['method'] == 'POST' # Test that these values get filled in: del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] resp = req.send(client_app) assert resp.status_code == 200, resp.status req = Request.blank(server.url) del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] assert req.send(client_app).status_code == 200 req.headers['Host'] = server.url.lstrip('http://') del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] resp = req.send(client_app) assert resp.status_code == 200, resp.status del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] del req.headers['Host'] assert req.environ.get('SERVER_NAME') is None assert req.environ.get('SERVER_PORT') is None assert req.environ.get('HTTP_HOST') is None with pytest.raises(ValueError): req.send(client_app) req = Request.blank(server.url) req.environ['CONTENT_LENGTH'] = 'not a number' assert req.send(client_app).status_code == 200 def no_length_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return [b'ok'] @pytest.mark.usefixtures("serve") def test_no_content_length(serve, client_app=None): with serve(no_length_app) as server: req = Request.blank(server.url) resp = req.send(client_app) assert resp.status_code == 200, resp.status @wsgify def cookie_app(req): resp = Response('test') resp.headers.add('Set-Cookie', 'a=b') resp.headers.add('Set-Cookie', 'c=d') resp.headerlist.append(('X-Crazy', 'value\r\n continuation')) return resp @pytest.mark.usefixtures("serve") def test_client_cookies(serve, client_app=None): with serve(cookie_app) as server: req = Request.blank(server.url + '/?test') resp = req.send(client_app) assert resp.headers.getall('Set-Cookie') == ['a=b', 'c=d'] assert resp.headers['X-Crazy'] == 'value, continuation', repr(resp.headers['X-Crazy']) @wsgify def slow_app(req): time.sleep(2) return Response('ok') @pytest.mark.usefixtures("serve") def test_client_slow(serve, client_app=None): if client_app is None: client_app = SendRequest() if not client_app._timeout_supported(client_app.HTTPConnection): # timeout isn't supported return with serve(slow_app) as server: req = Request.blank(server.url) req.environ['webob.client.timeout'] = 0.1 resp = req.send(client_app) assert resp.status_code == 504, resp.status WebOb-1.8.6/tests/test_compat.py0000644000076600000240000001415613611751037017543 0ustar xistencestaff00000000000000import unittest from io import BytesIO import pytest import sys from webob.compat import text_type class text_Tests(unittest.TestCase): def _callFUT(self, *arg, **kw): from webob.compat import text_ return text_(*arg, **kw) def test_binary(self): result = self._callFUT(b'123') self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'123', 'ascii')) def test_binary_alternate_decoding(self): result = self._callFUT(b'La Pe\xc3\xb1a', 'utf-8') self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'La Pe\xc3\xb1a', 'utf-8')) def test_binary_decoding_error(self): self.assertRaises(UnicodeDecodeError, self._callFUT, b'\xff', 'utf-8') def test_text(self): result = self._callFUT(text_type(b'123', 'ascii')) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'123', 'ascii')) class bytes_Tests(unittest.TestCase): def _callFUT(self, *arg, **kw): from webob.compat import bytes_ return bytes_(*arg, **kw) def test_binary(self): result = self._callFUT(b'123') self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'123') def test_text(self): val = text_type(b'123', 'ascii') result = self._callFUT(val) self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'123') def test_text_alternate_encoding(self): val = text_type(b'La Pe\xc3\xb1a', 'utf-8') result = self._callFUT(val, 'utf-8') self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'La Pe\xc3\xb1a') class Test_cgi_FieldStorage_Py3_tests(object): def test_fieldstorage_not_multipart(self): from webob.compat import cgi_FieldStorage POSTDATA = b'{"name": "Bert"}' env = { 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'text/plain', 'CONTENT_LENGTH': str(len(POSTDATA)) } fp = BytesIO(POSTDATA) fs = cgi_FieldStorage(fp, environ=env) assert fs.list is None assert fs.value == b'{"name": "Bert"}' @pytest.mark.skipif( sys.version_info < (3, 0), reason="FieldStorage on Python 2.7 is broken, see " "https://github.com/Pylons/webob/issues/293" ) def test_fieldstorage_part_content_length(self): from webob.compat import cgi_FieldStorage BOUNDARY = "JfISa01" POSTDATA = """--JfISa01 Content-Disposition: form-data; name="submit-name" Content-Length: 5 Larry --JfISa01""" env = { 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 'CONTENT_LENGTH': str(len(POSTDATA))} fp = BytesIO(POSTDATA.encode('latin-1')) fs = cgi_FieldStorage(fp, environ=env) assert len(fs.list) == 1 assert fs.list[0].name == 'submit-name' assert fs.list[0].value == 'Larry' def test_my_fieldstorage_part_content_length(self): from webob.compat import cgi_FieldStorage BOUNDARY = "4ddfd368-cb07-4b9e-b003-876010298a6c" POSTDATA = """--4ddfd368-cb07-4b9e-b003-876010298a6c Content-Disposition: form-data; name="object"; filename="file.txt" Content-Type: text/plain Content-Length: 5 Content-Transfer-Encoding: 7bit ADMIN --4ddfd368-cb07-4b9e-b003-876010298a6c Content-Disposition: form-data; name="sign_date" Content-Type: application/json; charset=UTF-8 Content-Length: 22 Content-Transfer-Encoding: 7bit "2016-11-23T12:22:41Z" --4ddfd368-cb07-4b9e-b003-876010298a6c Content-Disposition: form-data; name="staffId" Content-Type: text/plain; charset=UTF-8 Content-Length: 5 Content-Transfer-Encoding: 7bit ADMIN --4ddfd368-cb07-4b9e-b003-876010298a6c--""" env = { 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 'CONTENT_LENGTH': str(len(POSTDATA))} fp = BytesIO(POSTDATA.encode('latin-1')) fs = cgi_FieldStorage(fp, environ=env) assert len(fs.list) == 3 expect = [{'name':'object', 'filename':'file.txt', 'value':b'ADMIN'}, {'name':'sign_date', 'filename':None, 'value':'"2016-11-23T12:22:41Z"'}, {'name':'staffId', 'filename':None, 'value':'ADMIN'}] for x in range(len(fs.list)): for k, exp in expect[x].items(): got = getattr(fs.list[x], k) assert got == exp def test_fieldstorage_multipart_leading_whitespace(self): from webob.compat import cgi_FieldStorage BOUNDARY = "---------------------------721837373350705526688164684" POSTDATA = """-----------------------------721837373350705526688164684 Content-Disposition: form-data; name="id" 1234 -----------------------------721837373350705526688164684 Content-Disposition: form-data; name="title" -----------------------------721837373350705526688164684 Content-Disposition: form-data; name="file"; filename="test.txt" Content-Type: text/plain Testing 123. -----------------------------721837373350705526688164684 Content-Disposition: form-data; name="submit" Add\x20 -----------------------------721837373350705526688164684-- """ env = { 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 'CONTENT_LENGTH': '560'} # Add some leading whitespace to our post data that will cause the # first line to not be the innerboundary. fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1')) fs = cgi_FieldStorage(fp, environ=env) assert len(fs.list) == 4 expect = [{'name':'id', 'filename':None, 'value':'1234'}, {'name':'title', 'filename':None, 'value':''}, {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'}, {'name':'submit', 'filename':None, 'value':' Add '}] for x in range(len(fs.list)): for k, exp in expect[x].items(): got = getattr(fs.list[x], k) assert got == exp WebOb-1.8.6/tests/test_cookies.py0000644000076600000240000006350213611751472017716 0ustar xistencestaff00000000000000import pytest from datetime import timedelta from webob import cookies from webob.compat import text_ from webob.compat import native_ py2only = pytest.mark.skipif("sys.version_info >= (3, 0)") py3only = pytest.mark.skipif("sys.version_info < (3, 0)") def setup_module(module): cookies._should_raise = True def teardown_module(module): cookies._should_raise = False def test_cookie_empty(): c = cookies.Cookie() # empty cookie assert repr(c) == '' def test_cookie_one_value(): c = cookies.Cookie('dismiss-top=6') vals = list(c.values()) assert len(vals) == 1 assert vals[0].name == b'dismiss-top' assert vals[0].value == b'6' def test_cookie_one_value_with_trailing_semi(): c = cookies.Cookie('dismiss-top=6;') vals = list(c.values()) assert len(vals) == 1 assert vals[0].name == b'dismiss-top' assert vals[0].value == b'6' c = cookies.Cookie('dismiss-top=6;') def test_cookie_escaped_unquoted(): assert list(cookies.parse_cookie('x=\\040')) == [(b'x', b' ')] def test_cookie_complex(): c = cookies.Cookie('dismiss-top=6; CP=null*, ' 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"') def d(v): return v.decode('ascii') c_dict = dict((d(k), d(v.value)) for k, v in c.items()) assert c_dict == { 'a': '42,', 'CP': 'null*', 'PHPSESSID': '0a539d42abc001cdc762809248d4beed', 'dismiss-top': '6' } def test_cookie_complex_serialize(): c = cookies.Cookie('dismiss-top=6; CP=null*, ' 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42"') assert c.serialize() == 'CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;'\ ' a=42; dismiss-top=6' def test_cookie_load_multiple(): c = cookies.Cookie('a=1; Secure=true') vals = list(c.values()) assert len(vals) == 1 assert c[b'a'][b'secure'] == b'true' def test_cookie_secure(): c = cookies.Cookie() c[text_('foo')] = b'bar' c[b'foo'].secure = True assert c.serialize() == 'foo=bar; secure' def test_cookie_httponly(): c = cookies.Cookie() c['foo'] = b'bar' c[b'foo'].httponly = True assert c.serialize() == 'foo=bar; HttpOnly' def test_cookie_samesite_strict(): c = cookies.Cookie() c[b"foo"] = b"bar" c[b"foo"].samesite = b"Strict" assert c.serialize() == "foo=bar; SameSite=Strict" def test_cookie_samesite_lax(): c = cookies.Cookie() c[b"foo"] = b"bar" c[b"foo"].samesite = b"Lax" assert c.serialize() == "foo=bar; SameSite=Lax" def test_cookie_samesite_none(): c = cookies.Cookie() c[b"foo"] = b"bar" c[b"foo"].samesite = b"None" c[b"foo"].secure = True assert c.serialize() == "foo=bar; secure; SameSite=None" def test_cookie_samesite_none_not_secure(): c = cookies.Cookie() c[b"foo"] = b"bar" c[b"foo"].samesite = b"None" with pytest.raises(ValueError): c.serialize() def test_cookie_samesite_future__default(): # ensure default behavior when unsupported values are provided c = cookies.Cookie() with pytest.raises(ValueError) as excinfo: c[b"foo"] = b"bar" c[b"foo"].samesite = b"Future" c.serialize() assert excinfo.value.args[0] == "SameSite must be 'strict', 'lax', or 'none'" def test_cookie_samesite_future__monkeypatched(monkeypatch): # disable validation so future args pass monkeypatch.setattr(cookies, "SAMESITE_VALIDATION", False) c = cookies.Cookie() c[b"foo"] = b"bar" c[b"foo"].samesite = b"Future" assert c.serialize() == "foo=bar; SameSite=Future" # ensure we can toggle it to True and re-achieve default behavior... monkeypatch.setattr(cookies, "SAMESITE_VALIDATION", True) with pytest.raises(ValueError) as excinfo: c[b"foo"] = b"bar" c[b"foo"].samesite = b"Future" c.serialize() assert excinfo.value.args[0] == "SameSite must be 'strict', 'lax', or 'none'" def test_cookie_reserved_keys(): c = cookies.Cookie('dismiss-top=6; CP=null*; $version=42; a=42') assert '$version' not in c c = cookies.Cookie('$reserved=42; a=$42') assert list(c.keys()) == [b'a'] def test_serialize_cookie_date(): """ Testing webob.cookies.serialize_cookie_date. Missing scenarios: * input value is an str, should be returned verbatim * input value is an int, should be converted to timedelta and we should continue the rest of the process """ date_one = cookies.serialize_cookie_date(b'Tue, 04-Jan-2011 13:43:50 GMT') assert date_one == b'Tue, 04-Jan-2011 13:43:50 GMT' date_two = cookies.serialize_cookie_date(text_('Tue, 04-Jan-2011 13:43:50 GMT')) assert date_two == b'Tue, 04-Jan-2011 13:43:50 GMT' assert cookies.serialize_cookie_date(None) is None cdate_delta = cookies.serialize_cookie_date(timedelta(seconds=10)) cdate_int = cookies.serialize_cookie_date(10) assert cdate_delta == cdate_int def test_serialize_samesite(): assert cookies.serialize_samesite(b"Lax") == b"Lax" assert cookies.serialize_samesite(b"Strict") == b"Strict" assert cookies.serialize_samesite(b"None") == b"None" with pytest.raises(ValueError): cookies.serialize_samesite(b"SomethingElse") def test_ch_unquote(): assert cookies._unquote(b'"hello world') == b'"hello world' assert cookies._unquote(b'hello world') == b'hello world' assert cookies._unquote(b'"hello world"') == b'hello world' # Spaces are not valid in cookies, we support getting them, but won't # support sending them with pytest.raises(ValueError): cookies._value_quote(b'hello world') # quotation mark escaped w/ backslash is unquoted correctly (support # pre webob 1.3 cookies) assert cookies._unquote(b'"\\""') == b'"' # we also are able to unquote the newer \\042 serialization of quotation # mark assert cookies._unquote(b'"\\042"') == b'"' # New cookies can not contain quotes. with pytest.raises(ValueError): cookies._value_quote(b'"') # backslash escaped w/ backslash is unquoted correctly (support # pre webob 1.3 cookies) assert cookies._unquote(b'"\\\\"') == b'\\' # we also are able to unquote the newer \\134 serialization of backslash assert cookies._unquote(b'"\\134"') == b'\\' # Cookies may not contain a backslash with pytest.raises(ValueError): cookies._value_quote(b'\\') # misc byte escaped as octal assert cookies._unquote(b'"\\377"') == b'\xff' with pytest.raises(ValueError): cookies._value_quote(b'\xff') # combination assert cookies._unquote(b'"a\\"\\377"') == b'a"\xff' with pytest.raises(ValueError): cookies._value_quote(b'a"\xff') def test_cookie_invalid_name(): c = cookies.Cookie() c['La Pe\xc3\xb1a'] = '1' assert len(c) == 0 def test_morsel_serialize_with_expires(): morsel = cookies.Morsel(b'bleh', b'blah') morsel.expires = b'Tue, 04-Jan-2011 13:43:50 GMT' result = morsel.serialize() assert result, 'bleh=blah; expires=Tue, 04-Jan-2011 13:43:50 GMT' def test_serialize_max_age_timedelta(): import datetime val = datetime.timedelta(86400) result = cookies.serialize_max_age(val) assert result == b'7464960000' def test_serialize_max_age_int(): val = 86400 result = cookies.serialize_max_age(val) assert result == b'86400' def test_serialize_max_age_str(): val = '86400' result = cookies.serialize_max_age(val) assert result == b'86400' def test_parse_qmark_in_val(): v = r'x="\"\073\054\""; expires=Sun, 12-Jun-2011 23:16:01 GMT' c = cookies.Cookie(v) assert c[b'x'].value == b'";,"' assert c[b'x'].expires, b'Sun == 12-Jun-2011 23:16:01 GMT' def test_morsel_repr(): v = cookies.Morsel(b'a', b'b') result = repr(v) assert result == "" class TestRequestCookies(object): def _makeOne(self, environ): from webob.cookies import RequestCookies return RequestCookies(environ) def test_get_no_cache_key_in_environ_no_http_cookie_header(self): environ = {} inst = self._makeOne(environ) assert inst.get('a') is None parsed = environ['webob._parsed_cookies'] assert parsed == ({}, '') def test_get_no_cache_key_in_environ_has_http_cookie_header(self): header = 'a=1; b=2' environ = {'HTTP_COOKIE': header} inst = self._makeOne(environ) assert inst.get('a') == '1' parsed = environ['webob._parsed_cookies'][0] assert parsed['a'] == '1' assert parsed['b'] == '2' assert environ['HTTP_COOKIE'] == header # no change def test_get_cache_key_in_environ_no_http_cookie_header(self): environ = {'webob._parsed_cookies': ({}, '')} inst = self._makeOne(environ) assert inst.get('a') is None parsed = environ['webob._parsed_cookies'] assert parsed == ({}, '') def test_get_cache_key_in_environ_has_http_cookie_header(self): header = 'a=1; b=2' environ = {'HTTP_COOKIE': header, 'webob._parsed_cookies': ({}, '')} inst = self._makeOne(environ) assert inst.get('a') == '1' parsed = environ['webob._parsed_cookies'][0] assert parsed['a'] == '1' assert parsed['b'] == '2' assert environ['HTTP_COOKIE'] == header # no change def test_get_missing_with_default(self): environ = {} inst = self._makeOne(environ) assert inst.get('a', '') == '' def test___setitem__name_not_string_type(self): inst = self._makeOne({}) with pytest.raises(TypeError): inst.__setitem__(None, 1) def test___setitem__name_not_encodeable_to_ascii(self): name = native_(b'La Pe\xc3\xb1a', 'utf-8') inst = self._makeOne({}) with pytest.raises(TypeError): inst.__setitem__(name, 'abc') def test___setitem__name_not_rfc2109_valid(self): name = '$a' inst = self._makeOne({}) with pytest.raises(TypeError): inst.__setitem__(name, 'abc') def test___setitem__value_not_string_type(self): inst = self._makeOne({}) with pytest.raises(ValueError): inst.__setitem__('a', None) def test___setitem__value_not_utf_8_decodeable(self): value = text_(b'La Pe\xc3\xb1a', 'utf-8') value = value.encode('utf-16') inst = self._makeOne({}) with pytest.raises(ValueError): inst.__setitem__('a', value) def test__setitem__success_no_existing_headers(self): value = native_(b'test_cookie', 'utf-8') environ = {} inst = self._makeOne(environ) inst['a'] = value assert environ['HTTP_COOKIE'] == 'a=test_cookie' def test__setitem__success_append(self): value = native_(b'test_cookie', 'utf-8') environ = {'HTTP_COOKIE': 'a=1; b=2'} inst = self._makeOne(environ) inst['c'] = value assert environ['HTTP_COOKIE'] == 'a=1; b=2; c=test_cookie' def test__setitem__success_replace(self): environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) inst['b'] = 'abc' assert environ['HTTP_COOKIE'] == 'a=1; b=abc; c=3' inst['c'] = '4' assert environ['HTTP_COOKIE'] == 'a=1; b=abc; c=4' def test__delitem__fail_no_http_cookie(self): environ = {} inst = self._makeOne(environ) with pytest.raises(KeyError): inst.__delitem__('a') assert environ == {} def test__delitem__fail_with_http_cookie(self): environ = {'HTTP_COOKIE': ''} inst = self._makeOne(environ) with pytest.raises(KeyError): inst.__delitem__('a') assert environ == {'HTTP_COOKIE': ''} def test__delitem__success(self): environ = {'HTTP_COOKIE': 'a=1'} inst = self._makeOne(environ) del inst['a'] assert environ['HTTP_COOKIE'] == '' assert inst._cache == {} def test_keys(self): environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) assert sorted(list(inst.keys())) == ['a', 'b', 'c'] def test_values(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) assert sorted(list(inst.values())) == ['1', '3', val] def test_items(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) assert sorted(list(inst.items())) == [('a', '1'), ('b', val), ('c', '3')] @py2only def test_iterkeys(self): environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) assert sorted(list(inst.iterkeys())) == ['a', 'b', 'c'] @py3only def test_iterkeys_py3(self): environ = {'HTTP_COOKIE': b'a=1; b="La Pe\\303\\261a"; c=3'.decode('utf-8')} inst = self._makeOne(environ) assert sorted(list(inst.keys())) == ['a', 'b', 'c'] @py2only def test_itervalues(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) sorted(list(inst.itervalues())) == ['1', '3', val] @py3only def test_itervalues_py3(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': b'a=1; b="La Pe\\303\\261a"; c=3'.decode('utf-8')} inst = self._makeOne(environ) sorted(list(inst.values())) == ['1', '3', val] @py2only def test_iteritems(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': 'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) assert sorted(list(inst.iteritems())) == [('a', '1'), ('b', val), ('c', '3')] @py3only def test_iteritems_py3(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE': b'a=1; b="La Pe\\303\\261a"; c=3'.decode('utf-8')} inst = self._makeOne(environ) assert sorted(list(inst.items())) == [('a', '1'), ('b', val), ('c', '3')] def test___contains__(self): environ = {'HTTP_COOKIE': 'a=1'} inst = self._makeOne(environ) assert 'a' in inst assert 'b' not in inst def test___iter__(self): environ = {'HTTP_COOKIE': 'a=1; b=2; c=3'} inst = self._makeOne(environ) assert sorted(list(iter(inst))) == ['a', 'b', 'c'] def test___len__(self): environ = {'HTTP_COOKIE': 'a=1; b=2; c=3'} inst = self._makeOne(environ) assert len(inst) == 3 del inst['a'] assert len(inst) == 2 def test_clear(self): environ = {'HTTP_COOKIE': 'a=1; b=2; c=3'} inst = self._makeOne(environ) inst.clear() assert environ['HTTP_COOKIE'] == '' assert inst.get('a') is None def test___repr__(self): environ = {'HTTP_COOKIE': 'a=1; b=2; c=3'} inst = self._makeOne(environ) r = repr(inst) assert r.startswith('') class TestCookieMakeCookie(object): def makeOne(self, name, value, **kw): from webob.cookies import make_cookie return make_cookie(name, value, **kw) def test_make_cookie_max_age(self): cookie = self.makeOne('test_cookie', 'value', max_age=500) assert 'test_cookie=value' in cookie assert 'Max-Age=500;' in cookie assert 'expires' in cookie def test_make_cookie_max_age_timedelta(self): from datetime import timedelta cookie = self.makeOne('test_cookie', 'value', max_age=timedelta(seconds=500)) assert 'test_cookie=value' in cookie assert 'Max-Age=500;' in cookie assert 'expires' in cookie assert 'expires=500' not in cookie def test_make_cookie_max_age_str_valid_int(self): cookie = self.makeOne('test_cookie', 'value', max_age='500') assert 'test_cookie=value' in cookie assert 'Max-Age=500;' in cookie assert 'expires' in cookie assert 'expires=500' not in cookie def test_make_cookie_max_age_str_invalid_int(self): with pytest.raises(ValueError): self.makeOne('test_cookie', 'value', max_age='test') def test_make_cookie_comment(self): cookie = self.makeOne('test_cookie', 'value', comment='lolwhy') assert 'test_cookie=value' in cookie assert 'Comment=lolwhy' in cookie def test_make_cookie_path(self): cookie = self.makeOne('test_cookie', 'value', path='/foo/bar/baz') assert 'test_cookie=value' in cookie assert 'Path=/foo/bar/baz' in cookie @pytest.mark.parametrize("samesite", ["Strict", "Lax", "None"]) def test_make_cookie_samesite(self, samesite): cookie = self.makeOne("test_cookie", "value", samesite=samesite, secure=True) assert "test_cookie=value" in cookie assert "SameSite=" + samesite in cookie assert 'test_cookie=value' in cookie assert 'SameSite=' + samesite in cookie class CommonCookieProfile(object): def makeDummyRequest(self, **kw): class Dummy(object): def __init__(self, **kwargs): self.__dict__.update(**kwargs) d = Dummy(**kw) d.response = Dummy() d.response.headerlist = list() return d def makeOneRequest(self): request = self.makeDummyRequest(environ=dict()) request.environ['HTTP_HOST'] = 'www.example.net' request.cookies = dict() return request class TestCookieProfile(CommonCookieProfile): def makeOne(self, name='uns', **kw): if 'request' in kw: request = kw['request'] del kw['request'] else: request = self.makeOneRequest() from webob.cookies import CookieProfile return CookieProfile(name, **kw)(request) def test_cookie_creation(self): cookie = self.makeOne() from webob.cookies import CookieProfile assert isinstance(cookie, CookieProfile) def test_cookie_name(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") for cookie in cookie_list: assert cookie[1].startswith('uns') assert 'uns="";' not in cookie[1] def test_cookie_no_request(self): from webob.cookies import CookieProfile cookie = CookieProfile('uns') with pytest.raises(ValueError): cookie.get_value() def test_get_value_serializer_raises_value_error(self): class RaisingSerializer(object): def loads(self, val): raise ValueError('foo') cookie = self.makeOne(serializer=RaisingSerializer()) assert cookie.get_value() is None def test_with_cookies(self): request = self.makeOneRequest() request.cookies['uns'] = 'InRlc3Qi' cookie = self.makeOne(request=request) ret = cookie.get_value() assert ret == "test" def test_with_invalid_cookies(self): request = self.makeOneRequest() request.cookies['uns'] = 'InRlc3Q' cookie = self.makeOne(request=request) ret = cookie.get_value() assert ret is None class TestSignedCookieProfile(CommonCookieProfile): def makeOne(self, secret='seekrit', salt='salty', name='uns', **kw): if 'request' in kw: request = kw['request'] del kw['request'] else: request = self.makeOneRequest() from webob.cookies import SignedCookieProfile as CookieProfile return CookieProfile(secret, salt, name, **kw)(request) def test_cookie_name(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") for cookie in cookie_list: assert cookie[1].startswith('uns') assert 'uns="";' not in cookie[1] def test_cookie_expire(self): cookie = self.makeOne() cookie_list = cookie.get_headers(None, max_age=0) for cookie in cookie_list: assert 'Max-Age=0;' in cookie[1] def test_cookie_max_age(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test", max_age=60) for cookie in cookie_list: assert 'Max-Age=60;' in cookie[1] assert 'expires=' in cookie[1] def test_cookie_raw(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") assert isinstance(cookie_list, list) def test_set_cookie(self): request = self.makeOneRequest() cookie = self.makeOne(request=request) ret = cookie.set_cookies(request.response, "test") assert ret == request.response def test_no_cookie(self): cookie = self.makeOne() ret = cookie.get_value() assert ret is None def test_with_cookies(self): request = self.makeOneRequest() request.cookies['uns'] = ( 'FLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx_uX-OA1AhssA-CNeVKpWksQ' 'a0ktMhuQDdjzmDwgzbptiJ0ZXN0Ig' ) cookie = self.makeOne(request=request) ret = cookie.get_value() assert ret == "test" def test_with_bad_cookie_invalid_base64(self): request = self.makeOneRequest() request.cookies['uns'] = ( "gAJVBHRlc3RxAS4KjKfwGmCkliC4ba99rWUdpy_{}riHzK7MQFPsbSgYTgALHa" "SHrRkd3lyE8c4w5ruxAKOyj2h5oF69Ix7ERZv_") cookie = self.makeOne(request=request) val = cookie.get_value() assert val is None def test_with_bad_cookie_invalid_signature(self): request = self.makeOneRequest() request.cookies['uns'] = ( "InRlc3QiFLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx/uX+OA1AhssA" "+CNeVKpWksQa0ktMhuQDdjzmDwgzbptg==") cookie = self.makeOne(secret='sekrit!', request=request) val = cookie.get_value() assert val is None def test_with_domain(self): cookie = self.makeOne(domains=("testing.example.net",)) ret = cookie.get_headers("test") passed = False for cookie in ret: if 'Domain=testing.example.net' in cookie[1]: passed = True assert passed assert len(ret) == 1 def test_with_domains(self): cookie = self.makeOne( domains=("testing.example.net", "testing2.example.net") ) ret = cookie.get_headers("test") passed = 0 for cookie in ret: if 'Domain=testing.example.net' in cookie[1]: passed += 1 if 'Domain=testing2.example.net' in cookie[1]: passed += 1 assert passed == 2 assert len(ret) == 2 def test_flag_secure(self): cookie = self.makeOne(secure=True) ret = cookie.get_headers("test") for cookie in ret: assert '; secure' in cookie[1] def test_flag_http_only(self): cookie = self.makeOne(httponly=True) ret = cookie.get_headers("test") for cookie in ret: assert '; HttpOnly' in cookie[1] @pytest.mark.parametrize("samesite", [b"Strict", b"Lax", b"None"]) def test_with_samesite_bytes(self, samesite): cookie = self.makeOne(samesite=samesite, secure=True) ret = cookie.get_headers("test") for cookie in ret: assert "; SameSite=" + samesite.decode('ascii') in cookie[1] @pytest.mark.parametrize("samesite", ["Strict", "Lax", "None"]) def test_with_samesite(self, samesite): cookie = self.makeOne(samesite=samesite, secure=True) ret = cookie.get_headers("test") for cookie in ret: assert "; SameSite=" + samesite in cookie[1] def test_cookie_length(self): cookie = self.makeOne() longstring = 'a' * 4096 with pytest.raises(ValueError): cookie.get_headers(longstring) def test_very_long_key(self): longstring = 'a' * 1024 cookie = self.makeOne(secret=longstring) cookie.get_headers("test") def serialize(secret, salt, data): import hmac import base64 import json from hashlib import sha1 from webob.compat import bytes_ salted_secret = bytes_(salt or '', 'utf-8') + bytes_(secret, 'utf-8') cstruct = bytes_(json.dumps(data)) sig = hmac.new(salted_secret, cstruct, sha1).digest() return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') class TestSignedSerializer(object): def makeOne(self, secret, salt, hashalg='sha1', **kw): from webob.cookies import SignedSerializer return SignedSerializer(secret, salt, hashalg=hashalg, **kw) def test_serialize(self): ser = self.makeOne('seekrit', 'salty') assert ser.dumps('test') == serialize('seekrit', 'salty', 'test') def test_deserialize(self): ser = self.makeOne('seekrit', 'salty') assert ser.loads(serialize('seekrit', 'salty', 'test')) == 'test' def test_with_highorder_secret(self): secret = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') ser = self.makeOne(secret, 'salty') assert ser.loads(serialize(secret, 'salty', 'test')) == 'test' def test_with_highorder_salt(self): salt = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') ser = self.makeOne('secret', salt) assert ser.loads(serialize('secret', salt, 'test')) == 'test' # bw-compat with webob <= 1.3.1 where secrets were encoded with latin-1 def test_with_latin1_secret(self): secret = b'La Pe\xc3\xb1a' ser = self.makeOne(secret.decode('latin-1'), 'salty') assert ser.loads(serialize(secret, 'salty', 'test')), 'test' # bw-compat with webob <= 1.3.1 where salts were encoded with latin-1 def test_with_latin1_salt(self): salt = b'La Pe\xc3\xb1a' ser = self.makeOne('secret', salt.decode('latin-1')) assert ser.loads(serialize('secret', salt, 'test')) == 'test' WebOb-1.8.6/tests/test_cookies_bw.py0000644000076600000240000000106613611751037020400 0ustar xistencestaff00000000000000import warnings from webob import cookies def setup_module(module): cookies._should_raise = False def teardown_module(module): cookies._should_raise = False def test_invalid_cookie_space(): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") # Trigger a warning. cookies._value_quote(b'hello world') assert len(w) == 1 assert issubclass(w[-1].category, RuntimeWarning) is True assert "ValueError" in str(w[-1].message) WebOb-1.8.6/tests/test_datetime_utils.py0000644000076600000240000001147013611751037021270 0ustar xistencestaff00000000000000import pytest import datetime import calendar from email.utils import formatdate from webob import datetime_utils def test_UTC(): """Test missing function in _UTC""" x = datetime_utils.UTC assert x.tzname(datetime.datetime.now()) == 'UTC' assert x.dst(datetime.datetime.now()) == datetime.timedelta(0) assert x.utcoffset(datetime.datetime.now()) == datetime.timedelta(0) assert repr(x) == 'UTC' def test_parse_date(): """Testing datetime_utils.parse_date. We need to verify the following scenarios: * a nil submitted value * a submitted value that cannot be parse into a date * a valid RFC2822 date with and without timezone """ ret = datetime_utils.parse_date(None) assert ret is None, ("We passed a None value to parse_date. We should get" " None but instead we got %s" % ret) ret = datetime_utils.parse_date('Hi There') assert ret is None, ("We passed an invalid value to parse_date. We should" " get None but instead we got %s" % ret) ret = datetime_utils.parse_date(1) assert ret is None, ("We passed an invalid value to parse_date. We should" " get None but instead we got %s" % ret) ret = datetime_utils.parse_date('\xc3') assert ret is None, ("We passed an invalid value to parse_date. We should" " get None but instead we got %s" % ret) ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08 -0500') assert ret == datetime.datetime( 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC) ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08') assert ret == datetime.datetime( 1995, 11, 20, 19, 12, 8, tzinfo=datetime_utils.UTC) ret = datetime_utils.parse_date(Uncooperative()) assert ret is None class Uncooperative(object): def __str__(self): raise NotImplementedError def test_serialize_date(): """Testing datetime_utils.serialize_date We need to verify the following scenarios: * on py3, passing an binary date, return the same date but str * on py2, passing an unicode date, return the same date but str * passing a timedelta, return now plus the delta * passing an invalid object, should raise ValueError """ from webob.compat import text_ ret = datetime_utils.serialize_date('Mon, 20 Nov 1995 19:12:08 GMT') assert isinstance(ret, str) assert ret == 'Mon, 20 Nov 1995 19:12:08 GMT' ret = datetime_utils.serialize_date(text_('Mon, 20 Nov 1995 19:12:08 GMT')) assert isinstance(ret, str) assert ret == 'Mon, 20 Nov 1995 19:12:08 GMT' dt = formatdate( calendar.timegm( (datetime.datetime.now() + datetime.timedelta(1)).timetuple()), usegmt=True) assert dt == datetime_utils.serialize_date(datetime.timedelta(1)) with pytest.raises(ValueError): datetime_utils.serialize_date(None) def test_parse_date_delta(): """Testing datetime_utils.parse_date_delta We need to verify the following scenarios: * passing a nil value, should return nil * passing a value that fails the conversion to int, should call parse_date """ assert datetime_utils.parse_date_delta(None) is None, ('Passing none value,' 'should return None') ret = datetime_utils.parse_date_delta('Mon, 20 Nov 1995 19:12:08 -0500') assert ret == datetime.datetime( 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC) WHEN = datetime.datetime(2011, 3, 16, 10, 10, 37, tzinfo=datetime_utils.UTC) with _NowRestorer(WHEN): ret = datetime_utils.parse_date_delta(1) assert ret == WHEN + datetime.timedelta(0, 1) def test_serialize_date_delta(): """Testing datetime_utils.serialize_date_delta We need to verify the following scenarios: * if we pass something that's not an int or float, it should delegate the task to serialize_date """ assert datetime_utils.serialize_date_delta(1) == '1' assert datetime_utils.serialize_date_delta(1.5) == '1' ret = datetime_utils.serialize_date_delta('Mon, 20 Nov 1995 19:12:08 GMT') assert type(ret) is (str) assert ret == 'Mon, 20 Nov 1995 19:12:08 GMT' def test_timedelta_to_seconds(): val = datetime.timedelta(86400) result = datetime_utils.timedelta_to_seconds(val) assert result == 7464960000 class _NowRestorer(object): def __init__(self, new_now): self._new_now = new_now self._old_now = None def __enter__(self): import webob.datetime_utils self._old_now = webob.datetime_utils._now webob.datetime_utils._now = lambda: self._new_now def __exit__(self, exc_type, exc_value, traceback): import webob.datetime_utils webob.datetime_utils._now = self._old_now WebOb-1.8.6/tests/test_dec.py0000644000076600000240000002452613611751037017015 0ustar xistencestaff00000000000000import unittest from webob.request import Request from webob.response import Response from webob.dec import wsgify from webob.compat import bytes_ from webob.compat import text_ class DecoratorTests(unittest.TestCase): def _testit(self, app, req): if isinstance(req, str): req = Request.blank(req) resp = req.get_response(app) return resp def test_wsgify(self): resp_str = 'hey, this is a test: %s' @wsgify def test_app(req): return bytes_(resp_str % req.url) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, bytes_(resp_str % 'http://localhost/a%20url')) self.assertEqual(resp.content_length, 45) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_empty_repr(self): self.assertTrue('wsgify at' in repr(wsgify())) def test_wsgify_args(self): resp_str = b'hey hey my my' @wsgify(args=(resp_str,)) def test_app(req, strarg): return strarg resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 13) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_kwargs(self): resp_str = b'hey hey my my' @wsgify(kwargs=dict(strarg=resp_str)) def test_app(req, strarg=''): return strarg resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 13) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_raise_httpexception(self): from webob.exc import HTTPBadRequest @wsgify def test_app(req): raise HTTPBadRequest resp = self._testit(test_app, '/a url') self.assertTrue(resp.body.startswith(b'400 Bad Request')) self.assertEqual(resp.content_type, 'text/plain') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_no___get__(self): # use a class instance instead of a fn so we wrap something w/ # no __get__ class TestApp(object): def __call__(self, req): return 'nothing to see here' test_app = wsgify(TestApp()) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'nothing to see here') self.assertTrue(test_app.__get__(test_app) is test_app) def test_wsgify_app_returns_unicode(self): def test_app(req): return text_('some text') test_app = wsgify(test_app) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'some text') def test_wsgify_args_no_func(self): test_app = wsgify(None, args=(1,)) self.assertRaises(TypeError, self._testit, test_app, '/a url') def test_wsgify_call_args(self): resp_str = "args: %s, kwargs: %s" def show_vars(req, *args, **kwargs): return bytes_(resp_str % (sorted(args), sorted(kwargs.items()))) app = wsgify(show_vars, args=('foo', 'bar'), kwargs={'a': 1, 'b': 2}) resp = app(Request.blank('/')) self.assertEqual(resp, bytes_(resp_str % ("['bar', 'foo']", "[('a', 1), ('b', 2)]"))) def test_wsgify_call_args_override(self): resp_str = "args: %s, kwargs: %s" def show_vars(req, *args, **kwargs): return bytes_(resp_str % (sorted(args), sorted(kwargs.items()))) app = wsgify(show_vars, args=('foo', 'bar'), kwargs={'a': 1, 'b': 2}) resp = app(Request.blank('/'), 'qux', c=3) self.assertEqual(resp, bytes_(resp_str % ("['qux']", "[('c', 3)]"))) def test_wsgify_wrong_sig(self): @wsgify def test_app(req): return 'What have you done for me lately?' req = dict() self.assertRaises(TypeError, test_app, req, 1, 2) self.assertRaises(TypeError, test_app, req, 1, key='word') def test_wsgify_none_response(self): @wsgify def test_app(req): return resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'') self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.content_length, 0) def test_wsgify_get(self): resp_str = b"What'choo talkin' about, Willis?" @wsgify def test_app(req): return Response(resp_str) resp = test_app.get('/url/path') self.assertEqual(resp.body, resp_str) def test_wsgify_post(self): post_dict = dict(speaker='Robin', words='Holy test coverage, Batman!') @wsgify def test_app(req): return Response('%s: %s' % (req.POST['speaker'], req.POST['words'])) resp = test_app.post('/url/path', post_dict) self.assertEqual(resp.body, bytes_('%s: %s' % (post_dict['speaker'], post_dict['words']))) def test_wsgify_request_method(self): resp_str = b'Nice body!' @wsgify def test_app(req): self.assertEqual(req.method, 'PUT') return Response(req.body) resp = test_app.request('/url/path', method='PUT', body=resp_str) self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 10) self.assertEqual(resp.content_type, 'text/html') def test_wsgify_undecorated(self): def test_app(req): return Response('whoa') wrapped_test_app = wsgify(test_app) self.assertTrue(wrapped_test_app.undecorated is test_app) def test_wsgify_custom_request(self): resp_str = 'hey, this is a test: %s' class MyRequest(Request): pass @wsgify(RequestClass=MyRequest) def test_app(req): return bytes_(resp_str % req.url) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, bytes_(resp_str % 'http://localhost/a%20url')) self.assertEqual(resp.content_length, 45) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_middleware(self): resp_str = "These are the vars: %s" @wsgify.middleware def set_urlvar(req, app, **vars): req.urlvars.update(vars) return app(req) from webob.dec import _MiddlewareFactory self.assertTrue(set_urlvar.__class__ is _MiddlewareFactory) r = repr(set_urlvar) self.assertTrue('set_urlvar' in r) @wsgify def show_vars(req): return resp_str % (sorted(req.urlvars.items())) show_vars2 = set_urlvar(show_vars, a=1, b=2) resp = self._testit(show_vars2, '/path') self.assertEqual(resp.body, bytes_(resp_str % "[('a', 1), ('b', 2)]")) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') self.assertEqual(resp.content_length, 40) def test_middleware_call_kwargs(self): resp_str = "kwargs: %s" @wsgify.middleware def set_args(req, app, **kwargs): req.urlvars = kwargs return req.get_response(app) @wsgify def show_vars(req): return resp_str % sorted(req.urlvars.items()) app = set_args(show_vars, a=1, b=2) resp = app(Request.blank('/')) self.assertEqual(resp.body, bytes_(resp_str % "[('a', 1), ('b', 2)]")) def test_middleware_call_kwargs_override(self): resp_str = "kwargs: %s" @wsgify.middleware def set_args(req, app, **kwargs): req.urlvars = kwargs return req.get_response(app) @wsgify def show_vars(req): return resp_str % sorted(req.urlvars.items()) app = set_args(show_vars, a=1, b=2) resp = app(Request.blank('/'), c=3) self.assertEqual(resp.body, bytes_(resp_str % "[('c', 3)]")) def test_middleware_as_decorator(self): resp_str = "These are the vars: %s" @wsgify.middleware def set_urlvar(req, app, **vars): req.urlvars.update(vars) return app(req) @set_urlvar(a=1,b=2) @wsgify def show_vars(req): return resp_str % (sorted(req.urlvars.items())) resp = self._testit(show_vars, '/path') self.assertEqual(resp.body, bytes_(resp_str % "[('a', 1), ('b', 2)]")) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') self.assertEqual(resp.content_length, 40) def test_unbound_middleware(self): @wsgify def test_app(req): return Response('Say wha!?') unbound = wsgify.middleware(None, test_app, some='thing') from webob.dec import _UnboundMiddleware self.assertTrue(unbound.__class__ is _UnboundMiddleware) self.assertEqual(unbound.kw, dict(some='thing')) @unbound def middle(req, app, **kw): return app(req) self.assertTrue(middle.__class__ is wsgify) self.assertTrue('test_app' in repr(unbound)) def test_unbound_middleware_no_app(self): unbound = wsgify.middleware(None, None) from webob.dec import _UnboundMiddleware self.assertTrue(unbound.__class__ is _UnboundMiddleware) self.assertEqual(unbound.kw, dict()) def test_classapp(self): class HostMap(dict): @wsgify def __call__(self, req): return self[req.host.split(':')[0]] app = HostMap() app['example.com'] = Response('1') app['other.com'] = Response('2') resp = Request.blank('http://example.com/').get_response(wsgify(app)) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') self.assertEqual(resp.content_length, 1) self.assertEqual(resp.body, b'1') def test_middleware_direct_call(self): @wsgify.middleware def mw(req, app): return 'foo' app = mw(Response()) self.assertEqual(app(Request.blank('/')), 'foo') WebOb-1.8.6/tests/test_descriptors.py0000644000076600000240000006471213611751037020624 0ustar xistencestaff00000000000000from datetime import tzinfo from datetime import timedelta import pytest from webob.compat import ( native_, text_, ) from webob.request import Request class GMT(tzinfo): """UTC""" ZERO = timedelta(0) def utcoffset(self, dt): return self.ZERO def tzname(self, dt): return "UTC" def dst(self, dt): return self.ZERO class MockDescriptor: _val = 'avalue' def __get__(self, obj, type=None): return self._val def __set__(self, obj, val): self._val = val def __delete__(self, obj): self._val = None def test_environ_getter_docstring(): from webob.descriptors import environ_getter desc = environ_getter('akey') assert desc.__doc__ == "Gets and sets the ``akey`` key in the environment." def test_environ_getter_nodefault_keyerror(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey') with pytest.raises(KeyError): desc.fget(req) def test_environ_getter_nodefault_fget(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey') desc.fset(req, 'bar') assert req.environ['akey'] == 'bar' def test_environ_getter_nodefault_fdel(): from webob.descriptors import environ_getter desc = environ_getter('akey') assert desc.fdel is None def test_environ_getter_default_fget(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') assert desc.fget(req) == 'the_default' def test_environ_getter_default_fset(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'bar') assert req.environ['akey'] == 'bar' def test_environ_getter_default_fset_none(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'baz') desc.fset(req, None) assert 'akey' not in req.environ def test_environ_getter_default_fdel(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'baz') assert 'akey' in req.environ desc.fdel(req) assert 'akey' not in req.environ def test_environ_getter_rfc_section(): from webob.descriptors import environ_getter desc = environ_getter('HTTP_X_AKEY', rfc_section='14.3') assert desc.__doc__ == "Gets and sets the ``X-Akey`` header "\ "(`HTTP spec section 14.3 "\ "`_)." def test_upath_property_fget(): from webob.descriptors import upath_property req = Request.blank('/') desc = upath_property('akey') assert desc.fget(req) == '' def test_upath_property_fset(): from webob.descriptors import upath_property req = Request.blank('/') desc = upath_property('akey') desc.fset(req, 'avalue') assert desc.fget(req) == 'avalue' def test_header_getter_doc(): from webob.descriptors import header_getter desc = header_getter('X-Header', '14.3') assert('http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3' in desc.__doc__) assert '``X-Header`` header' in desc.__doc__ def test_header_getter_fget(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') assert desc.fget(resp) is None def test_header_getter_fset(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') assert desc.fget(resp) == 'avalue' def test_header_getter_fset_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') desc.fset(resp, None) assert desc.fget(resp) is None def test_header_getter_fset_text(): from webob.compat import text_ from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, text_('avalue')) assert desc.fget(resp) == 'avalue' def test_header_getter_fset_text_control_chars(): from webob.compat import text_ from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') with pytest.raises(ValueError): desc.fset(resp, text_('\n')) def test_header_getter_fdel(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue2') desc.fdel(resp) assert desc.fget(resp) is None def test_header_getter_unicode_fget_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') assert desc.fget(resp) is None def test_header_getter_unicode_fget(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') assert desc.fget(resp) == 'avalue' def test_header_getter_unicode_fset_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, None) assert desc.fget(resp) is None def test_header_getter_unicode_fset(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue2') assert desc.fget(resp) == 'avalue2' def test_header_getter_unicode_fdel(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue3') desc.fdel(resp) assert desc.fget(resp) is None def test_converter_not_prop(): from webob.descriptors import converter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int with pytest.raises(AssertionError): converter( ('CONTENT_LENGTH', None, '14.13'), parse_int_safe, serialize_int, 'int') def test_converter_with_name_docstring(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13' in desc.__doc__ assert '``Content-Length`` header' in desc.__doc__ def test_converter_with_name_fget(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') assert desc.fget(req) == 666 def test_converter_with_name_fset(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') desc.fset(req, '999') assert desc.fget(req) == 999 def test_converter_without_name_fget(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int) assert desc.fget(req) == 666 def test_converter_without_name_fset(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int) desc.fset(req, '999') assert desc.fget(req) == 999 def test_converter_none_for_wrong_type(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', 'sixsixsix', '14.13'), parse_int_safe, serialize_int, 'int') assert desc.fget(req) is None def test_converter_delete(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') with pytest.raises(KeyError): desc.fdel(req) def test_list_header(): from webob.descriptors import list_header desc = list_header('CONTENT_LENGTH', '14.13') assert type(desc) == property def test_parse_list_single(): from webob.descriptors import parse_list result = parse_list('avalue') assert result == ('avalue',) def test_parse_list_multiple(): from webob.descriptors import parse_list result = parse_list('avalue,avalue2') assert result == ('avalue', 'avalue2') def test_parse_list_none(): from webob.descriptors import parse_list result = parse_list(None) assert result is None def test_parse_list_unicode_single(): from webob.descriptors import parse_list result = parse_list('avalue') assert result == ('avalue',) def test_parse_list_unicode_multiple(): from webob.descriptors import parse_list result = parse_list('avalue,avalue2') assert result == ('avalue', 'avalue2') def test_serialize_list(): from webob.descriptors import serialize_list result = serialize_list(('avalue', 'avalue2')) assert result == 'avalue, avalue2' def test_serialize_list_string(): from webob.descriptors import serialize_list result = serialize_list('avalue') assert result == 'avalue' def test_serialize_list_unicode(): from webob.descriptors import serialize_list result = serialize_list('avalue') assert result == 'avalue' def test_converter_date(): import datetime from webob.descriptors import converter_date from webob.descriptors import environ_getter req = Request.blank('/') UTC = GMT() desc = converter_date(environ_getter( "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8")) assert desc.fget(req) == datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC) def test_converter_date_docstring(): from webob.descriptors import converter_date from webob.descriptors import environ_getter desc = converter_date(environ_getter( "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8")) assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8' in desc.__doc__ assert '``Date`` header' in desc.__doc__ def test_date_header_fget_none(): from webob import Response from webob.descriptors import date_header resp = Response('aresponse') desc = date_header('HTTP_DATE', "14.8") assert desc.fget(resp) is None def test_date_header_fset_fget(): import datetime from webob import Response from webob.descriptors import date_header resp = Response('aresponse') UTC = GMT() desc = date_header('HTTP_DATE', "14.8") desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT") assert desc.fget(resp) == datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC) def test_date_header_fdel(): from webob import Response from webob.descriptors import date_header resp = Response('aresponse') desc = date_header('HTTP_DATE', "14.8") desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT") desc.fdel(resp) assert desc.fget(resp) is None def test_deprecated_property(): from webob.descriptors import deprecated_property class Foo(object): pass Foo.attr = deprecated_property('attr', 'attr', 'whatever', '1.2') foo = Foo() with pytest.raises(DeprecationWarning): getattr(foo, 'attr') with pytest.raises(DeprecationWarning): setattr(foo, 'attr', {}) with pytest.raises(DeprecationWarning): delattr(foo, 'attr') def test_parse_etag_response(): from webob.descriptors import parse_etag_response etresp = parse_etag_response("etag") assert etresp == "etag" def test_parse_etag_response_quoted(): from webob.descriptors import parse_etag_response etresp = parse_etag_response('"etag"') assert etresp == "etag" def test_parse_etag_response_is_none(): from webob.descriptors import parse_etag_response etresp = parse_etag_response(None) assert etresp is None def test_serialize_etag_response(): from webob.descriptors import serialize_etag_response etresp = serialize_etag_response("etag") assert etresp == '"etag"' def test_serialize_if_range_string(): from webob.descriptors import serialize_if_range val = serialize_if_range("avalue") assert val == "avalue" def test_serialize_if_range_unicode(): from webob.descriptors import serialize_if_range val = serialize_if_range("avalue") assert val == "avalue" def test_serialize_if_range_datetime(): import datetime from webob.descriptors import serialize_if_range UTC = GMT() val = serialize_if_range(datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC)) assert val, "Tue == 15 Nov 1994 08:12:31 GMT" def test_serialize_if_range_other(): from webob.descriptors import serialize_if_range val = serialize_if_range(123456) assert val == '123456' def test_parse_range_none(): from webob.descriptors import parse_range assert parse_range(None) is None def test_parse_range_type(): from webob.byterange import Range from webob.descriptors import parse_range val = parse_range("bytes=1-500") assert type(val) is type(Range.parse("bytes=1-500")) def test_parse_range_values(): from webob.byterange import Range range = Range.parse("bytes=1-500") assert range.start == 1 assert range.end == 501 def test_serialize_range_none(): from webob.descriptors import serialize_range val = serialize_range(None) assert val is None def test_serialize_range(): from webob.descriptors import serialize_range val = serialize_range((1, 500)) assert val == 'bytes=1-499' def test_parse_int_none(): from webob.descriptors import parse_int val = parse_int(None) assert val is None def test_parse_int_emptystr(): from webob.descriptors import parse_int val = parse_int('') assert val is None def test_parse_int(): from webob.descriptors import parse_int val = parse_int('123') assert val == 123 def test_parse_int_invalid(): from webob.descriptors import parse_int with pytest.raises(ValueError): parse_int('abc') def test_parse_int_safe_none(): from webob.descriptors import parse_int_safe assert parse_int_safe(None) is None def test_parse_int_safe_emptystr(): from webob.descriptors import parse_int_safe assert parse_int_safe('') is None def test_parse_int_safe(): from webob.descriptors import parse_int_safe assert parse_int_safe('123') == 123 def test_parse_int_safe_invalid(): from webob.descriptors import parse_int_safe assert parse_int_safe('abc') is None def test_serialize_int(): from webob.descriptors import serialize_int assert serialize_int is str def test_parse_content_range_none(): from webob.descriptors import parse_content_range assert parse_content_range(None) is None def test_parse_content_range_emptystr(): from webob.descriptors import parse_content_range assert parse_content_range(' ') is None def test_parse_content_range_length(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") assert val.length == ContentRange.parse("bytes 0-499/1234").length def test_parse_content_range_start(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") assert val.start == ContentRange.parse("bytes 0-499/1234").start def test_parse_content_range_stop(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") assert val.stop == ContentRange.parse("bytes 0-499/1234").stop def test_serialize_content_range_none(): from webob.descriptors import serialize_content_range assert serialize_content_range(None) == 'None' ### XXX: Seems wrong def test_serialize_content_range_emptystr(): from webob.descriptors import serialize_content_range assert serialize_content_range('') is None def test_serialize_content_range_invalid(): from webob.descriptors import serialize_content_range with pytest.raises(ValueError): serialize_content_range((1,)) def test_serialize_content_range_asterisk(): from webob.descriptors import serialize_content_range assert serialize_content_range((0, 500)) == 'bytes 0-499/*' def test_serialize_content_range_defined(): from webob.descriptors import serialize_content_range assert serialize_content_range((0, 500, 1234)) == 'bytes 0-499/1234' def test_parse_auth_params_leading_capital_letter(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic Realm=WebOb') assert val == {'ealm': 'WebOb'} def test_parse_auth_params_trailing_capital_letter(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic realM=WebOb') assert val == {} def test_parse_auth_params_doublequotes(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic realm="Web Object"') assert val == {'realm': 'Web Object'} def test_parse_auth_params_multiple_values(): from webob.descriptors import parse_auth_params val = parse_auth_params("foo='blah &&234', qop=foo, nonce='qwerty1234'") assert val == {'nonce': "'qwerty1234'", 'foo': "'blah &&234'", 'qop': 'foo'} def test_parse_auth_params_truncate_on_comma(): from webob.descriptors import parse_auth_params val = parse_auth_params("Basic realm=WebOb,this_will_truncate") assert val == {'realm': 'WebOb'} def test_parse_auth_params_emptystr(): from webob.descriptors import parse_auth_params assert parse_auth_params('') == {} def test_parse_auth_params_bad_whitespace(): from webob.descriptors import parse_auth_params assert parse_auth_params('a= "2 ", b =3, c=4 ') == { 'a': '2 ', 'b': '3', 'c': '4' } def test_authorization2(): from webob.descriptors import parse_auth_params for s, d in [ ('x=y', {'x': 'y'}), ('x="y"', {'x': 'y'}), ('x=y,z=z', {'x': 'y', 'z': 'z'}), ('x=y, z=z', {'x': 'y', 'z': 'z'}), ('x="y",z=z', {'x': 'y', 'z': 'z'}), ('x="y", z=z', {'x': 'y', 'z': 'z'}), ('x="y,x", z=z', {'x': 'y,x', 'z': 'z'}), ]: assert parse_auth_params(s) == d def test_parse_auth_none(): from webob.descriptors import parse_auth assert parse_auth(None) is None def test_parse_auth_emptystr(): from webob.descriptors import parse_auth assert parse_auth('') == ('', '') def test_parse_auth_bearer(): from webob.descriptors import parse_auth assert parse_auth('Bearer token').authtype == 'Bearer' assert parse_auth('Bearer token').params == 'token' def test_parse_auth_unknown_nospace(): from webob.descriptors import parse_auth assert parse_auth('NoSpace') == ('NoSpace', '') def test_parse_auth_known_nospace(): from webob.descriptors import parse_auth assert parse_auth('Digest') == ('Digest', {}) def test_parse_auth_basic(): from webob.descriptors import parse_auth assert parse_auth("Basic realm=WebOb") == ('Basic', 'realm=WebOb') def test_parse_auth_basic_quoted(): from webob.descriptors import parse_auth assert parse_auth('Basic realm="Web Ob"') == ('Basic', {'realm': 'Web Ob'}) def test_parse_auth_basic_quoted_multiple_unknown(): from webob.descriptors import parse_auth assert parse_auth("foo='blah &&234', qop=foo, nonce='qwerty1234'") == ( "foo='blah", "&&234', qop=foo, nonce='qwerty1234'" ) def test_parse_auth_basic_quoted_known_multiple(): from webob.descriptors import parse_auth assert parse_auth("Basic realm='blah &&234', qop=foo, nonce='qwerty1234'") == ( 'Basic', "realm='blah &&234', qop=foo, nonce='qwerty1234'" ) def test_serialize_auth_none(): from webob.descriptors import serialize_auth assert serialize_auth(None) is None def test_serialize_auth_emptystr(): from webob.descriptors import serialize_auth assert serialize_auth('') == '' def test_serialize_auth_str(): from webob.descriptors import serialize_auth assert serialize_auth('some string') == 'some string' def test_serialize_auth_parsed_emptystr(): from webob.descriptors import serialize_auth assert serialize_auth(('', '')) == ' ' def test_serialize_auth_parsed_unknown_nospace(): from webob.descriptors import serialize_auth assert serialize_auth(('NoSpace', '')) == 'NoSpace ' def test_serialize_auth_parsed_known_nospace(): from webob.descriptors import serialize_auth assert serialize_auth(('Digest', {})) == 'Digest ' def test_serialize_auth_basic_quoted(): from webob.descriptors import serialize_auth val = serialize_auth(('Basic', 'realm="WebOb"')) assert val == 'Basic realm="WebOb"' def test_serialize_auth_digest_multiple(): from webob.descriptors import serialize_auth val = serialize_auth(('Digest', 'realm="WebOb", nonce=abcde12345, qop=foo')) flags = val[len('Digest'):] result = sorted([ x.strip() for x in flags.split(',') ]) assert result == ['nonce=abcde12345', 'qop=foo', 'realm="WebOb"'] def test_serialize_auth_digest_tuple(): from webob.descriptors import serialize_auth val = serialize_auth(('Digest', {'realm':'"WebOb"', 'nonce':'abcde12345', 'qop':'foo'})) flags = val[len('Digest'):] result = sorted([ x.strip() for x in flags.split(',') ]) assert result == ['nonce="abcde12345"', 'qop="foo"', 'realm=""WebOb""'] _nodefault = object() class _TestEnvironDecoder(object): def _callFUT(self, key, default=_nodefault, rfc_section=None, encattr=None): from webob.descriptors import environ_decoder if default is _nodefault: return environ_decoder(key, rfc_section=rfc_section, encattr=encattr) else: return environ_decoder(key, default=default, rfc_section=rfc_section, encattr=encattr) def test_docstring(self): desc = self._callFUT('akey') assert desc.__doc__ == "Gets and sets the ``akey`` key in the environment." def test_nodefault_keyerror(self): req = self._makeRequest() desc = self._callFUT('akey') with pytest.raises(KeyError): desc.fget(req) def test_nodefault_fget(self): req = self._makeRequest() desc = self._callFUT('akey') desc.fset(req, 'bar') assert req.environ['akey'] == 'bar' def test_nodefault_fdel(self): desc = self._callFUT('akey') assert desc.fdel is None def test_default_fget(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') assert desc.fget(req) == 'the_default' def test_default_fset(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'bar') assert req.environ['akey'] == 'bar' def test_default_fset_none(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'baz') desc.fset(req, None) assert 'akey' not in req.environ def test_default_fdel(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'baz') assert 'akey' in req.environ desc.fdel(req) assert 'akey' not in req.environ def test_rfc_section(self): desc = self._callFUT('HTTP_X_AKEY', rfc_section='14.3') assert desc.__doc__ == "Gets and sets the ``X-Akey`` header "\ "(`HTTP spec section 14.3 "\ "`_)." def test_fset_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() desc.fset(req, text_(b'\xc3\xab', 'utf-8')) assert req.environ['HTTP_X_AKEY'] == native_(b'\xc3\xab', 'latin-1') class TestEnvironDecoder(_TestEnvironDecoder): def _makeRequest(self): from webob.request import BaseRequest req = BaseRequest.blank('/') return req def test_fget_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() req.environ['HTTP_X_AKEY'] = native_(b'\xc3\xab') result = desc.fget(req) assert result == text_(b'\xc3\xab', 'utf-8') class TestEnvironDecoderLegacy(_TestEnvironDecoder): def _makeRequest(self): from webob.request import LegacyRequest req = LegacyRequest.blank('/') return req def test_fget_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() req.environ['HTTP_X_AKEY'] = native_(b'\xc3\xab', 'latin-1') result = desc.fget(req) assert result == native_(b'\xc3\xab', 'latin-1') def test_default_fget_nonascii(self): req = self._makeRequest() desc = self._callFUT('akey', default=b'the_default') assert desc.fget(req).__class__ == bytes WebOb-1.8.6/tests/test_etag.py0000644000076600000240000001212113611751037017166 0ustar xistencestaff00000000000000import pytest from webob.etag import ETagMatcher, IfRange, etag_property class Test_etag_properties(object): def _makeDummyRequest(self, **kw): """ Return a DummyRequest object with attrs from kwargs. Use like: dr = _makeDummyRequest(environment={'userid': 'johngalt'}) Then you can: uid = dr.environment.get('userid', 'SomeDefault') """ class Dummy(object): def __init__(self, **kwargs): self.__dict__.update(**kwargs) d = Dummy(**kw) return d def test_fget_missing_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={}) assert ep.fget(req) == "DEFAULT" def test_fget_found_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY': '"VALUE"'}) res = ep.fget(req) assert res.etags == ['VALUE'] def test_fget_star_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY': '*'}) res = ep.fget(req) import webob.etag assert type(res) == webob.etag._AnyETag assert res.__dict__ == {} def test_fset_None(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY': '*'}) res = ep.fset(req, None) assert res is None def test_fset_not_None(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY': 'OLDVAL'}) res = ep.fset(req, "NEWVAL") assert res is None assert req.environ['KEY'] == 'NEWVAL' def test_fedl(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY': 'VAL', 'QUAY': 'VALYOU'}) res = ep.fdel(req) assert res is None assert 'KEY' not in req.environ assert req.environ['QUAY'] == 'VALYOU' class Test_AnyETag(object): def _getTargetClass(self): from webob.etag import _AnyETag return _AnyETag def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test___repr__(self): etag = self._makeOne() assert etag.__repr__() == '' def test___nonzero__(self): etag = self._makeOne() assert etag.__nonzero__() is False def test___contains__something(self): etag = self._makeOne() assert 'anything' in etag def test___str__(self): etag = self._makeOne() assert str(etag) == '*' class Test_NoETag(object): def _getTargetClass(self): from webob.etag import _NoETag return _NoETag def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test___repr__(self): etag = self._makeOne() assert etag.__repr__() == '' def test___nonzero__(self): etag = self._makeOne() assert etag.__nonzero__() is False def test___contains__something(self): etag = self._makeOne() assert 'anything' not in etag def test___str__(self): etag = self._makeOne() assert str(etag) == '' class Test_Parse(object): def test_parse_None(self): et = ETagMatcher.parse(None) assert et.etags == [] def test_parse_anyetag(self): # these tests smell bad, are they useful? et = ETagMatcher.parse('*') assert et.__dict__ == {} assert et.__repr__() == '' def test_parse_one(self): et = ETagMatcher.parse('"ONE"') assert et.etags == ['ONE'] def test_parse_invalid(self): for tag in ['one', 'one, two', '"one two']: et = ETagMatcher.parse(tag) assert et.etags == [tag] et = ETagMatcher.parse('"foo" and w/"weak"', strong=False) assert et.etags == ['foo'] def test_parse_commasep(self): et = ETagMatcher.parse('"ONE", "TWO"') assert et.etags, ['ONE' == 'TWO'] def test_parse_commasep_w_weak(self): et = ETagMatcher.parse('"ONE", W/"TWO"') assert et.etags == ['ONE'] et = ETagMatcher.parse('"ONE", W/"TWO"', strong=False) assert et.etags, ['ONE' == 'TWO'] def test_parse_quoted(self): et = ETagMatcher.parse('"ONE"') assert et.etags == ['ONE'] def test_parse_quoted_two(self): et = ETagMatcher.parse('"ONE", "TWO"') assert et.etags, ['ONE' == 'TWO'] def test_parse_quoted_two_weak(self): et = ETagMatcher.parse('"ONE", W/"TWO"') assert et.etags == ['ONE'] et = ETagMatcher.parse('"ONE", W/"TWO"', strong=False) assert et.etags, ['ONE' == 'TWO'] class Test_IfRange(object): def test___repr__(self): assert repr(IfRange(None)) == 'IfRange(None)' def test___repr__etag(self): assert repr(IfRange('ETAG')) == "IfRange('ETAG')" def test___repr__date(self): ir = IfRange.parse('Fri, 09 Nov 2001 01:08:47 GMT') assert repr(ir) == 'IfRangeDate(datetime.datetime(2001, 11, 9, 1, 8, 47, tzinfo=UTC))' WebOb-1.8.6/tests/test_etag_nose.py0000644000076600000240000000453013611751037020217 0ustar xistencestaff00000000000000import pytest from webob.etag import IfRange, ETagMatcher from webob import Response def test_if_range_None(): ir = IfRange.parse(None) assert str(ir) == '' assert not ir assert Response() in ir assert Response(etag='foo') in ir assert Response(etag='foo GMT') in ir def test_if_range_match_date(): date = 'Fri, 09 Nov 2001 01:08:47 GMT' ir = IfRange.parse(date) assert str(ir) == date assert Response() not in ir assert Response(etag='etag') not in ir assert Response(etag=date) not in ir assert Response(last_modified='Fri, 09 Nov 2001 01:00:00 GMT') in ir assert Response(last_modified='Fri, 10 Nov 2001 01:00:00 GMT') not in ir def test_if_range_match_etag(): ir = IfRange.parse('ETAG') assert str(ir) == '"ETAG"' assert Response() not in ir assert Response(etag='other') not in ir assert Response(etag='ETAG') in ir assert Response(etag='W/"ETAG"') not in ir def test_if_range_match_etag_weak(): ir = IfRange.parse('W/"ETAG"') assert str(ir) == '' assert Response(etag='ETAG') not in ir assert Response(etag='W/"ETAG"') not in ir def test_if_range_repr(): assert repr(IfRange.parse(None)) == 'IfRange()' assert str(IfRange.parse(None)) == '' def test_resp_etag(): def t(tag, res, raw, strong): assert Response(etag=tag).etag == res assert Response(etag=tag).headers.get('etag') == raw assert Response(etag=tag).etag_strong == strong t('foo', 'foo', '"foo"', 'foo') t('"foo"', 'foo', '"foo"', 'foo') t('a"b', 'a"b', '"a\\"b"', 'a"b') t('W/"foo"', 'foo', 'W/"foo"', None) t('W/"a\\"b"', 'a"b', 'W/"a\\"b"', None) t(('foo', True), 'foo', '"foo"', 'foo') t(('foo', False), 'foo', 'W/"foo"', None) t(('"foo"', True), '"foo"', r'"\"foo\""', '"foo"') t(('W/"foo"', True), 'W/"foo"', r'"W/\"foo\""', 'W/"foo"') t(('W/"foo"', False), 'W/"foo"', r'W/"W/\"foo\""', None) def test_matcher(): matcher = ETagMatcher(['ETAGS']) matcher = ETagMatcher(['ETAGS']) assert matcher.etags == ['ETAGS'] assert "ETAGS" in matcher assert "WEAK" not in matcher assert "BEER" not in matcher assert None not in matcher assert repr(matcher) == '' assert str(matcher) == '"ETAGS"' matcher2 = ETagMatcher(("ETAG1","ETAG2")) assert repr(matcher2) == '' WebOb-1.8.6/tests/test_exc.py0000644000076600000240000004264713611751037017045 0ustar xistencestaff00000000000000import pytest import json from webob.request import Request from webob.dec import wsgify from webob import exc as webob_exc @wsgify def method_not_allowed_app(req): if req.method != 'GET': raise webob_exc.HTTPMethodNotAllowed() return 'hello!' def test_noescape_null(): assert webob_exc.no_escape(None) == '' def test_noescape_not_basestring(): assert webob_exc.no_escape(42) == '42' def test_noescape_unicode(): class DummyUnicodeObject(object): def __unicode__(self): return '42' duo = DummyUnicodeObject() assert webob_exc.no_escape(duo) == '42' def test_strip_tags_empty(): assert webob_exc.strip_tags('') == '' def test_strip_tags_newline_to_space(): assert webob_exc.strip_tags('a\nb') == 'a b' def test_strip_tags_zaps_carriage_return(): assert webob_exc.strip_tags('a\rb') == 'ab' def test_strip_tags_br_to_newline(): assert webob_exc.strip_tags('a
b') == 'a\nb' def test_strip_tags_zaps_comments(): assert webob_exc.strip_tags('a') == 'ab' def test_strip_tags_zaps_tags(): assert webob_exc.strip_tags('foobaz') == 'foobaz' def test_HTTPException(): _called = [] _result = object() def _response(environ, start_response): _called.append((environ, start_response)) return _result environ = {} start_response = object() exc = webob_exc.HTTPException('testing', _response) assert exc.wsgi_response is _response result = exc(environ, start_response) assert result is result assert _called == [(environ, start_response)] def test_exception_with_unicode_data(): req = Request.blank('/', method='POST') res = req.get_response(method_not_allowed_app) assert res.status_code == 405 def test_WSGIHTTPException_headers(): exc = webob_exc.WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), ('Set-Cookie', 'a=2')]) mixed = exc.headers.mixed() assert mixed['set-cookie'] == ['a=1', 'a=2'] def test_WSGIHTTPException_w_body_template(): from string import Template TEMPLATE = '$foo: $bar' exc = webob_exc.WSGIHTTPException(body_template=TEMPLATE) assert exc.body_template == TEMPLATE assert isinstance(exc.body_template_obj, Template) assert exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}) == 'FOO: BAR' def test_WSGIHTTPException_w_empty_body(): class EmptyOnly(webob_exc.WSGIHTTPException): empty_body = True exc = EmptyOnly(content_type='text/plain', content_length=234) assert 'content_type' not in exc.__dict__ assert 'content_length' not in exc.__dict__ def test_WSGIHTTPException___str__(): exc1 = webob_exc.WSGIHTTPException(detail='Detail') assert str(exc1) == 'Detail' class Explain(webob_exc.WSGIHTTPException): explanation = 'Explanation' assert str(Explain()) == 'Explanation' def test_WSGIHTTPException_plain_body_no_comment(): class Explain(webob_exc.WSGIHTTPException): code = '999' title = 'Testing' explanation = 'Explanation' exc = Explain(detail='Detail') assert exc.plain_body({}) == '999 Testing\n\nExplanation\n\n Detail ' def test_WSGIHTTPException_html_body_w_comment(): class Explain(webob_exc.WSGIHTTPException): code = '999' title = 'Testing' explanation = 'Explanation' exc = Explain(detail='Detail', comment='Comment') assert exc.html_body({}) == ( '\n' ' \n' ' 999 Testing\n' ' \n' ' \n' '

999 Testing

\n' ' Explanation

\n' 'Detail\n' '\n\n' ' \n' '' ) def test_WSGIHTTPException_json_body_no_comment(): class ValidationError(webob_exc.WSGIHTTPException): code = '422' title = 'Validation Failed' explanation = 'Validation of an attribute failed.' exc = ValidationError(detail='Attribute "xyz" is invalid.') body = exc.json_body({}) assert json.loads(body) == { "code": "422 Validation Failed", "title": "Validation Failed", "message": "Validation of an attribute failed.

\nAttribute" ' "xyz" is invalid.\n\n', } def test_WSGIHTTPException_respects_application_json(): class ValidationError(webob_exc.WSGIHTTPException): code = '422' title = 'Validation Failed' explanation = 'Validation of an attribute failed.' def start_response(status, headers, exc_info=None): # check that json doesn't contain a charset assert ('Content-Type', 'application/json') in headers pass exc = ValidationError(detail='Attribute "xyz" is invalid.') resp = exc.generate_response(environ={ 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'PUT', 'HTTP_ACCEPT': 'application/json', }, start_response=start_response) assert json.loads(resp[0].decode('utf-8')) == { "code": "422 Validation Failed", "title": "Validation Failed", "message": "Validation of an attribute failed.

\nAttribute" ' "xyz" is invalid.\n\n', } def test_WSGIHTTPException_respects_accept_text_html(): def start_response(status, headers, exc_info=None): for header in headers: if header[0] == 'Content-Type': assert header[1].startswith('text/html') exc = webob_exc.WSGIHTTPException() resp = exc.generate_response(environ={ 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'GET', 'HTTP_ACCEPT': 'text/html', }, start_response=start_response) def test_WSGIHTTPException_respects_accept_text_plain(): def start_response(status, headers, exc_info=None): for header in headers: if header[0] == 'Content-Type': assert header[1].startswith('text/plain') exc = webob_exc.WSGIHTTPException() resp = exc.generate_response(environ={ 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'GET', 'HTTP_ACCEPT': 'text/plain', }, start_response=start_response) def test_WSGIHTTPException_respects_accept_star_star(): def start_response(status, headers, exc_info=None): for header in headers: if header[0] == 'Content-Type': assert header[1].startswith('text/html') exc = webob_exc.WSGIHTTPException() resp = exc.generate_response(environ={ 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'GET', 'HTTP_ACCEPT': '*/*', }, start_response=start_response) def test_WSGIHTTPException_allows_custom_json_formatter(): def json_formatter(body, status, title, environ): return {"fake": True} class ValidationError(webob_exc.WSGIHTTPException): code = '422' title = 'Validation Failed' explanation = 'Validation of an attribute failed.' exc = ValidationError(detail='Attribute "xyz" is invalid.', json_formatter=json_formatter) body = exc.json_body({}) assert json.loads(body) == {"fake": True} def test_WSGIHTTPException_generate_response(): def start_response(status, headers, exc_info=None): assert ('Content-Type', 'text/html; charset=UTF-8') in headers pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'PUT', 'HTTP_ACCEPT': 'text/html' } excep = webob_exc.WSGIHTTPException() assert excep(environ, start_response) == [ b'\n' b' \n' b' 500 Internal Server Error\n' b' \n' b' \n' b'

500 Internal Server Error

\n' b'

\n' b'\n' b'\n\n' b' \n' b''] def test_WSGIHTTPException_call_w_body(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'PUT' } excep = webob_exc.WSGIHTTPException() excep.body = b'test' assert excep(environ,start_response) == [b'test'] def test_WSGIHTTPException_wsgi_response(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = webob_exc.WSGIHTTPException() assert excep.wsgi_response(environ,start_response) == [] def test_WSGIHTTPException_exception_newstyle(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = webob_exc.WSGIHTTPException() webob_exc.newstyle_exceptions = True assert excep(environ,start_response) == [] def test_WSGIHTTPException_exception_no_newstyle(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = webob_exc.WSGIHTTPException() webob_exc.newstyle_exceptions = False assert excep(environ,start_response) == [] def test_HTTPOk_head_of_proxied_head(): # first set up a response to a HEAD request HELLO_WORLD = "Hi!\n" CONTENT_TYPE = "application/hello" def head_app(environ, start_response): """An application object that understands HEAD""" status = '200 OK' response_headers = [('Content-Type', CONTENT_TYPE), ('Content-Length', len(HELLO_WORLD))] start_response(status, response_headers) if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [HELLO_WORLD] def verify_response(resp, description): assert resp.content_type == CONTENT_TYPE, description assert resp.content_length == len(HELLO_WORLD), description assert resp.body == b'', description req = Request.blank('/', method='HEAD') resp1 = req.get_response(head_app) verify_response(resp1, "first response") # Copy the response like a proxy server would. # Copying an empty body has set content_length # so copy the headers only afterwards. resp2 = webob_exc.status_map[resp1.status_int](request=req) resp2.body = resp1.body resp2.headerlist = resp1.headerlist verify_response(resp2, "copied response") # evaluate it again resp3 = req.get_response(resp2) verify_response(resp3, "evaluated copy") def test_HTTPMove(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = webob_exc._HTTPMove() assert m( environ, start_response ) == [] def test_HTTPMove_location_not_none(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = webob_exc._HTTPMove(location='http://example.com') assert m( environ, start_response ) == [] def test_HTTPMove_location_newlines(): environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } with pytest.raises(ValueError): webob_exc._HTTPMove(location='http://example.com\r\nX-Test: false') def test_HTTPMove_add_slash_and_location(): def start_response(status, headers, exc_info=None): pass with pytest.raises(TypeError): webob_exc._HTTPMove(location='http://example.com', add_slash=True) def test_HTTPMove_call_add_slash(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = webob_exc._HTTPMove() m.add_slash = True assert m( environ, start_response ) == [] def test_HTTPMove_call_query_string(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } m = webob_exc._HTTPMove() m.add_slash = True environ[ 'QUERY_STRING' ] = 'querystring' environ['PATH_INFO'] = '/' assert m( environ, start_response ) == [] def test_HTTPFound_unused_environ_variable(): class Crashy(object): def __str__(self): raise Exception('I crashed!') def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'HTTP_ACCEPT': 'text/html', 'crashy': Crashy() } m = webob_exc._HTTPMove(location='http://www.example.com') assert m(environ, start_response) == [ b'\n' b' \n' b' 500 Internal Server Error\n' b' \n' b' \n' b'

500 Internal Server Error

\n' b' The resource has been moved to ' b'' b'http://www.example.com;\n' b'you should be redirected automatically.\n' b'\n\n' b' \n' b''] def test_HTTPExceptionMiddleware_ok(): def app(environ, start_response): return '123' application = app m = webob_exc.HTTPExceptionMiddleware(application) environ = {} start_response = None res = m(environ, start_response) assert res == '123' def test_HTTPExceptionMiddleware_exception(): def wsgi_response(environ, start_response): return '123' def app(environ, start_response): raise webob_exc.HTTPException(None, wsgi_response) application = app m = webob_exc.HTTPExceptionMiddleware(application) environ = {} start_response = None res = m(environ, start_response) assert res == '123' def test_HTTPExceptionMiddleware_exception_exc_info_none(): class DummySys: def exc_info(self): return None def wsgi_response(environ, start_response): return start_response('200 OK', [], exc_info=None) def app(environ, start_response): raise webob_exc.HTTPException(None, wsgi_response) application = app m = webob_exc.HTTPExceptionMiddleware(application) environ = {} def start_response(status, headers, exc_info): pass try: old_sys = webob_exc.sys sys = DummySys() res = m(environ, start_response) assert res is None finally: webob_exc.sys = old_sys def test_status_map_is_deterministic(): for code, cls in ( (200, webob_exc.HTTPOk), (201, webob_exc.HTTPCreated), (202, webob_exc.HTTPAccepted), (203, webob_exc.HTTPNonAuthoritativeInformation), (204, webob_exc.HTTPNoContent), (205, webob_exc.HTTPResetContent), (206, webob_exc.HTTPPartialContent), (300, webob_exc.HTTPMultipleChoices), (301, webob_exc.HTTPMovedPermanently), (302, webob_exc.HTTPFound), (303, webob_exc.HTTPSeeOther), (304, webob_exc.HTTPNotModified), (305, webob_exc.HTTPUseProxy), (307, webob_exc.HTTPTemporaryRedirect), (308, webob_exc.HTTPPermanentRedirect), (400, webob_exc.HTTPBadRequest), (401, webob_exc.HTTPUnauthorized), (402, webob_exc.HTTPPaymentRequired), (403, webob_exc.HTTPForbidden), (404, webob_exc.HTTPNotFound), (405, webob_exc.HTTPMethodNotAllowed), (406, webob_exc.HTTPNotAcceptable), (407, webob_exc.HTTPProxyAuthenticationRequired), (408, webob_exc.HTTPRequestTimeout), (409, webob_exc.HTTPConflict), (410, webob_exc.HTTPGone), (411, webob_exc.HTTPLengthRequired), (412, webob_exc.HTTPPreconditionFailed), (413, webob_exc.HTTPRequestEntityTooLarge), (414, webob_exc.HTTPRequestURITooLong), (415, webob_exc.HTTPUnsupportedMediaType), (416, webob_exc.HTTPRequestRangeNotSatisfiable), (417, webob_exc.HTTPExpectationFailed), (422, webob_exc.HTTPUnprocessableEntity), (423, webob_exc.HTTPLocked), (424, webob_exc.HTTPFailedDependency), (428, webob_exc.HTTPPreconditionRequired), (429, webob_exc.HTTPTooManyRequests), (431, webob_exc.HTTPRequestHeaderFieldsTooLarge), (451, webob_exc.HTTPUnavailableForLegalReasons), (500, webob_exc.HTTPInternalServerError), (501, webob_exc.HTTPNotImplemented), (502, webob_exc.HTTPBadGateway), (503, webob_exc.HTTPServiceUnavailable), (504, webob_exc.HTTPGatewayTimeout), (505, webob_exc.HTTPVersionNotSupported), (511, webob_exc.HTTPNetworkAuthenticationRequired), ): assert webob_exc.status_map[code] == cls WebOb-1.8.6/tests/test_headers.py0000644000076600000240000000605513611751037017672 0ustar xistencestaff00000000000000import pytest from webob import headers def test_ResponseHeaders_delitem_notpresent(): """Deleting a missing key from ResponseHeaders should raise a KeyError""" d = headers.ResponseHeaders() with pytest.raises(KeyError): d.__delitem__('b') def test_ResponseHeaders_delitem_present(): """ Deleting a present key should not raise an error at all """ d = headers.ResponseHeaders(a=1) del d['a'] assert 'a' not in d def test_ResponseHeaders_setdefault(): """Testing set_default for ResponseHeaders""" d = headers.ResponseHeaders(a=1) res = d.setdefault('b', 1) assert res == d['b'] == 1 res = d.setdefault('b', 10) assert res == d['b'] == 1 res = d.setdefault('B', 10) assert res == d['b'] == d['B'] == 1 def test_ResponseHeader_pop(): """Testing if pop return TypeError when more than len(*args)>1 plus other assorted tests""" d = headers.ResponseHeaders(a=1, b=2, c=3, d=4) with pytest.raises(TypeError): d.pop('a', 'z', 'y') assert d.pop('a') == 1 assert 'a' not in d assert d.pop('B') == 2 assert 'b' not in d assert d.pop('c', 'u') == 3 assert 'c' not in d assert d.pop('e', 'u') == 'u' assert 'e' not in d with pytest.raises(KeyError): d.pop('z') def test_ResponseHeaders_getitem_miss(): d = headers.ResponseHeaders() with pytest.raises(KeyError): d.__getitem__('a') def test_ResponseHeaders_getall(): d = headers.ResponseHeaders() d.add('a', 1) d.add('a', 2) result = d.getall('a') assert result == [1,2] def test_ResponseHeaders_mixed(): d = headers.ResponseHeaders() d.add('a', 1) d.add('a', 2) d['b'] = 1 result = d.mixed() assert result == {'a':[1,2], 'b':1} def test_ResponseHeaders_setitem_scalar_replaces_seq(): d = headers.ResponseHeaders() d.add('a', 2) d['a'] = 1 result = d.getall('a') assert result == [1] def test_ResponseHeaders_contains(): d = headers.ResponseHeaders() d['a'] = 1 assert 'a' in d assert not 'b' in d def test_EnvironHeaders_delitem(): d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'}) del d['CONTENT-LENGTH'] assert not d with pytest.raises(KeyError): d.__delitem__('CONTENT-LENGTH') def test_EnvironHeaders_getitem(): d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'}) assert d['CONTENT-LENGTH'] == '10' def test_EnvironHeaders_setitem(): d = headers.EnvironHeaders({}) d['abc'] = '10' assert d['abc'] == '10' def test_EnvironHeaders_contains(): d = headers.EnvironHeaders({}) d['a'] = '10' assert 'a' in d assert 'b' not in d def test__trans_key_not_basestring(): result = headers._trans_key(None) assert result == None def test__trans_key_not_a_header(): result = headers._trans_key('') assert result == None def test__trans_key_key2header(): result = headers._trans_key('CONTENT_TYPE') assert result == 'Content-Type' def test__trans_key_httpheader(): result = headers._trans_key('HTTP_FOO_BAR') assert result == 'Foo-Bar' WebOb-1.8.6/tests/test_in_wsgiref.py0000644000076600000240000000674513611751037020421 0ustar xistencestaff00000000000000import sys import logging import socket import cgi import pytest from webob.request import Request from webob.response import Response from webob.compat import url_open from webob.compat import bytes_ from webob.compat import reraise from webob.compat import Queue from webob.compat import Empty log = logging.getLogger(__name__) @pytest.mark.usefixtures("serve") def test_request_reading(serve): """ Test actual request/response cycle in the presence of Request.copy() and other methods that can potentially hang. """ with serve(_test_app_req_reading) as server: for key in _test_ops_req_read: resp = url_open(server.url + key, timeout=3) assert resp.read() == b"ok" def _test_app_req_reading(env, sr): req = Request(env) log.debug('starting test operation: %s', req.path_info) test_op = _test_ops_req_read[req.path_info] test_op(req) log.debug('done') r = Response("ok") return r(env, sr) _test_ops_req_read = { '/copy': lambda req: req.copy(), '/read-all': lambda req: req.body_file.read(), '/read-0': lambda req: req.body_file.read(0), '/make-seekable': lambda req: req.make_body_seekable() } @pytest.mark.usefixtures("serve") def test_interrupted_request(serve): with serve(_test_app_req_interrupt) as server: for path in _test_ops_req_interrupt: _send_interrupted_req(server, path) try: res = _global_res.get(timeout=1) except Empty: raise AssertionError("Error during test %s", path) if res is not None: print("Error during test:", path) reraise(res) _global_res = Queue() def _test_app_req_interrupt(env, sr): target_cl = 100000 try: req = Request(env) cl = req.content_length if cl != target_cl: raise AssertionError( 'request.content_length is %s instead of %s' % (cl, target_cl)) op = _test_ops_req_interrupt[req.path_info] log.info("Running test: %s", req.path_info) with pytest.raises(IOError): op(req) except: _global_res.put(sys.exc_info()) else: _global_res.put(None) sr('200 OK', []) return [] def _req_int_cgi(req): assert req.body_file.read(0) == b'' cgi.FieldStorage( fp=req.body_file, environ=req.environ, ) def _req_int_readline(req): try: assert req.body_file.readline() == b'a=b\n' except IOError: # too early to detect disconnect raise AssertionError("False disconnect alert") req.body_file.readline() _test_ops_req_interrupt = { '/copy': lambda req: req.copy(), '/read-body': lambda req: req.body, '/read-post': lambda req: req.POST, '/read-all': lambda req: req.body_file.read(), '/read-too-much': lambda req: req.body_file.read(1 << 22), '/readline': _req_int_readline, '/readlines': lambda req: req.body_file.readlines(), '/read-cgi': _req_int_cgi, '/make-seekable': lambda req: req.make_body_seekable() } def _send_interrupted_req(server, path='/'): sock = socket.socket() sock.connect(('localhost', server.server_port)) f = sock.makefile('wb') f.write(bytes_(_interrupted_req % path)) f.flush() f.close() sock.close() _interrupted_req = ( "POST %s HTTP/1.0\r\n" "content-type: application/x-www-form-urlencoded\r\n" "content-length: 100000\r\n" "\r\n" ) _interrupted_req += 'a=b\nz=' + 'x' * 10000 WebOb-1.8.6/tests/test_misc.py0000644000076600000240000000403313611751037017204 0ustar xistencestaff00000000000000import pytest from webob.util import html_escape from webob.compat import ( text_, PY3 ) py2only = pytest.mark.skipif("sys.version_info >= (3, 0)") py3only = pytest.mark.skipif("sys.version_info < (3, 0)") class t_esc_HTML(object): def __html__(self): return '
hello
' class t_esc_Unicode(object): def __unicode__(self): return text_(b'\xe9') class t_esc_UnsafeAttrs(object): attr = 'value' def __getattr__(self, k): return self.attr def __repr__(self): return '' class t_esc_SuperMoose(object): def __str__(self): return text_(b'm\xf8ose').encode('utf-8') def __unicode__(self): return text_(b'm\xf8ose') @pytest.mark.parametrize("input,expected", [ ('these chars: < > & "', 'these chars: < > & "'), (' ', ' '), ('è', '&egrave;'), # The apostrophe is *not* escaped, which some might consider to be # a serious bug (see, e.g. http://www.cvedetails.com/cve/CVE-2010-2480/) pytest.param("'", "'", marks=py2only), pytest.param("'", "'", marks=py3only), (text_('the majestic m\xf8ose'), 'the majestic møose'), # 8-bit strings are passed through (text_('\xe9'), 'é'), # ``None`` is treated specially, and returns the empty string. (None, ''), # Objects that define a ``__html__`` method handle their own escaping (t_esc_HTML(), '
hello
'), # Things that are not strings are converted to strings and then escaped (42, '42'), # If an object implements both ``__str__`` and ``__unicode__``, the latter # is preferred (t_esc_SuperMoose(), 'møose'), (t_esc_Unicode(), 'é'), (t_esc_UnsafeAttrs(), '<UnsafeAttrs>'), pytest.param(Exception("expected a '<'."), "expected a '<'.", marks=py2only), pytest.param( Exception("expected a '<'."), "expected a '<'.", marks=py3only ), ]) def test_html_escape(input, expected): assert expected == html_escape(input) WebOb-1.8.6/tests/test_multidict.py0000644000076600000240000003710513611751037020255 0ustar xistencestaff00000000000000# -*- coding: utf-8 -*- import unittest from webob import multidict from webob.compat import text_ class BaseDictTests(object): def setUp(self): self._list = [('a', text_('\xe9')), ('a', 'e'), ('a', 'f'), ('b', '1')] self.data = multidict.MultiDict(self._list) self.d = self._get_instance() def _get_instance(self, **kwargs): if kwargs: data = multidict.MultiDict(kwargs) else: data = self.data.copy() return self.klass(data) def test_len(self): self.assertEqual(len(self.d), 4) def test_getone(self): self.assertEqual(self.d.getone('b'), '1') def test_getone_missing(self): self.assertRaises(KeyError, self.d.getone, 'z') def test_getone_multiple_raises(self): self.assertRaises(KeyError, self.d.getone, 'a') def test_getall(self): self.assertEqual(list(self.d.getall('b')), ['1']) def test_dict_of_lists(self): self.assertEqual( self.d.dict_of_lists(), {'a': [text_('\xe9'), 'e', 'f'], 'b': ['1']}) def test_dict_api(self): self.assertTrue('a' in self.d.mixed()) self.assertTrue('a' in self.d.keys()) self.assertTrue('a' in self.d.iterkeys()) self.assertTrue(('b', '1') in self.d.items()) self.assertTrue(('b', '1') in self.d.iteritems()) self.assertTrue('1' in self.d.values()) self.assertTrue('1' in self.d.itervalues()) self.assertEqual(len(self.d), 4) def test_set_del_item(self): d = self._get_instance() self.assertTrue('a' in d) del d['a'] self.assertTrue(not 'a' in d) def test_pop(self): d = self._get_instance() d['a'] = '1' self.assertEqual(d.pop('a'), '1') self.assertEqual(d.pop('x', '1'), '1') def test_pop_wrong_args(self): d = self._get_instance() self.assertRaises(TypeError, d.pop, 'a', '1', '1') def test_pop_missing(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'z') def test_popitem(self): d = self._get_instance() self.assertEqual(d.popitem(), ('b', '1')) def test_update(self): d = self._get_instance() d.update(e='1') self.assertTrue('e' in d) d.update(dict(x='1')) self.assertTrue('x' in d) d.update([('y', '1')]) self.assertTrue('y' in d) def test_setdefault(self): d = self._get_instance() d.setdefault('a', '1') self.assertNotEqual(d['a'], '1') d.setdefault('e', '1') self.assertTrue('e' in d) def test_add(self): d = multidict.MultiDict({'a': '1'}) d.add('a', '2') self.assertEqual(list(d.getall('a')), ['1', '2']) d = self._get_instance() d.add('b', '3') self.assertEqual(list(d.getall('b')), ['1', '3']) def test_copy(self): assert self.d.copy() is not self.d if hasattr(self.d, 'multi'): self.assertFalse(self.d.copy().multi is self.d.multi) self.assertFalse(self.d.copy() is self.d.multi) def test_clear(self): d = self._get_instance() d.clear() self.assertEqual(len(d), 0) def test_nonzero(self): d = self._get_instance() self.assertTrue(d) d.clear() self.assertFalse(d) def test_repr(self): self.assertTrue(repr(self._get_instance())) def test_too_many_args(self): from webob.multidict import MultiDict self.assertRaises(TypeError, MultiDict, '1', 2) def test_no_args(self): from webob.multidict import MultiDict md = MultiDict() self.assertEqual(md._items, []) def test_kwargs(self): from webob.multidict import MultiDict md = MultiDict(kw1='val1') self.assertEqual(md._items, [('kw1','val1')]) def test_view_list_not_list(self): from webob.multidict import MultiDict d = MultiDict() self.assertRaises(TypeError, d.view_list, 42) def test_view_list(self): from webob.multidict import MultiDict d = MultiDict() self.assertEqual(d.view_list([1,2])._items, [1,2]) def test_from_fieldstorage_with_filename(self): from webob.multidict import MultiDict d = MultiDict() fs = DummyFieldStorage('a', '1', 'file') self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':fs.list[0]})) def test_from_fieldstorage_without_filename(self): from webob.multidict import MultiDict d = MultiDict() fs = DummyFieldStorage('a', '1') self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':'1'})) def test_from_fieldstorage_with_charset(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'\r\n' b'\x1b$B$3$s$K$A$O\x1b(B' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) def test_from_fieldstorage_with_base64_encoding(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'Content-Transfer-Encoding: base64\r\n' b'\r\n' b'GyRCJDMkcyRLJEEkTxsoQg==' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) def test_from_fieldstorage_with_quoted_printable_encoding(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'Content-Transfer-Encoding: quoted-printable\r\n' b'\r\n' b'=1B$B$3$s$K$A$O=1B(B' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) class MultiDictTestCase(BaseDictTests, unittest.TestCase): klass = multidict.MultiDict def test_update_behavior_warning(self): import warnings class Foo(dict): def __len__(self): return 0 foo = Foo() foo['a'] = 1 d = self._get_instance() with warnings.catch_warnings(record=True) as w: d.update(foo) self.assertEqual(len(w), 1) def test_repr_with_password(self): d = self._get_instance(password='pwd') self.assertEqual(repr(d), "MultiDict([('password', '******')])") class NestedMultiDictTestCase(BaseDictTests, unittest.TestCase): klass = multidict.NestedMultiDict def test_getitem(self): d = self.klass({'a':1}) self.assertEqual(d['a'], 1) def test_getitem_raises(self): d = self._get_instance() self.assertRaises(KeyError, d.__getitem__, 'z') def test_contains(self): d = self._get_instance() assert 'a' in d assert 'z' not in d def test_add(self): d = self._get_instance() self.assertRaises(KeyError, d.add, 'b', 3) def test_set_del_item(self): d = self._get_instance() self.assertRaises(KeyError, d.__delitem__, 'a') self.assertRaises(KeyError, d.__setitem__, 'a', 1) def test_update(self): d = self._get_instance() self.assertRaises(KeyError, d.update, e=1) self.assertRaises(KeyError, d.update, dict(x=1)) self.assertRaises(KeyError, d.update, [('y', 1)]) def test_setdefault(self): d = self._get_instance() self.assertRaises(KeyError, d.setdefault, 'a', 1) def test_pop(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'a') self.assertRaises(KeyError, d.pop, 'a', 1) def test_popitem(self): d = self._get_instance() self.assertRaises(KeyError, d.popitem, 'a') def test_pop_wrong_args(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'a', 1, 1) def test_clear(self): d = self._get_instance() self.assertRaises(KeyError, d.clear) def test_nonzero(self): d = self._get_instance() self.assertEqual(d.__nonzero__(), True) d.dicts = [{}] self.assertEqual(d.__nonzero__(), False) assert not d class TestGetDict(BaseDictTests, unittest.TestCase): klass = multidict.GetDict def _get_instance(self, environ=None, **kwargs): if environ is None: environ = {} if kwargs: data = multidict.MultiDict(kwargs) else: data = self.data.copy() return self.klass(data, environ) def test_inititems(self): #The first argument passed into the __init__ method class Arg: def items(self): return [('a', text_('\xe9')), ('a', 'e'), ('a', 'f'), ('b', 1)] d = self._get_instance() d._items = None d.__init__(Arg(), lambda:None) self.assertEqual(self.d._items, self._list) def test_nullextend(self): d = self._get_instance() self.assertEqual(d.extend(), None) d.extend(test = 'a') self.assertEqual(d['test'], 'a') def test_extend_from_items(self): values = {'a': '1', 'b': '2', 'c': '3'} class MappingWithItems: def items(self): return values.items() d = self._get_instance() d.extend(MappingWithItems()) self.assertTrue(set(values.items()).issubset(d._items)) def test_extend_from_keys(self): values = {'a': '1', 'b': '2', 'c': '3'} class MappingWithoutItems: def __getitem__(self, item): return values[item] def keys(self): return values.keys() d = self._get_instance() d.extend(MappingWithoutItems()) self.assertTrue(set(values.items()).issubset(d._items)) def test_extend_from_iterable(self): items = [('a', '1')] d = self._get_instance() d.extend(iter(items)) self.assertTrue(set(items).issubset(d._items)) def test_repr_with_password(self): d = self._get_instance(password='pwd') self.assertEqual(repr(d), "GET([('password', '******')])") def test_setitem_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d['a'] = '2' self.assertEqual(env['QUERY_STRING'], 'b=1&a=2') def test_add_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.add('a', '2') self.assertEqual(env['QUERY_STRING'], 'a=%C3%A9&a=e&a=f&b=1&a=2') def test_delitem_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) del d['a'] self.assertEqual(env['QUERY_STRING'], 'b=1') def test_clear_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.clear() self.assertEqual(env['QUERY_STRING'], '') def test_setdefault_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.setdefault('c', '2') self.assertEqual(env['QUERY_STRING'], 'a=%C3%A9&a=e&a=f&b=1&c=2') def test_pop_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.pop('a') self.assertEqual(env['QUERY_STRING'], 'a=e&a=f&b=1') def test_popitem_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.popitem() self.assertEqual(env['QUERY_STRING'], 'a=%C3%A9&a=e&a=f') def test_update_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.update([('a', '2')]) self.assertEqual(env['QUERY_STRING'], 'b=1&a=2') def test_extend_updates_QUERY_STRING(self): env = {} d = self._get_instance(environ=env) d.extend([('a', '2')]) self.assertEqual(env['QUERY_STRING'], 'a=%C3%A9&a=e&a=f&b=1&a=2') class NoVarsTestCase(unittest.TestCase): klass = multidict.NoVars def _get_instance(self): return self.klass() def test_getitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__getitem__, 'a') def test_setitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__setitem__, 'a') def test_delitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__delitem__, 'a') def test_get(self): d = self._get_instance() self.assertEqual(d.get('a', default = 'b'), 'b') def test_getall(self): d = self._get_instance() self.assertEqual(d.getall('a'), []) def test_getone(self): d = self._get_instance() self.assertRaises(KeyError, d.getone, 'a') def test_mixed(self): d = self._get_instance() self.assertEqual(d.mixed(), {}) def test_contains(self): d = self._get_instance() assert 'a' not in d def test_copy(self): d = self._get_instance() self.assertEqual(d.copy(), d) def test_len(self): d = self._get_instance() self.assertEqual(len(d), 0) def test_repr(self): d = self._get_instance() self.assertEqual(repr(d), '') def test_keys(self): d = self._get_instance() self.assertEqual(list(d.keys()), []) def test_iterkeys(self): d = self._get_instance() self.assertEqual(list(d.iterkeys()), []) class DummyField(object): def __init__(self, name, value, filename=None): self.name = name self.value = value self.filename = filename self.type_options = {} self.headers = {} class DummyFieldStorage(object): def __init__(self, name, value, filename=None): self.list = [DummyField(name, value, filename)] WebOb-1.8.6/tests/test_request.py0000644000076600000240000043407613611751037017757 0ustar xistencestaff00000000000000# -*- coding: utf-8 -*- import sys import warnings from io import ( BytesIO, StringIO, ) import pytest from webob.acceptparse import ( AcceptCharsetInvalidHeader, AcceptCharsetNoHeader, AcceptCharsetValidHeader, AcceptEncodingInvalidHeader, AcceptEncodingNoHeader, AcceptEncodingValidHeader, AcceptInvalidHeader, AcceptLanguageInvalidHeader, AcceptLanguageNoHeader, AcceptLanguageValidHeader, AcceptNoHeader, AcceptValidHeader, ) from webob.compat import ( MutableMapping, bytes_, native_, text_type, text_, ) from webob.multidict import NoVars py2only = pytest.mark.skipif("sys.version_info >= (3, 0)") py3only = pytest.mark.skipif("sys.version_info < (3, 0)") class TestRequestCommon(object): # unit tests of non-bytes-vs-text-specific methods of request object def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_ctor_environ_getter_raises_WTF(self): with pytest.raises(TypeError): self._makeOne({}, environ_getter=object()) def test_ctor_wo_environ_raises_WTF(self): with pytest.raises(TypeError): self._makeOne(None) def test_ctor_w_environ(self): environ = {} req = self._makeOne(environ) assert req.environ == environ def test_ctor_w_non_utf8_charset(self): environ = {} with pytest.raises(DeprecationWarning): self._makeOne(environ, charset='latin-1') def test_scheme(self): environ = {'wsgi.url_scheme': 'something:'} req = self._makeOne(environ) assert req.scheme == 'something:' def test_body_file_getter(self): body = b'input' INPUT = BytesIO(body) environ = { 'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) assert req.body_file is not INPUT def test_body_file_getter_seekable(self): body = b'input' INPUT = BytesIO(body) environ = { 'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', 'webob.is_body_seekable': True, } req = self._makeOne(environ) assert req.body_file is INPUT def test_body_file_getter_cache(self): body = b'input' INPUT = BytesIO(body) environ = { 'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) assert req.body_file is req.body_file def test_body_file_getter_unreadable(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'FOO'} req = self._makeOne(environ) assert req.body_file_raw is INPUT assert req.body_file is not INPUT assert req.body_file.read() == b'' def test_body_file_setter_w_bytes(self): req = self._blankOne('/') with pytest.raises(ValueError): setattr(req, 'body_file', b'foo') def test_body_file_setter_non_bytes(self): BEFORE = BytesIO(b'before') AFTER = BytesIO(b'after') environ = { 'wsgi.input': BEFORE, 'CONTENT_LENGTH': len('before'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body_file = AFTER assert req.body_file is AFTER assert req.content_length == None def test_body_file_deleter(self): body = b'input' INPUT = BytesIO(body) environ = { 'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) del req.body_file assert req.body_file.getvalue() == b'' assert req.content_length == 0 def test_body_file_raw(self): INPUT = BytesIO(b'input') environ = { 'wsgi.input': INPUT, 'CONTENT_LENGTH': len('input'), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) assert req.body_file_raw is INPUT def test_body_file_seekable_input_not_seekable(self): data = b'input' INPUT = BytesIO(data) INPUT.seek(1, 0) # consume environ = { 'wsgi.input': INPUT, 'webob.is_body_seekable': False, 'CONTENT_LENGTH': len(data) - 1, 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) seekable = req.body_file_seekable assert seekable is not INPUT assert seekable.getvalue() == b'nput' def test_body_file_seekable_input_is_seekable(self): INPUT = BytesIO(b'input') INPUT.seek(1, 0) # consume environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('input')-1, 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) seekable = req.body_file_seekable assert seekable is INPUT def test_urlvars_getter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) assert req.urlvars == {'foo': 'bar'} def test_urlvars_getter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), } req = self._makeOne(environ) assert req.urlvars == {'foo': 'bar'} def test_urlvars_getter_wo_keys(self): environ = {} req = self._makeOne(environ) assert req.urlvars == {} assert environ['wsgiorg.routing_args'] == ((), {}) def test_urlvars_setter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} assert req.urlvars == {'baz': 'bam'} assert environ['paste.urlvars'] == {'baz': 'bam'} assert 'wsgiorg.routing_args' not in environ def test_urlvars_setter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} assert req.urlvars == {'baz': 'bam'} assert environ['wsgiorg.routing_args'] == ((), {'baz': 'bam'}) assert 'paste.urlvars' not in environ def test_urlvars_setter_wo_keys(self): environ = {} req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} assert req.urlvars == {'baz': 'bam'} assert environ['wsgiorg.routing_args'] == ((), {'baz': 'bam'}) assert 'paste.urlvars' not in environ def test_urlvars_deleter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) del req.urlvars assert req.urlvars == {} assert 'paste.urlvars' not in environ assert environ['wsgiorg.routing_args'] == ((), {}) def test_urlvars_deleter_w_wsgiorg_key_non_empty_tuple(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) del req.urlvars assert req.urlvars == {} assert environ['wsgiorg.routing_args'] == (('a', 'b'), {}) assert 'paste.urlvars' not in environ def test_urlvars_deleter_w_wsgiorg_key_empty_tuple(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) del req.urlvars assert req.urlvars == {} assert environ['wsgiorg.routing_args'] == ((), {}) assert 'paste.urlvars' not in environ def test_urlvars_deleter_wo_keys(self): environ = {} req = self._makeOne(environ) del req.urlvars assert req.urlvars == {} assert environ['wsgiorg.routing_args'] == ((), {}) assert 'paste.urlvars' not in environ def test_urlargs_getter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) assert req.urlargs == () def test_urlargs_getter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), } req = self._makeOne(environ) assert req.urlargs, ('a' == 'b') def test_urlargs_getter_wo_keys(self): environ = {} req = self._makeOne(environ) assert req.urlargs == () assert 'wsgiorg.routing_args' not in environ def test_urlargs_setter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) req.urlargs = ('a', 'b') assert req.urlargs == ('a', 'b') assert environ['wsgiorg.routing_args'] == (('a', 'b'), {'foo': 'bar'}) assert 'paste.urlvars' not in environ def test_urlargs_setter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), } req = self._makeOne(environ) req.urlargs = ('a', 'b') assert req.urlargs == ('a', 'b') assert environ['wsgiorg.routing_args'] == (('a', 'b'), {'foo': 'bar'}) def test_urlargs_setter_wo_keys(self): environ = {} req = self._makeOne(environ) req.urlargs = ('a', 'b') assert req.urlargs == ('a', 'b') assert environ['wsgiorg.routing_args'] == (('a', 'b'), {}) assert 'paste.urlvars' not in environ def test_urlargs_deleter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), } req = self._makeOne(environ) del req.urlargs assert req.urlargs == () assert environ['wsgiorg.routing_args'] == ((), {'foo': 'bar'}) def test_urlargs_deleter_w_wsgiorg_key_empty(self): environ = {'wsgiorg.routing_args': ((), {}), } req = self._makeOne(environ) del req.urlargs assert req.urlargs == () assert 'paste.urlvars' not in environ assert 'wsgiorg.routing_args' not in environ def test_urlargs_deleter_wo_keys(self): environ = {} req = self._makeOne(environ) del req.urlargs assert req.urlargs == () assert 'paste.urlvars' not in environ assert 'wsgiorg.routing_args' not in environ def test_cookies_empty_environ(self): req = self._makeOne({}) assert req.cookies == {} def test_cookies_is_mutable(self): req = self._makeOne({}) cookies = req.cookies cookies['a'] = '1' assert req.cookies['a'] == '1' def test_cookies_w_webob_parsed_cookies_matching_source(self): environ = { 'HTTP_COOKIE': 'a=b', 'webob._parsed_cookies': ('a=b', {'a': 'b'}), } req = self._makeOne(environ) assert req.cookies == {'a': 'b'} def test_cookies_w_webob_parsed_cookies_mismatched_source(self): environ = { 'HTTP_COOKIE': 'a=b', 'webob._parsed_cookies': ('a=b;c=d', {'a': 'b', 'c': 'd'}), } req = self._makeOne(environ) assert req.cookies == {'a': 'b'} def test_set_cookies(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._makeOne(environ) req.cookies = {'a':'1', 'b': '2'} assert req.cookies == {'a': '1', 'b':'2'} rcookies = [x.strip() for x in environ['HTTP_COOKIE'].split(';')] assert sorted(rcookies) == ['a=1', 'b=2'] # body def test_body_getter(self): INPUT = BytesIO(b'input') environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('input'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) assert req.body == b'input' assert req.content_length == len(b'input') def test_body_setter_None(self): INPUT = BytesIO(b'input') environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len(b'input'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body = None assert req.body == b'' assert req.content_length == 0 assert req.is_body_seekable def test_body_setter_non_string_raises(self): req = self._makeOne({}) def _test(): req.body = object() with pytest.raises(TypeError): _test() def test_body_setter_value(self): BEFORE = BytesIO(b'before') environ = {'wsgi.input': BEFORE, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('before'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body = b'after' assert req.body == b'after' assert req.content_length == len(b'after') assert req.is_body_seekable def test_body_deleter_None(self): data = b'input' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len(data), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) del req.body assert req.body == b'' assert req.content_length == 0 assert req.is_body_seekable # JSON def test_json_body(self): body = b'{"a":1}' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) assert req.json == {"a": 1} assert req.json_body == {"a": 1} req.json = {"b": 2} assert req.body == b'{"b":2}' del req.json assert req.body == b'' def test_json_body_array(self): body = b'[{"a":1}, {"b":2}]' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) assert req.json, [{"a": 1} == {"b": 2}] assert req.json_body == [{"a": 1}, {"b": 2}] req.json = [{"b": 2}] assert req.body == b'[{"b":2}]' del req.json assert req.body == b'' # .text def test_text_body(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) assert req.body == b'test' assert req.text == 'test' req.text = text_('\u1000') assert req.body == '\u1000'.encode(req.charset) del req.text assert req.body == b'' def set_bad_text(): req.text = 1 with pytest.raises(TypeError): set_bad_text() def test__text_get_without_charset(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) req._charset = '' with pytest.raises(AttributeError): getattr(req, 'text') def test__text_set_without_charset(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) req._charset = '' with pytest.raises(AttributeError): setattr(req, 'text', 'abc') # POST def test_POST_not_POST_or_PUT(self): environ = {'REQUEST_METHOD': 'GET'} req = self._makeOne(environ) result = req.POST assert isinstance(result, NoVars) assert result.reason.startswith('Not an HTML form') @pytest.mark.parametrize('method', ['POST', 'PUT', 'PATCH', 'DELETE']) def test_POST_existing_cache_hit(self, method): data = b'input' wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'REQUEST_METHOD': method, 'webob._parsed_post_vars': ({'foo': 'bar'}, wsgi_input), } req = self._makeOne(environ) result = req.POST assert result == {'foo': 'bar'} @pytest.mark.parametrize('method', ['PUT', 'PATCH', 'DELETE']) def test_POST_not_POST_missing_content_type(self, method): data = b'input' wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'REQUEST_METHOD': method, } req = self._makeOne(environ) result = req.POST assert isinstance(result, NoVars) assert result.reason.startswith('Not an HTML form submission') def test_POST_missing_content_type(self): data = b'var1=value1&var2=value2&rep=1&rep=2' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(data), 'webob.is_body_seekable': True, } req = self._makeOne(environ) result = req.POST assert result['var1'] == 'value1' @pytest.mark.parametrize('method', ['POST', 'PUT', 'PATCH', 'DELETE']) def test_POST_json_no_content_type(self, method): data = b'{"password": "last centurion", "email": "rory@wiggy.net"}' wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'REQUEST_METHOD': method, 'CONTENT_LENGTH': len(data), 'webob.is_body_seekable': True, } req = self._makeOne(environ) r_1 = req.body r_2 = req.POST r_3 = req.body assert r_1 == b'{"password": "last centurion", "email": "rory@wiggy.net"}' assert r_3 == b'{"password": "last centurion", "email": "rory@wiggy.net"}' @pytest.mark.parametrize('method', ['POST', 'PUT', 'PATCH', 'DELETE']) def test_POST_bad_content_type(self, method): data = b'input' wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'REQUEST_METHOD': method, 'CONTENT_TYPE': 'text/plain', } req = self._makeOne(environ) result = req.POST assert isinstance(result, NoVars) assert result.reason.startswith('Not an HTML form submission') @pytest.mark.parametrize('method', ['POST', 'PUT', 'PATCH', 'DELETE']) def test_POST_urlencoded(self, method): data = b'var1=value1&var2=value2&rep=1&rep=2' wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'REQUEST_METHOD': method, 'CONTENT_LENGTH': len(data), 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'webob.is_body_seekable': True, } req = self._makeOne(environ) result = req.POST assert result['var1'] == 'value1' @pytest.mark.parametrize('method', ['POST', 'PUT', 'PATCH', 'DELETE']) def test_POST_multipart(self, method): data = ( b'------------------------------deb95b63e42a\n' b'Content-Disposition: form-data; name="foo"\n' b'\n' b'foo\n' b'------------------------------deb95b63e42a\n' b'Content-Disposition: form-data; name="bar"; filename="bar.txt"\n' b'Content-type: application/octet-stream\n' b'\n' b'these are the contents of the file "bar.txt"\n' b'\n' b'------------------------------deb95b63e42a--\n') wsgi_input = BytesIO(data) environ = { 'wsgi.input': wsgi_input, 'webob.is_body_seekable': True, 'REQUEST_METHOD': method, 'CONTENT_TYPE': 'multipart/form-data; ' 'boundary=----------------------------deb95b63e42a', 'CONTENT_LENGTH': len(data), } req = self._makeOne(environ) result = req.POST assert result['foo'] == 'foo' bar = result['bar'] assert bar.name == 'bar' assert bar.filename == 'bar.txt' assert bar.file.read() == b'these are the contents of the file "bar.txt"\n' # GET def test_GET_reflects_query_string(self): environ = { 'QUERY_STRING': 'foo=123', } req = self._makeOne(environ) result = req.GET assert result == {'foo': '123'} req.query_string = 'foo=456' result = req.GET assert result == {'foo': '456'} req.query_string = '' result = req.GET assert result == {} def test_GET_updates_query_string(self): req = self._makeOne({}) result = req.query_string assert result == '' req.GET['foo'] = '123' result = req.query_string assert result == 'foo=123' del req.GET['foo'] result = req.query_string assert result == '' # cookies def test_cookies_wo_webob_parsed_cookies(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._blankOne('/', environ) assert req.cookies == {'a': 'b'} # copy def test_copy_get(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._blankOne('/', environ) clone = req.copy_get() for k, v in req.environ.items(): if k in ('CONTENT_LENGTH', 'webob.is_body_seekable'): assert k not in clone.environ elif k == 'wsgi.input': assert clone.environ[k] is not v else: assert clone.environ[k] == v def test_remove_conditional_headers_accept_encoding(self): req = self._blankOne('/') req.accept_encoding='gzip,deflate' req.remove_conditional_headers() assert bool(req.accept_encoding) == False def test_remove_conditional_headers_if_modified_since(self): from webob.datetime_utils import UTC from datetime import datetime req = self._blankOne('/') req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) req.remove_conditional_headers() assert req.if_modified_since == None def test_remove_conditional_headers_if_none_match(self): req = self._blankOne('/') req.if_none_match = 'foo' assert req.if_none_match req.remove_conditional_headers() assert not req.if_none_match def test_remove_conditional_headers_if_range(self): req = self._blankOne('/') req.if_range = 'foo, bar' req.remove_conditional_headers() assert bool(req.if_range) == False def test_remove_conditional_headers_range(self): req = self._blankOne('/') req.range = 'bytes=0-100' req.remove_conditional_headers() assert req.range == None def test_is_body_readable_POST(self): req = self._blankOne('/', environ={'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': '100'}) assert req.is_body_readable def test_is_body_readable_PATCH(self): req = self._blankOne('/', environ={'REQUEST_METHOD': 'PATCH', 'CONTENT_LENGTH': '100'}) assert req.is_body_readable def test_is_body_readable_GET(self): req = self._blankOne('/', environ={'REQUEST_METHOD': 'GET', 'CONTENT_LENGTH': '100'}) assert req.is_body_readable def test_is_body_readable_unknown_method_and_content_length(self): req = self._blankOne('/', environ={'REQUEST_METHOD': 'WTF', 'CONTENT_LENGTH': '100'}) assert req.is_body_readable def test_is_body_readable_special_flag(self): req = self._blankOne('/', environ={'REQUEST_METHOD': 'WTF', 'webob.is_body_readable': True}) assert req.is_body_readable # is_body_seekable # make_body_seekable # copy_body # make_tempfile # remove_conditional_headers # accept def test_accept_no_header(self): req = self._makeOne(environ={}) header = req.accept assert isinstance(header, AcceptNoHeader) assert header.header_value is None def test_accept_invalid_header(self): header_value = 'text/html;param=val;q=1;extparam=\x19' req = self._makeOne(environ={'HTTP_ACCEPT': header_value}) header = req.accept assert isinstance(header, AcceptInvalidHeader) assert header.header_value == header_value def test_accept_valid_header(self): header_value = ',,text/html;p1="v1";p2=v2;q=0.9;e1="v1";e2;e3=v3,' req = self._makeOne(environ={'HTTP_ACCEPT': header_value}) header = req.accept assert isinstance(header, AcceptValidHeader) assert header.header_value == header_value # accept_charset def test_accept_charset_no_header(self): req = self._makeOne(environ={}) header = req.accept_charset assert isinstance(header, AcceptCharsetNoHeader) assert header.header_value is None @pytest.mark.parametrize('header_value', [ '', ', utf-7;q=0.2, utf-8;q =0.3' ]) def test_accept_charset_invalid_header(self, header_value): req = self._makeOne(environ={'HTTP_ACCEPT_CHARSET': header_value}) header = req.accept_charset assert isinstance(header, AcceptCharsetInvalidHeader) assert header.header_value == header_value def test_accept_charset_valid_header(self): header_value = \ 'iso-8859-5;q=0.372,unicode-1-1;q=0.977,UTF-8, *;q=0.000' req = self._makeOne(environ={'HTTP_ACCEPT_CHARSET': header_value}) header = req.accept_charset assert isinstance(header, AcceptCharsetValidHeader) assert header.header_value == header_value # accept_encoding def test_accept_encoding_no_header(self): req = self._makeOne(environ={}) header = req.accept_encoding assert isinstance(header, AcceptEncodingNoHeader) assert header.header_value is None @pytest.mark.parametrize('header_value', [ ', ', ', gzip;q=0.2, compress;q =0.3', ]) def test_accept_encoding_invalid_header(self, header_value): req = self._makeOne(environ={'HTTP_ACCEPT_ENCODING': header_value}) header = req.accept_encoding assert isinstance(header, AcceptEncodingInvalidHeader) assert header.header_value == header_value def test_accept_encoding_valid_header(self): header_value = \ 'compress;q=0.372,gzip;q=0.977,, *;q=0.000' req = self._makeOne(environ={'HTTP_ACCEPT_ENCODING': header_value}) header = req.accept_encoding assert isinstance(header, AcceptEncodingValidHeader) assert header.header_value == header_value # accept_language def test_accept_language_no_header(self): req = self._makeOne(environ={}) header = req.accept_language assert isinstance(header, AcceptLanguageNoHeader) assert header.header_value is None @pytest.mark.parametrize('header_value', ['', ', da;q=0.2, en-gb;q =0.3']) def test_accept_language_invalid_header(self, header_value): req = self._makeOne(environ={'HTTP_ACCEPT_LANGUAGE': header_value}) header = req.accept_language assert isinstance(header, AcceptLanguageInvalidHeader) assert header.header_value == header_value def test_accept_language_valid_header(self): header_value = \ 'zh-Hant;q=0.372,zh-CN-a-myExt-x-private;q=0.977,de,*;q=0.000' req = self._makeOne(environ={'HTTP_ACCEPT_LANGUAGE': header_value}) header = req.accept_language assert isinstance(header, AcceptLanguageValidHeader) assert header.header_value == header_value # authorization # cache_control def test_cache_control_reflects_environ(self): environ = { 'HTTP_CACHE_CONTROL': 'max-age=5', } req = self._makeOne(environ) result = req.cache_control assert result.properties == {'max-age': 5} req.environ.update(HTTP_CACHE_CONTROL='max-age=10') result = req.cache_control assert result.properties == {'max-age': 10} req.environ.update(HTTP_CACHE_CONTROL='') result = req.cache_control assert result.properties == {} def test_cache_control_updates_environ(self): environ = {} req = self._makeOne(environ) req.cache_control.max_age = 5 result = req.environ['HTTP_CACHE_CONTROL'] assert result == 'max-age=5' req.cache_control.max_age = 10 result = req.environ['HTTP_CACHE_CONTROL'] assert result == 'max-age=10' req.cache_control = None result = req.environ['HTTP_CACHE_CONTROL'] assert result == '' del req.cache_control assert 'HTTP_CACHE_CONTROL' not in req.environ def test_cache_control_set_dict(self): environ = {} req = self._makeOne(environ) req.cache_control = {'max-age': 5} result = req.cache_control assert result.max_age == 5 def test_cache_control_set_object(self): from webob.cachecontrol import CacheControl environ = {} req = self._makeOne(environ) req.cache_control = CacheControl({'max-age': 5}, type='request') result = req.cache_control assert result.max_age == 5 def test_cache_control_gets_cached(self): environ = {} req = self._makeOne(environ) assert req.cache_control is req.cache_control #if_match #if_none_match #date #if_modified_since #if_unmodified_since #if_range #max_forwards #pragma #range #referer #referrer #user_agent #__repr__ #__str__ #from_file #call_application def test_call_application_calls_application(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): start_response('200 OK', [('content-type', 'text/plain')]) return ['...\n'] status, headers, output = req.call_application(application) assert status == '200 OK' assert headers == [('content-type', 'text/plain')] assert ''.join(output) == '...\n' def test_call_application_provides_write(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): write = start_response('200 OK', [('content-type', 'text/plain')]) write('...\n') return [] status, headers, output = req.call_application(application) assert status == '200 OK' assert headers == [('content-type', 'text/plain')] assert ''.join(output) == '...\n' def test_call_application_closes_iterable_when_mixed_w_write_calls(self): environ = { 'test._call_application_called_close': False } req = self._makeOne(environ) def application(environ, start_response): write = start_response('200 OK', [('content-type', 'text/plain')]) class AppIter(object): def __iter__(self): yield '...\n' def close(self): environ['test._call_application_called_close'] = True write('...\n') return AppIter() status, headers, output = req.call_application(application) assert ''.join(output) == '...\n...\n' assert environ['test._call_application_called_close'] == True def test_call_application_raises_exc_info(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): try: raise RuntimeError('OH NOES') except: exc_info = sys.exc_info() start_response('200 OK', [('content-type', 'text/plain')], exc_info) return ['...\n'] with pytest.raises(RuntimeError): req.call_application(application) def test_call_application_returns_exc_info(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): try: raise RuntimeError('OH NOES') except: exc_info = sys.exc_info() start_response('200 OK', [('content-type', 'text/plain')], exc_info) return ['...\n'] status, headers, output, exc_info = req.call_application( application, True) assert status == '200 OK' assert headers == [('content-type', 'text/plain')] assert ''.join(output) == '...\n' assert exc_info[0] == RuntimeError #get_response def test_blank__method_subtitution(self): request = self._blankOne('/', environ={'REQUEST_METHOD': 'PUT'}) assert request.method == 'PUT' request = self._blankOne( '/', environ={'REQUEST_METHOD': 'PUT'}, POST={}) assert request.method == 'PUT' request = self._blankOne( '/', environ={'REQUEST_METHOD': 'HEAD'}, POST={}) assert request.method == 'POST' def test_blank__ctype_in_env(self): request = self._blankOne( '/', environ={'CONTENT_TYPE': 'application/json'}) assert request.content_type == 'application/json' assert request.method == 'GET' request = self._blankOne( '/', environ={'CONTENT_TYPE': 'application/json'}, POST='') assert request.content_type == 'application/json' assert request.method == 'POST' def test_blank__ctype_in_headers(self): request = self._blankOne( '/', headers={'Content-type': 'application/json'}) assert request.content_type == 'application/json' assert request.method == 'GET' request = self._blankOne( '/', headers={'Content-Type': 'application/json'}, POST='') assert request.content_type == 'application/json' assert request.method == 'POST' def test_blank__ctype_as_kw(self): request = self._blankOne('/', content_type='application/json') assert request.content_type == 'application/json' assert request.method == 'GET' request = self._blankOne('/', content_type='application/json', POST='') assert request.content_type == 'application/json' assert request.method == 'POST' def test_blank__str_post_data_for_unsupported_ctype(self): with pytest.raises(ValueError): self._blankOne('/', content_type='application/json', POST={}) def test_blank__post_urlencoded(self): from webob.multidict import MultiDict POST = MultiDict() POST["first"] = 1 POST["second"] = 2 request = self._blankOne('/', POST=POST) assert request.method == 'POST' assert request.content_type == 'application/x-www-form-urlencoded' assert request.body == b'first=1&second=2' assert request.content_length == 16 def test_blank__post_multipart(self): from webob.multidict import MultiDict POST = MultiDict() POST["first"] = "1" POST["second"] = "2" request = self._blankOne('/', POST=POST, content_type='multipart/form-data; ' 'boundary=boundary') assert request.method == 'POST' assert request.content_type == 'multipart/form-data' expected = ( b'--boundary\r\n' b'Content-Disposition: form-data; name="first"\r\n\r\n' b'1\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="second"\r\n\r\n' b'2\r\n' b'--boundary--') assert request.body == expected assert request.content_length == 139 def test_blank__post_files(self): import cgi from webob.request import _get_multipart_boundary from webob.multidict import MultiDict POST = MultiDict() POST["first"] = ('filename1', BytesIO(b'1')) POST["second"] = ('filename2', '2') POST["third"] = "3" request = self._blankOne('/', POST=POST) assert request.method == 'POST' assert request.content_type == 'multipart/form-data' boundary = bytes_( _get_multipart_boundary(request.headers['content-type'])) body_norm = request.body.replace(boundary, b'boundary') expected = ( b'--boundary\r\n' b'Content-Disposition: form-data; name="first"; ' b'filename="filename1"\r\n\r\n' b'1\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="second"; ' b'filename="filename2"\r\n\r\n' b'2\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="third"\r\n\r\n' b'3\r\n' b'--boundary--' ) assert body_norm == expected assert request.content_length == 294 assert isinstance(request.POST['first'], cgi.FieldStorage) assert isinstance(request.POST['second'], cgi.FieldStorage) assert request.POST['first'].value == b'1' assert request.POST['second'].value == b'2' assert request.POST['third'] == '3' def test_blank__post_file_w_wrong_ctype(self): with pytest.raises(ValueError): self._blankOne( '/', POST={'first': ('filename1', '1')}, content_type='application/x-www-form-urlencoded') #from_bytes def test_from_bytes_extra_data(self): _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type') cls = self._getTargetClass() with pytest.raises(ValueError): cls.from_bytes(_test_req_copy+b'EXTRA!') #as_bytes def test_as_bytes_skip_body(self): cls = self._getTargetClass() req = cls.from_bytes(_test_req) body = req.as_bytes(skip_body=True) assert body.count(b'\r\n\r\n') == 0 assert req.as_bytes(skip_body=337) == req.as_bytes() body = req.as_bytes(337-1).split(b'\r\n\r\n', 1)[1] assert body == b'' def test_charset_in_content_type(self): Request = self._getTargetClass() # should raise no exception req = Request({ 'REQUEST_METHOD': 'POST', 'QUERY_STRING':'a=b', 'CONTENT_TYPE':'text/html;charset=ascii' }) assert req.charset == 'ascii' assert dict(req.GET) == {'a': 'b'} assert dict(req.POST) == {} req.charset = 'ascii' # no exception with pytest.raises(DeprecationWarning): setattr(req, 'charset', 'utf-8') # again no exception req = Request({ 'REQUEST_METHOD': 'POST', 'QUERY_STRING':'a=b', 'CONTENT_TYPE':'multipart/form-data;charset=ascii' }) assert req.charset == 'ascii' assert dict(req.GET) == {'a': 'b'} with pytest.raises(DeprecationWarning): getattr(req, 'POST') def test_limited_length_file_repr(self): from webob.request import Request req = Request.blank('/', POST='x') req.body_file_raw = 'dummy' req.is_body_seekable = False assert repr(req.body_file.raw), "" @pytest.mark.parametrize("is_seekable", [False, True]) def test_request_wrong_clen(self, is_seekable): from webob.request import Request tlen = 1<<20 req = Request.blank('/', POST='x'*tlen) assert req.content_length == tlen req.body_file = _Helper_test_request_wrong_clen(req.body_file) assert req.content_length == None req.content_length = tlen + 100 req.is_body_seekable = is_seekable assert req.content_length == tlen+100 # this raises AssertionError if the body reading # trusts content_length too much with pytest.raises(IOError): req.copy_body() class TestBaseRequest(object): # tests of methods of a base request which are encoding-specific def _getTargetClass(self): from webob.request import BaseRequest return BaseRequest def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_method(self): environ = {'REQUEST_METHOD': 'OPTIONS', } req = self._makeOne(environ) result = req.method assert result.__class__ == str assert result == 'OPTIONS' def test_http_version(self): environ = {'SERVER_PROTOCOL': '1.1', } req = self._makeOne(environ) result = req.http_version assert result == '1.1' def test_script_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) assert req.script_name == '/script' def test_path_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.path_info == '/path/info' def test_content_length_getter(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) assert req.content_length == 1234 def test_content_length_setter_w_str(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) req.content_length = '3456' assert req.content_length == 3456 def test_remote_user(self): environ = {'REMOTE_USER': 'phred', } req = self._makeOne(environ) assert req.remote_user == 'phred' def test_remote_addr(self): environ = {'REMOTE_ADDR': '1.2.3.4', } req = self._makeOne(environ) assert req.remote_addr == '1.2.3.4' def test_query_string(self): environ = {'QUERY_STRING': 'foo=bar&baz=bam', } req = self._makeOne(environ) assert req.query_string == 'foo=bar&baz=bam' def test_server_name(self): environ = {'SERVER_NAME': 'somehost.tld', } req = self._makeOne(environ) assert req.server_name == 'somehost.tld' def test_server_port_getter(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) assert req.server_port == 6666 def test_server_port_setter_with_string(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) req.server_port = '6667' assert req.server_port == 6667 def test_uscript_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) assert isinstance(req.uscript_name, text_type) assert req.uscript_name == '/script' def test_upath_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert isinstance(req.upath_info, text_type) assert req.upath_info == '/path/info' def test_upath_info_set_unicode(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) req.upath_info = text_('/another') assert isinstance(req.upath_info, text_type) assert req.upath_info == '/another' def test_content_type_getter_no_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar', } req = self._makeOne(environ) assert req.content_type == 'application/xml+foobar' def test_content_type_getter_w_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) assert req.content_type == 'application/xml+foobar' def test_content_type_setter_w_None(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = None assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_content_type_setter_existing_paramter_no_new_paramter(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = 'text/xml' assert req.content_type == 'text/xml' assert environ['CONTENT_TYPE'] == 'text/xml;charset="utf8"' def test_content_type_deleter_clears_environ_value(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) del req.content_type assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_content_type_deleter_no_environ_value(self): environ = {} req = self._makeOne(environ) del req.content_type assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_headers_getter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) headers = req.headers assert headers == {'Content-Type': CONTENT_TYPE, 'Content-Length': '123'} def test_headers_setter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) req.headers = {'Qux': 'Spam'} assert req.headers == {'Qux': 'Spam'} assert environ == {'HTTP_QUX': 'Spam'} def test_no_headers_deleter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) def _test(): del req.headers with pytest.raises(AttributeError): _test() def test_client_addr_xff_singleval(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_xff_multival(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1, 192.168.1.2', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_prefers_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_no_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.2' def test_client_addr_no_xff_no_remote_addr(self): environ = {} req = self._makeOne(environ) assert req.client_addr == None def test_host_port_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_port == '80' def test_host_port_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) assert req.host_port == '80' def test_host_port_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_port == '8888' def test_host_port_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_port == '443' def test_host_port_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) assert req.host_port == '443' def test_host_port_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_port == '8888' def test_host_port_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_PORT': '4333', } req = self._makeOne(environ) assert req.host_port == '4333' def test_host_url_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_url == 'http://example.com' def test_host_url_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) assert req.host_url == 'http://example.com' def test_host_url_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_url == 'http://example.com:8888' def test_host_url_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_url == 'https://example.com' def test_host_url_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) assert req.host_url == 'https://example.com' def test_host_url_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:4333', } req = self._makeOne(environ) assert req.host_url == 'https://example.com:4333' def test_host_url_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '4333', } req = self._makeOne(environ) assert req.host_url == 'https://example.com:4333' def test_application_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.application_url assert app_url.__class__ == str assert app_url == 'http://localhost/%C3%AB' def test_path_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.path_url assert app_url.__class__ == str assert app_url == 'http://localhost/%C3%AB/%C3%AB' def test_path(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.path assert app_url.__class__ == str assert app_url == '/%C3%AB/%C3%AB' def test_path_qs_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.path_qs == '/script/path/info' def test_path_qs_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.path_qs == '/script/path/info?foo=bar&baz=bam' def test_url_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.url == 'http://example.com/script/path/info' def test_url_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.url == 'http://example.com/script/path/info?foo=bar&baz=bam' def test_relative_url_to_app_true_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('other/page', True) == 'http://example.com/script/other/page' def test_relative_url_to_app_true_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('/other/page', True) == 'http://example.com/other/page' def test_relative_url_to_app_false_other_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('/other/page', False) == 'http://example.com/other/page' def test_relative_url_to_app_false_other_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('other/page', False) == 'http://example.com/script/path/other/page' def test_path_info_pop_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == None assert environ['SCRIPT_NAME'] == '/script' def test_path_info_pop_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == '' assert environ['SCRIPT_NAME'] == '/script/' assert environ['PATH_INFO'] == '' def test_path_info_pop_non_empty_no_pattern(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script/path' assert environ['PATH_INFO'] == '/info' def test_path_info_pop_non_empty_w_pattern_miss(self): import re PATTERN = re.compile('miss') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) assert popped == None assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/path/info' def test_path_info_pop_non_empty_w_pattern_hit(self): import re PATTERN = re.compile('path') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script/path' assert environ['PATH_INFO'] == '/info' def test_path_info_pop_skips_empty_elements(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '//path/info', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script//path' assert environ['PATH_INFO'] == '/info' def test_path_info_peek_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == None assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '' def test_path_info_peek_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == '' assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/' def test_path_info_peek_non_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == 'path' assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/path' def test_is_xhr_no_header(self): req = self._makeOne({}) assert not req.is_xhr def test_is_xhr_header_miss(self): environ = {'HTTP_X_REQUESTED_WITH': 'notAnXMLHTTPRequest'} req = self._makeOne(environ) assert not req.is_xhr def test_is_xhr_header_hit(self): environ = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} req = self._makeOne(environ) assert req.is_xhr # host def test_host_getter_w_HTTP_HOST(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) assert req.host == 'example.com:8888' def test_host_getter_wo_HTTP_HOST(self): environ = {'SERVER_NAME': 'example.com', 'SERVER_PORT': '8888'} req = self._makeOne(environ) assert req.host == 'example.com:8888' def test_host_setter(self): environ = {} req = self._makeOne(environ) req.host = 'example.com:8888' assert environ['HTTP_HOST'] == 'example.com:8888' def test_host_deleter_hit(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) del req.host assert 'HTTP_HOST' not in environ def test_host_deleter_miss(self): environ = {} req = self._makeOne(environ) del req.host # doesn't raise def test_domain_nocolon(self): environ = {'HTTP_HOST':'example.com'} req = self._makeOne(environ) assert req.domain == 'example.com' def test_domain_withcolon(self): environ = {'HTTP_HOST':'example.com:8888'} req = self._makeOne(environ) assert req.domain == 'example.com' def test_domain_with_ipv6(self): environ = {'HTTP_HOST': '[2001:DB8::1]:6453'} req = self._makeOne(environ) assert req.domain == '[2001:DB8::1]' def test_domain_with_ipv6_no_port(self): environ = {'HTTP_HOST': '[2001:DB8::1]'} req = self._makeOne(environ) assert req.domain == '[2001:DB8::1]' def test_encget_raises_without_default(self): inst = self._makeOne({}) with pytest.raises(KeyError): inst.encget('a') def test_encget_doesnt_raises_with_default(self): inst = self._makeOne({}) assert inst.encget('a', None) == None def test_encget_with_encattr(self): val = native_(b'\xc3\xab', 'latin-1') inst = self._makeOne({'a': val}) assert inst.encget('a', encattr='url_encoding') == text_(b'\xc3\xab', 'utf-8') def test_encget_with_encattr_latin_1(self): val = native_(b'\xc3\xab', 'latin-1') inst = self._makeOne({'a': val}) inst.my_encoding = 'latin-1' assert inst.encget('a', encattr='my_encoding') == text_(b'\xc3\xab', 'latin-1') def test_encget_no_encattr(self): val = native_(b'\xc3\xab', 'latin-1') inst = self._makeOne({'a': val}) assert inst.encget('a') == val def test_relative_url(self): inst = self._blankOne('/%C3%AB/c') result = inst.relative_url('a') assert result.__class__ == str assert result == 'http://localhost/%C3%AB/a' def test_header_getter(self): val = native_(b'abc', 'latin-1') inst = self._makeOne({'HTTP_FLUB': val}) result = inst.headers['Flub'] assert result.__class__ == str assert result == 'abc' def test_json_body(self): inst = self._makeOne({}) inst.body = b'{"a":"1"}' assert inst.json_body == {'a':'1'} inst.json_body = {'a': '2'} assert inst.body == b'{"a":"2"}' def test_host_get(self): inst = self._makeOne({'HTTP_HOST':'example.com'}) result = inst.host assert result.__class__ == str assert result == 'example.com' def test_host_get_w_no_http_host(self): inst = self._makeOne({'SERVER_NAME':'example.com', 'SERVER_PORT':'80'}) result = inst.host assert result.__class__ == str assert result == 'example.com:80' class TestLegacyRequest(object): # tests of methods of a bytesrequest which deal with http environment vars def _getTargetClass(self): from webob.request import LegacyRequest return LegacyRequest def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_method(self): environ = {'REQUEST_METHOD': 'OPTIONS', } req = self._makeOne(environ) assert req.method == 'OPTIONS' def test_http_version(self): environ = {'SERVER_PROTOCOL': '1.1', } req = self._makeOne(environ) assert req.http_version == '1.1' def test_script_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) assert req.script_name == '/script' def test_path_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.path_info == '/path/info' def test_content_length_getter(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) assert req.content_length == 1234 def test_content_length_setter_w_str(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) req.content_length = '3456' assert req.content_length == 3456 def test_remote_user(self): environ = {'REMOTE_USER': 'phred', } req = self._makeOne(environ) assert req.remote_user == 'phred' def test_remote_addr(self): environ = {'REMOTE_ADDR': '1.2.3.4', } req = self._makeOne(environ) assert req.remote_addr == '1.2.3.4' def test_remote_host(self): environ = {'REMOTE_HOST': 'example.com', } req = self._makeOne(environ) assert req.remote_host == 'example.com' def test_remote_host_not_set(self): environ = {} req = self._makeOne(environ) assert req.remote_host is None def test_query_string(self): environ = {'QUERY_STRING': 'foo=bar&baz=bam', } req = self._makeOne(environ) assert req.query_string == 'foo=bar&baz=bam' def test_server_name(self): environ = {'SERVER_NAME': 'somehost.tld', } req = self._makeOne(environ) assert req.server_name == 'somehost.tld' def test_server_port_getter(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) assert req.server_port == 6666 def test_server_port_setter_with_string(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) req.server_port = '6667' assert req.server_port == 6667 def test_uscript_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) assert isinstance(req.uscript_name, text_type) result = req.uscript_name assert result.__class__ == text_type assert result == '/script' def test_upath_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) result = req.upath_info assert isinstance(result, text_type) assert result == '/path/info' def test_upath_info_set_unicode(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) req.upath_info = text_('/another') result = req.upath_info assert isinstance(result, text_type) assert result == '/another' def test_content_type_getter_no_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar', } req = self._makeOne(environ) assert req.content_type == 'application/xml+foobar' def test_content_type_getter_w_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) assert req.content_type == 'application/xml+foobar' def test_content_type_setter_w_None(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = None assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_content_type_setter_existing_paramter_no_new_paramter(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = 'text/xml' assert req.content_type == 'text/xml' assert environ['CONTENT_TYPE'] == 'text/xml;charset="utf8"' def test_content_type_deleter_clears_environ_value(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) del req.content_type assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_content_type_deleter_no_environ_value(self): environ = {} req = self._makeOne(environ) del req.content_type assert req.content_type == '' assert 'CONTENT_TYPE' not in environ def test_headers_getter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) headers = req.headers assert headers == {'Content-Type':CONTENT_TYPE, 'Content-Length': '123'} def test_headers_setter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) req.headers = {'Qux': 'Spam'} assert req.headers == {'Qux': 'Spam'} assert environ['HTTP_QUX'] == native_('Spam') assert environ == {'HTTP_QUX': 'Spam'} def test_no_headers_deleter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) def _test(): del req.headers with pytest.raises(AttributeError): _test() def test_client_addr_xff_singleval(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_xff_multival(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1, 192.168.1.2', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_prefers_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.1' def test_client_addr_no_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', } req = self._makeOne(environ) assert req.client_addr == '192.168.1.2' def test_client_addr_no_xff_no_remote_addr(self): environ = {} req = self._makeOne(environ) assert req.client_addr == None def test_host_port_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_port == '80' def test_host_port_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) assert req.host_port == '80' def test_host_port_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_port == '8888' def test_host_port_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_port == '443' def test_host_port_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) assert req.host_port == '443' def test_host_port_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_port == '8888' def test_host_port_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_PORT': '4333', } req = self._makeOne(environ) assert req.host_port == '4333' def test_host_port_ipv6(self): environ = {'HTTP_HOST': '[2001:DB8::1]:6453'} req = self._makeOne(environ) assert req.host_port == '6453' def test_host_port_ipv6(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': '[2001:DB8::1]' } req = self._makeOne(environ) assert req.host_port == '443' def test_host_url_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_url == 'http://example.com' def test_host_url_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) assert req.host_url == 'http://example.com' def test_host_url_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) assert req.host_url == 'http://example.com:8888' def test_host_url_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) assert req.host_url == 'https://example.com' def test_host_url_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) assert req.host_url == 'https://example.com' def test_host_url_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:4333', } req = self._makeOne(environ) assert req.host_url == 'https://example.com:4333' def test_host_url_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '4333', } req = self._makeOne(environ) assert req.host_url == 'https://example.com:4333' def test_host_url_http_ipv6_host(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': '[2001:DB8::1]:6453' } req = self._makeOne(environ) assert req.host_url == 'https://[2001:DB8::1]:6453' def test_host_url_http_ipv6_host_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': '[2001:DB8::1]' } req = self._makeOne(environ) assert req.host_url == 'https://[2001:DB8::1]' @py2only def test_application_url_py2(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' app_url = inst.application_url assert app_url == 'http://localhost/%C3%AB' @py3only def test_application_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' app_url = inst.application_url assert app_url == 'http://localhost/%C3%83%C2%AB' @py2only def test_path_url_py2(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path_url assert result == 'http://localhost/%C3%AB/%C3%AB' @py3only def test_path_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path_url assert result == 'http://localhost/%C3%83%C2%AB/%C3%83%C2%AB' @py2only def test_path_py2(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path assert result == '/%C3%AB/%C3%AB' @py3only def test_path(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path assert result == '/%C3%83%C2%AB/%C3%83%C2%AB' def test_path_qs_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.path_qs == '/script/path/info' def test_path_qs_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.path_qs == '/script/path/info?foo=bar&baz=bam' def test_url_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) assert req.url == 'http://example.com/script/path/info' def test_url_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.url == 'http://example.com/script/path/info?foo=bar&baz=bam' def test_relative_url_to_app_true_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('other/page' == True, 'http://example.com/script/other/page') def test_relative_url_to_app_true_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('/other/page' == True, 'http://example.com/other/page') def test_relative_url_to_app_false_other_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('/other/page', False) == 'http://example.com/other/page' def test_relative_url_to_app_false_other_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) assert req.relative_url('other/page', False) == 'http://example.com/script/path/other/page' def test_path_info_pop_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == None assert environ['SCRIPT_NAME'] == '/script' def test_path_info_pop_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == '' assert environ['SCRIPT_NAME'] == '/script/' assert environ['PATH_INFO'] == '' def test_path_info_pop_non_empty_no_pattern(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script/path' assert environ['PATH_INFO'] == '/info' def test_path_info_pop_non_empty_w_pattern_miss(self): import re PATTERN = re.compile('miss') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) assert popped == None assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/path/info' def test_path_info_pop_non_empty_w_pattern_hit(self): import re PATTERN = re.compile('path') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script/path' assert environ['PATH_INFO'] == '/info' def test_path_info_pop_skips_empty_elements(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '//path/info', } req = self._makeOne(environ) popped = req.path_info_pop() assert popped == 'path' assert environ['SCRIPT_NAME'] == '/script//path' assert environ['PATH_INFO'] == '/info' def test_path_info_peek_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == None assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '' def test_path_info_peek_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == '' assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/' def test_path_info_peek_non_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path', } req = self._makeOne(environ) peeked = req.path_info_peek() assert peeked == 'path' assert environ['SCRIPT_NAME'] == '/script' assert environ['PATH_INFO'] == '/path' def test_is_xhr_no_header(self): req = self._makeOne({}) assert not req.is_xhr def test_is_xhr_header_miss(self): environ = {'HTTP_X_REQUESTED_WITH': 'notAnXMLHTTPRequest'} req = self._makeOne(environ) assert not req.is_xhr def test_is_xhr_header_hit(self): environ = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} req = self._makeOne(environ) assert req.is_xhr # host def test_host_getter_w_HTTP_HOST(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) assert req.host == 'example.com:8888' def test_host_getter_wo_HTTP_HOST(self): environ = {'SERVER_NAME': 'example.com', 'SERVER_PORT': '8888'} req = self._makeOne(environ) assert req.host == 'example.com:8888' def test_host_setter(self): environ = {} req = self._makeOne(environ) req.host = 'example.com:8888' assert environ['HTTP_HOST'] == 'example.com:8888' def test_host_deleter_hit(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) del req.host assert 'HTTP_HOST' not in environ def test_host_deleter_miss(self): environ = {} req = self._makeOne(environ) del req.host # doesn't raise def test_encget_raises_without_default(self): inst = self._makeOne({}) with pytest.raises(KeyError): inst.encget('a') def test_encget_doesnt_raises_with_default(self): inst = self._makeOne({}) assert inst.encget('a', None) == None def test_encget_with_encattr(self): val = native_(b'\xc3\xab', 'latin-1') inst = self._makeOne({'a':val}) assert inst.encget('a', encattr='url_encoding') == native_(b'\xc3\xab', 'latin-1') def test_encget_no_encattr(self): val = native_(b'\xc3\xab', 'latin-1') inst = self._makeOne({'a': val}) assert inst.encget('a'), native_(b'\xc3\xab' == 'latin-1') @py2only def test_relative_url_py2(self): inst = self._blankOne('/%C3%AB/c') result = inst.relative_url('a') assert result == 'http://localhost/%C3%AB/a' @py3only def test_relative_url(self): inst = self._blankOne('/%C3%AB/c') result = inst.relative_url('a') assert result == 'http://localhost/%C3%83%C2%AB/a' def test_header_getter(self): val = native_(b'abc', 'latin-1') inst = self._makeOne({'HTTP_FLUB':val}) result = inst.headers['Flub'] assert result == 'abc' def test_json_body(self): inst = self._makeOne({}) inst.body = b'{"a":"1"}' assert inst.json_body == {'a':'1'} def test_host_get_w_http_host(self): inst = self._makeOne({'HTTP_HOST':'example.com'}) result = inst.host assert result == 'example.com' def test_host_get_w_no_http_host(self): inst = self._makeOne({'SERVER_NAME':'example.com', 'SERVER_PORT':'80'}) result = inst.host assert result == 'example.com:80' class TestRequestConstructorWarnings(object): def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def test_ctor_w_unicode_errors(self): with warnings.catch_warnings(record=True) as w: # still emit if warning was printed previously warnings.simplefilter('always') self._makeOne({}, unicode_errors=True) assert len(w) == 1 def test_ctor_w_decode_param_names(self): with warnings.catch_warnings(record=True) as w: # still emit if warning was printed previously warnings.simplefilter('always') self._makeOne({}, decode_param_names=True) assert len(w) == 1 class TestRequestWithAdhocAttr(object): def _blankOne(self, *arg, **kw): from webob.request import Request return Request.blank(*arg, **kw) def test_adhoc_attrs_set(self): req = self._blankOne('/') req.foo = 1 assert req.environ['webob.adhoc_attrs'] == {'foo': 1} def test_adhoc_attrs_set_nonadhoc(self): req = self._blankOne('/', environ={'webob.adhoc_attrs':{}}) req.request_body_tempfile_limit = 1 assert req.environ['webob.adhoc_attrs'] == {} def test_adhoc_attrs_get(self): req = self._blankOne('/', environ={'webob.adhoc_attrs': {'foo': 1}}) assert req.foo == 1 def test_adhoc_attrs_get_missing(self): req = self._blankOne('/') with pytest.raises(AttributeError): getattr(req, 'some_attr') def test_adhoc_attrs_del(self): req = self._blankOne('/', environ={'webob.adhoc_attrs': {'foo': 1}}) del req.foo assert req.environ['webob.adhoc_attrs'] == {} def test_adhoc_attrs_del_missing(self): req = self._blankOne('/') with pytest.raises(AttributeError): delattr(req, 'some_attr') class TestRequest_functional(object): # functional tests of request def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_gets(self): request = self._blankOne('/') status, headerlist, app_iter = request.call_application(simpleapp) assert status == '200 OK' res = b''.join(app_iter) assert b'Hello' in res assert b"MultiDict([])" in res assert b"post is ', ): assert bytes_(thing) in res def test_bad_cookie(self): req = self._blankOne('/') req.headers['Cookie'] = '070-it-:>') def test_from_garbage_file(self): # If we pass a file with garbage to from_file method it should # raise an error plus missing bits in from_file method io = BytesIO(b'hello world') cls = self._getTargetClass() with pytest.raises(ValueError): cls.from_file(io) val_file = BytesIO( b"GET /webob/ HTTP/1.1\n" b"Host: pythonpaste.org\n" b"User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13)" b"Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13\n" b"Accept: " b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;" b"q=0.8\n" b"Accept-Language: en-us,en;q=0.5\n" b"Accept-Encoding: gzip,deflate\n" b"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n" # duplicate on purpose b"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n" b"Keep-Alive: 115\n" b"Connection: keep-alive\n" ) req = cls.from_file(val_file) assert isinstance(req, cls) assert not repr(req).endswith('(invalid WSGI environ)>') val_file = BytesIO( b"GET /webob/ HTTP/1.1\n" b"Host pythonpaste.org\n" ) with pytest.raises(ValueError): cls.from_file(val_file) def test_from_file_patch(self): cls = self._getTargetClass() req = cls.from_bytes(_test_req_patch) assert "PATCH" == req.method assert len(req.body) assert req.body in _test_req_patch assert _test_req_patch == req.as_bytes() def test_from_bytes(self): # A valid request without a Content-Length header should still read # the full body. # Also test parity between as_string and from_bytes / from_file. import cgi cls = self._getTargetClass() req = cls.from_bytes(_test_req) assert isinstance(req, cls) assert not repr(req).endswith('(invalid WSGI environ)>') assert '\n' not in req.http_version or '\r' in req.http_version assert ',' not in req.host assert req.content_length is not None assert req.content_length == 337 assert b'foo' in req.body bar_contents = b"these are the contents of the file 'bar.txt'\r\n" assert bar_contents in req.body assert req.params['foo'] == 'foo' bar = req.params['bar'] assert isinstance(bar, cgi.FieldStorage) assert bar.type == 'application/octet-stream' bar.file.seek(0) assert bar.file.read() == bar_contents # out should equal contents, except for the Content-Length header, # so insert that. _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type' ) assert req.as_bytes() == _test_req_copy req2 = cls.from_bytes(_test_req2) assert 'host' not in req2.headers assert req2.as_bytes() == _test_req2.rstrip() with pytest.raises(ValueError): cls.from_bytes(_test_req2 + b'xx') def test_from_text(self): import cgi cls = self._getTargetClass() req = cls.from_text(text_(_test_req, 'utf-8')) assert isinstance(req, cls) assert not repr(req).endswith('(invalid WSGI environ)>') assert '\n' not in req.http_version or '\r' in req.http_version assert ',' not in req.host assert req.content_length is not None assert req.content_length == 337 assert b'foo' in req.body bar_contents = b"these are the contents of the file 'bar.txt'\r\n" assert bar_contents in req.body assert req.params['foo'] == 'foo' bar = req.params['bar'] assert isinstance(bar, cgi.FieldStorage) assert bar.type == 'application/octet-stream' bar.file.seek(0) assert bar.file.read() == bar_contents # out should equal contents, except for the Content-Length header, # so insert that. _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type' ) assert req.as_bytes() == _test_req_copy req2 = cls.from_bytes(_test_req2) assert 'host' not in req2.headers assert req2.as_bytes() == _test_req2.rstrip() with pytest.raises(ValueError): cls.from_bytes(_test_req2 + b'xx') def test_blank(self): # BaseRequest.blank class method with pytest.raises(ValueError): self._blankOne( 'www.example.com/foo?hello=world', None, 'www.example.com/foo?hello=world') with pytest.raises(ValueError): self._blankOne( 'gopher.example.com/foo?hello=world', None, 'gopher://gopher.example.com') req = self._blankOne('www.example.com/foo?hello=world', None, 'http://www.example.com') assert req.environ.get('HTTP_HOST', None) == 'www.example.com:80' assert req.environ.get('PATH_INFO', None) == 'www.example.com/foo' assert req.environ.get('QUERY_STRING', None) == 'hello=world' assert req.environ.get('REQUEST_METHOD', None) == 'GET' req = self._blankOne( 'www.example.com/secure?hello=world', None, 'https://www.example.com/secure') assert req.environ.get('HTTP_HOST', None) == 'www.example.com:443' assert req.environ.get('PATH_INFO', None) == 'www.example.com/secure' assert req.environ.get('QUERY_STRING', None) == 'hello=world' assert req.environ.get('REQUEST_METHOD', None) == 'GET' assert req.environ.get('SCRIPT_NAME', None) == '/secure' assert req.environ.get('SERVER_NAME', None) == 'www.example.com' assert req.environ.get('SERVER_PORT', None) == '443' def test_post_does_not_reparse(self): # test that there's no repetitive parsing is happening on every # req.POST access req = self._blankOne( '/', content_type='multipart/form-data; boundary=boundary', POST=_cgi_escaping_body ) post1 = req.POST assert 'webob._parsed_post_vars' in req.environ post2 = req.POST assert post1 is post2 def test_middleware_body(self): def app(env, sr): sr('200 OK', []) return [env['wsgi.input'].read()] def mw(env, sr): req = self._makeOne(env) data = req.body_file.read() resp = req.get_response(app) resp.headers['x-data'] = data return resp(env, sr) req = self._blankOne('/', method='PUT', body=b'abc') resp = req.get_response(mw) assert resp.body == b'abc' assert resp.headers['x-data'] == b'abc' def test_body_file_noseek(self): req = self._blankOne('/', method='PUT', body=b'abc') lst = [req.body_file.read(1) for i in range(3)] assert lst == [b'a', b'b', b'c'] def test_cgi_escaping_fix(self): req = self._blankOne( '/', content_type='multipart/form-data; boundary=boundary', POST=_cgi_escaping_body ) assert list(req.POST.keys()) == ['%20%22"'] req.body_file.read() assert list(req.POST.keys()) == ['%20%22"'] def test_content_type_none(self): r = self._blankOne('/', content_type='text/html') assert r.content_type == 'text/html' r.content_type = None def test_body_file_seekable(self): r = self._blankOne('/', method='POST') r.body_file = BytesIO(b'body') assert r.body_file_seekable.read() == b'body' def test_request_init(self): # port from doctest (docs/reference.txt) req = self._blankOne('/article?id=1') assert req.environ['HTTP_HOST'] == 'localhost:80' assert req.environ['PATH_INFO'] == '/article' assert req.environ['QUERY_STRING'] == 'id=1' assert req.environ['REQUEST_METHOD'] == 'GET' assert req.environ['SCRIPT_NAME'] == '' assert req.environ['SERVER_NAME'] == 'localhost' assert req.environ['SERVER_PORT'] == '80' assert req.environ['SERVER_PROTOCOL'] == 'HTTP/1.0' assert (hasattr(req.environ['wsgi.errors'], 'write') and hasattr(req.environ['wsgi.errors'], 'flush')) assert (hasattr(req.environ['wsgi.input'], 'next') or hasattr(req.environ['wsgi.input'], '__next__')) assert req.environ['wsgi.multiprocess'] == False assert req.environ['wsgi.multithread'] == False assert req.environ['wsgi.run_once'] == False assert req.environ['wsgi.url_scheme'] == 'http' assert req.environ['wsgi.version'], (1 == 0) # Test body assert hasattr(req.body_file, 'read') assert req.body == b'' req.method = 'PUT' req.body = b'test' assert hasattr(req.body_file, 'read') assert req.body == b'test' # Test method & URL assert req.method == 'PUT' assert req.scheme == 'http' assert req.script_name == '' # The base of the URL req.script_name = '/blog' # make it more interesting assert req.path_info == '/article' # Content-Type of the request body assert req.content_type == '' # The auth'ed user (there is none set) assert req.remote_user is None assert req.remote_addr is None assert req.host == 'localhost:80' assert req.host_url == 'http://localhost' assert req.application_url == 'http://localhost/blog' assert req.path_url == 'http://localhost/blog/article' assert req.url == 'http://localhost/blog/article?id=1' assert req.path == '/blog/article' assert req.path_qs == '/blog/article?id=1' assert req.query_string == 'id=1' assert req.relative_url('archive') == 'http://localhost/blog/archive' # Doesn't change request assert req.path_info_peek() == 'article' # Does change request! assert req.path_info_pop() == 'article' assert req.script_name == '/blog/article' assert req.path_info == '' # Headers req.headers['Content-Type'] = 'application/x-www-urlencoded' assert sorted(req.headers.items()) == [ ('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80') ] assert req.environ['CONTENT_TYPE'] == 'application/x-www-urlencoded' def test_request_query_and_POST_vars(self): # port from doctest (docs/reference.txt) # Query & POST variables from webob.multidict import MultiDict from webob.multidict import NestedMultiDict from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) assert req.GET == GET assert req.GET['check'] == 'b' assert req.GET.getall('check'), ['a' == 'b'] assert list(req.GET.items()) == [('check', 'a'), ('check', 'b'), ('name', 'Bob')] assert isinstance(req.POST, NoVars) # NoVars can be read like a dict, but not written assert list(req.POST.items()) == [] req.method = 'POST' req.body = b'name=Joe&email=joe@example.com' assert req.POST == MultiDict( [ ('name', 'Joe'), ('email', 'joe@example.com') ] ) assert req.POST['name'] == 'Joe' assert isinstance(req.params, NestedMultiDict) assert list(req.params.items()) == [ ('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com') ] assert req.params['name'] == 'Bob' assert req.params.getall('name'), ['Bob' == 'Joe'] @pytest.mark.filterwarnings('ignore:.*best_match.*') def test_request_put(self): from datetime import datetime from webob import Response from webob import UTC from webob.acceptparse import Accept from webob.byterange import Range from webob.etag import ETagMatcher from webob.multidict import MultiDict from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') req.method = 'PUT' req.body = b'var1=value1&var2=value2&rep=1&rep=2' req.environ['CONTENT_LENGTH'] = str(len(req.body)) req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) assert req.GET == GET assert req.POST == MultiDict([ ('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')] ) assert list(req.GET.items()) == [('check', 'a'), ('check', 'b'), ('name', 'Bob')] # Unicode req.charset = 'utf8' assert list(req.GET.items()) == [('check', 'a'), ('check', 'b'), ('name', 'Bob')] # Cookies req.headers['Cookie'] = 'test=value' assert isinstance(req.cookies, MutableMapping) assert list(req.cookies.items()) == [('test', 'value')] req.charset = None assert req.cookies == {'test': 'value'} # Accept-* headers assert 'text/html' in req.accept req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' assert isinstance(req.accept, Accept) assert 'text/html' in req.accept assert req.accept.best_match(['text/html', 'application/xhtml+xml']) == 'application/xhtml+xml' req.accept_language = 'es, pt-BR' assert req.accept_language.best_match(['es']) == 'es' # Conditional Requests server_token = 'opaque-token' # shouldn't return 304 assert not server_token in req.if_none_match req.if_none_match = server_token assert isinstance(req.if_none_match, ETagMatcher) # You *should* return 304 assert server_token in req.if_none_match # if_none_match should use weak matching weak_token = 'W/"%s"' % server_token req.if_none_match = weak_token assert req.headers['if-none-match'] == weak_token assert server_token in req.if_none_match req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) assert req.headers['If-Modified-Since'] == 'Sun, 01 Jan 2006 12:00:00 GMT' server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) assert req.if_modified_since assert req.if_modified_since >= server_modified assert not req.if_range assert Response(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0)) in req.if_range req.if_range = 'opaque-etag' assert Response(etag='other-etag') not in req.if_range assert Response(etag='opaque-etag') in req.if_range res = Response(etag='opaque-etag') assert res in req.if_range req.range = 'bytes=0-100' assert isinstance(req.range, Range) assert tuple(req.range), (0 == 101) cr = req.range.content_range(length=1000) assert tuple(cr), (0, 101 == 1000) assert server_token in req.if_match # No If-Match means everything is ok req.if_match = server_token assert server_token in req.if_match # Still OK req.if_match = 'other-token' # Not OK, should return 412 Precondition Failed: assert server_token not in req.if_match def test_request_patch(self): from webob.multidict import MultiDict from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') req.method = 'PATCH' req.body = b'var1=value1&var2=value2&rep=1&rep=2' req.environ['CONTENT_LENGTH'] = str(len(req.body)) req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) assert req.GET == GET assert req.POST == MultiDict([ ('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')] ) def test_call_WSGI_app(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hi!'] assert req.call_application(wsgi_app) == ('200 OK', [('Content-Type', 'text/plain')], [b'Hi!']) res = req.get_response(wsgi_app) from webob.response import Response assert isinstance(res, Response) assert res.status == '200 OK' from webob.headers import ResponseHeaders assert isinstance(res.headers, ResponseHeaders) assert list(res.headers.items()) == [('Content-Type', 'text/plain')] assert res.body == b'Hi!' def test_call_WSGI_app_204(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('204 No Content', []) return [b''] assert req.call_application(wsgi_app) == ('204 No Content', [], [b'']) res = req.get_response(wsgi_app) from webob.response import Response assert isinstance(res, Response) assert res.status == '204 No Content' from webob.headers import ResponseHeaders assert isinstance(res.headers, ResponseHeaders) assert list(res.headers.items()) == [] assert res.body == b'' def test_call_WSGI_app_no_content_type(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('200 OK', []) return [b''] assert req.call_application(wsgi_app) == ('200 OK', [], [b'']) res = req.get_response(wsgi_app) from webob.response import Response assert isinstance(res, Response) assert res.status == '200 OK' assert res.content_type is None from webob.headers import ResponseHeaders assert isinstance(res.headers, ResponseHeaders) assert list(res.headers.items()) == [] assert res.body == b'' def test_get_response_catch_exc_info_true(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hi!'] res = req.get_response(wsgi_app, catch_exc_info=True) from webob.response import Response assert isinstance(res, Response) assert res.status == '200 OK' from webob.headers import ResponseHeaders assert isinstance(res.headers, ResponseHeaders) assert list(res.headers.items()) == [('Content-Type', 'text/plain')] assert res.body == b'Hi!' def equal_req(self, req, inp): cls = self._getTargetClass() req2 = cls.from_file(inp) assert req.url == req2.url headers1 = dict(req.headers) headers2 = dict(req2.headers) assert int(headers1.get('Content-Length', '0')) == int(headers2.get('Content-Length', '0')) if 'Content-Length' in headers1: del headers1['Content-Length'] if 'Content-Length' in headers2: del headers2['Content-Length'] assert headers1 == headers2 req_body = req.body req2_body = req2.body assert req_body == req2_body @pytest.mark.filterwarnings('ignore:FakeCGIBody') class TestFakeCGIBody(object): def test_encode_multipart_value_type_options(self): from cgi import FieldStorage from webob.request import BaseRequest, FakeCGIBody from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="bananas"; ' b'filename="bananas.txt"\r\n' b'Content-type: text/plain; charset="utf-7"\r\n' b'\r\n' b"these are the contents of the file 'bananas.txt'\r\n" b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) assert vars['bananas'].__class__ == FieldStorage fake_body = FakeCGIBody(vars, multipart_type) assert fake_body.read() == body def test_encode_multipart_no_boundary(self): from webob.request import FakeCGIBody with pytest.raises(ValueError): FakeCGIBody({}, 'multipart/form-data') def test_repr(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') body.read(1) import re assert re.sub(r'\b0x[0-9a-f]+\b', '', repr(body)) == " viewing {'bananas': 'ba...nas'}>" def test_fileno(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') assert body.fileno() == None def test_iter(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') assert list(body) == [ b'--foobar\r\n', b'Content-Disposition: form-data; name="bananas"\r\n', b'\r\n', b'bananas\r\n', b'--foobar--', ] def test_readline(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') assert body.readline() == b'--foobar\r\n' assert body.readline() == b'Content-Disposition: form-data; name="bananas"\r\n' assert body.readline() == b'\r\n' assert body.readline() == b'bananas\r\n' assert body.readline() == b'--foobar--' # subsequent calls to readline will return '' def test_read_bad_content_type(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'application/jibberjabber') with pytest.raises(AssertionError): body.read() def test_read_urlencoded(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'application/x-www-form-urlencoded') assert body.read() == b'bananas=bananas' def test_readable(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'application/something') assert body.readable() class Test_cgi_FieldStorage__repr__patch(object): def _callFUT(self, fake): from webob.request import _cgi_FieldStorage__repr__patch return _cgi_FieldStorage__repr__patch(fake) def test_with_file(self): class Fake(object): name = 'name' file = 'file' filename = 'filename' value = 'value' fake = Fake() result = self._callFUT(fake) assert result, "FieldStorage('name' == 'filename')" def test_without_file(self): class Fake(object): name = 'name' file = None filename = 'filename' value = 'value' fake = Fake() result = self._callFUT(fake) assert result, "FieldStorage('name', 'filename' == 'value')" class TestLimitedLengthFile(object): def _makeOne(self, file, maxlen): from webob.request import LimitedLengthFile return LimitedLengthFile(file, maxlen) def test_fileno(self): class DummyFile(object): def fileno(self): return 1 dummyfile = DummyFile() inst = self._makeOne(dummyfile, 0) assert inst.fileno() == 1 class Test_environ_from_url(object): def _callFUT(self, *arg, **kw): from webob.request import environ_from_url return environ_from_url(*arg, **kw) def test_environ_from_url(self): # Generating an environ just from an url plus testing environ_add_POST with pytest.raises(TypeError): self._callFUT('http://www.example.com/foo?bar=baz#qux') with pytest.raises(TypeError): self._callFUT('gopher://gopher.example.com') req = self._callFUT('http://www.example.com/foo?bar=baz') assert req.get('HTTP_HOST', None) == 'www.example.com:80' assert req.get('PATH_INFO', None) == '/foo' assert req.get('QUERY_STRING', None) == 'bar=baz' assert req.get('REQUEST_METHOD', None) == 'GET' assert req.get('SCRIPT_NAME', None) == '' assert req.get('SERVER_NAME', None) == 'www.example.com' assert req.get('SERVER_PORT', None) == '80' req = self._callFUT('https://www.example.com/foo?bar=baz') assert req.get('HTTP_HOST', None) == 'www.example.com:443' assert req.get('PATH_INFO', None) == '/foo' assert req.get('QUERY_STRING', None) == 'bar=baz' assert req.get('REQUEST_METHOD', None) == 'GET' assert req.get('SCRIPT_NAME', None) == '' assert req.get('SERVER_NAME', None) == 'www.example.com' assert req.get('SERVER_PORT', None) == '443' from webob.request import environ_add_POST environ_add_POST(req, None) assert 'CONTENT_TYPE' not in req assert 'CONTENT_LENGTH' not in req environ_add_POST(req, {'hello': 'world'}) assert req.get('HTTP_HOST', None), 'www.example.com:443' assert req.get('PATH_INFO', None) == '/foo' assert req.get('QUERY_STRING', None) == 'bar=baz' assert req.get('REQUEST_METHOD', None) == 'POST' assert req.get('SCRIPT_NAME', None) == '' assert req.get('SERVER_NAME', None) == 'www.example.com' assert req.get('SERVER_PORT', None) == '443' assert req.get('CONTENT_LENGTH', None) == '11' assert req.get('CONTENT_TYPE', None) == 'application/x-www-form-urlencoded' assert req['wsgi.input'].read() == b'hello=world' def test_environ_from_url_highorder_path_info(self): from webob.request import Request env = self._callFUT('/%E6%B5%81') assert env['PATH_INFO'] == '/\xe6\xb5\x81' request = Request(env) expected = text_(b'/\xe6\xb5\x81', 'utf-8') # u'/\u6d41' assert request.path_info == expected assert request.upath_info == expected def test_fileupload_mime_type_detection(self): from webob.request import Request # sometimes on win the detected mime type for .jpg will be # image/pjpeg for ex. so use a non-standard extesion to avoid that import mimetypes mimetypes.add_type('application/x-foo', '.foo') request = Request.blank("/", POST=dict(file1=("foo.foo", "xxx"), file2=("bar.mp3", "xxx"))) assert "audio/mpeg" in request.body.decode('ascii', str(request)) assert 'application/x-foo' in request.body.decode('ascii', str(request)) class TestRequestMultipart(object): def test_multipart_with_charset(self): from webob.request import Request req = Request.from_bytes(_test_req_multipart_charset) assert req.POST['title'].encode('utf8') == text_('こんにちは', 'utf-8').encode('utf8') def simpleapp(environ, start_response): from webob.request import Request status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) request = Request(environ) request.remote_user = 'bob' return [bytes_(x) for x in [ 'Hello world!\n', 'The get is %r' % request.GET, ' and Val is %s\n' % repr(request.GET.get('name')), 'The languages are: %s\n' % ([o for o, _ in sorted( request.accept_language.parsed or (), key=lambda x: x[1], # sort by quality reverse=True, )]), 'The accepttypes is: %s\n' % ','.join([ o for o, _ in request.accept.acceptable_offers([ 'application/xml', 'text/html', ]) ]), 'post is %r\n' % request.POST, 'params is %r\n' % request.params, 'cookies is %r\n' % request.cookies, 'body: %r\n' % request.body, 'method: %s\n' % request.method, 'remote_user: %r\n' % request.environ['REMOTE_USER'], 'host_url: %r; application_url: %r; path_url: %r; url: %r\n' % ( request.host_url, request.application_url, request.path_url, request.url), 'urlvars: %r\n' % request.urlvars, 'urlargs: %r\n' % (request.urlargs, ), 'is_xhr: %r\n' % request.is_xhr, 'if_modified_since: %r\n' % request.if_modified_since, 'user_agent: %r\n' % request.user_agent, 'if_none_match: %r\n' % request.if_none_match, ]] _cgi_escaping_body = '''--boundary Content-Disposition: form-data; name="%20%22"" --boundary--''' def _norm_req(s): return b'\r\n'.join(s.strip().replace(b'\r', b'').split(b'\n')) _test_req = b""" POST /webob/ HTTP/1.0 Accept: */* Cache-Control: max-age=0 Content-Type: multipart/form-data; boundary=----------------------------deb95b63e42a Host: pythonpaste.org User-Agent: UserAgent/1.0 (identifier-version) library/7.0 otherlibrary/0.8 ------------------------------deb95b63e42a Content-Disposition: form-data; name="foo" foo ------------------------------deb95b63e42a Content-Disposition: form-data; name="bar"; filename="bar.txt" Content-type: application/octet-stream these are the contents of the file 'bar.txt' ------------------------------deb95b63e42a-- """ _test_req_patch = b""" PATCH /webob/ HTTP/1.1 Content-Length: 14 Content-Type: application/json {"foo": "bar"} """ _test_req2 = b""" POST / HTTP/1.0 Content-Length: 0 """ _test_req_multipart_charset = b""" POST /upload/ HTTP/1.1 Host: foo.com User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.8,ja;q=0.6 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Content-Type: multipart/form-data; boundary=000e0ce0b196b4ee6804c6c8af94 Content-Length: 926 --000e0ce0b196b4ee6804c6c8af94 Content-Type: text/plain; charset=ISO-2022-JP Content-Disposition: form-data; name=title Content-Transfer-Encoding: 7bit \x1b$B$3$s$K$A$O\x1b(B --000e0ce0b196b4ee6804c6c8af94 Content-Type: text/plain; charset=ISO-8859-1 Content-Disposition: form-data; name=submit Submit --000e0ce0b196b4ee6804c6c8af94 Content-Type: message/external-body; charset=ISO-8859-1; blob-key=AMIfv94TgpPBtKTL3a0U9Qh1QCX7OWSsmdkIoD2ws45kP9zQAGTOfGNz4U18j7CVXzODk85WtiL5gZUFklTGY3y4G0Jz3KTPtJBOFDvQHQew7YUymRIpgUXgENS_fSEmInAIQdpSc2E78MRBVEZY392uhph3r-In96t8Z58WIRc-Yikx1bnarWo Content-Disposition: form-data; name=file; filename="photo.jpg" Content-Type: image/jpeg Content-Length: 38491 X-AppEngine-Upload-Creation: 2012-08-08 15:32:29.035959 Content-MD5: ZjRmNGRhYmNhZTkyNzcyOWQ5ZGUwNDgzOWFkNDAxN2Y= Content-Disposition: form-data; name=file; filename="photo.jpg" --000e0ce0b196b4ee6804c6c8af94--""" _test_req = _norm_req(_test_req) _test_req_patch = _norm_req(_test_req_patch) _test_req2 = _norm_req(_test_req2) + b'\r\n' _test_req_multipart_charset = _norm_req(_test_req_multipart_charset) class UnseekableInput(object): def __init__(self, data): self.data = data self.pos = 0 def read(self, size=-1): if size == -1: t = self.data[self.pos:] self.pos = len(self.data) return t else: if self.pos + size > len(self.data): size = len(self.data) - self.pos t = self.data[self.pos:self.pos + size] self.pos += size return t class UnseekableInputWithSeek(UnseekableInput): def seek(self, pos, rel=0): raise IOError("Invalid seek!") class _Helper_test_request_wrong_clen(object): def __init__(self, f): self.f = f self.file_ended = False def read(self, *args): r = self.f.read(*args) if not r: if self.file_ended: raise AssertionError("Reading should stop after first empty string") self.file_ended = True return r def seek(self, pos): pass WebOb-1.8.6/tests/test_response.py0000644000076600000240000012526013611751037020115 0ustar xistencestaff00000000000000import zlib import io import sys import pytest from webob.request import BaseRequest from webob.request import Request from webob.response import Response from webob.compat import text_ from webob.compat import bytes_ from webob import cookies def setup_module(module): cookies._should_raise = True def teardown_module(module): cookies._should_raise = False def simple_app(environ, start_response): start_response('200 OK', [ ('Content-Type', 'text/html; charset=UTF-8'), ]) return ['OK'] def test_response(): req = BaseRequest.blank('/') res = req.get_response(simple_app) assert res.status == '200 OK' assert res.status_code == 200 assert res.body == "OK" assert res.charset == "UTF-8" assert res.content_type == 'text/html' res.status = 404 assert res.status == '404 Not Found' assert res.status_code == 404 res.body = b'Not OK' assert b''.join(res.app_iter) == b'Not OK' res.charset = 'iso8859-1' assert 'text/html; charset=iso8859-1' == res.headers['content-type'] res.content_type = 'text/xml' assert 'text/xml; charset=UTF-8' == res.headers['content-type'] res.content_type = 'text/xml; charset=UTF-8' assert 'text/xml; charset=UTF-8' == res.headers['content-type'] res.headers = {'content-type': 'text/html'} assert res.headers['content-type'] == 'text/html' assert res.headerlist == [('content-type', 'text/html')] res.set_cookie('x', 'y') assert res.headers['set-cookie'].strip(';') == 'x=y; Path=/' res.set_cookie(text_('x'), text_('y')) assert res.headers['set-cookie'].strip(';') == 'x=y; Path=/' res = Response('a body', '200 OK', content_type='text/html') res.encode_content() assert res.content_encoding == 'gzip' assert res.body == b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xffKTH\xcaO\xa9\x04\x00\xf6\x86GI\x06\x00\x00\x00' res.decode_content() assert res.content_encoding is None assert res.body == b'a body' res.set_cookie('x', text_(b'foo')) # test unicode value with pytest.raises(TypeError): Response(app_iter=iter(['a']), body="somebody") del req.environ with pytest.raises(TypeError): Response(charset=None, content_type='image/jpeg', body=text_(b"unicode body")) with pytest.raises(TypeError): Response(wrong_key='dummy') with pytest.raises(TypeError): resp = Response() resp.body = text_(b"unicode body") def test_set_response_status_binary(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status == b'200 OK' assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_str_no_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status = '200' assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_str_generic_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status = '299' assert res.status_code == 299 assert res.status == '299 Success' def test_set_response_status_code(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status_code = 200 assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_bad(): req = BaseRequest.blank('/') res = req.get_response(simple_app) def status_test(): res.status = 'ThisShouldFail' with pytest.raises(ValueError): status_test() def test_set_response_status_code_generic_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status_code = 299 assert res.status_code == 299 assert res.status == '299 Success' def test_content_type(): r = Response() # default ctype and charset assert r.content_type == 'text/html' assert r.charset == 'UTF-8' # setting to none, removes the header r.content_type = None assert r.content_type is None assert r.charset is None # can set missing ctype r.content_type = None assert r.content_type is None def test_init_content_type_w_charset(): v = 'text/plain;charset=ISO-8859-1' assert Response(content_type=v).headers['content-type'] == v def test_init_adds_default_charset_when_not_json(): content_type = 'text/plain' expected = 'text/plain; charset=UTF-8' assert Response(content_type=content_type).headers['content-type'] == expected def test_init_no_charset_when_json(): content_type = 'application/json' expected = content_type assert Response(content_type=content_type).headers['content-type'] == expected def test_init_keeps_specified_charset_when_json(): content_type = 'application/json; charset=ISO-8859-1' expected = content_type assert Response(content_type=content_type).headers['content-type'] == expected def test_init_doesnt_add_default_content_type_with_bodyless_status(): assert Response(status='204 No Content').content_type is None def test_content_type_supports_unicode(): content_type = u"text/html" resp = Response() resp.content_type = content_type assert isinstance(resp.headers["Content-Type"], str) @pytest.mark.skipif("sys.version_info < (3, 0)") def test_content_type_not_binary(): content_type = b"text/html" resp = Response() with pytest.raises(TypeError): resp.content_type = content_type def test_cookies(): res = Response() # test unicode value res.set_cookie('x', "test") # utf8 encoded assert res.headers.getall('set-cookie') == ['x=test; Path=/'] r2 = res.merge_cookies(simple_app) r2 = BaseRequest.blank('/').get_response(r2) assert r2.headerlist == [ ('Content-Type', 'text/html; charset=UTF-8'), ('Set-Cookie', 'x=test; Path=/'), ] def test_unicode_cookies_error_raised(): res = Response() with pytest.raises(ValueError): Response.set_cookie( res, 'x', text_(b'\\N{BLACK SQUARE}', 'unicode_escape')) def test_unicode_cookies_warning_issued(): import warnings cookies._should_raise = False with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") # Trigger a warning. res = Response() res.set_cookie('x', text_(b'\\N{BLACK SQUARE}', 'unicode_escape')) assert len(w) == 1 assert issubclass(w[-1].category, RuntimeWarning) is True assert "ValueError" in str(w[-1].message) cookies._should_raise = True def test_cookies_raises_typeerror(): res = Response() with pytest.raises(TypeError): res.set_cookie() def test_http_only_cookie(): req = Request.blank('/') res = req.get_response(Response('blah')) res.set_cookie("foo", "foo", httponly=True) assert res.headers['set-cookie'] == 'foo=foo; Path=/; HttpOnly' def test_headers(): r = Response() tval = 'application/x-test' r.headers.update({'content-type': tval}) assert r.headers.getall('content-type') == [tval] r.headers.clear() assert not r.headerlist def test_response_copy(): r = Response(app_iter=iter(['a'])) r2 = r.copy() assert r.body == 'a' assert r2.body == 'a' def test_response_copy_content_md5(): res = Response() res.md5_etag(set_content_md5=True) assert res.content_md5 res2 = res.copy() assert res.content_md5 assert res2.content_md5 assert res.content_md5 == res2.content_md5 def test_HEAD_closes(): req = Request.blank('/') req.method = 'HEAD' app_iter = io.BytesIO(b'foo') res = req.get_response(Response(app_iter=app_iter)) assert res.status_code == 200 assert res.body == b'' assert app_iter.closed def test_HEAD_conditional_response_returns_empty_response(): req = Request.blank('/', method='HEAD', if_none_match='none') res = Response(conditional_response=True) def start_response(status, headerlist): pass result = res(req.environ, start_response) assert not list(result) def test_HEAD_conditional_response_range_empty_response(): req = Request.blank('/', method='HEAD', range=(4, 5)) res = Response('Are we not men?', conditional_response=True) assert req.get_response(res).body == b'' def test_conditional_response_if_none_match_false(): req = Request.blank('/', if_none_match='foo') resp = Response(app_iter=['foo\n'], conditional_response=True, etag='bar') resp = req.get_response(resp) assert resp.status_code == 200 def test_conditional_response_if_none_match_true(): req = Request.blank('/', if_none_match='foo') resp = Response(app_iter=['foo\n'], conditional_response=True, etag='foo') resp = req.get_response(resp) assert resp.status_code == 304 def test_conditional_response_if_none_match_weak(): req = Request.blank('/', headers={'if-none-match': '"bar"'}) req_weak = Request.blank('/', headers={'if-none-match': 'W/"bar"'}) resp = Response(app_iter=['foo\n'], conditional_response=True, etag='bar') resp_weak = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': 'W/"bar"'}) for rq in [req, req_weak]: for rp in [resp, resp_weak]: rq.get_response(rp).status_code == 304 r2 = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': '"foo"'}) r2_weak = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': 'W/"foo"'}) req_weak.get_response(r2).status_code == 200 req.get_response(r2_weak) == 200 def test_conditional_response_if_modified_since_false(): from datetime import datetime, timedelta req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0)) resp = Response(app_iter=['foo\n'], conditional_response=True, last_modified=req.if_modified_since - timedelta(seconds=1)) resp = req.get_response(resp) assert resp.status_code == 304 def test_conditional_response_if_modified_since_true(): from datetime import datetime, timedelta req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0)) resp = Response(app_iter=['foo\n'], conditional_response=True, last_modified=req.if_modified_since + timedelta(seconds=1)) resp = req.get_response(resp) assert resp.status_code == 200 def test_conditional_response_range_not_satisfiable_response(): req = Request.blank('/', range='bytes=100-200') resp = Response(app_iter=['foo\n'], content_length=4, conditional_response=True) resp = req.get_response(resp) assert resp.status_code == 416 assert resp.content_range.start is None assert resp.content_range.stop is None assert resp.content_range.length == 4 assert resp.body == b'Requested range not satisfiable: bytes=100-200' def test_HEAD_conditional_response_range_not_satisfiable_response(): req = Request.blank('/', method='HEAD', range='bytes=100-200') resp = Response(app_iter=['foo\n'], content_length=4, conditional_response=True) resp = req.get_response(resp) assert resp.status_code == 416 assert resp.content_range.start is None assert resp.content_range.stop is None assert resp.content_range.length == 4 assert resp.body == b'' def test_md5_etag(): res = Response() res.body = b"""\ In A.D. 2101 War was beginning. Captain: What happen ? Mechanic: Somebody set up us the bomb. Operator: We get signal. Captain: What ! Operator: Main screen turn on. Captain: It's You !! Cats: How are you gentlemen !! Cats: All your base are belong to us. Cats: You are on the way to destruction. Captain: What you say !! Cats: You have no chance to survive make your time. Cats: HA HA HA HA .... Captain: Take off every 'zig' !! Captain: You know what you doing. Captain: Move 'zig'. Captain: For great justice.""" res.md5_etag() assert res.etag assert '\n' not in res.etag assert res.etag == 'pN8sSTUrEaPRzmurGptqmw' assert res.content_md5 is None def test_md5_etag_set_content_md5(): res = Response() body = b'The quick brown fox jumps over the lazy dog' res.md5_etag(body, set_content_md5=True) assert res.content_md5 == 'nhB9nTcrtoJr2B01QqQZ1g==' def test_decode_content_defaults_to_identity(): res = Response() res.body = b'There be dragons' res.decode_content() assert res.body == b'There be dragons' def test_decode_content_with_deflate(): res = Response() body = b'Hey Hey Hey' # Simulate inflate by chopping the headers off # the gzip encoded data res.body = zlib.compress(body)[2:-4] res.content_encoding = 'deflate' res.decode_content() assert res.body == body assert res.content_encoding is None def test_content_length(): r0 = Response('x' * 10, content_length=10) req_head = Request.blank('/', method='HEAD') r1 = req_head.get_response(r0) assert r1.status_code == 200 assert r1.body == b'' assert r1.content_length == 10 req_get = Request.blank('/') r2 = req_get.get_response(r0) assert r2.status_code == 200 assert r2.body == b'x' * 10 assert r2.content_length == 10 r3 = Response(app_iter=[b'x'] * 10) assert r3.content_length is None assert r3.body == b'x' * 10 assert r3.content_length == 10 r4 = Response(app_iter=[b'x'] * 10, content_length=20) # wrong content_length assert r4.content_length == 20 with pytest.raises(AssertionError): r4.body req_range = Request.blank('/', range=(0, 5)) r0.conditional_response = True r5 = req_range.get_response(r0) assert r5.status_code == 206 assert r5.body == b'xxxxx' assert r5.content_length == 5 def test_app_iter_range(): req = Request.blank('/', range=(2, 5)) for app_iter in [ [b'012345'], [b'0', b'12345'], [b'0', b'1234', b'5'], [b'01', b'2345'], [b'01', b'234', b'5'], [b'012', b'34', b'5'], [b'012', b'3', b'4', b'5'], [b'012', b'3', b'45'], [b'0', b'12', b'34', b'5'], [b'0', b'12', b'345'], ]: r = Response( app_iter=app_iter, content_length=6, conditional_response=True, ) res = req.get_response(r) assert list(res.content_range) == [2, 5, 6] assert res.body, b'234' def test_app_iter_range_inner_method(): class FakeAppIter: def app_iter_range(self, start, stop): return 'you win', start, stop res = Response(app_iter=FakeAppIter()) assert res.app_iter_range(30, 40), ('you win', 30 == 40) def test_has_body(): empty = Response() assert not empty.has_body with_list = Response(app_iter=['1']) assert with_list.has_body with_empty_list = Response(app_iter=[b'']) assert not with_empty_list.has_body with_body = Response(body='Seomthing') assert with_body.has_body with_none_app_iter = Response(app_iter=None) assert not with_none_app_iter.has_body with_none_body = Response(body=None) assert not with_none_body.has_body # key feature: has_body should not read app_iter app_iter = iter(['1', '2']) not_iterating = Response(app_iter=app_iter) assert not_iterating.has_body assert next(app_iter) == '1' # messed with private attribute but method should nonetheless not # return True messing_with_privates = Response() messing_with_privates._app_iter = None assert not messing_with_privates.has_body def test_str_crlf(): res = Response('test') assert '\r\n' in str(res) def test_from_file(): res = Response('test') inp = io.BytesIO(bytes_(str(res))) equal_resp(res, inp) def test_from_file2(): res = Response(app_iter=iter([b'test ', b'body']), content_type='text/plain') inp = io.BytesIO(bytes_(str(res))) equal_resp(res, inp) def test_from_text_file(): res = Response('test') inp = io.StringIO(text_(str(res), 'utf-8')) equal_resp(res, inp) res = Response(app_iter=iter([b'test ', b'body']), content_type='text/plain') inp = io.StringIO(text_(str(res), 'utf-8')) equal_resp(res, inp) def equal_resp(res, inp): res2 = Response.from_file(inp) assert res.body == res2.body assert res.headers == res2.headers def test_from_file_w_leading_space_in_header(): # Make sure the removal of code dealing with leading spaces is safe res1 = Response() file_w_space = io.BytesIO( b'200 OK\n\tContent-Type: text/html; charset=UTF-8') res2 = Response.from_file(file_w_space) assert res1.headers == res2.headers def test_file_bad_header(): file_w_bh = io.BytesIO(b'200 OK\nBad Header') with pytest.raises(ValueError): Response.from_file(file_w_bh) def test_from_file_not_unicode_headers(): inp = io.BytesIO( b'200 OK\n\tContent-Type: text/html; charset=UTF-8') res = Response.from_file(inp) assert res.headerlist[0][0].__class__ == str def test_file_with_http_version(): inp = io.BytesIO(b'HTTP/1.1 200 OK\r\n\r\nSome data...') res = Response.from_file(inp) assert res.status_code == 200 assert res.status == '200 OK' def test_file_with_http_version_more_status(): inp = io.BytesIO(b'HTTP/1.1 404 Not Found\r\n\r\nSome data...') res = Response.from_file(inp) assert res.status_code == 404 assert res.status == '404 Not Found' def test_set_status(): res = Response() res.status = "200" assert res.status == "200 OK" with pytest.raises(TypeError): setattr(res, 'status', (200,)) def test_set_headerlist(): res = Response() # looks like a list res.headerlist = (('Content-Type', 'text/html; charset=UTF-8'),) assert res.headerlist == [('Content-Type', 'text/html; charset=UTF-8')] # has items res.headerlist = {'Content-Type': 'text/html; charset=UTF-8'} assert res.headerlist == [('Content-Type', 'text/html; charset=UTF-8')] del res.headerlist assert res.headerlist == [] def test_request_uri_no_script_name(): from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'test.com', } assert _request_uri(environ) == 'http://test.com/' def test_request_uri_https(): from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'https', 'SERVER_NAME': 'test.com', 'SERVER_PORT': '443', 'SCRIPT_NAME': '/foobar', } assert _request_uri(environ) == 'https://test.com/foobar' def test_app_iter_range_starts_after_iter_end(): from webob.response import AppIterRange range = AppIterRange(iter([]), start=1, stop=1) assert list(range) == [] def test_resp_write_app_iter_non_list(): res = Response(app_iter=(b'a', b'b')) assert res.content_length is None res.write(b'c') assert res.body == b'abc' assert res.content_length == 3 def test_response_file_body_writelines(): from webob.response import ResponseBodyFile res = Response(app_iter=[b'foo']) rbo = ResponseBodyFile(res) rbo.writelines(['bar', 'baz']) assert res.app_iter == [b'foo', b'bar', b'baz'] rbo.flush() # noop assert res.app_iter, [b'foo', b'bar', b'baz'] @pytest.mark.xfail(sys.version_info >= (3,6), reason="Python 3.6 and up requires that rbo is seekable.") def test_response_file_body_tell(): import zipfile from webob.response import ResponseBodyFile rbo = ResponseBodyFile(Response()) assert rbo.tell() == 0 writer = zipfile.ZipFile(rbo, 'w') writer.writestr('zinfo_or_arcname', b'foo') writer.close() assert rbo.tell() == 133 def test_response_file_body_tell_text(): from webob.response import ResponseBodyFile rbo = ResponseBodyFile(Response()) assert rbo.tell() == 0 rbo.write('123456789') assert rbo.tell() == 9 def test_response_write_non_str(): res = Response() with pytest.raises(TypeError): res.write(object()) def test_response_file_body_write_empty_app_iter(): res = Response('foo') res.write('baz') assert res.app_iter == [b'foo', b'baz'] def test_response_file_body_write_empty_body(): res = Response('') res.write('baz') assert res.app_iter == [b'', b'baz'] def test_response_file_body_close_not_implemented(): rbo = Response().body_file with pytest.raises(NotImplementedError): rbo.close() def test_response_file_body_repr(): rbo = Response().body_file rbo.response = 'yo' assert repr(rbo) == "" def test_body_get_is_none(): res = Response() res._app_iter = None with pytest.raises(TypeError): Response(app_iter=iter(['a']), body="somebody") with pytest.raises(AttributeError): res.__getattribute__('body') def test_body_get_is_unicode_notverylong(): res = Response(app_iter=(text_(b'foo'),)) with pytest.raises(TypeError): res.__getattribute__('body') def test_body_get_is_unicode(): res = Response(app_iter=(['x'] * 51 + [text_(b'x')])) with pytest.raises(TypeError): res.__getattribute__('body') def test_body_set_not_unicode_or_str(): res = Response() with pytest.raises(TypeError): res.__setattr__('body', object()) def test_body_set_unicode(): res = Response() with pytest.raises(TypeError): res.__setattr__('body', text_(b'abc')) def test_body_set_under_body_doesnt_exist(): res = Response('abc') assert res.body == b'abc' assert res.content_length == 3 def test_body_del(): res = Response('123') del res.body assert res.body == b'' assert res.content_length == 0 def test_text_get_no_charset(): res = Response(charset=None) assert '' == res.text def test_text_get_no_default_body_encoding(): res = Response(charset=None) res.default_body_encoding = None with pytest.raises(AttributeError): assert '' == res.text def test_unicode_body(): res = Response() res.charset = 'utf-8' bbody = b'La Pe\xc3\xb1a' # binary string ubody = text_(bbody, 'utf-8') # unicode string res.body = bbody assert res.unicode_body == ubody res.ubody = ubody assert res.body == bbody del res.ubody assert res.body == b'' def test_text_get_decode(): res = Response() res.charset = 'utf-8' res.body = b'La Pe\xc3\xb1a' assert res.text, text_(b'La Pe\xc3\xb1a') def test_text_set_no_charset(): res = Response() res.charset = None res.text = text_('abc') assert res.text == 'abc' def test_text_set_no_default_body_encoding(): res = Response() res.charset = None res.default_body_encoding = None with pytest.raises(AttributeError): res.text = text_('abc') def test_text_set_not_unicode(): res = Response() res.charset = 'utf-8' with pytest.raises(TypeError): res.__setattr__('text', b'La Pe\xc3\xb1a') def test_text_del(): res = Response('123') del res.text assert res.body == b'' assert res.content_length == 0 def test_body_file_del(): res = Response() res.body = b'123' assert res.content_length == 3 assert res.app_iter == [b'123'] del res.body_file assert res.body == b'' assert res.content_length == 0 def test_write_unicode(): res = Response() res.text = text_(b'La Pe\xc3\xb1a', 'utf-8') res.write(text_(b'a')) assert res.text, text_(b'La Pe\xc3\xb1aa' == 'utf-8') def test_write_unicode_no_charset(): res = Response(charset=None) with pytest.raises(TypeError): res.write(text_(b'a')) def test_write_text(): res = Response() res.body = b'abc' res.write(text_(b'a')) assert res.text == 'abca' def test_app_iter_del(): res = Response( content_length=3, app_iter=['123'], ) del res.app_iter assert res.body == b'' assert res.content_length is None def test_charset_set_no_content_type_header(): res = Response() res.headers.pop('Content-Type', None) with pytest.raises(AttributeError): res.charset = 'utf-8' def test_charset_del_no_content_type_header(): res = Response() res.headers.pop('Content-Type', None) assert res._charset__del() is None def test_content_type_params_get_no_semicolon_in_content_type_header(): res = Response() res.headers['Content-Type'] = 'foo' assert res.content_type_params == {} def test_content_type_params_get_semicolon_in_content_type_header(): res = Response() res.headers['Content-Type'] = 'foo;encoding=utf-8' assert res.content_type_params == {'encoding': 'utf-8'} def test_content_type_params_set_value_dict_empty(): res = Response() res.headers['Content-Type'] = 'foo;bar' res.content_type_params = None assert res.headers['Content-Type'] == 'foo' def test_content_type_params_set_ok_param_quoting(): res = Response() res.content_type_params = {'a': ''} assert res.headers['Content-Type'] == 'text/html; a=""' def test_charset_delete(): res = Response() del res.charset assert res.charset is None def test_set_cookie_overwrite(): res = Response() res.set_cookie('a', '1') res.set_cookie('a', '2', overwrite=True) assert res.headerlist[-1] == ('Set-Cookie', 'a=2; Path=/') def test_set_cookie_value_is_None(): res = Response() res.set_cookie('a', None) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] == 'Max-Age=0' assert val[1] == 'Path=/' assert val[2] == 'a=' assert val[3].startswith('expires') def test_set_cookie_expires_is_None_and_max_age_is_int(): res = Response() res.set_cookie('a', '1', max_age=100) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] == 'Max-Age=100' assert val[1] == 'Path=/' assert val[2] == 'a=1' assert val[3].startswith('expires') def test_set_cookie_expires_is_None_and_max_age_is_timedelta(): from datetime import timedelta res = Response() res.set_cookie('a', '1', max_age=timedelta(seconds=100)) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] == 'Max-Age=100' assert val[1] == 'Path=/' assert val[2] == 'a=1' assert val[3].startswith('expires') @pytest.mark.filterwarnings('ignore:"expires" will be removed"') def test_set_cookie_expires_is_datetime_and_max_age_is_None(): import datetime res = Response() then = datetime.datetime.utcnow() + datetime.timedelta(days=1) res.set_cookie('a', '1', expires=then) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] in ('Max-Age=86399', 'Max-Age=86400') assert val[1] == 'Path=/' assert val[2] == 'a=1' assert val[3].startswith('expires') @pytest.mark.filterwarnings('ignore:"expires" will be removed"') def test_set_cookie_expires_is_timedelta_and_max_age_is_None(): import datetime res = Response() then = datetime.timedelta(days=1) res.set_cookie('a', '1', expires=then) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] in ('Max-Age=86399', 'Max-Age=86400') assert val[1] == 'Path=/' assert val[2] == 'a=1' assert val[3].startswith('expires') @pytest.mark.filterwarnings('ignore:"expires" will be removed"') def test_set_cookie_expires_is_datetime_tz_and_max_age_is_None(): import datetime res = Response() class FixedOffset(datetime.tzinfo): def __init__(self, offset, name): self.__offset = datetime.timedelta(minutes=offset) self.__name = name def utcoffset(self, dt): return self.__offset def tzname(self, dt): return self.__name def dst(self, dt): return datetime.timedelta(0) then = datetime.datetime.now(FixedOffset(60, 'UTC+1')) + datetime.timedelta(days=1) res.set_cookie('a', '1', expires=then) assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] in ('Max-Age=86399', 'Max-Age=86400') assert val[1] == 'Path=/' assert val[2] == 'a=1' assert val[3].startswith('expires') def test_delete_cookie(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a') assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] == 'Max-Age=0' assert val[1] == 'Path=/' assert val[2] == 'a=' assert val[3].startswith('expires') def test_delete_cookie_with_path(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a', path='/abc') assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() assert val[0] == 'Max-Age=0' assert val[1] == 'Path=/abc' assert val[2] == 'a=' assert val[3].startswith('expires') def test_delete_cookie_with_domain(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a', path='/abc', domain='example.com') assert res.headerlist[-1][0] == 'Set-Cookie' val = [x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 5 val.sort() assert val[0] == 'Domain=example.com' assert val[1] == 'Max-Age=0' assert val[2] == 'Path=/abc' assert val[3] == 'a=' assert val[4].startswith('expires') def test_unset_cookie_not_existing_and_not_strict(): res = Response() res.unset_cookie('a', strict=False) # no exception def test_unset_cookie_not_existing_and_strict(): res = Response() with pytest.raises(KeyError): res.unset_cookie('a') def test_unset_cookie_key_in_cookies(): res = Response() res.headers.add('Set-Cookie', 'a=2; Path=/') res.headers.add('Set-Cookie', 'b=3; Path=/') res.unset_cookie('a') assert res.headers.getall('Set-Cookie') == ['b=3; Path=/'] res.unset_cookie(text_('b')) assert res.headers.getall('Set-Cookie') == [] def test_merge_cookies_no_set_cookie(): res = Response() result = res.merge_cookies('abc') assert result == 'abc' def test_merge_cookies_resp_is_Response(): inner_res = Response() res = Response() res.set_cookie('a', '1') result = res.merge_cookies(inner_res) assert result.headers.getall('Set-Cookie') == ['a=1; Path=/'] def test_merge_cookies_resp_is_wsgi_callable(): L = [] def dummy_wsgi_callable(environ, start_response): L.append((environ, start_response)) return 'abc' res = Response() res.set_cookie('a', '1') wsgiapp = res.merge_cookies(dummy_wsgi_callable) environ = {} def dummy_start_response(status, headers, exc_info=None): assert headers, [('Set-Cookie' == 'a=1; Path=/')] result = wsgiapp(environ, dummy_start_response) assert result == 'abc' assert len(L) == 1 L[0][1]('200 OK', []) # invoke dummy_start_response assertion def test_body_get_body_is_None_len_app_iter_is_zero(): res = Response() res._app_iter = io.BytesIO() res._body = None result = res.body assert result == b'' def test_cache_control_get(): res = Response() assert repr(res.cache_control) == "" assert res.cache_control.max_age is None def test_location(): res = Response() res.status = '301' res.location = '/test.html' assert res.location == '/test.html' req = Request.blank('/') assert req.get_response(res).location == 'http://localhost/test.html' res.location = '/test2.html' assert req.get_response(res).location == 'http://localhost/test2.html' @pytest.mark.xfail(sys.version_info < (3,0), reason="Python 2.x unicode != str, WSGI requires str. Test " "added due to https://github.com/Pylons/webob/issues/247. " "PEP3333 requires environ variables are str, Django messes " "with the environ and changes it from str to unicode.") def test_location_unicode(): environ = { 'REQUEST_METHOD': 'GET', 'wsgi.url_scheme': 'http', 'HTTP_HOST': u'test.com', } res = Response() res.status = '301' res.location = '/test.html' def start_response(status, headerlist): for (header, val) in headerlist: if header.lower() == 'location': assert val == 'http://test.com/test.html' assert isinstance(val, str) res(environ, start_response) def test_request_uri_http(): # covers webob/response.py:1152 from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'SERVER_NAME': 'test.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/foobar', } assert _request_uri(environ) == 'http://test.com/foobar' def test_request_uri_no_script_name2(): # covers webob/response.py:1160 # There is a test_request_uri_no_script_name in test_response.py, but it # sets SCRIPT_NAME. from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'test.com', 'PATH_INFO': '/foobar', } assert _request_uri(environ) == 'http://test.com/foobar' def test_cache_control_object_max_age_ten(): res = Response() res.cache_control.max_age = 10 assert repr(res.cache_control) == "" assert res.headers['cache-control'] == 'max-age=10' def test_cache_control_set_object_error(): res = Response() with pytest.raises(AttributeError): setattr(res.cache_control, 'max_stale', 10) def test_cache_expires_set(): res = Response() res.cache_expires = True assert repr(res.cache_control) == "" def test_status_code_set(): res = Response() res.status_code = 400 assert res._status == '400 Bad Request' res.status_int = 404 assert res._status == '404 Not Found' def test_cache_control_set_dict(): res = Response() res.cache_control = {'a': 'b'} assert repr(res.cache_control) == "" def test_cache_control_set_None(): res = Response() res.cache_control = None assert repr(res.cache_control) == "" def test_cache_control_set_unicode(): res = Response() res.cache_control = text_(b'abc') assert repr(res.cache_control) == "" def test_cache_control_set_control_obj_is_not_None(): class DummyCacheControl(object): def __init__(self): self.header_value = 1 self.properties = {'bleh': 1} res = Response() res._cache_control_obj = DummyCacheControl() res.cache_control = {} assert res.cache_control.properties == {} def test_cache_control_del(): res = Response() del res.cache_control assert repr(res.cache_control) == "" def test_body_file_get(): res = Response() result = res.body_file from webob.response import ResponseBodyFile assert result.__class__ == ResponseBodyFile def test_body_file_write_no_charset(): res = Response with pytest.raises(TypeError): res.write(text_('foo')) def test_body_file_write_unicode_encodes(): s = text_(b'La Pe\xc3\xb1a', 'utf-8') res = Response() res.write(s) assert res.app_iter, [b'' == b'La Pe\xc3\xb1a'] def test_repr(): res = Response() assert repr(res).endswith('200 OK>') def test_cache_expires_set_timedelta(): res = Response() from datetime import timedelta delta = timedelta(seconds=60) res.cache_expires(seconds=delta) assert res.cache_control.max_age == 60 def test_cache_expires_set_int(): res = Response() res.cache_expires(seconds=60) assert res.cache_control.max_age == 60 def test_cache_expires_set_None(): res = Response() res.cache_expires(seconds=None, a=1) assert res.cache_control.a == 1 def test_cache_expires_set_zero(): res = Response() res.cache_expires(seconds=0) assert res.cache_control.no_store is True assert res.cache_control.no_cache == '*' assert res.cache_control.must_revalidate is True assert res.cache_control.max_age == 0 assert res.cache_control.post_check == 0 def test_encode_content_unknown(): res = Response() with pytest.raises(AssertionError): res.encode_content('badencoding') def test_encode_content_identity(): res = Response() result = res.encode_content('identity') assert result is None def test_encode_content_gzip_already_gzipped(): res = Response() res.content_encoding = 'gzip' result = res.encode_content('gzip') assert result is None def test_encode_content_gzip_notyet_gzipped(): res = Response() res.app_iter = io.BytesIO(b'foo') result = res.encode_content('gzip') assert result is None assert res.content_length == 23 assert res.app_iter == [ b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', b'K\xcb\xcf\x07\x00', b'!es\x8c\x03\x00\x00\x00' ] def test_encode_content_gzip_notyet_gzipped_lazy(): res = Response() res.app_iter = io.BytesIO(b'foo') result = res.encode_content('gzip', lazy=True) assert result is None assert res.content_length is None assert list(res.app_iter) == [ b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', b'K\xcb\xcf\x07\x00', b'!es\x8c\x03\x00\x00\x00' ] def test_encode_content_gzip_buffer_coverage(): # this test is to provide 100% coverage of # Response.encode_content was necessary in order to get # request https://github.com/Pylons/webob/pull/85 into upstream res = Response() DATA = b"abcdefghijklmnopqrstuvwxyz0123456789" * 1000000 res.app_iter = io.BytesIO(DATA) res.encode_content('gzip') result = list(res.app_iter) assert len(b"".join(result)) < len(DATA) def test_decode_content_identity(): res = Response() res.content_encoding = 'identity' result = res.decode_content() assert result is None def test_decode_content_weird(): res = Response() res.content_encoding = 'weird' with pytest.raises(ValueError): res.decode_content() def test_decode_content_gzip(): from gzip import GzipFile io_ = io.BytesIO() gzip_f = GzipFile(filename='', mode='w', fileobj=io_) gzip_f.write(b'abc') gzip_f.close() body = io_.getvalue() res = Response() res.content_encoding = 'gzip' res.body = body res.decode_content() assert res.body == b'abc' def test__make_location_absolute_has_scheme_only(): result = Response._make_location_absolute( { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80' }, 'http:' ) assert result == 'http:' def test__make_location_absolute_path(): result = Response._make_location_absolute( { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80' }, '/abc' ) assert result == 'http://example.com/abc' def test__make_location_absolute_already_absolute(): result = Response._make_location_absolute( { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80' }, 'https://funcptr.net/' ) assert result == 'https://funcptr.net/' def test_response_set_body_file1(): data = b'abc' file = io.BytesIO(data) r = Response(body_file=file) assert r.body == data def test_response_set_body_file2(): data = b'abcdef' * 1024 file = io.BytesIO(data) r = Response(body_file=file) assert r.body == data def test_response_json_body(): r = Response(json_body={'a': 1}) assert r.body == b'{"a":1}', repr(r.body) assert r.content_type == 'application/json' r = Response() r.json_body = {"b": 1} assert r.content_type == 'text/html' del r.json_body assert r.body == b'' def test_cache_expires_set_zero_then_nonzero(): res = Response() res.cache_expires(seconds=0) res.cache_expires(seconds=1) assert res.pragma is None assert not res.cache_control.no_cache assert not res.cache_control.no_store assert not res.cache_control.must_revalidate assert res.cache_control.max_age == 1 def test_default_content_type(): class NoDefault(Response): default_content_type = None res = NoDefault() assert res.content_type is None def test_default_charset(): class DefaultCharset(Response): default_charset = 'UTF-16' res = DefaultCharset() assert res.content_type == 'text/html' assert res.charset == 'UTF-16' assert res.headers['Content-Type'] == 'text/html; charset=UTF-16' def test_header_list_no_defaults(): res = Response(headerlist=[]) assert res.headerlist == [('Content-Length', '0')] assert res.content_type is None assert res.charset is None assert res.body == b'' def test_204_has_no_body(): res = Response(status='204 No Content') assert res.body == b'' assert res.content_length is None assert res.headerlist == [] def test_204_app_iter_set(): res = Response(status='204', app_iter=[b'test']) # You are on your own in this case... you set app_iter you bought it assert res.body == b'test' assert res.content_length is None assert res.headerlist == [] def test_explicit_charset(): res = Response(charset='UTF-16') assert res.content_type == 'text/html' assert res.charset == 'UTF-16' def test_set_content_type(): res = Response(content_type='application/json') res.content_type = 'application/foo' assert res.content_type == 'application/foo' def test_raises_no_charset(): with pytest.raises(TypeError): Response(content_type='image/jpeg', body=text_(b'test')) def test_raises_none_charset(): with pytest.raises(TypeError): Response( content_type='image/jpeg', body=text_(b'test'), charset=None) def test_doesnt_raise_with_charset_content_type_has_no_charset(): res = Response(content_type='image/jpeg', body=text_(b'test'), charset='utf-8') assert res.body == b'test' assert res.content_type == 'image/jpeg' assert res.charset is None def test_content_type_has_charset(): res = Response(content_type='application/foo; charset=UTF-8', body=text_(b'test')) assert res.body == b'test' assert res.content_type == 'application/foo' assert res.charset == 'UTF-8' assert res.headers['Content-Type'] == 'application/foo; charset=UTF-8' def test_app_iter_is_same(): class app_iter(object): pass my_app_iter = app_iter() res = Response(status=204, app_iter=my_app_iter) assert res.app_iter == my_app_iter assert isinstance(res.app_iter, app_iter) WebOb-1.8.6/tests/test_static.py0000644000076600000240000002236713611751037017552 0ustar xistencestaff00000000000000from io import BytesIO from os.path import getmtime import tempfile from time import gmtime import os import shutil import unittest from webob import static from webob.compat import bytes_ from webob.request import Request, environ_from_url from webob.response import Response def get_response(app, path='/', **req_kw): """Convenient function to query an application""" req = Request(environ_from_url(path), **req_kw) return req.get_response(app) def create_file(content, *paths): """Convenient function to create a new file with some content""" path = os.path.join(*paths) with open(path, 'wb') as fp: fp.write(bytes_(content)) return path class TestFileApp(unittest.TestCase): def setUp(self): fp = tempfile.NamedTemporaryFile(suffix=".py", delete=False) self.tempfile = fp.name fp.write(b"import this\n") fp.close() def tearDown(self): os.unlink(self.tempfile) def test_fileapp(self): app = static.FileApp(self.tempfile) resp1 = get_response(app) assert resp1.content_type in ('text/x-python', 'text/plain') self.assertEqual(resp1.charset, 'UTF-8') self.assertEqual(resp1.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) self.assertEqual(resp1.body, b"import this\n") resp2 = get_response(app) assert resp2.content_type in ('text/x-python', 'text/plain') self.assertEqual(resp2.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) self.assertEqual(resp2.body, b"import this\n") resp3 = get_response(app, range=(7, 11)) self.assertEqual(resp3.status_code, 206) self.assertEqual(tuple(resp3.content_range)[:2], (7, 11)) self.assertEqual(resp3.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) self.assertEqual(resp3.body, bytes_('this')) def test_unexisting_file(self): app = static.FileApp('/tmp/this/doesnt/exist') self.assertEqual(404, get_response(app).status_code) def test_allowed_methods(self): app = static.FileApp(self.tempfile) # Alias resp = lambda method: get_response(app, method=method) self.assertEqual(200, resp(method='GET').status_code) self.assertEqual(200, resp(method='HEAD').status_code) self.assertEqual(405, resp(method='POST').status_code) # Actually any other method is not allowed self.assertEqual(405, resp(method='xxx').status_code) def test_exception_while_opening_file(self): # Mock the built-in ``open()`` function to allow finner control about # what we are testing. def open_ioerror(*args, **kwargs): raise IOError() def open_oserror(*args, **kwargs): raise OSError() app = static.FileApp(self.tempfile) app._open = open_ioerror self.assertEqual(403, get_response(app).status_code) app._open = open_oserror self.assertEqual(403, get_response(app).status_code) def test_use_wsgi_filewrapper(self): class TestWrapper(object): def __init__(self, file, block_size): self.file = file self.block_size = block_size environ = environ_from_url('/') environ['wsgi.file_wrapper'] = TestWrapper app = static.FileApp(self.tempfile) app_iter = Request(environ).get_response(app).app_iter self.assertTrue(isinstance(app_iter, TestWrapper)) self.assertEqual(bytes_('import this\n'), app_iter.file.read()) self.assertEqual(static.BLOCK_SIZE, app_iter.block_size) class TestFileIter(unittest.TestCase): def test_empty_file(self): fp = BytesIO() fi = static.FileIter(fp) self.assertRaises(StopIteration, next, iter(fi)) def test_seek(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(seek=4) self.assertEqual(bytes_("456789"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=4) self.assertEqual(bytes_("0123"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit_and_seek(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=4, seek=1) self.assertEqual(bytes_("123"), next(i)) self.assertRaises(StopIteration, next, i) def test_multiple_reads(self): fp = BytesIO(bytes_("012")) i = static.FileIter(fp).app_iter_range(block_size=1) self.assertEqual(bytes_("0"), next(i)) self.assertEqual(bytes_("1"), next(i)) self.assertEqual(bytes_("2"), next(i)) self.assertRaises(StopIteration, next, i) def test_seek_bigger_than_limit(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=1, seek=2) # XXX: this should not return anything actually, since we are starting # to read after the place we wanted to stop. self.assertEqual(bytes_("23456789"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit_is_zero(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=0) self.assertRaises(StopIteration, next, i) class TestDirectoryApp(unittest.TestCase): def setUp(self): self.test_dir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.test_dir) def test_empty_directory(self): app = static.DirectoryApp(self.test_dir) self.assertEqual(404, get_response(app).status_code) self.assertEqual(404, get_response(app, '/foo').status_code) def test_serve_file(self): app = static.DirectoryApp(self.test_dir) create_file('abcde', self.test_dir, 'bar') self.assertEqual(404, get_response(app).status_code) self.assertEqual(404, get_response(app, '/foo').status_code) resp = get_response(app, '/bar') self.assertEqual(200, resp.status_code) self.assertEqual(bytes_('abcde'), resp.body) def test_dont_serve_file_in_parent_directory(self): # We'll have: # /TEST_DIR/ # /TEST_DIR/bar # /TEST_DIR/foo/ <- serve this directory create_file('abcde', self.test_dir, 'bar') serve_path = os.path.join(self.test_dir, 'foo') os.mkdir(serve_path) app = static.DirectoryApp(serve_path) # The file exists, but is outside the served dir. self.assertEqual(403, get_response(app, '/../bar').status_code) def test_dont_leak_parent_directory_file_existance(self): # We'll have: # /TEST_DIR/ # /TEST_DIR/foo/ <- serve this directory serve_path = os.path.join(self.test_dir, 'foo') os.mkdir(serve_path) app = static.DirectoryApp(serve_path) # The file exists, but is outside the served dir. self.assertEqual(403, get_response(app, '/../bar2').status_code) def test_file_app_arguments(self): app = static.DirectoryApp(self.test_dir, content_type='xxx/yyy') create_file('abcde', self.test_dir, 'bar') resp = get_response(app, '/bar') self.assertEqual(200, resp.status_code) self.assertEqual('xxx/yyy', resp.content_type) def test_file_app_factory(self): def make_fileapp(*args, **kwargs): make_fileapp.called = True return Response() make_fileapp.called = False app = static.DirectoryApp(self.test_dir) app.make_fileapp = make_fileapp create_file('abcde', self.test_dir, 'bar') get_response(app, '/bar') self.assertTrue(make_fileapp.called) def test_must_serve_directory(self): serve_path = create_file('abcde', self.test_dir, 'bar') self.assertRaises(IOError, static.DirectoryApp, serve_path) def test_index_page(self): os.mkdir(os.path.join(self.test_dir, 'index-test')) create_file(bytes_('index'), self.test_dir, 'index-test', 'index.html') app = static.DirectoryApp(self.test_dir) resp = get_response(app, '/index-test') self.assertEqual(resp.status_code, 301) self.assertTrue(resp.location.endswith('/index-test/')) resp = get_response(app, '/index-test?test') self.assertTrue(resp.location.endswith('/index-test/?test')) resp = get_response(app, '/index-test/') self.assertEqual(resp.status_code, 200) self.assertEqual(resp.body, bytes_('index')) self.assertEqual(resp.content_type, 'text/html') resp = get_response(app, '/index-test/index.html') self.assertEqual(resp.status_code, 200) self.assertEqual(resp.body, bytes_('index')) redir_app = static.DirectoryApp(self.test_dir, hide_index_with_redirect=True) resp = get_response(redir_app, '/index-test/index.html') self.assertEqual(resp.status_code, 301) self.assertTrue(resp.location.endswith('/index-test/')) resp = get_response(redir_app, '/index-test/index.html?test') self.assertTrue(resp.location.endswith('/index-test/?test')) page_app = static.DirectoryApp(self.test_dir, index_page='something-else.html') self.assertEqual(get_response(page_app, '/index-test/').status_code, 404) WebOb-1.8.6/tests/test_transcode.py0000644000076600000240000000400713611751037020234 0ustar xistencestaff00000000000000from webob.request import Request, Transcoder from webob.response import Response from webob.compat import text_, native_ t1 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"\r\n\r\n\xea\xf3...\r\n--BOUNDARY--' t2 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"; filename="file"\r\n\r\n\xea\xf3...\r\n--BOUNDARY--' t3 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"; filename="\xea\xf3..."\r\n\r\nfoo\r\n--BOUNDARY--' def test_transcode(): def tapp(env, sr): req = Request(env) req = req.decode() v = req.POST[req.query_string] if hasattr(v, 'filename'): r = Response(text_('%s\n%r' % (v.filename, v.value))) else: r = Response(v) return r(env, sr) text = b'\xea\xf3...'.decode('cp1251') def test(post): req = Request.blank('/?a', POST=post) req.environ['CONTENT_TYPE'] = 'multipart/form-data; charset=windows-1251; boundary=BOUNDARY' return req.get_response(tapp) r = test(t1) assert r.text == text r = test(t2) assert r.text == 'file\n%r' % text.encode('cp1251') r = test(t3) assert r.text, "%s\n%r" % (text == b'foo') def test_transcode_query(): req = Request.blank('/?%EF%F0%E8=%E2%E5%F2') req2 = req.decode('cp1251') assert req2.query_string == '%D0%BF%D1%80%D0%B8=%D0%B2%D0%B5%D1%82' def test_transcode_non_multipart(): req = Request.blank('/?a', POST='%EF%F0%E8=%E2%E5%F2') req._content_type_raw = 'application/x-www-form-urlencoded' req2 = req.decode('cp1251') assert native_(req2.body) == '%D0%BF%D1%80%D0%B8=%D0%B2%D0%B5%D1%82' def test_transcode_non_form(): req = Request.blank('/?a', POST='%EF%F0%E8=%E2%E5%F2') req._content_type_raw = 'application/x-foo' req2 = req.decode('cp1251') assert native_(req2.body) == '%EF%F0%E8=%E2%E5%F2' def test_transcode_noop(): req = Request.blank('/') assert req.decode() is req def test_transcode_query_ascii(): t = Transcoder('ascii') assert t.transcode_query('a') == 'a' WebOb-1.8.6/tests/test_util.py0000644000076600000240000000631113611751037017227 0ustar xistencestaff00000000000000import unittest from webob.response import Response class Test_warn_deprecation(unittest.TestCase): def setUp(self): import warnings self.oldwarn = warnings.warn warnings.warn = self._warn self.warnings = [] def tearDown(self): import warnings warnings.warn = self.oldwarn del self.warnings def _callFUT(self, text, version, stacklevel): from webob.util import warn_deprecation return warn_deprecation(text, version, stacklevel) def _warn(self, text, type, stacklevel=1): self.warnings.append(locals()) def test_multidict_update_warning(self): # test warning when duplicate keys are passed r = Response() r.headers.update([ ('Set-Cookie', 'a=b'), ('Set-Cookie', 'x=y'), ]) self.assertEqual(len(self.warnings), 1) deprecation_warning = self.warnings[0] self.assertEqual(deprecation_warning['type'], UserWarning) assert 'Consider using .extend()' in deprecation_warning['text'] def test_multidict_update_warning_unnecessary(self): # no warning on normal operation r = Response() r.headers.update([('Set-Cookie', 'a=b')]) self.assertEqual(len(self.warnings), 0) def test_warn_deprecation(self): v = '1.3.0' from webob.util import warn_deprecation self.assertRaises(DeprecationWarning, warn_deprecation, 'foo', v[:3], 1) def test_warn_deprecation_future_version(self): v = '9.9.9' from webob.util import warn_deprecation warn_deprecation('foo', v[:3], 1) self.assertEqual(len(self.warnings), 1) class Test_strings_differ(unittest.TestCase): def _callFUT(self, *args, **kw): from webob.util import strings_differ return strings_differ(*args, **kw) def test_it(self): self.assertFalse(self._callFUT(b'foo', b'foo')) self.assertTrue(self._callFUT(b'123', b'345')) self.assertTrue(self._callFUT(b'1234', b'123')) self.assertTrue(self._callFUT(b'123', b'1234')) def test_it_with_internal_comparator(self): result = self._callFUT(b'foo', b'foo', compare_digest=None) self.assertFalse(result) result = self._callFUT(b'123', b'abc', compare_digest=None) self.assertTrue(result) def test_it_with_external_comparator(self): class DummyComparator(object): called = False def __init__(self, ret_val): self.ret_val = ret_val def __call__(self, a, b): self.called = True return self.ret_val dummy_compare = DummyComparator(True) result = self._callFUT(b'foo', b'foo', compare_digest=dummy_compare) self.assertTrue(dummy_compare.called) self.assertFalse(result) dummy_compare = DummyComparator(False) result = self._callFUT(b'123', b'345', compare_digest=dummy_compare) self.assertTrue(dummy_compare.called) self.assertTrue(result) dummy_compare = DummyComparator(False) result = self._callFUT(b'abc', b'abc', compare_digest=dummy_compare) self.assertTrue(dummy_compare.called) self.assertTrue(result) WebOb-1.8.6/tox.ini0000644000076600000240000000200313611751037015004 0ustar xistencestaff00000000000000[tox] envlist = py27,py34,py35,py36,py37,pypy, docs,coverage,pep8 skip_missing_interpreters = True [testenv] # Most of these are defaults but if you specify any you can't fall back # to defaults for others. basepython = py27: python2.7 py34: python3.4 py35: python3.5 py36: python3.6 py37: python3.7 pypy: pypy py2: python2.7 py3: python3.5 commands = pip install webob[testing] py.test tests --junitxml=pytest-{envname}.xml --cov --cov-report=term-missing {posargs:} setenv = COVERAGE_FILE=.coverage.{envname} [testenv:coverage] basepython = python3.6 commands = coverage combine coverage xml coverage report --show-missing --fail-under=100 deps = coverage setenv = COVERAGE_FILE=.coverage [testenv:docs] basepython = python3.6 whitelist_externals = make commands = pip install webob[docs] make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E" [testenv:pep8] basepython = python3.6 commands = flake8 src/webob/ deps = flake8