diff -Nru python-falcon-1.0.0/AUTHORS python-falcon-1.4.1/AUTHORS --- python-falcon-1.0.0/AUTHORS 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/AUTHORS 2018-08-08 23:08:36.000000000 +0000 @@ -1,7 +1,8 @@ -Kurt Griffiths (kgriffs) created Falcon and is its current maintainer. +Kurt Griffiths (kgriffs) created Falcon and currently co-maintains the +framework along with John Vrbanac (jmvrbanac). -Many thanks to all the mighty fine contributors to the project, listed -below in order of date of first contribution: +Many thanks to all of the project's stylish and talented contributors, +listed below by date of first contribution: * Alejandro Cabrera (cabrera) * Chad Lung (chadlung) @@ -38,7 +39,7 @@ * Rahman Syed (rsyed83) * Max Brauer (mamachanko) * Jen Montes (jenmontes) -* Carl George (cgtx) +* Carl George (carlwgeorge) * Lahache Stéphane (steffgrez) * John Vrbanac (jmvrbanac) * Steve McMaster (mcmasterathl) @@ -49,7 +50,6 @@ * Christian Pedersen (chripede) * Harrison Pincket (hpincket) * Usman Ehtesham Gul (ueg1990) -* Carl George (carlgeorge) * Adam Yala (adamyala) * Grigory Bakunov (bobuk) * Vincent Raiken (Freezerburn) @@ -65,3 +65,16 @@ * David Larlet (davidbgk) * Fran Fitzpatrick (fxfitz) * Matthew Miller (masterkale) +* michaelboulton +* Jesse Jarzynka (jessehudl) +* Michael Olund (molund) +* Yash Mehrotra (yashmehrotra) +* Stephen Milner (ashcrow) +* ksonj + +(et al.) + +For a full list of contributors, please visit: + + https://github.com/falconry/falcon/graphs/contributors + diff -Nru python-falcon-1.0.0/CHANGES.rst python-falcon-1.4.1/CHANGES.rst --- python-falcon-1.0.0/CHANGES.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/CHANGES.rst 2018-08-08 23:08:36.000000000 +0000 @@ -1,3 +1,346 @@ +1.4.1 +===== + +Breaking Changes +---------------- + +(None) + +Changes to Supported Platforms +------------------------------ + +(None) + +New & Improved +-------------- + +(None) + +Fixed +----- + +- Reverted the breaking change in 1.4.0 to ``falcon.testing.Result.json``. + Minor releases should have no breaking changes. +- The README was not rendering properly on PyPI. This was fixed and a validation + step was added to the build process. + +1.4.0 +===== + +Breaking Changes +---------------- + +- ``testing.Result.json`` now returns ``None`` when the response body is + empty, rather than raising an error. + +Changes to Supported Platforms +------------------------------ + +- Python 3 is now supported on PyPy as of PyPy3.5 v5.10. +- Support for CPython 3.3 is now deprecated and will be removed in + Falcon 2.0. +- As with the previous release, Python 2.6 and Jython 2.7 remain deprecated and + will no longer be supported in Falcon 2.0. + +New & Improved +-------------- + +- We added a new method, ``API.add_static_route()``, that makes it easy to + serve files from a local directory. This feature provides an alternative to + serving files from the web server when you don't have that option, when + authorization is required, or for testing purposes. +- Arguments can now be passed to hooks. +- The default JSON media type handler will now use + ``ujson``, if available, to speed up JSON (de)serialization under CPython. +- Semantic validation via the *format* keyword is now enabled for the + ``falcon.media.validators.jsonschema.validate()`` JSON Schema decorator. +- We added a new helper, ``falcon.Request.get_param_as_uuid()``, to the + ``Request`` class. +- We added a new property, ``downloadable_as``, to the + ``Response`` class for setting the Content-Disposition header. +- Falcon now supports WebDAV methods (RFC 3253), such as UPDATE and REPORT. +- ``falcon.routing.create_http_method_map`` has been refactored into two + new methods, ``falcon.routing.map_http_methods`` and + ``falcon.routing.set_default_responders``, so that + custom routers can better pick and choose the functionality they need. The + original method is still available for backwards-compatibility, but will + be removed in a future release. +- We added a new `json` param to ``falcon.testing.simulate_request()`` et al. + to automatically serialize the request body from a JSON serializable object + or type (for a complete list of serializable types, see + `json.JSONEncoder `_). +- ``TestClient``'s ``simulate_*()`` methods now call + ``TestClient.simulate_request`` to make it easier for subclasses to + override ``TestClient``'s behavior. +- ``TestClient`` can now be configured with a default set of headers to + send with every request. +- The FAQ has been reorganized and greatly expanded. +- We restyled the docs to match https://falconframework.org + +Fixed +----- + +- Forwarded headers containing quoted strings with commas were not being parsed + correctly. This has been fixed, and the parser generally made more robust. +- ``falcon.media.JSONHandler`` was raising an error under Python 2.x when + serializing strings containing Unicode code points. This issue has been + fixed. +- Overriding a resource class and calling its responders via ``super()`` did + not work when passing URI template params as positional arguments. This has + now been fixed. +- Python 3.6 was generating warnings for strings containing ``'\s'`` within + Falcon. These strings have been converted to raw strings to mitigate the + warning. +- Several syntax errors were found and fixed in the code examples used in the + docs. + +Contributors to this Release +---------------------------- + +Many thanks to all of our talented and stylish contributors to this release! + +- GriffGeorge +- hynek +- kgriffs +- rhemz +- santeyio +- timc13 +- tyronegroves +- vytas7 +- zhanghanyun + +1.3.0 +===== + +Breaking Changes +---------------- + +(None) + +Changes to Supported Platforms +------------------------------ + +- CPython 3.6 is now fully supported. +- Falcon appears to work well on PyPy3.5, but we are waiting until + that platform is out of beta before officially supporting it. +- Support for both CPython 2.6 and Jython 2.7 is now deprecated and + will be discontinued in Falcon 2.0. + +New & Improved +-------------- + +- We added built-in resource representation serialization and + deserialization, including input validation based on JSON Schema. +- URI template field converters are now supported. We expect to expand + this feature over time. +- A new method, `get_param_as_datetime()`, was added to + the ``Request`` class. +- A number of attributes were added to the ``Request`` class to + make proxy information easier to consume. These include the + `forwarded`, `forwarded_uri`, `forwarded_scheme`, `forwarded_host`, + and `forwarded_prefix` attributes. The `prefix` attribute was also + added as part of this work. +- A `referer` attribute was added to the ``Request`` class. +- We implemented `__repr__()` for ``Request``, ``Response``, and + ``HTTPError`` to aid in debugging. +- A number of Internet media type constants were defined to make it + easier to check and set content type headers. +- Several new 5xx error classes were implemented. + + +Fixed +----- + +- If even a single cookie in the request to the server is malformed, + none of the cookies will be parsed (all-or-nothing). Change the + parser to simply skip bad cookies (best-effort). +- ``API`` instances are not pickleable. Modify the default router + to fix this. + +1.2.0 +===== + +Breaking Changes +---------------- + +(None) + +New & Improved +-------------- + +- A new `default` kwarg was added to ``falcon.Request.get_header``. +- A `delete_header()` method was added to ``falcon.Response``. +- Several new HTTP status codes and error classes were added, such as + ``falcon.HTTPFailedDependency``. +- If `ujson` is installed it will be used in lieu of `json` to speed up + error serialization and query string parsing under CPython. PyPy users + should continue to use `json`. +- The `independent_middleware` kwarg was added to ``falcon.API`` to + enable the execution of `process_response()` middleware methods, even + when `process_request()` raises an error. +- Single-character field names are now allowed in URL templates when + specifying a route. +- A detailed error message is now returned when an attempt is made to + add a route that conflicts with one that has already been added. +- The HTTP protocol version can now be specified when simulating + requests with the testing framework. +- The ``falcon.ResponseOptions`` class was added, along with a + `secure_cookies_by_default` option to control the default value of + the "secure" attribute when setting cookies. This can make testing + easier by providing a way to toggle whether or not HTTPS is required. +- `port`, `netloc` and `scheme` properties were added to the + ``falcon.Request`` class. The `protocol` property is now + deprecated and will be removed in a future release. +- The `strip_url_path_trailing_slash` was added + to ``falcon.RequestOptions`` to control whether or not to retain + the trailing slash in the URL path, if one is present. When this + option is enabled (the default), the URL path is normalized by + stripping the trailing slash character. This lets the application + define a single route to a resource for a path that may or may not end + in a forward slash. However, this behavior can be problematic in + certain cases, such as when working with authentication schemes that + employ URL-based signatures. Therefore, the + `strip_url_path_trailing_slash` option was introduced to make this + behavior configurable. +- Improved the documentation for ``falcon.HTTPError``, particularly + around customizing error serialization. +- Misc. improvements to the look and feel of Falcon's documentation. +- The tutorial in the docs was revamped, and now includes guidance on + testing Falcon applications. + +Fixed +----- + +- Certain non-alphanumeric characters, such as parenthesis, are not + handled properly in complex URI template path segments that are + comprised of both literal text and field definitions. +- When the WSGI server does not provide a `wsgi.file_wrapper` object, + Falcon wraps ``Response.stream`` in a simple iterator + object that does not implement `close()`. The iterator should be + modified to implement a `close()` method that calls the underlying + stream's `close()` to free system resources. +- The testing framework does not correctly parse cookies under Jython. +- Whitespace is not stripped when parsing cookies in the testing + framework. +- The Vary header is not always set by the default error serializer. +- While not specified in PEP-3333 that the status returned to the WSGI + server must be of type `str`, setting the status on the response to a + `unicode` string under Python 2.6 or 2.7 can cause WSGI servers to + raise an error. Therefore, the status string must first be converted + if it is of the wrong type. +- The default OPTIONS responder returns 204, when it should return + 200. RFC 7231 specifically states that Content-Length should be zero + in the response to an OPTIONS request, which implies a status code of + 200 since RFC 7230 states that Content-Length must not be set in any + response with a status code of 204. + +1.1.0 +===== + +Breaking Changes +---------------- + +(None) + +New & Improved +-------------- + +- A new `bounded_stream` property was added to ``falcon.Request`` + that can be used in place of the `stream` property to mitigate + the blocking behavior of input objects used by some WSGI servers. +- A new `uri_template` property was added to ``falcon.Request`` + to expose the template for the route corresponding to the + path requested by the user agent. +- A `context` property was added to ``falcon.Response`` to mirror + the same property that is already available for + ``falcon.Request``. +- JSON-encoded query parameter values can now be retrieved and decoded + in a single step via ``falcon.Request.get_param_as_dict()``. +- CSV-style parsing of query parameter values can now be disabled. +- ``falcon.Request.get_param_as_bool()`` now recognizes "on" and + "off" in support of IE's default checkbox values. +- An `accept_ranges` property was added to ``falcon.Response`` to + facilitate setting the Accept-Ranges header. +- Added the ``falcon.HTTPUriTooLong`` and + ``falcon.HTTPGone`` error classes. +- When a title is not specified for ``falcon.HTTPError``, it now + defaults to the HTTP status text. +- All parameters are now optional for most error classes. +- Cookie-related documentation has been clarified and expanded +- The ``falcon.testing.Cookie`` class was added to represent a + cookie returned by a simulated request. ``falcon.testing.Result`` + now exposes a `cookies` attribute for examining returned cookies. +- pytest support was added to Falcon's testing framework. Apps can now + choose to either write unittest- or pytest-style tests. +- The test runner for Falcon's own tests was switched from nose + to pytest. +- When simulating a request using Falcon's testing framework, query + string parameters can now be specified as a ``dict``, as + an alternative to passing a raw query string. +- A flag is now passed to the `process_request` middleware method to + signal whether or not an exception was raised while processing the + request. A shim was added to avoid breaking existing middleware + methods that do not yet accept this new parameter. +- A new CLI utility, `falcon-print-routes`, was added that takes in a + module:callable, introspects the routes, and prints the + results to stdout. This utility is automatically installed along + with the framework:: + + $ falcon-print-routes commissaire:api + -> /api/v0/status + -> /api/v0/cluster/{name} + -> /api/v0/cluster/{name}/hosts + -> /api/v0/cluster/{name}/hosts/{address} + +- Custom attributes can now be attached to instances of + ``falcon.Request`` and ``falcon.Response``. This can be + used as an alternative to adding values to the `context` property, + or implementing custom subclasses. +- ``falcon.get_http_status()`` was implemented to provide a way to + look up a full HTTP status line, given just a status code. + +Fixed +----- + +- When ``auto_parse_form_urlencoded`` is set to ``True``, the + framework now checks the HTTP method before attempting to consume and + parse the body. +- Before attempting to read the body of a form-encoded request, the + framework now checks the Content-Length header to ensure that a + non-empty body is expected. This helps prevent bad requests from + causing a blocking read when running behind certain WSGI servers. +- When the requested method is not implemented for the target resource, + the framework now raises ``falcon.HTTPMethodNotAllowed``, rather + than modifying the ``falcon.Request`` object directly. This + improves visibility for custom error handlers and for middleware + methods. +- Error class docstrings have been updated to reflect the latest RFCs. +- When an error is raised by a resource method or a hook, the error + will now always be processed (including setting the appropriate + properties of the ``falcon.Response`` object) before middleware + methods are called. +- A case was fixed in which middleware processing did not + continue when an instance of ``falcon.HTTPError`` or + ``falcon.HTTPStatus`` was raised. +- The ``falcon.uri.encode()`` method will now attempt to detect + whether the specified string has already been encoded, and return + it unchanged if that is the case. +- The default OPTIONS responder now explicitly sets Content-Length + to zero in the response. +- ``falcon.testing.Result`` now assumes that the response body + is encoded as UTF-8 when the character set is not specified, rather + than raising an error when attempting to decode the response body. +- When simulating requests, Falcon's testing framework now properly + tunnels Unicode characters through the WSGI interface. +- ``import falcon.uri`` now works, in addition to + ``from falcon import uri``. +- URI template fields are now validated up front, when the route is + added, to ensure they are valid Python identifiers. This prevents + cryptic errors from being raised later on when requests are routed. +- When running under Python 3, ``inspect.signature()`` is used + instead of ``inspect.getargspec()`` to provide compatibility with + annotated functions. + 1.0.0 ===== diff -Nru python-falcon-1.0.0/CODEOFCONDUCT.md python-falcon-1.4.1/CODEOFCONDUCT.md --- python-falcon-1.0.0/CODEOFCONDUCT.md 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/CODEOFCONDUCT.md 2018-08-08 23:08:36.000000000 +0000 @@ -2,7 +2,7 @@ All contributors and maintainers of this project are subject to this Code of Conduct. -We pledge to respect everyone who contributes to the *Falcon* project or other associated activities by (including but not limited to) creating project issues, submitting pull requests, and providing feedback on the same. We also pledge to respect everyone who participates in discussions on the project's mailing list, in the project's chat channel, and at meetups and conferences. +We pledge to respect everyone who contributes to the *Falcon* project or other associated activities by (including but not limited to) creating project issues, submitting pull requests, and providing feedback on the same. We also pledge to respect everyone who participates in discussions both online and at meetups and conferences. Unacceptable behavior includes (but is not limited to) offensive verbal comments related to gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion, technology choices, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention. diff -Nru python-falcon-1.0.0/CONTRIBUTING.md python-falcon-1.4.1/CONTRIBUTING.md --- python-falcon-1.0.0/CONTRIBUTING.md 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/CONTRIBUTING.md 2018-08-08 23:08:36.000000000 +0000 @@ -1,54 +1,136 @@ -## Contributing +## Contributer's Guide -Hi, thanks for your interest in the project! We welcome pull requests from developers of all skill levels. +Thanks for your interest in the project! We welcome pull requests from +developers of all skill levels. To get started, simply fork the master branch +on GitHub to your personal account and then clone the fork into your +development environment. -Kurt Griffiths (kgriffs) is the creator and current maintainer of the Falcon framework, along with a group of talented and stylish volunteers. Please don't hesitate to reach out if you have any questions, or just need a little help getting started. +If you would like to contribute but don't already have something in mind, +we invite you to take a look at the issues listed under our [next milestone][ms]. +If you see one you'd like to work on, please leave a quick comment so that we don't +end up with duplicated effort. Thanks in advance! -Before submitting a pull request, please ensure you have added or updated tests as appropriate, and that all existing tests still pass with your changes on both Python 2 and Python 3. Please also ensure that your coding style follows PEP 8 and doesn't cause pyflakes to complain. +Kurt Griffiths (**kgriffs** on GH, Gitter, and Twitter) is the original +creator of the Falcon framework, and currently co-maintains the project +along with John Vrbanac (**jmvrbanac** on GH and Gitter, and +**jvrbanac** on Twitter). Falcon is developed by a growing community of +users and contributors just like you. -You can check all this by running the following from within the falcon project directory (requires Python 2.7 and Python 3.3 to be installed on your system): +Please don't hesitate to reach out if you have any questions, or just need a +little help getting started. You can find us in +[falconry/dev][gitter] on Gitter. + +Please note that all contributors and maintainers of this project are subject to our [Code of Conduct][coc]. + +### Pull Requests + +Before submitting a pull request, please ensure you have added or updated tests as appropriate, and that all existing tests still pass with your changes on both Python 2 and Python 3. Please also ensure that your coding style follows PEP 8. + +You can check all this by running the following from within the Falcon project directory (requires Python 2.7 and Python 3.6 to be installed on your system): + +```bash +$ pip install tox +$ tox -e py27,py36,pep8 +``` + +You may also use Python 3.3, 3.4 or 3.5 if you don't have 3.6 installed on your system. This is just a quick sanity check to verify that your patch works across both Python 2 and Python 3. + +If you are using pyenv and get an error along the lines of "failed to get version_info", you will need to activate all the Python versions required by tox before trying again. For example: ```bash -$ tox -e py27,py33,pep8 +$ pyenv shell 2.7.13 3.6.2 ``` ### Running tests against Jython In addition to the tests run with tox against CPython, Cython, and PyPy, Travis runs tests against Jython 2.7 outside of tox. If you need to run these tests locally, do the following: -* Install JDK 7 or better -* run `travis_scripts/install_jython2.7.sh` -- this will install jython at `~/jython` -* Install testing requirements `~/jython/bin/pip install -r tools/test-requires` - * May need to set `export JYTHON_HOME=~/jython` first -* Run tests `~/jython/bin/nosetests` -Note: coverage does not support Jython, so the coverage tests will fail. +First, install JDK 7 or better. Then install Jython at `~/jython`: + +```bash +$ tools/travis/install_jython2.7.sh +``` + +Now install all testing dependencies. If you get an error, you may need to `export JYTHON_HOME=~/jython`. Also note that *pytest-xdist* and *pytest-cov* are not compatible with Jython, and therefore must be removed: + +```bash +$ ~/jython/bin/pip install -r requirements/tests +$ ~/jython/bin/pip uninstall -y pytest-xdist pytest-cov +``` + +Finally, run the tests: + +```bash +$ ~/jython/bin/pytest tests +``` ### Test coverage Pull requests must maintain 100% test coverage of all code branches. This helps ensure the quality of the Falcon framework. To check coverage before submitting a pull request: ```bash -$ tox -e py26,py27,py34 && tools/combine_coverage.sh +$ tools/mintest.sh +``` + +It is necessary to combine test coverage from multiple environments in order to account for branches in the code that are only taken for a given Python version. + +The script generates an HTML coverage report that can be viewed by simply opening `.coverage_html/index.html` in a browser. This can be helpful in tracking down specific lines or branches that are missing coverage. + +### Debugging + +We use pytest to run all of our tests. Pytest supports pdb and will break as expected on any +`pdb.set_trace()` calls. If you would like to use pdb++ instead of the standard Python +debugger, run one of the following tox environments: + +```bash +$ tox -e py2_debug +$ tox -e py3_debug +``` + +If you wish, you can customize Falcon's `tox.ini` to install alternative debuggers, such as ipdb or pudb. + +### Benchmarking + +A few simple benchmarks are included with the source under ``falcon/bench``. These can be taken as a rough measure of the performance impact (if any) that your changes have on the framework. You can run these tests by invoking one of the tox environments included for this purpose (see also the ``tox.ini`` file). For example: + +```bash +$ tox -e py27_bench +``` + +Note that you may pass additional arguments via tox to the falcon-bench command: + +```bash +$ tox -e py27_bench -- -h +$ tox -e py27_bench -- -b falcon -i 20000 +``` + +Alternatively, you may run falcon-bench directly by creating a new virtual environment and installing falcon directly in development mode. In this example we use pyenv with pyenv-virtualenv from within a falcon source directory: + +```bash +$ pyenv virtualenv 3.6.2 falcon-sandbox-36 +$ pyenv shell falcon-sandbox-36 +$ pip install -r requirements/bench +$ pip install -e . +$ falcon-bench ``` -This generates an HTML coverage report that can be viewed by simply opening `.coverage_html/index.html` in a browser. +Note that benchmark results for the same code will vary between runs based on a number of factors, including overall system load and CPU scheduling. These factors may be somewhat mitigated by running the benchmarks on a Linux server dedicated to this purpose, and pinning the benchmark process to a specific CPU core. ### Documentation -To check documentation changes (including docstrings), before submitting a PR, ensure the tox job -builds the documentation correctly: +To check documentation changes (including docstrings), before submitting a PR, ensure the tox job builds the documentation correctly: ```bash $ tox -e docs # OS X -$ open doc/_build/html/index.html +$ open docs/_build/html/index.html # Gnome -$ gnome-open doc/_build/html/index.html +$ gnome-open docs/_build/html/index.html # Generic X Windows -$ xdg-open doc/_build/html/index.html +$ xdg-open docs/_build/html/index.html ``` ### Code style rules @@ -122,6 +204,12 @@ The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**. [ajs]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit -[docstrings]: http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html#example-google-style-python-docstrings +[docstrings]: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html#example-google-style-python-docstrings [goog-style]: http://google-styleguide.googlecode.com/svn/trunk/pyguide.html#Comments -[rtd]: http://falcon.readthedocs.org +[rtd]: https://falcon.readthedocs.io +[coc]: https://github.com/falconry/falcon/blob/master/CODEOFCONDUCT.md +[freenode]: https://www.freenode.net/ +[gitter]: https://gitter.im/falconry/dev +[ml-join]: mailto:users-join@mail.falconframework.org?subject=join +[ml-archive]: https://mail.falconframework.org/archives/list/users@mail.falconframework.org/ +[ms]: https://github.com/falconry/falcon/milestones diff -Nru python-falcon-1.0.0/.coveragerc python-falcon-1.4.1/.coveragerc --- python-falcon-1.0.0/.coveragerc 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/.coveragerc 2018-08-08 23:08:36.000000000 +0000 @@ -1,5 +1,9 @@ [run] -parallel = True branch = True source = falcon omit = falcon/tests*,falcon/cmd*,falcon/bench* + +parallel = True + +[report] +show_missing = True diff -Nru python-falcon-1.0.0/debian/changelog python-falcon-1.4.1/debian/changelog --- python-falcon-1.0.0/debian/changelog 2018-11-03 12:08:51.000000000 +0000 +++ python-falcon-1.4.1/debian/changelog 2019-07-19 18:22:48.000000000 +0000 @@ -1,32 +1,20 @@ -python-falcon (1.0.0-2build5) disco; urgency=medium +python-falcon (1.4.1-1) unstable; urgency=medium - * No-change rebuild to build without python3.6 support. + [ Thomas Goirand ] + * d/control: Set Vcs-* to salsa.debian.org. + * Running wrap-and-sort -bast. + * Updating maintainer field. + * Updating copyright format url. + * Fixed new upstream VCS URL. + * New upstream release (Closes: #930134). + * Remove Python 2 support. + * Fixed (build-)depends for this release. + * Standards-Version is now 4.4.0. - -- Matthias Klose Sat, 03 Nov 2018 12:08:51 +0000 + [ Ondřej Nový ] + * Use debhelper-compat instead of debian/compat. -python-falcon (1.0.0-2build4) cosmic; urgency=medium - - * No change rebuild to add support for Python 3.7. - - -- Michael Hudson-Doyle Fri, 10 Aug 2018 14:57:50 +1200 - -python-falcon (1.0.0-2build3) artful; urgency=medium - - * No-change rebuild to drop _PyFPE support. - - -- Matthias Klose Tue, 05 Sep 2017 08:23:04 +0000 - -python-falcon (1.0.0-2build2) artful; urgency=medium - - * No-change rebuild to build to drop python3.5. - - -- Matthias Klose Sat, 05 Aug 2017 18:38:11 +0000 - -python-falcon (1.0.0-2build1) artful; urgency=medium - - * No change rebuild to add Python 3.6 support. - - -- Michael Hudson-Doyle Tue, 16 May 2017 20:19:04 +1200 + -- Thomas Goirand Fri, 19 Jul 2019 20:22:48 +0200 python-falcon (1.0.0-2) unstable; urgency=medium diff -Nru python-falcon-1.0.0/debian/compat python-falcon-1.4.1/debian/compat --- python-falcon-1.0.0/debian/compat 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -9 diff -Nru python-falcon-1.0.0/debian/control python-falcon-1.4.1/debian/control --- python-falcon-1.0.0/debian/control 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/control 2019-07-19 18:22:48.000000000 +0000 @@ -1,79 +1,44 @@ Source: python-falcon Section: python Priority: optional -Maintainer: PKG OpenStack -Uploaders: Thomas Goirand , -Build-Depends: cython, - cython3, - debhelper (>= 9), - dh-python, - lsb-release, - openstack-pkg-tools (>= 52~), - python-all, - python-all-dev, - python-ddt, - python-mimeparse, - python-nose, - python-requests, - python-setuptools, - python-six, - python-testtools, - python-yaml, - python3-all, - python3-all-dev, - python3-ddt, - python3-mimeparse, - python3-nose, - python3-requests, - python3-setuptools, - python3-six, - python3-testtools, - python3-yaml, -Standards-Version: 3.9.8 -Vcs-Browser: https://git.openstack.org/cgit/openstack/deb-python-falcon?h=debian%2Fnewton -Vcs-Git: https://git.openstack.org/openstack/deb-python-falcon -b debian/newton +Maintainer: Debian OpenStack +Uploaders: + Thomas Goirand , +Build-Depends: + cython, + cython3, + debhelper-compat (= 11), + dh-python, + lsb-release, + openstack-pkg-tools (>= 99~), + python3-all, + python3-all-dev, + python3-ddt, + python3-jsonschema, + python3-mimeparse (>= 1.5.2), + python3-msgpack, + python3-nose, + python3-pytest, + python3-requests, + python3-setuptools, + python3-six, + python3-testtools, + python3-yaml, +Standards-Version: 4.4.0 +Vcs-Browser: https://salsa.debian.org/openstack-team/python/python-falcon +Vcs-Git: https://salsa.debian.org/openstack-team/python/python-falcon.git Homepage: http://falconframework.org -Package: python-falcon -Architecture: any -Depends: python-mimeparse, - python-six, - ${misc:Depends}, - ${python:Depends}, - ${shlibs:Depends}, -Recommends: ${python:Recommends}, -Description: supersonic micro-framework for building cloud APIs - Python 2.x - Falcon is a high-performance Python framework for building cloud APIs. It - encourages the REST architectural style, and tries to do as little as possible - while remaining highly effective. - . - Unlike other Python web frameworks, Falcon won't bottleneck your API's - performance under highly concurrent workloads. Many frameworks max out at - serving simple "hello world" requests at a few thousand req/sec, while Falcon - can easily serve many more on the same hardware. - . - Falcon isn't very opinionated. In other words, the framework leaves a lot of - decisions and implementation details to you. - . - Features: - * Intuitive routing via URI templates and resource classes - * Easy access to headers and bodies through request and response classes - * Idiomatic HTTP error responses via a handy exception base class - * DRY request processing using global, resource, and method hooks - * Snappy unit testing through WSGI helpers and mocks - * 20% speed boost when Cython is available - * Python 2.6, Python 2.7, PyPy and Python 3.3 support - . - This package provides the Python 2.x module. - Package: python3-falcon Architecture: any -Depends: python3-mimeparse, - python3-six, - ${misc:Depends}, - ${python3:Depends}, - ${shlibs:Depends}, -Recommends: ${python3:Recommends}, +Depends: + python3-jsonschema, + python3-mimeparse (>= 1.5.2), + python3-msgpack, + python3-six, + ${misc:Depends}, + ${python3:Depends}, + ${shlibs:Depends}, Description: supersonic micro-framework for building cloud APIs - Python 3.x Falcon is a high-performance Python framework for building cloud APIs. It encourages the REST architectural style, and tries to do as little as possible diff -Nru python-falcon-1.0.0/debian/copyright python-falcon-1.4.1/debian/copyright --- python-falcon-1.0.0/debian/copyright 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/copyright 2019-07-19 18:22:48.000000000 +0000 @@ -1,7 +1,7 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: falcon Upstream-Contact: Kurt Griffiths -Source: https://github.com/racker/falcon +Source: https://github.com/falconry/falcon Files: * Copyright: (c) 2013, Kurt Griffiths diff -Nru python-falcon-1.0.0/debian/gbp.conf python-falcon-1.4.1/debian/gbp.conf --- python-falcon-1.0.0/debian/gbp.conf 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -[DEFAULT] -upstream-branch = master -debian-branch = debian/newton -upstream-tag = %(version)s -compression = xz - -[buildpackage] -export-dir = ../build-area/ - diff -Nru python-falcon-1.0.0/debian/patches/fix-non-ascii-in-doc.patch python-falcon-1.4.1/debian/patches/fix-non-ascii-in-doc.patch --- python-falcon-1.0.0/debian/patches/fix-non-ascii-in-doc.patch 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/patches/fix-non-ascii-in-doc.patch 2019-07-19 18:22:48.000000000 +0000 @@ -7,12 +7,12 @@ =================================================================== --- python-falcon.orig/README.rst +++ python-falcon/README.rst -@@ -4,7 +4,7 @@ Falcon |Docs| |Build Status| |codecov.io +@@ -6,7 +6,7 @@ Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away. - *- Antoine de Saint-Exupéry* + *- Antoine de Saint-Exupery* - Falcon is a `high-performance Python - framework `__ for building cloud + `Falcon `__ is a reliable, + high-performance Python web framework for building diff -Nru python-falcon-1.0.0/debian/patches/fix-spelling.patch python-falcon-1.4.1/debian/patches/fix-spelling.patch --- python-falcon-1.0.0/debian/patches/fix-spelling.patch 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/patches/fix-spelling.patch 2019-07-19 18:22:48.000000000 +0000 @@ -7,7 +7,7 @@ =================================================================== --- python-falcon.orig/falcon/request.py +++ python-falcon/falcon/request.py -@@ -189,7 +189,7 @@ class Request(object): +@@ -350,7 +350,7 @@ class Request(object): where -1 is the last byte, -2 is the second-to-last byte, and so forth. diff -Nru python-falcon-1.0.0/debian/patches/series python-falcon-1.4.1/debian/patches/series --- python-falcon-1.0.0/debian/patches/series 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/patches/series 2019-07-19 18:22:48.000000000 +0000 @@ -1,3 +1,3 @@ fix-non-ascii-in-doc.patch fix-spelling.patch -disable-failing-test.patch +#disable-failing-test.patch diff -Nru python-falcon-1.0.0/debian/python3-falcon.install python-falcon-1.4.1/debian/python3-falcon.install --- python-falcon-1.0.0/debian/python3-falcon.install 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/debian/python3-falcon.install 2019-07-19 18:22:48.000000000 +0000 @@ -0,0 +1 @@ +/usr/* \ No newline at end of file diff -Nru python-falcon-1.0.0/debian/python3-falcon.postinst python-falcon-1.4.1/debian/python3-falcon.postinst --- python-falcon-1.0.0/debian/python3-falcon.postinst 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/python3-falcon.postinst 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$1" = "configure" ] ; then - update-alternatives --install /usr/bin/falcon-bench falcon-bench /usr/bin/python3-falcon-bench 200 -fi - -#DEBHELPER# - -exit 0 diff -Nru python-falcon-1.0.0/debian/python-falcon.postinst python-falcon-1.4.1/debian/python-falcon.postinst --- python-falcon-1.0.0/debian/python-falcon.postinst 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/python-falcon.postinst 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$1" = "configure" ] ; then - update-alternatives --install /usr/bin/falcon-bench falcon-bench /usr/bin/python2-falcon-bench 300 -fi - -#DEBHELPER# - -exit 0 diff -Nru python-falcon-1.0.0/debian/python-falcon.postrm python-falcon-1.4.1/debian/python-falcon.postrm --- python-falcon-1.0.0/debian/python-falcon.postrm 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/python-falcon.postrm 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$1" = "remove" ] || [ "$1" = "disappear" ] ; then - update-alternatives --remove falcon-bench /usr/bin/python2-falcon-bench -fi - -#DEBHELPER# - -exit 0 diff -Nru python-falcon-1.0.0/debian/python-falcon.prerm python-falcon-1.4.1/debian/python-falcon.prerm --- python-falcon-1.0.0/debian/python-falcon.prerm 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/python-falcon.prerm 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/bin/sh - -set -e - -if [ "$1" = "remove" ] ; then - update-alternatives --remove falcon-bench /usr/bin/python2-falcon-bench -fi - -#DEBHELPER# - -exit 0 diff -Nru python-falcon-1.0.0/debian/rules python-falcon-1.4.1/debian/rules --- python-falcon-1.0.0/debian/rules 2016-10-07 17:21:25.000000000 +0000 +++ python-falcon-1.4.1/debian/rules 2019-07-19 18:22:48.000000000 +0000 @@ -2,26 +2,33 @@ DEB_RELEASE_NAME=$(shell lsb_release --codename | awk '{print $$2}') -UPSTREAM_GIT := https://github.com/racker/falcon.git +UPSTREAM_GIT := https://github.com/falconry/falcon include /usr/share/openstack-pkg-tools/pkgos.make export DEB_BUILD_MAINT_OPTIONS = hardening=+bindnow %: - dh $@ --buildsystem=python_distutils --with python2,python3 + dh $@ --buildsystem=python_distutils --with python3 -override_dh_auto_install: - pkgos-dh_auto_install - rm -rf $(CURDIR)/debian/python*-falcon/usr/lib/python*/dist-packages/tests +override_dh_auto_clean: + find . -iname '*.pyc' -delete + for i in $$(find . -type d -iname __pycache__) ; do rm -rf $$i ; done override_dh_auto_test: + echo "Do nothing..." + +override_dh_auto_build: + echo "Do nothing..." + +override_dh_auto_install: + pkgos-dh_auto_install --no-py2 --in-tmp + ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) - nosetests - nosetests3 + for i in $(PYTHON3S) ; do \ + PYTHONPATH=$(CURDIR)/debian/tmp/usr/lib/python3/dist-packages PYTHON=python$$i python$$i -m pytest tests ; \ + done endif - -override_dh_python2: - dh_python2 --shebang=/usr/bin/python + rm -rf $(CURDIR)/debian/tmp/usr/lib/python*/dist-packages/tests override_dh_python3: dh_python3 --shebang=/usr/bin/python3 diff -Nru python-falcon-1.0.0/doc/api/api.rst python-falcon-1.4.1/doc/api/api.rst --- python-falcon-1.0.0/doc/api/api.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/api.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -.. _api: - -API Class -========= - -Falcon's API class is a WSGI "application" that you can host with any -standard-compliant WSGI server. - -.. code:: python - - import falcon - - api = application = falcon.API() - -.. autoclass:: falcon.API - :members: - -.. autoclass:: falcon.RequestOptions - :members: - - diff -Nru python-falcon-1.0.0/doc/api/cookies.rst python-falcon-1.4.1/doc/api/cookies.rst --- python-falcon-1.0.0/doc/api/cookies.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/cookies.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -.. _cookies: - -Cookies -------- - -Cookie support is available in Falcon version 0.3 or later. - -.. _getting-cookies: - -Getting Cookies -~~~~~~~~~~~~~~~ - -Cookies can be read from a request via the :py:attr:`~.Request.cookies` -request attribute: - -.. code:: python - - class Resource(object): - def on_get(self, req, resp): - - cookies = req.cookies - - if 'my_cookie' in cookies: - my_cookie_value = cookies['my_cookie'] - # .... - -The :py:attr:`~.Request.cookies` attribute is a regular -:py:class:`dict` object. - -.. tip :: - - The :py:attr:`~.Request.cookies` attribute returns a - copy of the response cookie dictionary. Assign it to a variable, as - shown in the above example, to improve performance when you need to - look up more than one cookie. - -.. _setting-cookies: - -Setting Cookies -~~~~~~~~~~~~~~~ - -Setting cookies on a response is done via :py:meth:`~.Response.set_cookie`. - -The :py:meth:`~.Response.set_cookie` method should be used instead of -:py:meth:`~.Response.set_header` or :py:meth:`~.Response.append_header`. -With :py:meth:`~.Response.set_header` you cannot set multiple headers -with the same name (which is how multiple cookies are sent to the client). -Furthermore, :py:meth:`~.Response.append_header` appends multiple values -to the same header field in a way that is not compatible with the special -format required by the `Set-Cookie` header. - -Simple example: - -.. code:: python - - class Resource(object): - def on_get(self, req, resp): - - # Set the cookie 'my_cookie' to the value 'my cookie value' - resp.set_cookie('my_cookie', 'my cookie value') - - -You can of course also set the domain, path and lifetime of the cookie. - -.. code:: python - - class Resource(object): - def on_get(self, req, resp): - # Set the maximum age of the cookie to 10 minutes (600 seconds) - # and the cookie's domain to 'example.com' - resp.set_cookie('my_cookie', 'my cookie value', - max_age=600, domain='example.com') - - -You can also instruct the client to remove a cookie with the -:py:meth:`~.Response.unset_cookie` method: - -.. code:: python - - class Resource(object): - def on_get(self, req, resp): - resp.set_cookie('bad_cookie', ':(') - - # Clear the bad cookie - resp.unset_cookie('bad_cookie') - -.. _cookie-secure-attribute: - -The Secure Attribute -~~~~~~~~~~~~~~~~~~~~ - -By default, Falcon sets the `secure` attribute for cookies. This -instructs the client to never transmit the cookie in the clear over -HTTP, in order to protect any sensitive data that cookie might -contain. If a cookie is set, and a subsequent request is made over -HTTP (rather than HTTPS), the client will not include that cookie in -the request. - -.. warning:: - - For this attribute to be effective, your application will need to - enforce HTTPS when setting the cookie, as well as in all - subsequent requests that require the cookie to be sent back from - the client. - -When running your application in a development environment, you can -disable this behavior by passing `secure=False` to -:py:meth:`~.Response.set_cookie`. This lets you test your app locally -without having to set up TLS. You can make this option configurable to -easily switch between development and production environments. - -See also: `RFC 6265, Section 4.1.2.5`_ - -.. _RFC 6265, Section 4.1.2.5: - https://tools.ietf.org/html/rfc6265#section-4.1.2.5 diff -Nru python-falcon-1.0.0/doc/api/errors.rst python-falcon-1.4.1/doc/api/errors.rst --- python-falcon-1.0.0/doc/api/errors.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/errors.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -.. _errors: - -Error Handling -============== - -When a request results in an error condition, you *could* manually set the -error status, appropriate response headers, and even an error body using the -``resp`` object. However, Falcon tries to make things a bit easier and more -consistent by providing a set of error classes you can raise from within -your app. Falcon catches any exception that inherits from -``falcon.HTTPError``, and automatically converts it to an appropriate HTTP -response. - -You may raise an instance of ``falcon.HTTPError`` directly, or use any one -of a number of predefined error classes that try to be idiomatic in -setting appropriate headers and bodies. - -Base Class ----------- - -.. autoclass:: falcon.HTTPError - :members: - -Mixins ------- - -.. autoclass:: falcon.http_error.NoRepresentation - :members: - -Predefined Errors ------------------ - -.. automodule:: falcon - :members: HTTPInvalidHeader, HTTPMissingHeader, - HTTPInvalidParam, HTTPMissingParam, - HTTPBadRequest, HTTPUnauthorized, HTTPForbidden, HTTPNotFound, - HTTPMethodNotAllowed, HTTPNotAcceptable, HTTPConflict, - HTTPLengthRequired, HTTPPreconditionFailed, HTTPUnsupportedMediaType, - HTTPRangeNotSatisfiable, HTTPUnprocessableEntity, HTTPTooManyRequests, - HTTPUnavailableForLegalReasons, HTTPInternalServerError, - HTTPBadGateway, HTTPServiceUnavailable diff -Nru python-falcon-1.0.0/doc/api/hooks.rst python-falcon-1.4.1/doc/api/hooks.rst --- python-falcon-1.0.0/doc/api/hooks.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/hooks.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,49 +0,0 @@ -.. _hooks: - -Hooks -===== - -Falcon supports *before* and *after* hooks. You install a hook simply by -applying one of the decorators below, either to an individual responder or -to an entire resource. - -For example, consider this hook that validates a POST request for -an image resource: - -.. code:: python - - def validate_image_type(req, resp, resource, params): - if req.content_type not in ALLOWED_IMAGE_TYPES: - msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' - raise falcon.HTTPBadRequest('Bad request', msg) - -You would attach this hook to an ``on_post`` responder like so: - -.. code:: python - - @falcon.before(validate_image_type) - def on_post(self, req, resp): - pass - -Or, suppose you had a hook that you would like to apply to *all* -responders for a given resource. In that case, you would simply -decorate the resource class: - -.. code:: python - - @falcon.before(extract_project_id) - class Message(object): - def on_post(self, req, resp): - pass - - def on_get(self, req, resp): - pass - -Falcon :ref:`middleware components ` can also be used to insert -logic before and after requests. However, unlike hooks, -:ref:`middleware components ` are triggered **globally** for all -requests. - -.. automodule:: falcon - :members: before, after - :undoc-members: diff -Nru python-falcon-1.0.0/doc/api/index.rst python-falcon-1.4.1/doc/api/index.rst --- python-falcon-1.0.0/doc/api/index.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/index.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -Classes and Functions -===================== - -.. toctree:: - :maxdepth: 2 - - api - request_and_response - cookies - status - errors - redirects - middleware - hooks - routing - util - testing diff -Nru python-falcon-1.0.0/doc/api/middleware.rst python-falcon-1.4.1/doc/api/middleware.rst --- python-falcon-1.0.0/doc/api/middleware.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/middleware.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,127 +0,0 @@ -.. _middleware: - -Middleware Components -===================== - -Middleware components provide a way to execute logic before the -framework routes each request, after each request is routed but before -the target responder is called, or just before the response is returned -for each request. Components are registered with the `middleware` kwarg -when instantiating Falcon's :ref:`API class `. - -.. Note:: - Unlike hooks, middleware methods apply globally to the entire API. - -Falcon's middleware interface is defined as follows: - -.. code:: python - - class ExampleComponent(object): - def process_request(self, req, resp): - """Process the request before routing it. - - Args: - req: Request object that will eventually be - routed to an on_* responder method. - resp: Response object that will be routed to - the on_* responder. - """ - - def process_resource(self, req, resp, resource, params): - """Process the request after routing. - - Note: - This method is only called when the request matches - a route to a resource. - - Args: - req: Request object that will be passed to the - routed responder. - resp: Response object that will be passed to the - responder. - resource: Resource object to which the request was - routed. - params: A dict-like object representing any additional - params derived from the route's URI template fields, - that will be passed to the resource's responder - method as keyword arguments. - """ - - def process_response(self, req, resp, resource): - """Post-processing of the response (after routing). - - Args: - req: Request object. - resp: Response object. - resource: Resource object to which the request was - routed. May be None if no route was found - for the request. - """ - -.. Tip:: - Because *process_request* executes before routing has occurred, if a - component modifies ``req.path`` in its *process_request* method, - the framework will use the modified value to route the request. - -.. Tip:: - The *process_resource* method is only called when the request matches - a route to a resource. To take action when a route is not found, a - :py:meth:`sink ` may be used instead. - -Each component's *process_request*, *process_resource*, and -*process_response* methods are executed hierarchically, as a stack, following -the ordering of the list passed via the `middleware` kwarg of -:ref:`falcon.API`. For example, if a list of middleware objects are -passed as ``[mob1, mob2, mob3]``, the order of execution is as follows:: - - mob1.process_request - mob2.process_request - mob3.process_request - mob1.process_resource - mob2.process_resource - mob3.process_resource - - mob3.process_response - mob2.process_response - mob1.process_response - -Note that each component need not implement all `process_*` -methods; in the case that one of the three methods is missing, -it is treated as a noop in the stack. For example, if ``mob2`` did -not implement *process_request* and ``mob3`` did not implement -*process_response*, the execution order would look -like this:: - - mob1.process_request - _ - mob3.process_request - mob1.process_resource - mob2.process_resource - mob3.process_resource - - _ - mob2.process_response - mob1.process_response - -If one of the *process_request* middleware methods raises an -error, it will be processed according to the error type. If -the type matches a registered error handler, that handler will -be invoked and then the framework will begin to unwind the -stack, skipping any lower layers. The error handler may itself -raise an instance of HTTPError, in which case the framework -will use the latter exception to update the *resp* object. -Regardless, the framework will continue unwinding the middleware -stack. For example, if *mob2.process_request* were to raise an -error, the framework would execute the stack as follows:: - - mob1.process_request - mob2.process_request - - mob2.process_response - mob1.process_response - -Finally, if one of the *process_response* methods raises an error, -or the routed on_* responder method itself raises an error, the -exception will be handled in a similar manner as above. Then, -the framework will execute any remaining middleware on the -stack. diff -Nru python-falcon-1.0.0/doc/api/redirects.rst python-falcon-1.4.1/doc/api/redirects.rst --- python-falcon-1.0.0/doc/api/redirects.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/redirects.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -.. _redirects: - -Redirection -=========== - -Falcon defines a set of exceptions that can be raised within a -middleware method, hook, or responder in order to trigger -a 3xx (Redirection) response to the client. Raising one of these -classes short-circuits request processing in a manner similar to -raising an instance or subclass of :py:class:`~.HTTPError` - - -Base Class ----------- - -.. autoclass:: falcon.http_status.HTTPStatus - :members: - - -Redirects ---------- - -.. automodule:: falcon - :members: HTTPMovedPermanently, HTTPFound, HTTPSeeOther, - HTTPTemporaryRedirect, HTTPPermanentRedirect diff -Nru python-falcon-1.0.0/doc/api/request_and_response.rst python-falcon-1.4.1/doc/api/request_and_response.rst --- python-falcon-1.0.0/doc/api/request_and_response.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/request_and_response.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -.. _request: - -Req/Resp -======== - -Instances of the Request and Response classes are passed into responders as the second -and third arguments, respectively. - -.. code:: python - - import falcon - - - class Resource(object): - - def on_get(self, req, resp): - resp.body = '{"message": "Hello world!"}' - resp.status = falcon.HTTP_200 - -Request -------- - -.. autoclass:: falcon.Request - :members: - -Response --------- - -.. autoclass:: falcon.Response - :members: - - - diff -Nru python-falcon-1.0.0/doc/api/routing.rst python-falcon-1.4.1/doc/api/routing.rst --- python-falcon-1.0.0/doc/api/routing.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/routing.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +0,0 @@ -.. _routing: - -Routing -======= - -The *falcon.routing* module contains utilities used internally by -:py:meth:`falcon.API` to route requests. They are exposed here for use by -custom routing engines. - -A custom router is any class that implements the following interface: - -.. code:: python - - class FancyRouter(object): - def add_route(self, uri_template, method_map, resource): - """Adds a route between URI path template and resource. - - Args: - uri_template (str): The URI template to add. - method_map (dict): A method map obtained by calling - falcon.routing.create_http_method_map. - resource (object): Instance of the resource class that - will handle requests for the given URI. - """ - - def find(self, uri): - """Search for a route that matches the given URI. - - Args: - uri (str): Request URI to match to a route. - - Returns: - tuple: A 3-member tuple composed of (resource, method_map, params) - or ``None`` if no route is found. - """ - -A custom routing engine may be specified when instantiating -:py:meth:`falcon.API`. For example: - -.. code:: python - - fancy = FancyRouter() - api = API(router=fancy) - -.. automodule:: falcon.routing - :members: create_http_method_map, compile_uri_template, CompiledRouter diff -Nru python-falcon-1.0.0/doc/api/status.rst python-falcon-1.4.1/doc/api/status.rst --- python-falcon-1.0.0/doc/api/status.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/status.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,161 +0,0 @@ -.. _status: - -Status Codes -============ - -Falcon provides a list of constants for common -`HTTP response status codes `_. - -For example: - -.. code:: python - - # Override the default "200 OK" response status - resp.status = falcon.HTTP_409 - -Or, using the more verbose name: - -.. code:: python - - resp.status = falcon.HTTP_CONFLICT - -Using these constants helps avoid typos and cuts down on the number of -string objects that must be created when preparing responses. - -Falcon also provides a generic `HTTPStatus` class. Raise this class from a hook, -middleware, or a responder to stop handling the request and skip to the response -handling. It takes status, additional headers and body as input arguments. - -HTTPStatus ----------- - -.. autoclass:: falcon.HTTPStatus - :members: - - -1xx Informational ------------------ - -.. code:: python - - HTTP_CONTINUE = HTTP_100 - HTTP_SWITCHING_PROTOCOLS = HTTP_101 - - HTTP_100 = '100 Continue' - HTTP_101 = '101 Switching Protocols' - -2xx Success ------------ - -.. code:: python - - HTTP_OK = HTTP_200 - HTTP_CREATED = HTTP_201 - - HTTP_200 = '200 OK' - HTTP_201 = '201 Created' - HTTP_202 = '202 Accepted' - HTTP_203 = '203 Non-Authoritative Information' - HTTP_204 = '204 No Content' - HTTP_205 = '205 Reset Content' - HTTP_206 = '206 Partial Content' - HTTP_226 = '226 IM Used' - -3xx Redirection ---------------- - -.. code:: python - - HTTP_MULTIPLE_CHOICES = HTTP_300 - HTTP_MOVED_PERMANENTLY = HTTP_301 - HTTP_FOUND = HTTP_302 - HTTP_SEE_OTHER = HTTP_303 - HTTP_NOT_MODIFIED = HTTP_304 - HTTP_USE_PROXY = HTTP_305 - HTTP_TEMPORARY_REDIRECT = HTTP_307 - - HTTP_300 = '300 Multiple Choices' - HTTP_301 = '301 Moved Permanently' - HTTP_302 = '302 Found' - HTTP_303 = '303 See Other' - HTTP_304 = '304 Not Modified' - HTTP_305 = '305 Use Proxy' - HTTP_307 = '307 Temporary Redirect' - -4xx Client Error ----------------- - -.. code:: python - - HTTP_BAD_REQUEST = HTTP_400 - HTTP_UNAUTHORIZED = HTTP_401 # <-- Really means "unauthenticated" - HTTP_PAYMENT_REQUIRED = HTTP_402 - HTTP_FORBIDDEN = HTTP_403 # <-- Really means "unauthorized" - HTTP_NOT_FOUND = HTTP_404 - HTTP_METHOD_NOT_ALLOWED = HTTP_405 - HTTP_NOT_ACCEPTABLE = HTTP_406 - HTTP_PROXY_AUTHENTICATION_REQUIRED = HTTP_407 - HTTP_REQUEST_TIMEOUT = HTTP_408 - HTTP_CONFLICT = HTTP_409 - HTTP_GONE = HTTP_410 - HTTP_LENGTH_REQUIRED = HTTP_411 - HTTP_PRECONDITION_FAILED = HTTP_412 - HTTP_REQUEST_ENTITY_TOO_LARGE = HTTP_413 - HTTP_REQUEST_URI_TOO_LONG = HTTP_414 - HTTP_UNSUPPORTED_MEDIA_TYPE = HTTP_415 - HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = HTTP_416 - HTTP_EXPECTATION_FAILED = HTTP_417 - HTTP_IM_A_TEAPOT = HTTP_418 - HTTP_UNPROCESSABLE_ENTITY = HTTP_422 - HTTP_UPGRADE_REQUIRED = HTTP_426 - HTTP_PRECONDITION_REQUIRED = HTTP_428 - HTTP_TOO_MANY_REQUESTS = HTTP_429 - HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = HTTP_431 - HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = HTTP_451 - - HTTP_400 = '400 Bad Request' - HTTP_401 = '401 Unauthorized' # <-- Really means "unauthenticated" - HTTP_402 = '402 Payment Required' - HTTP_403 = '403 Forbidden' # <-- Really means "unauthorized" - HTTP_404 = '404 Not Found' - HTTP_405 = '405 Method Not Allowed' - HTTP_406 = '406 Not Acceptable' - HTTP_407 = '407 Proxy Authentication Required' - HTTP_408 = '408 Request Time-out' - HTTP_409 = '409 Conflict' - HTTP_410 = '410 Gone' - HTTP_411 = '411 Length Required' - HTTP_412 = '412 Precondition Failed' - HTTP_413 = '413 Payload Too Large' - HTTP_414 = '414 URI Too Long' - HTTP_415 = '415 Unsupported Media Type' - HTTP_416 = '416 Range Not Satisfiable' - HTTP_417 = '417 Expectation Failed' - HTTP_418 = "418 I'm a teapot" - HTTP_422 = "422 Unprocessable Entity" - HTTP_426 = '426 Upgrade Required' - HTTP_428 = '428 Precondition Required' - HTTP_429 = '429 Too Many Requests' - HTTP_431 = '431 Request Header Fields Too Large' - HTTP_451 = '451 Unavailable For Legal Reasons' - -5xx Server Error ----------------- - -.. code:: python - - HTTP_INTERNAL_SERVER_ERROR = HTTP_500 - HTTP_NOT_IMPLEMENTED = HTTP_501 - HTTP_BAD_GATEWAY = HTTP_502 - HTTP_SERVICE_UNAVAILABLE = HTTP_503 - HTTP_GATEWAY_TIMEOUT = HTTP_504 - HTTP_HTTP_VERSION_NOT_SUPPORTED = HTTP_505 - HTTP_NETWORK_AUTHENTICATION_REQUIRED = HTTP_511 - - HTTP_500 = '500 Internal Server Error' - HTTP_501 = '501 Not Implemented' - HTTP_502 = '502 Bad Gateway' - HTTP_503 = '503 Service Unavailable' - HTTP_504 = '504 Gateway Time-out' - HTTP_505 = '505 HTTP Version not supported' - HTTP_511 = '511 Network Authentication Required' diff -Nru python-falcon-1.0.0/doc/api/testing.rst python-falcon-1.4.1/doc/api/testing.rst --- python-falcon-1.0.0/doc/api/testing.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/testing.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -.. _testing: - -Testing -======= - -.. autoclass:: falcon.testing.TestCase - :members: - -.. autoclass:: falcon.testing.Result - :members: - -.. autoclass:: falcon.testing.SimpleTestResource - :members: - -.. autoclass:: falcon.testing.StartResponseMock - :members: - -.. automodule:: falcon.testing - :members: capture_responder_args, rand_string, create_environ - -Deprecated ----------- - -.. autoclass:: falcon.testing.TestBase - :members: - -.. autoclass:: falcon.testing.TestResource - :members: diff -Nru python-falcon-1.0.0/doc/api/util.rst python-falcon-1.4.1/doc/api/util.rst --- python-falcon-1.0.0/doc/api/util.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/api/util.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -.. _util: - -Utilities -========= - -URI Functions -------------- - -.. automodule:: falcon.util.uri - :members: - -Miscellaneous -------------- - -.. automodule:: falcon - :members: deprecated, http_now, dt_to_http, http_date_to_dt, to_query_str - -.. autoclass:: falcon.util.TimezoneGMT - :members: diff -Nru python-falcon-1.0.0/doc/changes/0.2.0.rst python-falcon-1.4.1/doc/changes/0.2.0.rst --- python-falcon-1.0.0/doc/changes/0.2.0.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/changes/0.2.0.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,100 +0,0 @@ -Changelog for Falcon 0.2.0 -========================== - -Breaking Changes ----------------- - -- The deprecated util.misc.percent\_escape and - util.misc.percent\_unescape functions were removed. Please use the - functions in the util.uri module instead. -- The deprecated function, API.set\_default\_route, was removed. Please - use sinks instead. -- HTTPRangeNotSatisfiable no longer accepts a media\_type parameter. -- When using the comma-delimited list convention, - req.get\_param\_as\_list(...) will no longer insert placeholders, - using the None type, for empty elements. For example, where - previously the query string "foo=1,,3" would result in ['1', None, - '3'], it will now result in ['1', '3']. - -New & Improved --------------- - -- Since 0.1 we've added proper RTD docs to make it easier for everyone - to get started with the framework. Over time we will continue adding - content, and we would love your help! -- Falcon now supports "wsgi.filewrapper". You can assign any file-like - object to resp.stream and Falcon will use "wsgi.filewrapper" to more - efficiently pipe the data to the WSGI server. -- Support was added for automatically parsing requests containing - "application/x-www-form-urlencoded" content. Form fields are now - folded into req.params. -- Custom Request and Response classes are now supported. You can - specify custom types when instantiating falcon.API. -- A new middleware feature was added to the framework. Middleware - deprecates global hooks, and we encourage everyone to migrate as soon - as possible. -- A general-purpose dict attribute was added to Request. Middleware, - hooks, and responders can now use req.context to share contextual - information about the current request. -- A new method, append\_header, was added to falcon.API to allow - setting multiple values for the same header using comma separation. - Note that this will not work for setting cookies, but we plan to - address this in the next release (0.3). -- A new "resource" attribute was added to hooks. Old hooks that do not - accept this new attribute are shimmed so that they will continue to - function. While we have worked hard to minimize the performance - impact, we recommend migrating to the new function signature to avoid - any overhead. -- Error response bodies now support XML in addition to JSON. In - addition, the HTTPError serialization code was refactored to make it - easier to implement a custom error serializer. -- A new method, "set\_error\_serializer" was added to falcon.API. You - can use this method to override Falcon's default HTTPError serializer - if you need to support custom media types. -- Falcon's testing base class, testing.TestBase was improved to - facilitate Py3k testing. Notably, TestBase.simulate\_request now - takes an additional "decode" kwarg that can be used to automatically - decode byte-string PEP-3333 response bodies. -- An "add\_link" method was added to the Response class. Apps can use - this method to add one or more Link header values to a response. -- Added two new properties, req.host and req.subdomain, to make it - easier to get at the hostname info in the request. -- Allow a wider variety of characters to be used in query string - params. -- Internal APIs have been refactored to allow overriding the default - routing mechanism. Further modularization is planned for the next - release (0.3). -- Changed req.get\_param so that it behaves the same whether a list was - specified in the query string using the HTML form style (in which - each element is listed in a separate 'key=val' field) or in the more - compact API style (in which each element is comma-separated and - assigned to a single param instance, as in 'key=val1,val2,val3') -- Added a convenience method, set\_stream(...), to the Response class - for setting the stream and its length at the same time, which should - help people not forget to set both (and save a few keystrokes along - the way). -- Added several new error classes, including HTTPRequestEntityTooLarge, - HTTPInvalidParam, HTTPMissingParam, HTTPInvalidHeader and - HTTPMissingHeader. -- Python 3.4 is now fully supported. -- Various minor performance improvements - -Fixed ------ - -- Ensure 100% test coverage and fix any bugs identified in the process. -- Fix not recognizing the "bytes=" prefix in Range headers. -- Make HTTPNotFound and HTTPMethodNotAllowed fully compliant, according - to RFC 7231. -- Fixed the default on\_options responder causing a Cython type error. -- URI template strings can now be of type unicode under Python 2. -- When SCRIPT\_NAME is not present in the WSGI environ, return an empty - string for the req.app property. -- Global "after" hooks will now be executed even when a responder - raises an error. -- Fixed several minor issues regarding testing.create\_environ(...) -- Work around a wsgiref quirk, where if no content-length header is - submitted by the client, wsgiref will set the value of that header to - an empty string in the WSGI environ. -- Resolved an issue causing several source files to not be Cythonized. -- Docstrings have been edited for clarity and correctness. diff -Nru python-falcon-1.0.0/doc/changes/0.3.0.rst python-falcon-1.4.1/doc/changes/0.3.0.rst --- python-falcon-1.0.0/doc/changes/0.3.0.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/changes/0.3.0.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -Changelog for Falcon 0.3.0 -========================== - -Breaking Changes ----------------- -- Date headers are now returned as :py:class:`datetime.datetime` objects - instead of strings. - -New & Improved --------------- - -- This release includes a new router architecture for improved performance - and flexibility. -- A custom router can now be specified when instantiating the - :py:class:`API` class. -- URI templates can now include multiple parameterized fields within a - single path segment. -- Falcon now supports reading and writing cookies. -- Falcon now supports Jython 2.7. -- A method for getting a query param as a date was added to the - :py:class:`Request` class. -- Date headers are now returned as :py:class:`datetime.datetime` objects. -- A default value can now be specified when calling - :py:meth:`Request.get_param`. This provides an alternative to using the - pattern:: - value = req.get_param(name) or default_value -- Friendly constants for status codes were added (e.g., - :py:attr:`falcon.HTTP_NO_CONTENT` vs. :py:attr:`falcon.HTTP_204`.) -- Several minor performance optimizations were made to the code base. - -Fixed ------ - -- The query string parser was modified to improve handling of percent-encoded - data. -- Several errors in the documentation were corrected. -- The :py:mod:`six` package was pinned to 1.4.0 or better. - :py:attr:`six.PY2` is required by Falcon, but that wasn't added to - :py:mod:`six` until version 1.4.0. diff -Nru python-falcon-1.0.0/doc/changes/1.0.0.rst python-falcon-1.4.1/doc/changes/1.0.0.rst --- python-falcon-1.0.0/doc/changes/1.0.0.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/changes/1.0.0.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,152 +0,0 @@ -Changelog for Falcon 1.0.0 -========================== - -Breaking Changes ----------------- -- The deprecated global hooks feature has been removed. - :class:`~falcon.API` no longer accepts `before` and `after` - kwargs. Applications can work around this by migrating any logic - contained in global hooks to reside in middleware components instead. -- The middleware method :meth:`process_resource` must now accept - an additional `params` argument. This gives the middleware method an - opportunity to interact with the values for any fields defined in a - route's URI template. -- The middleware method :meth:`process_resource` is now skipped when - no route is found for the incoming request. This avoids having to - include an ``if resource is not None`` check when implementing this - method. A sink may be used instead to execute logic in the case that - no route is found. -- An option was added to toggle automatic parsing of form params. Falcon - will no longer automatically parse, by default, requests that have the - content type "application/x-www-form-urlencoded". This was done to - avoid unintended side-effects that may arise from consuming the - request stream. It also makes it more straightforward for applications - to customize and extend the handling of form submissions. Applications - that require this functionality must re-enable it explicitly, by - setting a new request option that was added for that purpose, per the - example below:: - - app = falcon.API() - app.req_options.auto_parse_form_urlencoded = True - -- The :class:`~falcon.HTTPUnauthorized` initializer now requires an - additional argument, `challenges`. Per RFC 7235, a server returning a - 401 must include a WWW-Authenticate header field containing at least - one challenge. -- The performance of composing the response body was - improved. As part of this work, the :attr:`Response.body_encoded` - attribute was removed. This property was only intended to be used by - the framework itself, but any dependent code can be migrated per - the example below:: - - # Before - body = resp.body_encoded - - # After - if resp.body: - body = resp.body.encode('utf-8') - else: - body = b'' - -New & Improved --------------- - -- A `code of conduct `_ - was added to solidify our community's commitment to sustaining a - welcoming, respectful culture. -- CPython 3.5 is now fully supported. -- The constants HTTP_422, HTTP_428, HTTP_429, HTTP_431, HTTP_451, and - HTTP_511 were added. -- The :class:`~falcon.HTTPUnprocessableEntity`, - :class:`~falcon.HTTPTooManyRequests`, and - :class:`~falcon.HTTPUnavailableForLegalReasons` error classes were - added. -- The :any:`HTTPStatus` class is now available directly under - the `falcon` module, and has been properly documented. -- Support for HTTP redirections was added via a set of - :any:`HTTPStatus` subclasses. This should avoid the problem - of hooks and responder methods possibly overriding the redirect. - Raising an instance of one of these new redirection classes will - short-circuit request processing, similar to raising an instance of - :class:`~falcon.HTTPError`. -- The default 404 responder now raises an instance of - :class:`~falcon.HTTPError` instead of manipulating the - response object directly. This makes it possible to customize the - response body using a custom error handler or serializer. -- A new method, :meth:`~falcon.Response.get_header`, was added to - :class:`~falcon.Response`. Previously there was no way to check if a - header had been set. The new :meth:`~falcon.Response.get_header` - method facilitates this and other use cases. -- :meth:`falcon.Request.client_accepts_msgpack` now recognizes - "application/msgpack", in addition to "application/x-msgpack". -- New :any:`access_route` and :any:`remote_addr` properties were added - to :class:`~falcon.Request` for getting upstream IP addresses. -- :class:`~falcon.Request` and :class:`~falcon.Response` now support - range units other than bytes. -- The :class:`~falcon.API` and - :class:`~falcon.testing.StartResponseMock` class types can now be - customized by inheriting from :class:`~falcon.testing.TestBase` and - overriding the `api_class` and `srmock_class` class attributes. -- Path segments with multiple field expressions may now be defined at - the same level as path segments having only a single field - expression. For example:: - - api.add_route('/files/{file_id}', resource_1) - api.add_route('/files/{file_id}.{ext}', resource_2) - -- Support was added to :any:`API.add_route()` for passing through - additional args and kwargs to custom routers. -- Digits and the underscore character are now allowed in the - :meth:`falcon.routing.compile_uri_template` helper, for use in custom - router implementations. -- A new testing framework was added that should be more intuitive to - use than the old one. Several of Falcon's own tests were ported to use - the new framework (the remainder to be ported in a - subsequent release.) The new testing framework performs wsgiref - validation on all requests. -- The performance of setting :attr:`Response.content_range` was - improved by ~50%. -- A new param, `obs_date`, was added to - :meth:`falcon.Request.get_header_as_datetime`, and defaults to - ``False``. This improves the method's performance when obsolete date - formats do not need to be supported. - -Fixed ------ - -- Field expressions at a given level in the routing tree no longer - mask alternative branches. When a single segment in a requested path - can match more than one node at that branch in the routing tree, and - the first branch taken happens to be the wrong one (i.e., the - subsequent nodes do not match, but they would have under a different - branch), the other branches that could result in a - successful resolution of the requested path will now be subsequently - tried, whereas previously the framework would behave as if no route - could be found. -- The user agent is now instructed to expire the cookie when it is - cleared via :meth:`~falcon.Response.unset_cookie`. -- Support was added for hooks that have been defined via - :meth:`functools.partial`. -- Tunneled UTF-8 characters in the request path are now properly - decoded, and a placeholder character is substituted for any invalid - code points. -- The instantiation of :any:`Request.context_type` is now - delayed until after all other properties of the - :class:`~falcon.Request` class have been initialized, in case the - context type's own initialization depends on any of - :class:`~falcon.Request`'s properties. -- A case was fixed in which reading from :any:`Request.stream` - could hang when using :mod:`wsgiref` to host the app. -- The default error serializer now sets the Vary header in responses. - Implementing this required passing the :class:`~falcon.Response` - object to the serializer, which would normally be a breaking change. - However, the framework was modified to detect old-style error - serializers and wrap them with a shim to make them compatible with - the new interface. -- A query string containing malformed percent-encoding no longer causes - the framework to raise an error. -- Additional tests were added for a few lines of code that were - previously not covered, due to deficiencies in code coverage reporting - that have since been corrected. -- The Cython note is no longer displayed when installing under Jython. -- Several errors and ambiguities in the documentation were corrected. diff -Nru python-falcon-1.0.0/doc/changes/index.rst python-falcon-1.4.1/doc/changes/index.rst --- python-falcon-1.0.0/doc/changes/index.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/changes/index.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -Changelogs -========== - -.. toctree:: - - 1.0.0 <1.0.0> - 0.3.0 <0.3.0> - 0.2.0 <0.2.0> diff -Nru python-falcon-1.0.0/doc/community/contrib-snip.rst python-falcon-1.4.1/doc/community/contrib-snip.rst --- python-falcon-1.0.0/doc/community/contrib-snip.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/community/contrib-snip.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -Mailing List ------------- -The Falcon community maintains a mailing list that you can use to share -your ideas and ask questions about the framework. We use the appropriately -minimalistic `Librelist `_ to host the discussions. - -To join the mailing list, simply send your first email to falcon@librelist.com! -This will automatically subscribe you to the mailing list *and* sends your email -along to the rest of the subscribers. For more information about managing your -subscription, check out the -`Librelist help page `_. - -All contributors and maintainers of this project are subject to our `Code -of Conduct `_. -We expect everyone who participates on the mailing list to act -professionally, and lead by example in encouraging constructive -discussions. Each individual in the community is responsible for creating -a positive, constructive, and productive culture. - -`Discussions are archived `_ -for posterity. - -Submit Issues -------------- -If you have an idea for a feature, run into something that is harder to -use than it should be, or find a bug, please let the crew know -in **#falconframework** and/or by -`submitting an issue `_. We -need your help to make Falcon awesome! - -Pay it Forward --------------- -We'd like to invite you to help other community members with their -questions in IRC, and to peer-review -`pull requests `_. If you use the -Chrome browser, we recommend installing the -`NotHub extension `_ to stay up to date with PRs. - -Code of Conduct ---------------- -All contributors and maintainers of this project are subject to our `Code -of Conduct `_. diff -Nru python-falcon-1.0.0/doc/community/contribute.rst python-falcon-1.4.1/doc/community/contribute.rst --- python-falcon-1.0.0/doc/community/contribute.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/community/contribute.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,56 +0,0 @@ -.. _contribute: - -Contribute to Falcon -==================== - -`Kurt Griffiths `_ is the creator and current -maintainer of the Falcon framework. He works with a growing team of -friendly and stylish volunteers like yourself, who review patches, -implement features, fix bugs, and write docs for the project. - -Your ideas and patches are always welcome! - -IRC ---- -If you are interested in helping out, please join the **#falconframework** -IRC channel on `Freenode `_. -It's the best way to discuss ideas, ask questions, and generally stay -in touch with fellow contributors. We recommend setting up a good -IRC bouncer, such as ZNC, which can record and play back any conversations -that happen when you are away. - -.. include:: contrib-snip.rst - -Pull Requests -------------- -Before submitting a pull request, please ensure you have added new -tests and updated existing ones as appropriate. We require 100% -code coverage. Also, please ensure your coding style follows PEP 8 and -doesn't make pyflakes sad. - -**Additional Style Rules** - -* Docstrings are required for classes, attributes, methods, and functions. -* Use `napolean-flavored`_ docstrings to make them readable both when - using the *help* function within a REPL, and when browsing - them on *Read the Docs*. -* Format non-trivial comments using your GitHub nick and an appropriate - prefix. Here are some examples:: - - # TODO(riker): Damage report! - # NOTE(riker): Well, that's certainly good to know. - # PERF(riker): Travel time to the nearest starbase? - # APPSEC(riker): In all trust, there is the possibility for betrayal. - -* Commit messages should be formatted using `AngularJS conventions`_ - (one-liners are OK for now but bodies and footers may be required as the - project matures). -* When catching exceptions, name the variable ``ex``. -* Use whitespace to separate logical blocks of code and to improve readability. -* Do not use single-character variable names except for trivial indexes when - looping, or in mathematical expressions implementing well-known formulae. -* Heavily document code that is especially complex or clever! -* When in doubt, optimize for readability. - -.. _napolean-flavored: http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html#example-google-style-python-docstrings -.. _AngularJS conventions: http://goo.gl/QpbS7 diff -Nru python-falcon-1.0.0/doc/community/faq.rst python-falcon-1.4.1/doc/community/faq.rst --- python-falcon-1.0.0/doc/community/faq.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/community/faq.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,182 +0,0 @@ -.. _faq: - -FAQ -=== - -How do I use WSGI middleware with Falcon? ------------------------------------------ -Instances of `falcon.API` are first-class WSGI apps, so you can use the -standard pattern outlined in PEP-3333. In your main "app" file, you would -simply wrap your api instance with a middleware app. For example: - -.. code:: python - - import my_restful_service - import some_middleware - - app = some_middleware.DoSomethingFancy(my_restful_service.api) - -See also the `WSGI middleware example `_ given in PEP-3333. - -Why doesn't Falcon come with batteries included? ------------------------------------------------- -The Python ecosystem offers a bunch of great libraries that you are welcome -to use from within your responders, hooks, and middleware components. Falcon -doesn't try to dictate what you should use, since that would take away your -freedom to choose the best tool for the job. - -How do I authenticate requests? -------------------------------- -Hooks and middleware components can be used together to authenticate and -authorize requests. For example, a middleware component could be used to -parse incoming credentials and place the results in ``req.context``. -Downstream components or hooks could then use this information to -authorize the request, taking into account the user's role and the requested -resource. - -Why doesn't Falcon create a new Resource instance for every request? --------------------------------------------------------------------- -Falcon generally tries to minimize the number of objects that it -instantiates. It does this for two reasons: first, to avoid the expense of -creating the object, and second to reduce memory usage. Therefore, when -adding a route, Falcon requires an *instance* of your resource class, rather -than the class type. That same instance will be used to serve all requests -coming in on that route. - -Is Falcon thread-safe? ----------------------- -New Request and Response objects are created for each incoming HTTP request. -However, a single instance of each resource class attached to a route is -shared among all requests. Therefore, as long as you are careful about the -way responders access class member variables to avoid conflicts, your -WSGI app should be thread-safe. - -That being said, Falcon-based services are usually deployed using green -threads (via the gevent library or similar) which aren't truly running -concurrently, so there may be some edge cases where Falcon is not -thread-safe that haven't been discovered yet. - -*Caveat emptor!* - -How do I implement both POSTing and GETing items for the same resource? ------------------------------------------------------------------------ -Suppose you wanted to implement the following endpoints:: - - # Resource Collection - POST /resources - GET /resources{?marker, limit} - - # Resource Item - GET /resources/{id} - PATCH /resources/{id} - DELETE /resources/{id} - -You can implement this sort of API by simply using two Python classes, one -to represent a single resource, and another to represent the collection of -said resources. It is common to place both classes in the same module. - -The Falcon community did some experimenting with routing both singleton -and collection-based operations to the same Python class, but it turned -out to make routing definitions more complicated and less intuitive. That -being said, we are always open to new ideas, so please let us know if you -discover another way. - -See also :ref:`this section of the tutorial `. - -How can I pass data from a hook to a responder, and between hooks? ------------------------------------------------------------------- -You can inject extra responder kwargs from a hook by adding them -to the *params* dict passed into the hook. You can also add custom data to -the ``req.context`` dict, as a way of passing contextual information around. - -Does Falcon set Content-Length or do I need to do that explicitly? ------------------------------------------------------------------- -Falcon will try to do this for you, based on the value of `resp.body`, -`resp.data`, or `resp.stream_len` (whichever is set in the response, checked -in that order.) - -For dynamically-generated content, you can choose to leave off `stream_len`, -in which case Falcon will then leave off the Content-Length header, and -hopefully your WSGI server will do the Right Thing™ (assuming you've told -it to enable keep-alive). - -.. note:: PEP-333 prohibits apps from setting hop-by-hop headers itself, - such as Transfer-Encoding. - -I'm setting a response body, but it isn't getting returned. What's going on? ----------------------------------------------------------------------------- -Falcon skips processing the response body when, according to the HTTP -spec, no body should be returned. If the client -sends a HEAD request, the framework will always return an empty body. -Falcon will also return an empty body whenever the response status is any -of the following:: - - falcon.HTTP_100 - falcon.HTTP_204 - falcon.HTTP_416 - falcon.HTTP_304 - -If you have another case where you body isn't being returned to the -client, it's probably a bug! Let us know in IRC or on the mailing list so -we can help. - -My app is setting a cookie, but it isn't being passed back in subsequent requests. ----------------------------------------------------------------------------------- -By default, Falcon enables the `secure` cookie attribute. Therefore, if you are -testing your app over HTTP (instead of HTTPS), the client will not send the -cookie in subsequent requests. See also :ref:`the cookie documentation ` - -Why does raising an error inside a resource crash my app? ---------------------------------------------------------- -Generally speaking, Falcon assumes that resource responders (such as *on_get*, -*on_post*, etc.) will, for the most part, do the right thing. In other words, -Falcon doesn't try very hard to protect responder code from itself. - -This approach reduces the number of (often) extraneous checks that Falcon -would otherwise have to perform, making the framework more efficient. With -that in mind, writing a high-quality API based on Falcon requires that: - -#. Resource responders set response variables to sane values. -#. Your code is well-tested, with high code coverage. -#. Errors are anticipated, detected, and handled appropriately within - each responder and with the aid of custom error handlers. - -.. tip:: Falcon will re-raise errors that do not inherit from - ``falcon.HTTPError`` unless you have registered a custom error - handler for that type (see also: :ref:`falcon.API `). - -Why are trailing slashes trimmed from req.path? ------------------------------------------------ -Falcon normalizes incoming URI paths to simplify later processing and -improve the predictability of application logic. In addition to stripping -a trailing slashes, if any, Falcon will convert empty paths to "/". - -Note also that routing is also normalized, so adding a route for "/foo/bar" -also implicitly adds a route for "/foo/bar/". Requests coming in for either -path will be sent to the same resource. - -Why are field names in URI templates restricted to certain characters? ----------------------------------------------------------------------- -Field names are restricted to the ASCII characters in the set ``[a-zA-Z_]``. -Using a restricted set of characters allows the framework to make -simplifying assumptions that reduce the overhead of parsing incoming requests. - -Why is my query parameter missing from the req object? ------------------------------------------------------- -If a query param does not have a value, Falcon will by default ignore that -parameter. For example, passing 'foo' or 'foo=' will result in the parameter -being ignored. - -If you would like to recognize such parameters, you must set the -`keep_blank_qs_values` request option to ``True``. Request options are set -globally for each instance of ``falcon.API`` through the `req_options` -attribute. For example: - -.. code:: python - - api.req_options.keep_blank_qs_values = True - - -.. If Falcon is designed for building web APIs, why does it support forms? -.. ---- -.. Doesn't support files, allows same code to handle both... diff -Nru python-falcon-1.0.0/doc/community/help.rst python-falcon-1.4.1/doc/community/help.rst --- python-falcon-1.0.0/doc/community/help.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/community/help.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -.. _help: - -Get Help -======== - -Welcome to the Falcon community! We are a pragmatic group of HTTP enthusiasts -working on the next generation of web apps and cloud services. We would love -to have you join us and share your ideas. - -Please help us spread the word and grow the community! - -IRC ---- -While you experiment with Falcon and work to familiarize yourself with -the WSGI framework, please consider joining the **#falconframework** -IRC channel on -`Freenode `_. It's a great place to -ask questions, share ideas, and get the scoop on what's new. - -.. include:: contrib-snip.rst diff -Nru python-falcon-1.0.0/doc/community/index.rst python-falcon-1.4.1/doc/community/index.rst --- python-falcon-1.0.0/doc/community/index.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/community/index.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,9 +0,0 @@ -Community Guide -=============== - -.. toctree:: - :maxdepth: 1 - - help - contribute - faq \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/conf.py python-falcon-1.4.1/doc/conf.py --- python-falcon-1.0.0/doc/conf.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/conf.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Falcon documentation build configuration file, created by -# sphinx-quickstart on Wed Mar 12 14:14:02 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) - -# Path to custom themes -sys.path.append(os.path.abspath('_themes')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinxcontrib.napoleon', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.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'Falcon' -copyright = u'2016, Rackspace Hosting et al. (as noted in the source code)' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1.0' -# The full version, including alpha/beta/rc tags. -release = '1.0.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -# pygments_style = 'flask_theme_support.FlaskyStyle' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = ['_themes'] -# html_theme = 'kr' - -# html_theme = 'default' - -if not on_rtd: - # Use the RTD theme explicitly if it is available - try: - import sphinx_rtd_theme - - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - html_theme = "sphinx_rtd_theme" - except ImportError: - pass - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = '../falcon.png' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = '_static/img/favicon.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -html_sidebars = { - 'index': ['side-primary.html', 'searchbox.html'], - '**': ['side-secondary.html', 'localtoc.html', - 'relations.html', 'searchbox.html'] -} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -html_show_copyright = False - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Falcondoc' - - -# -- 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': '', -} - -# 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 = [ - ('index', 'Falcon.tex', u'Falcon Documentation', - u'Kurt Griffiths et al.', '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 = [ - ('index', 'falcon', u'Falcon Documentation', - [u'Kurt Griffiths et al.'], 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 = [ - ('index', 'Falcon', u'Falcon Documentation', - u'Kurt Griffiths et al.', 'Falcon', '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 - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} diff -Nru python-falcon-1.0.0/doc/index.rst python-falcon-1.4.1/doc/index.rst --- python-falcon-1.0.0/doc/index.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/index.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,91 +0,0 @@ -.. Falcon documentation master file, created by - sphinx-quickstart on Mon Feb 17 18:21:12 2014. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -The Falcon Web Framework -================================= - -Release v\ |version| (:ref:`Installation `) - -Falcon is a minimalist WSGI library for building speedy web APIs and app -backends. We like to think of Falcon as the `Dieter Rams` of web frameworks. - -When it comes to building HTTP APIs, other frameworks weigh you down with tons -of dependencies and unnecessary abstractions. Falcon cuts to the chase with a -clean design that embraces HTTP and the REST architectural style. - -.. code:: python - - class CatalogItem(object): - - # ... - - @falcon.before(hooks.to_oid) - def on_get(self, id): - return self._collection.find_one(id) - - app = falcon.API(after=[hooks.serialize]) - app.add_route('/items/{id}', CatalogItem()) - - -What People are Saying ----------------------- - -"Falcon looks great so far. I hacked together a quick test for a -tiny server of mine and was ~40% faster with only 20 minutes of -work." - -"I'm loving #falconframework! Super clean and simple, I finally -have the speed and flexibility I need!" - -"I feel like I'm just talking HTTP at last, with nothing in the -middle. Falcon seems like the requests of backend." - -"The source code for falcon is so good, I almost prefer it to -documentation. It basically can't be wrong." - -"What other framework has integrated support for '786 TRY IT NOW' ?" - - -Features --------- - -Falcon tries to do as little as possible while remaining highly effective. - -- Routes based on URI templates RFC -- REST-inspired mapping of URIs to resources -- Global, resource, and method hooks -- Idiomatic HTTP error responses -- Full Unicode support -- Intuitive request and response objects -- Works great with async libraries like gevent -- Minimal attack surface for writing secure APIs -- 100% code coverage with a comprehensive test suite -- Only depends on six and mimeparse -- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.5 -- Compatible with PyPy and Jython - -Useful Links ------------- - -- `Falcon Home `_ -- `Falcon @ PyPI `_ -- `Falcon @ GitHub `_ - -Resources ---------- - -- `An Unladen Web Framework `_ -- `The Definitive Introduction to Falcon `_ - -Documentation -------------- - -.. toctree:: - :maxdepth: 2 - - community/index - user/index - api/index - changes/index diff -Nru python-falcon-1.0.0/doc/Makefile python-falcon-1.4.1/doc/Makefile --- python-falcon-1.0.0/doc/Makefile 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/Makefile 1970-01-01 00:00:00.000000000 +0000 @@ -1,177 +0,0 @@ -# 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 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 " 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)" - -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/Falcon.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Falcon.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Falcon" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Falcon" - @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." - -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." diff -Nru python-falcon-1.0.0/doc/_static/css/falcon.css python-falcon-1.4.1/doc/_static/css/falcon.css --- python-falcon-1.0.0/doc/_static/css/falcon.css 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_static/css/falcon.css 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -/* - -These are needed if using the RTD theme. - -.property { - margin-left: 1em; -} - -em { - margin-right: 0.15em; -} - -*/ - -/* The following are used for the KR theme. */ - -div.sphinxsidebarwrapper { - margin-right: 1.5em; -} - -img.logo { - margin-right: 0; - padding-left: 7px; -} - -h1.logo-text { - font-family:"Amethysta"; - text-shadow: 0 1px #ddd; - color: #333; - position: absolute; - z-index: 10; - top: 17px; - font-size: 18pt; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family:"Amethysta"; - - font-weight: normal; - margin: 30px 0px 10px -2px; - padding: 0; -} - -table.field-list th { - width: 95px; -} - Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/doc/_static/img/favicon.ico and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/doc/_static/img/favicon.ico differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/doc/_static/img/logo.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/doc/_static/img/logo.png differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/doc/_static/img/my-web-app.gif and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/doc/_static/img/my-web-app.gif differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/doc/_static/img/my-web-app.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/doc/_static/img/my-web-app.png differ diff -Nru python-falcon-1.0.0/doc/_templates/side-primary.html python-falcon-1.4.1/doc/_templates/side-primary.html --- python-falcon-1.0.0/doc/_templates/side-primary.html 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_templates/side-primary.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,62 +0,0 @@ - - - - - - -

Falcon

- - -

- -

- -

- Falcon is a high-performance Python framework for building web APIs. It - encourages the REST architectural style, and tries to do as little as - possible while remaining highly effective. -

- -

Useful Links

- - -

Get Involved

- -

- If you have an idea for improving the framework, or come across a bug, - please let us know by - submitting an issue - and/or sending in a pull request. -

- -

- We also have a Falcon mailing list where you can share your ideas and ask questions. -

- -

- Also, if you are ever in the mood to chat, the Falcon crew hangs out in - #falconframework on - Freenode. Everyone is - welcome to join; the channel is a great place to - ask questions and keep up to date with the latest developments. -

- -

Credits

- -

- Falcon is made possible by the generous support of Rackspace, and - by a team of talented volunteers like you from the broader Python community. -

- -

- The Falcon logo is based on a photograph by John O'Neill used under the - Creative Commons Attribution-Share Alike 3.0 Unported license. -

\ No newline at end of file diff -Nru python-falcon-1.0.0/doc/_templates/side-secondary.html python-falcon-1.4.1/doc/_templates/side-secondary.html --- python-falcon-1.0.0/doc/_templates/side-secondary.html 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_templates/side-secondary.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ - - - - - - -

Falcon

- - -

- -

- -

- Falcon is a high-performance Python framework for building web APIs. It - encourages the REST architectural style, and tries to do as little as - possible while remaining highly effective. -

- -

Useful Links

- diff -Nru python-falcon-1.0.0/doc/_themes/flask_theme_support.py python-falcon-1.4.1/doc/_themes/flask_theme_support.py --- python-falcon-1.0.0/doc/_themes/flask_theme_support.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/flask_theme_support.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } diff -Nru python-falcon-1.0.0/doc/_themes/.gitignore python-falcon-1.4.1/doc/_themes/.gitignore --- python-falcon-1.0.0/doc/_themes/.gitignore 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/.gitignore 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -*.pyc -*.pyo -.DS_Store diff -Nru python-falcon-1.0.0/doc/_themes/kr/layout.html python-falcon-1.4.1/doc/_themes/kr/layout.html --- python-falcon-1.0.0/doc/_themes/kr/layout.html 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr/layout.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -{%- extends "basic/layout.html" %} -{%- block extrahead %} - {{ super() }} - {% if theme_touch_icon %} - - {% endif %} - - -{% endblock %} -{%- block relbar2 %}{% endblock %} -{%- block footer %} - -{%- endblock %} diff -Nru python-falcon-1.0.0/doc/_themes/kr/relations.html python-falcon-1.4.1/doc/_themes/kr/relations.html --- python-falcon-1.0.0/doc/_themes/kr/relations.html 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr/relations.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -

Related Topics

- diff -Nru python-falcon-1.0.0/doc/_themes/kr/static/flasky.css_t python-falcon-1.4.1/doc/_themes/kr/static/flasky.css_t --- python-falcon-1.0.0/doc/_themes/kr/static/flasky.css_t 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr/static/flasky.css_t 1970-01-01 00:00:00.000000000 +0000 @@ -1,445 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz. - * :license: Flask Design License, see LICENSE for details. - */ - -{% set page_width = '940px' %} -{% set sidebar_width = '220px' %} - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro'; - font-size: 17px; - background-color: white; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - width: {{ page_width }}; - margin: 30px auto 0 auto; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ sidebar_width }}; -} - -div.sphinxsidebar { - width: {{ sidebar_width }}; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 0 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - width: {{ page_width }}; - margin: 20px auto 30px auto; - font-size: 14px; - color: #888; - text-align: right; -} - -div.footer a { - color: #888; -} - -div.related { - display: none; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; - border-bottom: 1px dotted #999; -} - -div.sphinxsidebar a:hover { - border-bottom: 1px solid #999; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 18px 10px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 0; - margin: -10px 0 0 -20px; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #444; - font-size: 24px; - font-weight: normal; - margin: 0 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p.logo a, -div.sphinxsidebar h3 a, -div.sphinxsidebar p.logo a:hover, -div.sphinxsidebar h3 a:hover { - border: none; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #ddd; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition tt.xref, div.admonition a tt { - border-bottom: 1px solid #fafafa; -} - -dd div.admonition { - margin-left: -60px; - padding-left: 60px; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight { - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; - background: #fdfdfd; - font-size: 0.9em; -} - -table.footnote + table.footnote { - margin-top: -15px; - border-top: none; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td.label { - width: 0px; - padding: 0.3em 0 0.3em 0.5em; -} - -table.footnote td { - padding: 0.3em 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -blockquote { - margin: 0 0 0 30px; - padding: 0; -} - -ul, ol { - margin: 10px 0 10px 30px; - padding: 0; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre, blockquote pre, li pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; - border-bottom: 1px solid white; -} - -a.reference { - text-decoration: none; - border-bottom: 1px dotted #004B6B; -} - -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - -a.footnote-reference { - text-decoration: none; - font-size: 0.7em; - vertical-align: top; - border-bottom: 1px dotted #004B6B; -} - -a.footnote-reference:hover { - border-bottom: 1px solid #6D4100; -} - -a:hover tt { - background: #EEE; -} - - -@media screen and (max-width: 600px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - -} - -/* misc. */ - -.revsys-inline { - display: none!important; -} \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/_themes/kr/static/small_flask.css python-falcon-1.4.1/doc/_themes/kr/static/small_flask.css --- python-falcon-1.0.0/doc/_themes/kr/static/small_flask.css 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr/static/small_flask.css 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -/* - * small_flask.css_t - * ~~~~~~~~~~~~~~~~~ - * - * :copyright: Copyright 2010 by Armin Ronacher. - * :license: Flask Design License, see LICENSE for details. - */ - -body { - margin: 0; - padding: 20px 30px; -} - -div.documentwrapper { - float: none; - background: white; -} - -div.sphinxsidebar { - display: block; - float: none; - width: 102.5%; - margin: 50px -30px -20px -30px; - padding: 10px 20px; - background: #333; - color: white; -} - -div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, -div.sphinxsidebar h3 a { - color: white; -} - -div.sphinxsidebar a { - color: #aaa; -} - -div.sphinxsidebar p.logo { - display: none; -} - -div.document { - width: 100%; - margin: 0; -} - -div.related { - display: block; - margin: 0; - padding: 10px 0 20px 0; -} - -div.related ul, -div.related ul li { - margin: 0; - padding: 0; -} - -div.footer { - display: none; -} - -div.bodywrapper { - margin: 0; -} - -div.body { - min-height: 0; - padding: 0; -} - -.rtd_doc_footer { - display: none; -} - -.document { - width: auto; -} - -.footer { - width: auto; -} - -.footer { - width: auto; -} - -.github { - display: none; -} \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/_themes/kr/theme.conf python-falcon-1.4.1/doc/_themes/kr/theme.conf --- python-falcon-1.0.0/doc/_themes/kr/theme.conf 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr/theme.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -pygments_style = flask_theme_support.FlaskyStyle - -[options] -touch_icon = diff -Nru python-falcon-1.0.0/doc/_themes/kr_small/layout.html python-falcon-1.4.1/doc/_themes/kr_small/layout.html --- python-falcon-1.0.0/doc/_themes/kr_small/layout.html 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr_small/layout.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -{% extends "basic/layout.html" %} -{% block header %} - {{ super() }} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{% block footer %} - {% if pagename == 'index' %} -
- {% endif %} -{% endblock %} -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %} - {% if theme_github_fork %} - Fork me on GitHub - {% endif %} -{% endblock %} -{% block sidebar1 %}{% endblock %} -{% block sidebar2 %}{% endblock %} diff -Nru python-falcon-1.0.0/doc/_themes/kr_small/static/flasky.css_t python-falcon-1.4.1/doc/_themes/kr_small/static/flasky.css_t --- python-falcon-1.0.0/doc/_themes/kr_small/static/flasky.css_t 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr_small/static/flasky.css_t 1970-01-01 00:00:00.000000000 +0000 @@ -1,287 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - color: #000; - background: white; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 40px auto 0 auto; - width: 700px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; - width: 650px; - margin: 0 auto 40px auto; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -{% if theme_index_logo %} -div.indexwrapper h1 { - text-indent: -999999px; - background: url({{ theme_index_logo }}) no-repeat center center; - height: {{ theme_index_logo_height }}; -} -{% endif %} - -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.85em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - padding: 0; - margin: 15px -30px; - padding: 8px; - line-height: 1.3em; - padding: 7px 30px; - background: #eee; - border-radius: 2px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff -Nru python-falcon-1.0.0/doc/_themes/kr_small/theme.conf python-falcon-1.4.1/doc/_themes/kr_small/theme.conf --- python-falcon-1.0.0/doc/_themes/kr_small/theme.conf 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/kr_small/theme.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css -nosidebar = true -pygments_style = flask_theme_support.FlaskyStyle - -[options] -index_logo = '' -index_logo_height = 120px -github_fork = '' diff -Nru python-falcon-1.0.0/doc/_themes/LICENSE python-falcon-1.4.1/doc/_themes/LICENSE --- python-falcon-1.0.0/doc/_themes/LICENSE 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -Modifications: - -Copyright (c) 2010 Kenneth Reitz. - - -Original Project: - -Copyright (c) 2010 by Armin Ronacher. - - -Some rights reserved. - -Redistribution and use in source and binary forms of the theme, with or -without modification, are permitted provided that the following conditions -are met: - -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - -* The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. - -We kindly ask you to only use these themes in an unmodified manner just -for Flask and Flask-related products, not for unrelated projects. If you -like the visual style and want to use it for your own projects, please -consider making some larger changes to the themes (such as changing -font faces, sizes, colors or margins). - -THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff -Nru python-falcon-1.0.0/doc/_themes/README.rst python-falcon-1.4.1/doc/_themes/README.rst --- python-falcon-1.0.0/doc/_themes/README.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/_themes/README.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -krTheme Sphinx Style -==================== - -This repository contains sphinx styles Kenneth Reitz uses in most of -his projects. It is a drivative of Mitsuhiko's themes for Flask and Flask related -projects. To use this style in your Sphinx documentation, follow -this guide: - -1. put this folder as _themes into your docs folder. Alternatively - you can also use git submodules to check out the contents there. - -2. add this to your conf.py: :: - - sys.path.append(os.path.abspath('_themes')) - html_theme_path = ['_themes'] - html_theme = 'kr' - -The following themes exist: - -**kr** - the standard flask documentation theme for large projects - -**kr_small** - small one-page theme. Intended to be used by very small addon libraries. - diff -Nru python-falcon-1.0.0/doc/user/advanced.rst python-falcon-1.4.1/doc/user/advanced.rst --- python-falcon-1.0.0/doc/user/advanced.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/advanced.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -.. Coming soon! sinks, serialization hooks, etc., server error stream, static file serving, file upload, etc. -.. if 'wsgi.file_wrapper' in environ: -.. multiple hooks per resource -.. after hooks -.. error responses for auth - 404 ? -.. error hooks -.. document all the individual error classes? -.. stacked hooks -.. auth whitelist -.. processing file uploads from forms -.. static file serving -.. wsgi middleware \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/user/big-picture-snip.rst python-falcon-1.4.1/doc/user/big-picture-snip.rst --- python-falcon-1.0.0/doc/user/big-picture-snip.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/big-picture-snip.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -The Big Picture ---------------- - -.. image:: ../_static/img/my-web-app.gif - :alt: Falcon-based web application architecture - :width: 600 - -| diff -Nru python-falcon-1.0.0/doc/user/deployment.rst python-falcon-1.4.1/doc/user/deployment.rst --- python-falcon-1.0.0/doc/user/deployment.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/deployment.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -.. _deployment: - -Deploying Your Web API -====================== - -[talk about, diagram async, host, etc.] - -[async front, async to backend options - asyncio, gevent, etc.] \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/user/index.rst python-falcon-1.4.1/doc/user/index.rst --- python-falcon-1.0.0/doc/user/index.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/index.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -User Guide -========== - -.. toctree:: - :maxdepth: 2 - - intro - install - quickstart - tutorial \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/user/install.rst python-falcon-1.4.1/doc/user/install.rst --- python-falcon-1.0.0/doc/user/install.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/install.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,107 +0,0 @@ -.. _install: - -Installation -============ - -PyPy ----- - -`PyPy `__ is the fastest way to run your Falcon app. -However, note that only the PyPy 2.7 compatible release is currently -supported. - -.. code:: bash - - $ pip install falcon - -CPython -------- - -Falcon also fully supports -`CPython `__ 2.6-3.5. - -A universal wheel is available on PyPI for the the Falcon framework. -Installing it is as simple as: - -.. code:: bash - - $ pip install falcon - -Installing the wheel is a great way to get up and running with Falcon -quickly in a development environment, but for an extra speed boost when -deploying your application in production, Falcon can compile itself with -Cython. - -The following commands tell pip to install Cython, and then to invoke -Falcon's ``setup.py``, which will in turn detect the presence of Cython -and then compile (AKA cythonize) the Falcon framework with the system's -default C compiler. - -.. code:: bash - - $ pip install cython - $ pip install --no-binary :all: falcon - -**Installing on OS X** - -Xcode Command Line Tools are required to compile Cython. Install them -with this command: - -.. code:: bash - - $ xcode-select --install - -The Clang compiler treats unrecognized command-line options as -errors; this can cause problems under Python 2.6, for example: - -.. code:: bash - - clang: error: unknown argument: '-mno-fused-madd' [-Wunused-command-line-argument-hard-error-in-future] - -You might also see warnings about unused functions. You can work around -these issues by setting additional Clang C compiler flags as follows: - -.. code:: bash - - $ export CFLAGS="-Qunused-arguments -Wno-unused-function" - - -WSGI Server ------------ - -Falcon speaks WSGI. If you want to actually serve a Falcon app, you will -want a good WSGI server. Gunicorn and uWSGI are some of the more popular -ones out there, but anything that can load a WSGI app will do. Gevent is -an async library that works well with both Gunicorn and uWSGI. - -.. code:: bash - - $ pip install gevent [gunicorn|uwsgi] - - -Source Code ------------ - -Falcon `lives on GitHub `_, making the -code easy to browse, download, fork, etc. Pull requests are always welcome! Also, -please remember to star the project if it makes you happy. - -Once you have cloned the repo or downloaded a tarball from GitHub, you -can install Falcon like this: - -.. code:: bash - - $ cd falcon - $ pip install . - -Or, if you want to edit the code, first fork the main repo, clone the fork -to your desktop, and then run the following to install it using symbolic -linking, so that when you change your code, the changes will be automagically -available to your app without having to reinstall the package: - -.. code:: bash - - $ cd falcon - $ pip install -e . - -Did we mention we love pull requests? :) diff -Nru python-falcon-1.0.0/doc/user/intro.rst python-falcon-1.4.1/doc/user/intro.rst --- python-falcon-1.0.0/doc/user/intro.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/intro.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -.. _introduction: - -Introduction -============ - -Falcon is a minimalist, high-performance web framework for building RESTful services and app backends with Python. Falcon works with any WSGI container that is compliant with PEP-3333, and works great with Python 2.6, Python 2.7, Python 3.3, Python 3.4 and PyPy, giving you a wide variety of deployment options. - - -How is Falcon different? ------------------------- - -First, Falcon is one of the fastest WSGI frameworks available. When there is a conflict between saving the developer a few keystrokes and saving a few microseconds to serve a request, Falcon is strongly biased toward the latter. That being said, Falcon strives to strike a good balance between usability and speed. - -Second, Falcon is lean. It doesn't try to be everything to everyone, focusing instead on a single use case: HTTP APIs. Falcon doesn't include a template engine, form helpers, or an ORM (although those are easy enough to add yourself). When you sit down to write a web service with Falcon, you choose your own adventure in terms of async I/O, serialization, data access, etc. In fact, Falcon only has two dependencies: `six`_, to make it easier to support both Python 2 and 3, and `mimeparse`_ for handling complex Accept headers. Neither of these packages pull in any further dependencies of their own. - -Third, Falcon eschews magic. When you use the framework, it's pretty obvious which inputs lead to which outputs. Also, it's blatantly obvious where variables originate. All this makes it easier to reason about the code and to debug edge cases in large-scale deployments of your application. - -.. _`six`: http://pythonhosted.org/six/ -.. _`mimeparse`: https://code.google.com/p/mimeparse/ - - -About Apache 2.0 ----------------- - -Falcon is released under the terms of the `Apache 2.0 License`_. This means that you can use it in your commercial applications without having to also open-source your own code. It also means that if someone happens to contribute code that is associated with a patent, you are granted a free license to use said patent. That's a pretty sweet deal. - -Now, if you do make changes to Falcon itself, please consider contributing your awesome work back to the community. - -.. _`Apache 2.0 License`: http://opensource.org/licenses/Apache-2.0 - - -Falcon License --------------- - -.. include:: ../../LICENSE \ No newline at end of file diff -Nru python-falcon-1.0.0/doc/user/quickstart.rst python-falcon-1.4.1/doc/user/quickstart.rst --- python-falcon-1.0.0/doc/user/quickstart.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/quickstart.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,288 +0,0 @@ -.. _quickstart: - -Quickstart -========== - -If you haven't done so already, please take a moment to -:ref:`install ` the Falcon web framework before -continuing. - - -.. include:: big-picture-snip.rst - - -Learning by Example -------------------- - -Here is a simple example from Falcon's README, showing how to get -started writing an API: - -.. code:: python - - # things.py - - # Let's get this party started! - import falcon - - - # Falcon follows the REST architectural style, meaning (among - # other things) that you think in terms of resources and state - # transitions, which map to HTTP verbs. - class ThingsResource(object): - def on_get(self, req, resp): - """Handles GET requests""" - resp.status = falcon.HTTP_200 # This is the default status - resp.body = ('\nTwo things awe me most, the starry sky ' - 'above me and the moral law within me.\n' - '\n' - ' ~ Immanuel Kant\n\n') - - # falcon.API instances are callable WSGI apps - app = falcon.API() - - # Resources are represented by long-lived class instances - things = ThingsResource() - - # things will handle all requests to the '/things' URL path - app.add_route('/things', things) - -You can run the above example using any WSGI server, such as uWSGI -or Gunicorn. For example: - -.. code:: bash - - $ pip install gunicorn - $ gunicorn things:app - -Then, in another terminal: - -.. code:: bash - - $ curl localhost:8000/things - -.. _quickstart-more-features: - -More Features -------------- - -Here is a more involved example that demonstrates reading headers and query -parameters, handling errors, and working with request and response bodies. - -.. code:: python - - import json - import logging - import uuid - from wsgiref import simple_server - - import falcon - import requests - - - class StorageEngine(object): - - def get_things(self, marker, limit): - return [{'id': str(uuid.uuid4()), 'color': 'green'}] - - def add_thing(self, thing): - thing['id'] = str(uuid.uuid4()) - return thing - - - class StorageError(Exception): - - @staticmethod - def handle(ex, req, resp, params): - description = ('Sorry, couldn\'t write your thing to the ' - 'database. It worked on my box.') - - raise falcon.HTTPError(falcon.HTTP_725, - 'Database Error', - description) - - - class SinkAdapter(object): - - engines = { - 'ddg': 'https://duckduckgo.com', - 'y': 'https://search.yahoo.com/search', - } - - def __call__(self, req, resp, engine): - url = self.engines[engine] - params = {'q': req.get_param('q', True)} - result = requests.get(url, params=params) - - resp.status = str(result.status_code) + ' ' + result.reason - resp.content_type = result.headers['content-type'] - resp.body = result.text - - - class AuthMiddleware(object): - - def process_request(self, req, resp): - token = req.get_header('Authorization') - account_id = req.get_header('Account-ID') - - challenges = ['Token type="Fernet"'] - - if token is None: - description = ('Please provide an auth token ' - 'as part of the request.') - - raise falcon.HTTPUnauthorized('Auth token required', - description, - challenges, - href='http://docs.example.com/auth') - - if not self._token_is_valid(token, account_id): - description = ('The provided auth token is not valid. ' - 'Please request a new token and try again.') - - raise falcon.HTTPUnauthorized('Authentication required', - description, - challenges, - href='http://docs.example.com/auth') - - def _token_is_valid(self, token, account_id): - return True # Suuuuuure it's valid... - - - class RequireJSON(object): - - def process_request(self, req, resp): - if not req.client_accepts_json: - raise falcon.HTTPNotAcceptable( - 'This API only supports responses encoded as JSON.', - href='http://docs.examples.com/api/json') - - if req.method in ('POST', 'PUT'): - if 'application/json' not in req.content_type: - raise falcon.HTTPUnsupportedMediaType( - 'This API only supports requests encoded as JSON.', - href='http://docs.examples.com/api/json') - - - class JSONTranslator(object): - - def process_request(self, req, resp): - # req.stream corresponds to the WSGI wsgi.input environ variable, - # and allows you to read bytes from the request body. - # - # See also: PEP 3333 - if req.content_length in (None, 0): - # Nothing to do - return - - body = req.stream.read() - if not body: - raise falcon.HTTPBadRequest('Empty request body', - 'A valid JSON document is required.') - - try: - req.context['doc'] = json.loads(body.decode('utf-8')) - - except (ValueError, UnicodeDecodeError): - raise falcon.HTTPError(falcon.HTTP_753, - 'Malformed JSON', - 'Could not decode the request body. The ' - 'JSON was incorrect or not encoded as ' - 'UTF-8.') - - def process_response(self, req, resp, resource): - if 'result' not in req.context: - return - - resp.body = json.dumps(req.context['result']) - - - def max_body(limit): - - def hook(req, resp, resource, params): - length = req.content_length - if length is not None and length > limit: - msg = ('The size of the request is too large. The body must not ' - 'exceed ' + str(limit) + ' bytes in length.') - - raise falcon.HTTPRequestEntityTooLarge( - 'Request body is too large', msg) - - return hook - - - class ThingsResource(object): - - def __init__(self, db): - self.db = db - self.logger = logging.getLogger('thingsapp.' + __name__) - - def on_get(self, req, resp, user_id): - marker = req.get_param('marker') or '' - limit = req.get_param_as_int('limit') or 50 - - try: - result = self.db.get_things(marker, limit) - except Exception as ex: - self.logger.error(ex) - - description = ('Aliens have attacked our base! We will ' - 'be back as soon as we fight them off. ' - 'We appreciate your patience.') - - raise falcon.HTTPServiceUnavailable( - 'Service Outage', - description, - 30) - - # An alternative way of doing DRY serialization would be to - # create a custom class that inherits from falcon.Request. This - # class could, for example, have an additional 'doc' property - # that would serialize to JSON under the covers. - req.context['result'] = result - - resp.set_header('Powered-By', 'Falcon') - resp.status = falcon.HTTP_200 - - @falcon.before(max_body(64 * 1024)) - def on_post(self, req, resp, user_id): - try: - doc = req.context['doc'] - except KeyError: - raise falcon.HTTPBadRequest( - 'Missing thing', - 'A thing must be submitted in the request body.') - - proper_thing = self.db.add_thing(doc) - - resp.status = falcon.HTTP_201 - resp.location = '/%s/things/%s' % (user_id, proper_thing['id']) - - - # Configure your WSGI server to load "things.app" (app is a WSGI callable) - app = falcon.API(middleware=[ - AuthMiddleware(), - RequireJSON(), - JSONTranslator(), - ]) - - db = StorageEngine() - things = ThingsResource(db) - app.add_route('/{user_id}/things', things) - - # If a responder ever raised an instance of StorageError, pass control to - # the given handler. - app.add_error_handler(StorageError, StorageError.handle) - - # Proxy some things to another service; this example shows how you might - # send parts of an API off to a legacy system that hasn't been upgraded - # yet, or perhaps is a single cluster that all data centers have to share. - sink = SinkAdapter() - app.add_sink(sink, r'/search/(?Pddg|y)\Z') - - # Useful for debugging problems in your API; works with pdb.set_trace(). You - # can also use Gunicorn to host your app. Gunicorn can be configured to - # auto-restart workers when it detects a code change, and it also works - # with pdb. - if __name__ == '__main__': - httpd = simple_server.make_server('127.0.0.1', 8000, app) - httpd.serve_forever() diff -Nru python-falcon-1.0.0/doc/user/tutorial.rst python-falcon-1.4.1/doc/user/tutorial.rst --- python-falcon-1.0.0/doc/user/tutorial.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/doc/user/tutorial.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,662 +0,0 @@ -.. _tutorial: - -Tutorial -======== - -In this tutorial we'll walk through building an API for a simple image sharing -service. Along the way, we'll discuss Falcon's major features and introduce -the terminology used by the framework. - - -.. include:: big-picture-snip.rst - - -First Steps ------------ - -Before continuing, be sure you've got Falcon :ref:`installed `. Then, -create a new project folder called "look" and cd into it: - -.. code:: bash - - $ mkdir look - $ cd look - -Next, let's create a new file that will be the entry point into your app: - -.. code:: bash - - $ touch app.py - -Open that file in your favorite text editor and add the following lines: - -.. code:: python - - import falcon - - api = application = falcon.API() - -That creates your WSGI application and aliases it as ``api``. You can use any -variable names you like, but we'll use ``application`` since that is what -Gunicorn expects it to be called, by default. - -A WSGI application is just a callable with a well-defined signature so that -you can host the application with any web server that understands the `WSGI -protocol `_. Let's take a look -at the falcon.API class. - -First, install IPython (if you don't already have it), and fire it up: - -.. code:: bash - - $ pip install ipython - $ ipython - -Now, type the following to introspect the falcon.API callable: - -.. code:: bash - - In [1]: import falcon - - In [2]: falcon.API.__call__? - -Alternatively, you can use the built-in ``help`` function: - -.. code:: bash - - In [3]: help(falcon.API.__call__) - -Note the method signature. ``env`` and ``start_response`` are standard -WSGI params. Falcon adds a thin abstraction on top of these params -so you don't have to interact with them directly. - -The Falcon framework contains extensive inline documentation that you can -query using the above technique. The team has worked hard to optimize -the docstrings for readability, so that you can quickly scan them and find -what you need. - -.. tip:: - - `bpython `_ is another super- - powered REPL that is good to have in your toolbox when - exploring a new library. - - -Hosting Your App ----------------- - -Now that you have a simple Falcon app, you can take it for a spin with -a WSGI server. Python includes a reference server for self-hosting, but -let's use something that you would actually deploy in production. - -.. code:: bash - - $ pip install gunicorn - $ gunicorn app - -Now try querying it with curl: - -.. code:: bash - - $ curl localhost:8000 -v - -You should get a 404. That's actually OK, because we haven't specified any -routes yet. Note that Falcon includes a default 404 response handler that -will fire for any requested path that doesn't match any routes. - -Curl is a bit of a pain to use, so let's install -`HTTPie `_ and use it from now on. - -.. code:: bash - - $ pip install --upgrade httpie - $ http localhost:8000 - - -Creating Resources ------------------- - -Falcon borrows some of its terminology from the REST architectural -style, so if you are familiar with that mindset, Falcon should be familiar. -On the other hand, if you have no idea what REST is, no worries; Falcon -was designed to be as intuitive as possible for anyone who understands -the basics of HTTP. - -In Falcon, you map incoming requests to things called "Resources". A -Resource is just a regular Python class that includes some methods that -follow a certain naming convention. Each of these methods corresponds to -an action that the API client can request be performed in order to fetch -or transform the resource in question. - -Since we are building an image-sharing API, let's create an "images" -resource. Create a new file, ``images.py`` within your project directory, -and add the following to it: - -.. code:: python - - import falcon - - - class Resource(object): - - def on_get(self, req, resp): - resp.body = '{"message": "Hello world!"}' - resp.status = falcon.HTTP_200 - -As you can see, ``Resource`` is just a regular class. You can name the -class anything you like. Falcon uses duck-typing, so you don't need to -inherit from any sort of special base class. - -The image resource above defines a single method, ``on_get``. For any -HTTP method you want your resource to support, simply add an ``on_x`` -class method to the resource, where ``x`` is any one of the standard -HTTP methods, lowercased (e.g., ``on_get``, ``on_put``, ``on_head``, etc.). - -We call these well-known methods "responders". Each responder takes (at -least) two params, one representing the HTTP request, and one representing -the HTTP response to that request. By convention, these are called -``req`` and ``resp``, respectively. Route templates and hooks can inject extra -params, as we shall see later on. - -Right now, the image resource responds to GET requests with a simple -``200 OK`` and a JSON body. Falcon's Internet media type defaults to -``application/json`` but you can set it to whatever you like. For example, -you could use `MessagePack `_, or any other -serialization format. - -If you'd like to use MessagePack in the above example, you'll need to -install the (de)serializer for Python running ``pip install msgpack-python`` -and then update your responder to set the response data and content_type -accordingly: - -.. code:: python - - import falcon - - import msgpack - - - class Resource(object): - - def on_get(self, req, resp): - resp.data = msgpack.packb({'message': 'Hello world!'}) - resp.content_type = 'application/msgpack' - resp.status = falcon.HTTP_200 - -Note the use of ``resp.data`` in lieu of ``resp.body``. If you assign a -bytestring to the latter, Falcon will figure it out, but you can -get a little performance boost by assigning directly to ``resp.data``. - -OK, now let's wire up this resource and see it in action. Go back to -``app.py`` and modify it so it looks something like this: - -.. code:: python - - import falcon - - import images - - - api = application = falcon.API() - - images = images.Resource() - api.add_route('/images', images) - -Now, when a request comes in for "/images", Falcon will call the -responder on the images resource that corresponds to the requested -HTTP method. - -Restart gunicorn, and then try sending a GET request to the resource: - -.. code:: bash - - $ http GET localhost:8000/images - - -Request and Response Objects ----------------------------- - -Each responder in a resource receives a request object that can be used to -read the headers, query parameters, and body of the request. You can use -the help function mentioned earlier to list the Request class members: - -.. code:: bash - - In [1]: import falcon - - In [2]: help(falcon.Request) - -Each responder also receives a response object that can be used for setting -the status code, headers, and body of the response. You can list the -Response class members using the same technique used above: - -.. code:: bash - - In [3]: help(falcon.Response) - -Let's see how this works. When a client POSTs to our images collection, we -want to create a new image resource. First, we'll need to specify where the -images will be saved (for a real service, you would want to use an object -storage service instead, such as Cloud Files or S3). - -Edit your ``images.py`` file and add the following to the resource: - -.. code:: python - - def __init__(self, storage_path): - self.storage_path = storage_path - -Then, edit ``app.py`` and pass in a path to the resource initializer. - -Next, let's implement the POST responder: - -.. code:: python - - import os - import uuid - import mimetypes - - import falcon - - - class Resource(object): - - def __init__(self, storage_path): - self.storage_path = storage_path - - def on_post(self, req, resp): - ext = mimetypes.guess_extension(req.content_type) - filename = '{uuid}{ext}'.format(uuid=uuid.uuid4(), ext=ext) - image_path = os.path.join(self.storage_path, filename) - - with open(image_path, 'wb') as image_file: - while True: - chunk = req.stream.read(4096) - if not chunk: - break - - image_file.write(chunk) - - resp.status = falcon.HTTP_201 - resp.location = '/images/' + filename - -As you can see, we generate a unique ID and filename for the new image, and -then write it out by reading from ``req.stream``. It's called ``stream`` instead -of ``body`` to emphasize the fact that you are really reading from an input -stream; Falcon never spools or decodes request data, instead giving you direct -access to the incoming binary stream provided by the WSGI server. - -Note that we are setting the -`HTTP response status code `_ to "201 Created". For a full list of -predefined status strings, simply call ``help`` on ``falcon.status_codes``: - -.. code:: bash - - In [4]: help(falcon.status_codes) - -The last line in the ``on_post`` responder sets the Location header for the -newly created resource. (We will create a route for that path in just a -minute.) Note that the Request and Response classes contain convenience -attributes for reading and setting common headers, but you can always -access any header by name with the ``req.get_header`` and ``resp.set_header`` -methods. - -Restart gunicorn, and then try sending a POST request to the resource -(substituting test.jpg for a path to any JPEG you like.) - -.. code:: bash - - $ http POST localhost:8000/images Content-Type:image/jpeg < test.jpg - -Now, if you check your storage directory, it should contain a copy of the -image you just POSTed. - -.. _tutorial-serving-images: - -Serving Images --------------- - -Now that we have a way of getting images into the service, we need a way -to get them back out. What we want to do is return an image when it is -requested using the path that came back in the Location header, like so: - -.. code:: bash - - $ http GET localhost:8000/images/87db45ff42 - -Now, we could add an ``on_get`` responder to our images resource, and that is -fine for simple resources like this, but that approach can lead to problems -when you need to respond differently to the same HTTP method (e.g., GET), -depending on whether the user wants to interact with a collection -of things, or a single thing. - -With that in mind, let's create a separate class to represent a single image, -as opposed to a collection of images. We will then add an ``on_get`` responder -to the new class. - -Go ahead and edit your ``images.py`` file to look something like this: - -.. code:: python - - import os - import uuid - import mimetypes - - import falcon - - - class Collection(object): - - def __init__(self, storage_path): - self.storage_path = storage_path - - def on_post(self, req, resp): - ext = mimetypes.guess_extension(req.content_type) - filename = '{uuid}{ext}'.format(uuid=uuid.uuid4(), ext=ext) - image_path = os.path.join(self.storage_path, filename) - - with open(image_path, 'wb') as image_file: - while True: - chunk = req.stream.read(4096) - if not chunk: - break - - image_file.write(chunk) - - resp.status = falcon.HTTP_201 - resp.location = '/images/' + filename - - - class Item(object): - - def __init__(self, storage_path): - self.storage_path = storage_path - - def on_get(self, req, resp, name): - resp.content_type = mimetypes.guess_type(name)[0] - image_path = os.path.join(self.storage_path, name) - resp.stream = open(image_path, 'rb') - resp.stream_len = os.path.getsize(image_path) - -As you can see, we renamed ``Resource`` to ``Collection`` and added a new ``Item`` -class to represent a single image resource. Also, note the ``name`` parameter -for the ``on_get`` responder. Any URI parameters that you specify in your routes -will be turned into corresponding kwargs and passed into the target responder as -such. We'll see how to specify URI parameters in a moment. - -Inside the ``on_get`` responder, -we set the Content-Type header based on the filename extension, and then -stream out the image directly from an open file handle. Note the use of -``resp.stream_len``. Whenever using ``resp.stream`` instead of ``resp.body`` or -``resp.data``, you have to also specify the expected length of the stream so -that the web client knows how much data to read from the response. - -.. note:: If you do not know the size of the stream in advance, you can work around - that by using chunked encoding, but that's beyond the scope of this - tutorial. - -If ``resp.status`` is not set explicitly, it defaults to ``200 OK``, which is -exactly what we want the ``on_get`` responder to do. - -Now, let's wire things up and give this a try. Go ahead and edit ``app.py`` to -look something like this: - -.. code:: python - - import falcon - - import images - - - api = application = falcon.API() - - storage_path = '/usr/local/var/look' - - image_collection = images.Collection(storage_path) - image = images.Item(storage_path) - - api.add_route('/images', image_collection) - api.add_route('/images/{name}', image) - -As you can see, we specified a new route, ``/images/{name}``. This causes -Falcon to expect all associated responders to accept a ``name`` -argument. - -.. note:: - - Falcon also supports more complex parameterized path segments containing - multiple values. For example, a GH-like API could use the following - template to add a route for diffing two branches:: - - /repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1} - -Now, restart gunicorn and post another picture to the service: - -.. code:: bash - - $ http POST localhost:8000/images Content-Type:image/jpeg < test.jpg - -Make a note of the path returned in the Location header, and use it to -try GETing the image: - -.. code:: bash - - $ http localhost:8000/images/6daa465b7b.jpeg - -HTTPie won't download the image by default, but you can see that the response -headers were set correctly. Just for fun, go ahead and paste the above URI -into your web browser. The image should display correctly. - - -.. Query Strings -.. ------------- - -.. *Coming soon...* - -Introducing Hooks ------------------ - -At this point you should have a pretty good understanding of the basic parts -that make up a Falcon-based API. Before we finish up, let's just take a few -minutes to clean up the code and add some error handling. - -First of all, let's check the incoming media type when something is posted -to make sure it is a common image type. We'll do this by using a Falcon -``before`` hook. - -First, let's define a list of media types our service will accept. Place this -constant near the top, just after the import statements in ``images.py``: - -.. code:: python - - ALLOWED_IMAGE_TYPES = ( - 'image/gif', - 'image/jpeg', - 'image/png', - ) - -The idea here is to only accept GIF, JPEG, and PNG images. You can add others -to the list if you like. - -Next, let's create a hook that will run before each request to post a -message. Add this method below the definition of ``ALLOWED_IMAGE_TYPES``: - -.. code:: python - - def validate_image_type(req, resp, resource, params): - if req.content_type not in ALLOWED_IMAGE_TYPES: - msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' - raise falcon.HTTPBadRequest('Bad request', msg) - -And then attach the hook to the ``on_post`` responder like so: - -.. code:: python - - @falcon.before(validate_image_type) - def on_post(self, req, resp): - -Now, before every call to that responder, Falcon will first invoke the -``validate_image_type`` method. There isn't anything special about that -method, other than it must accept four arguments. Every hook takes, as its -first two arguments, a reference to the same ``req`` and ``resp`` objects -that are passed into responders. ``resource`` argument is a Resource instance -associated with the request. The fourth argument, named ``params`` by -convention, is a reference to the kwarg dictionary Falcon creates for each -request. ``params`` will contain the route's URI template params and their -values, if any. - -As you can see in the example above, you can use ``req`` to get information -about the incoming request. However, you can also use ``resp`` to play with -the HTTP response as needed, and you can even inject extra kwargs for -responders in a DRY way, e.g.,: - -.. code:: python - - def extract_project_id(req, resp, resource, params): - """Adds `project_id` to the list of params for all responders. - - Meant to be used as a `before` hook. - """ - params['project_id'] = req.get_header('X-PROJECT-ID') - -Now, you can imagine that such a hook should apply to all responders for -a resource. You can apply hooks to an entire resource like so: - -.. code:: python - - @falcon.before(extract_project_id) - class Message(object): - - # ... - -Similar logic can be applied globally with middleware. -(See :ref:`falcon.middleware `) - -To learn more about hooks, take a look at the docstring for the ``API`` class, -as well the docstrings for the ``falcon.before`` and ``falcon.after`` decorators. - -Now that you've added a hook to validate the media type when an image is -POSTed, you can see it in action by passing in something nefarious: - -.. code:: bash - - $ http POST localhost:8000/images Content-Type:image/jpx < test.jpx - -That should return a ``400 Bad Request`` status and a nicely structured -error body. When something goes wrong, you usually want to give your users -some info to help them resolve the issue. The exception to this rule is when -an error occurs because the user is requested something they are not -authorized to access. In that case, you may wish to simply return -``404 Not Found`` with an empty body, in case a malicious user is fishing -for information that will help them crack your API. - -Error Handling --------------- - -Generally speaking, Falcon assumes that resource responders (*on_get*, -*on_post*, etc.) will, for the most part, do the right thing. In other words, -Falcon doesn't try very hard to protect responder code from itself. - -This approach reduces the number of (often) extraneous checks that Falcon -would otherwise have to perform, making the framework more efficient. With -that in mind, writing a high-quality API based on Falcon requires that: - -1. Resource responders set response variables to sane values. -2. Your code is well-tested, with high code coverage. -3. Errors are anticipated, detected, and handled appropriately within each - responder. - -.. tip:: - Falcon will re-raise errors that do not inherit from ``falcon.HTTPError`` - unless you have registered a custom error handler for that type - (see also: :ref:`falcon.API `). - -Speaking of error handling, when something goes horribly (or mildly) wrong, -you *could* manually set the error status, appropriate response headers, and -even an error body using the ``resp`` object. However, Falcon tries to make -things a bit easier by providing a set of exceptions you can raise when -something goes wrong. In fact, if Falcon catches any exception your responder -throws that inherits from ``falcon.HTTPError``, the framework will convert -that exception to an appropriate HTTP error response. - -You may raise an instance of ``falcon.HTTPError``, or use any one -of a number of predefined error classes that try to do "the right thing" in -setting appropriate headers and bodies. Have a look at the docs for -any of the following to get more information on how you can use them in your -API: - -.. code:: python - - falcon.HTTPBadGateway - falcon.HTTPBadRequest - falcon.HTTPConflict - falcon.HTTPError - falcon.HTTPForbidden - falcon.HTTPInternalServerError - falcon.HTTPLengthRequired - falcon.HTTPMethodNotAllowed - falcon.HTTPNotAcceptable - falcon.HTTPNotFound - falcon.HTTPPreconditionFailed - falcon.HTTPRangeNotSatisfiable - falcon.HTTPServiceUnavailable - falcon.HTTPUnauthorized - falcon.HTTPUnsupportedMediaType - falcon.HTTPUpgradeRequired - -For example, you could handle a missing image file like this: - -.. code:: python - - try: - resp.stream = open(image_path, 'rb') - except IOError: - raise falcon.HTTPNotFound() - -Or you could handle a bogus filename like this: - -.. code:: python - - VALID_IMAGE_NAME = re.compile(r'[a-f0-9]{10}\.(jpeg|gif|png)$') - - # ... - - class Item(object): - - def __init__(self, storage_path): - self.storage_path = storage_path - - def on_get(self, req, resp, name): - if not VALID_IMAGE_NAME.match(name): - raise falcon.HTTPNotFound() - -Sometimes you don't have much control over the type of exceptions that get -raised. To address this, Falcon lets you create custom handlers for any type -of error. For example, if your database throws exceptions that inherit from -NiftyDBError, you can install a special error handler just for NiftyDBError, -so you don't have to copy-paste your handler code across multiple responders. - -Have a look at the docstring for ``falcon.API.add_error_handler`` for more -information on using this feature to DRY up your code: - -.. code:: python - - In [71]: help(falcon.API.add_error_handler) - -What Now? ---------- - -Our friendly community is available to answer your questions and help you -work through sticky problems. See also: :ref:`Getting Help `. - -As mentioned previously, Falcon's docstrings are quite extensive, and so you -can learn a lot just by poking around Falcon's modules from a Python REPL, -such as `IPython `_ or -`bpython `_. - -Also, don't be shy about pulling up Falcon's source code on GitHub or in your -favorite text editor. The team has tried to make the code as straightforward -and readable as possible; where other documentation may fall short, the code basically -"can't be wrong." - - diff -Nru python-falcon-1.0.0/docker/benchmark.sh python-falcon-1.4.1/docker/benchmark.sh --- python-falcon-1.0.0/docker/benchmark.sh 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docker/benchmark.sh 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +#!/bin/sh + +echo "Installed Packages:\n==================" +pip list + +echo "\nBenchmark:\n==================" +falcon-bench diff -Nru python-falcon-1.0.0/docker/bench_py27_pip.Dockerfile python-falcon-1.4.1/docker/bench_py27_pip.Dockerfile --- python-falcon-1.0.0/docker/bench_py27_pip.Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docker/bench_py27_pip.Dockerfile 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,8 @@ +FROM python:2.7 +MAINTAINER Falcon Framework Maintainers + +RUN pip install falcon bottle django flask pecan +COPY ./benchmark.sh /benchmark.sh + +CMD /benchmark.sh + diff -Nru python-falcon-1.0.0/docker/bench_py35_pip.Dockerfile python-falcon-1.4.1/docker/bench_py35_pip.Dockerfile --- python-falcon-1.0.0/docker/bench_py35_pip.Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docker/bench_py35_pip.Dockerfile 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +FROM python:3.5 +MAINTAINER Falcon Framework Maintainers + +RUN pip install falcon bottle django flask pecan +COPY ./benchmark.sh /benchmark.sh + +CMD /benchmark.sh diff -Nru python-falcon-1.0.0/docker/bench_pypy2_pip.Dockerfile python-falcon-1.4.1/docker/bench_pypy2_pip.Dockerfile --- python-falcon-1.0.0/docker/bench_pypy2_pip.Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docker/bench_pypy2_pip.Dockerfile 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +FROM pypy:2 +MAINTAINER Falcon Framework Maintainers + +RUN pip install falcon bottle django flask pecan +COPY ./benchmark.sh benchmark.sh + +CMD /benchmark.sh diff -Nru python-falcon-1.0.0/docker/Makefile python-falcon-1.4.1/docker/Makefile --- python-falcon-1.0.0/docker/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docker/Makefile 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,6 @@ +all: build-benchmark-images + +build-benchmark-images: + sudo docker build -t falconry/falcon-bench:py27-pip -f bench_py27_pip.Dockerfile ./ + sudo docker build -t falconry/falcon-bench:py35-pip -f bench_py35_pip.Dockerfile ./ + sudo docker build -t falconry/falcon-bench:pypy2-pip -f bench_pypy2_pip.Dockerfile ./ diff -Nru python-falcon-1.0.0/docs/api/api.rst python-falcon-1.4.1/docs/api/api.rst --- python-falcon-1.0.0/docs/api/api.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/api.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,26 @@ +.. _api: + +The API Class +============= + +Falcon's API class is a WSGI "application" that you can host with any +standard-compliant WSGI server. + +.. code:: python + + import falcon + + app = falcon.API() + +.. autoclass:: falcon.API + :members: + +.. autoclass:: falcon.RequestOptions + :members: + +.. autoclass:: falcon.ResponseOptions + :members: + +.. _compiled_router_options: +.. autoclass:: falcon.routing.CompiledRouterOptions + :noindex: diff -Nru python-falcon-1.0.0/docs/api/cookies.rst python-falcon-1.4.1/docs/api/cookies.rst --- python-falcon-1.0.0/docs/api/cookies.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/cookies.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,116 @@ +.. _cookies: + +Cookies +------- + +Cookie support is available in Falcon version 0.3 or later. + +.. _getting-cookies: + +Getting Cookies +~~~~~~~~~~~~~~~ + +Cookies can be read from a request via the :py:attr:`~.Request.cookies` +request attribute: + +.. code:: python + + class Resource(object): + def on_get(self, req, resp): + + cookies = req.cookies + + if 'my_cookie' in cookies: + my_cookie_value = cookies['my_cookie'] + # .... + +The :py:attr:`~.Request.cookies` attribute is a regular +:py:class:`dict` object. + +.. tip :: + + The :py:attr:`~.Request.cookies` attribute returns a + copy of the response cookie dictionary. Assign it to a variable, as + shown in the above example, to improve performance when you need to + look up more than one cookie. + +.. _setting-cookies: + +Setting Cookies +~~~~~~~~~~~~~~~ + +Setting cookies on a response is done via :py:meth:`~.Response.set_cookie`. + +The :py:meth:`~.Response.set_cookie` method should be used instead of +:py:meth:`~.Response.set_header` or :py:meth:`~.Response.append_header`. +With :py:meth:`~.Response.set_header` you cannot set multiple headers +with the same name (which is how multiple cookies are sent to the client). +Furthermore, :py:meth:`~.Response.append_header` appends multiple values +to the same header field in a way that is not compatible with the special +format required by the `Set-Cookie` header. + +Simple example: + +.. code:: python + + class Resource(object): + def on_get(self, req, resp): + + # Set the cookie 'my_cookie' to the value 'my cookie value' + resp.set_cookie('my_cookie', 'my cookie value') + + +You can of course also set the domain, path and lifetime of the cookie. + +.. code:: python + + class Resource(object): + def on_get(self, req, resp): + # Set the maximum age of the cookie to 10 minutes (600 seconds) + # and the cookie's domain to 'example.com' + resp.set_cookie('my_cookie', 'my cookie value', + max_age=600, domain='example.com') + + +You can also instruct the client to remove a cookie with the +:py:meth:`~.Response.unset_cookie` method: + +.. code:: python + + class Resource(object): + def on_get(self, req, resp): + resp.set_cookie('bad_cookie', ':(') + + # Clear the bad cookie + resp.unset_cookie('bad_cookie') + +.. _cookie-secure-attribute: + +The Secure Attribute +~~~~~~~~~~~~~~~~~~~~ + +By default, Falcon sets the `secure` attribute for cookies. This +instructs the client to never transmit the cookie in the clear over +HTTP, in order to protect any sensitive data that cookie might +contain. If a cookie is set, and a subsequent request is made over +HTTP (rather than HTTPS), the client will not include that cookie in +the request. + +.. warning:: + + For this attribute to be effective, your web server or load + balancer will need to enforce HTTPS when setting the cookie, as + well as in all subsequent requests that require the cookie to be + sent back from the client. + +When running your application in a development environment, you can +disable this default behavior by setting +:py:attr:`~.ResponseOptions.secure_cookies_by_default` to ``False`` +via :any:`API.resp_options`. This lets you test your app locally +without having to set up TLS. You can make this option configurable to +easily switch between development and production environments. + +See also: `RFC 6265, Section 4.1.2.5`_ + +.. _RFC 6265, Section 4.1.2.5: + https://tools.ietf.org/html/rfc6265#section-4.1.2.5 diff -Nru python-falcon-1.0.0/docs/api/errors.rst python-falcon-1.4.1/docs/api/errors.rst --- python-falcon-1.0.0/docs/api/errors.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/errors.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,102 @@ +.. _errors: + +Error Handling +============== + +When it comes to error handling, you can always directly set the error +status, appropriate response headers, and error body using the ``resp`` +object. However, Falcon tries to make things a little easier by +providing a set of error classes you can raise when something goes +wrong. All of these classes inherit from :class:`~.HTTPError`. + +Falcon will convert any instance or subclass of :class:`~.HTTPError` +raised by a responder, hook, or middleware component into an appropriate +HTTP response. The default error serializer supports both JSON and XML. +If the client indicates acceptance of both JSON and XML with equal +weight, JSON will be chosen. Other media types may be supported by +overriding the default serializer via +:meth:`~.API.set_error_serializer`. + +.. note:: + + If a custom media type is used and the type includes a "+json" or + "+xml" suffix, the default serializer will convert the error to JSON + or XML, respectively. + +To customize what data is passed to the serializer, subclass +:class:`~.HTTPError` or any of its child classes, and override the +:meth:`~.HTTPError.to_dict` method. To also support XML, override the +:meth:`~.HTTPError.to_xml` method. For example:: + + class HTTPNotAcceptable(falcon.HTTPNotAcceptable): + + def __init__(self, acceptable): + description = ( + 'Please see "acceptable" for a list of media types ' + 'and profiles that are currently supported.' + ) + + super().__init__(description=description) + self._acceptable = acceptable + + def to_dict(self, obj_type=dict): + result = super().to_dict(obj_type) + result['acceptable'] = self._acceptable + + return result + +All classes are available directly in the ``falcon`` package namespace:: + + import falcon + + class MessageResource(object): + def on_get(self, req, resp): + + # ... + + raise falcon.HTTPBadRequest( + "TTL Out of Range", + "The message's TTL must be between 60 and 300 seconds, inclusive." + ) + + # ... + +Note also that any exception (not just instances of +:class:`~.HTTPError`) can be caught, logged, and otherwise handled +at the global level by registering one or more custom error handlers. +See also :meth:`~.API.add_error_handler` to learn more about this +feature. + +Base Class +---------- + +.. autoclass:: falcon.HTTPError + :members: + +Mixins +------ + +.. autoclass:: falcon.http_error.NoRepresentation + :members: + +.. _predefined_errors: + +Predefined Errors +----------------- + +.. automodule:: falcon + :members: HTTPBadRequest, + HTTPInvalidHeader, HTTPMissingHeader, + HTTPInvalidParam, HTTPMissingParam, + HTTPUnauthorized, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed, + HTTPNotAcceptable, HTTPConflict, HTTPGone, HTTPLengthRequired, + HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPUriTooLong, + HTTPUnsupportedMediaType, HTTPRangeNotSatisfiable, + HTTPUnprocessableEntity, HTTPLocked, HTTPFailedDependency, + HTTPPreconditionRequired, HTTPTooManyRequests, + HTTPRequestHeaderFieldsTooLarge, + HTTPUnavailableForLegalReasons, + HTTPInternalServerError, HTTPNotImplemented, HTTPBadGateway, + HTTPServiceUnavailable, HTTPGatewayTimeout, HTTPVersionNotSupported, + HTTPInsufficientStorage, HTTPLoopDetected, + HTTPNetworkAuthenticationRequired diff -Nru python-falcon-1.0.0/docs/api/hooks.rst python-falcon-1.4.1/docs/api/hooks.rst --- python-falcon-1.0.0/docs/api/hooks.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/hooks.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,80 @@ +.. _hooks: + +Hooks +===== + +Falcon supports *before* and *after* hooks. You install a hook simply by +applying one of the decorators below, either to an individual responder or +to an entire resource. + +For example, consider this hook that validates a POST request for +an image resource: + +.. code:: python + + def validate_image_type(req, resp, resource, params): + if req.content_type not in ALLOWED_IMAGE_TYPES: + msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' + raise falcon.HTTPBadRequest('Bad request', msg) + +You would attach this hook to an ``on_post`` responder like so: + +.. code:: python + + @falcon.before(validate_image_type) + def on_post(self, req, resp): + pass + +Or, suppose you had a hook that you would like to apply to *all* +responders for a given resource. In that case, you would simply +decorate the resource class: + +.. code:: python + + @falcon.before(extract_project_id) + class Message(object): + def on_post(self, req, resp, project_id): + pass + + def on_get(self, req, resp, project_id): + pass + +Note also that you can pass additional arguments to your hook function +as needed: + +.. code:: python + + def validate_image_type(req, resp, resource, params, allowed_types): + if req.content_type not in allowed_types: + msg = 'Image type not allowed.' + raise falcon.HTTPBadRequest('Bad request', msg) + + @falcon.before(validate_image_type, ['image/png']) + def on_post(self, req, resp): + pass + +Falcon supports using any callable as a hook. This allows for using a class +instead of a function: + +.. code:: python + + class Authorize(object): + def __init__(self, roles): + self._roles = roles + + def __call__(self, req, resp, resource, params): + pass + + @falcon.before(Authorize(['admin'])) + def on_post(self, req, resp): + pass + + +Falcon :ref:`middleware components ` can also be used to insert +logic before and after requests. However, unlike hooks, +:ref:`middleware components ` are triggered **globally** for all +requests. + +.. automodule:: falcon + :members: before, after + :undoc-members: diff -Nru python-falcon-1.0.0/docs/api/index.rst python-falcon-1.4.1/docs/api/index.rst --- python-falcon-1.0.0/docs/api/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/index.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,18 @@ +Classes and Functions +===================== + +.. toctree:: + :maxdepth: 2 + + api + request_and_response + cookies + status + errors + media + redirects + middleware + hooks + routing + util + testing diff -Nru python-falcon-1.0.0/docs/api/media.rst python-falcon-1.4.1/docs/api/media.rst --- python-falcon-1.0.0/docs/api/media.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/media.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,162 @@ +.. _media: + +Media +===== + +Falcon allows for easy and customizable internet media type handling. By default +Falcon only enables a single JSON handler. However, additional handlers +can be configured through the :any:`falcon.RequestOptions` and +:any:`falcon.ResponseOptions` objects specified on your :any:`falcon.API`. + +.. note:: + + To avoid unnecessary overhead, Falcon will only process request media + the first time the media property is referenced. Once it has been + referenced, it'll use the cached result for subsequent interactions. + +Usage +----- + +Zero configuration is needed if you're creating a JSON API. Just access +or set the ``media`` attribute as appropriate and let Falcon do the heavy +lifting for you. + +.. code:: python + + import falcon + + + class EchoResource(object): + def on_post(self, req, resp): + message = req.media.get('message') + + resp.media = {'message': message} + resp.status = falcon.HTTP_200 + +.. warning:: + + Once `media` is called on a request, it'll consume the request's stream. + +Validating Media +---------------- + +Falcon currently only provides a JSON Schema media validator; however, +JSON Schema is very versatile and can be used to validate any deserialized +media type that JSON also supports (i.e. dicts, lists, etc). + +.. autofunction:: falcon.media.validators.jsonschema.validate + +If JSON Schema does not meet your needs, a custom validator may be +implemented in a similar manner to the one above. + +Content-Type Negotiation +------------------------ + +Falcon currently only supports partial negotiation out of the box. By default, +when the ``media`` attribute is used it attempts to de/serialize based on the +``Content-Type`` header value. The missing link that Falcon doesn't provide +is the connection between the :any:`falcon.Request` ``Accept`` header provided +by a user and the :any:`falcon.Response` ``Content-Type`` header. + +If you do need full negotiation, it is very easy to bridge the gap using +middleware. Here is an example of how this can be done: + +.. code-block:: python + + class NegotiationMiddleware(object): + def process_request(self, req, resp): + resp.content_type = req.accept + + +Replacing the Default Handlers +------------------------------ + +When creating your API object you can either add or completely +replace all of the handlers. For example, lets say you want to write an API +that sends and receives MessagePack. We can easily do this by telling our +Falcon API that we want a default media-type of ``application/msgpack`` and +then create a new :any:`Handlers` object specifying the desired media type and +a handler that can process that data. + +.. code:: python + + import falcon + from falcon import media + + + handlers = media.Handlers({ + 'application/msgpack': media.MessagePackHandler(), + }) + + api = falcon.API(media_type='application/msgpack') + + api.req_options.media_handlers = handlers + api.resp_options.media_handlers = handlers + +Alternatively, if you would like to add an additional handler such as +MessagePack, this can be easily done in the following manner: + +.. code-block:: python + + import falcon + from falcon import media + + + extra_handlers = { + 'application/msgpack': media.MessagePackHandler(), + } + + api = falcon.API() + + api.req_options.media_handlers.update(extra_handlers) + api.resp_options.media_handlers.update(extra_handlers) + + +Supported Handler Types +----------------------- + +.. autoclass:: falcon.media.JSONHandler + :members: + +.. autoclass:: falcon.media.MessagePackHandler + :members: + +Custom Handler Type +------------------- + +If Falcon doesn't have an internet media type handler that supports your +use case, you can easily implement your own using the abstract base class +provided by Falcon: + +.. autoclass:: falcon.media.BaseHandler + :members: + :member-order: bysource + + +Handlers +-------- + +.. autoclass:: falcon.media.Handlers + :members: + + +.. _media_type_constants: + +Media Type Constants +-------------------- + +The ``falcon`` module provides a number of constants for +common media types, including the following: + +.. code:: python + + falcon.MEDIA_JSON + falcon.MEDIA_MSGPACK + falcon.MEDIA_YAML + falcon.MEDIA_XML + falcon.MEDIA_HTML + falcon.MEDIA_JS + falcon.MEDIA_TEXT + falcon.MEDIA_JPEG + falcon.MEDIA_PNG + falcon.MEDIA_GIF diff -Nru python-falcon-1.0.0/docs/api/middleware.rst python-falcon-1.4.1/docs/api/middleware.rst --- python-falcon-1.0.0/docs/api/middleware.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/middleware.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,130 @@ +.. _middleware: + +Middleware +========== + +Middleware components provide a way to execute logic before the +framework routes each request, after each request is routed but before +the target responder is called, or just before the response is returned +for each request. Components are registered with the `middleware` kwarg +when instantiating Falcon's :ref:`API class `. + +.. Note:: + Unlike hooks, middleware methods apply globally to the entire API. + +Falcon's middleware interface is defined as follows: + +.. code:: python + + class ExampleComponent(object): + def process_request(self, req, resp): + """Process the request before routing it. + + Args: + req: Request object that will eventually be + routed to an on_* responder method. + resp: Response object that will be routed to + the on_* responder. + """ + + def process_resource(self, req, resp, resource, params): + """Process the request after routing. + + Note: + This method is only called when the request matches + a route to a resource. + + Args: + req: Request object that will be passed to the + routed responder. + resp: Response object that will be passed to the + responder. + resource: Resource object to which the request was + routed. + params: A dict-like object representing any additional + params derived from the route's URI template fields, + that will be passed to the resource's responder + method as keyword arguments. + """ + + def process_response(self, req, resp, resource, req_succeeded): + """Post-processing of the response (after routing). + + Args: + req: Request object. + resp: Response object. + resource: Resource object to which the request was + routed. May be None if no route was found + for the request. + req_succeeded: True if no exceptions were raised while + the framework processed and routed the request; + otherwise False. + """ + +.. Tip:: + Because *process_request* executes before routing has occurred, if a + component modifies ``req.path`` in its *process_request* method, + the framework will use the modified value to route the request. + +.. Tip:: + The *process_resource* method is only called when the request matches + a route to a resource. To take action when a route is not found, a + :py:meth:`sink ` may be used instead. + +Each component's *process_request*, *process_resource*, and +*process_response* methods are executed hierarchically, as a stack, following +the ordering of the list passed via the `middleware` kwarg of +:ref:`falcon.API`. For example, if a list of middleware objects are +passed as ``[mob1, mob2, mob3]``, the order of execution is as follows:: + + mob1.process_request + mob2.process_request + mob3.process_request + mob1.process_resource + mob2.process_resource + mob3.process_resource + + mob3.process_response + mob2.process_response + mob1.process_response + +Note that each component need not implement all `process_*` +methods; in the case that one of the three methods is missing, +it is treated as a noop in the stack. For example, if ``mob2`` did +not implement *process_request* and ``mob3`` did not implement +*process_response*, the execution order would look +like this:: + + mob1.process_request + _ + mob3.process_request + mob1.process_resource + mob2.process_resource + mob3.process_resource + + _ + mob2.process_response + mob1.process_response + +If one of the *process_request* middleware methods raises an +error, it will be processed according to the error type. If +the type matches a registered error handler, that handler will +be invoked and then the framework will begin to unwind the +stack, skipping any lower layers. The error handler may itself +raise an instance of HTTPError, in which case the framework +will use the latter exception to update the *resp* object. +Regardless, the framework will continue unwinding the middleware +stack. For example, if *mob2.process_request* were to raise an +error, the framework would execute the stack as follows:: + + mob1.process_request + mob2.process_request + + mob2.process_response + mob1.process_response + +Finally, if one of the *process_response* methods raises an error, +or the routed on_* responder method itself raises an error, the +exception will be handled in a similar manner as above. Then, +the framework will execute any remaining middleware on the +stack. diff -Nru python-falcon-1.0.0/docs/api/redirects.rst python-falcon-1.4.1/docs/api/redirects.rst --- python-falcon-1.0.0/docs/api/redirects.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/redirects.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,17 @@ +.. _redirects: + +Redirection +=========== + +Falcon defines a set of exceptions that can be raised within a +middleware method, hook, or responder in order to trigger +a 3xx (Redirection) response to the client. Raising one of these +classes short-circuits request processing in a manner similar to +raising an instance or subclass of :py:class:`~.HTTPError` + +Redirects +--------- + +.. automodule:: falcon + :members: HTTPMovedPermanently, HTTPFound, HTTPSeeOther, + HTTPTemporaryRedirect, HTTPPermanentRedirect diff -Nru python-falcon-1.0.0/docs/api/request_and_response.rst python-falcon-1.4.1/docs/api/request_and_response.rst --- python-falcon-1.0.0/docs/api/request_and_response.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/request_and_response.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,36 @@ +.. _request: + +Request & Response +================== + +Instances of the Request and Response classes are passed into responders as the second +and third arguments, respectively. + +.. code:: python + + import falcon + + + class Resource(object): + + def on_get(self, req, resp): + resp.body = '{"message": "Hello world!"}' + resp.status = falcon.HTTP_200 + +Request +------- + +.. autoclass:: falcon.Request + :members: + +.. autoclass:: falcon.Forwarded + :members: + +Response +-------- + +.. autoclass:: falcon.Response + :members: + + + diff -Nru python-falcon-1.0.0/docs/api/routing.rst python-falcon-1.4.1/docs/api/routing.rst --- python-falcon-1.0.0/docs/api/routing.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/routing.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,292 @@ +.. _routing: + +Routing +======= + +Falcon routes incoming requests to resources based on a set of URI +templates. If the path requested by the client matches the template for +a given route, the request is then passed on to the associated resource +for processing. + +If no route matches the request, control then passes to a default +responder that simply raises an instance of :class:`~.HTTPNotFound`. +Normally this will result in sending a 404 response back to the +client. + +Here's a quick example to show how all the pieces fit together: + +.. code:: python + + import json + + import falcon + + class ImagesResource(object): + + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + # Create a JSON representation of the resource + resp.body = json.dumps(doc, ensure_ascii=False) + + # The following line can be omitted because 200 is the default + # status returned by the framework, but it is included here to + # illustrate how this may be overridden as needed. + resp.status = falcon.HTTP_200 + + api = application = falcon.API() + + images = ImagesResource() + api.add_route('/images', images) + + +Default Router +-------------- + +Falcon's default routing engine is based on a decision tree that is +first compiled into Python code, and then evaluated by the runtime. + +The :meth:`~.API.add_route` method is used to associate a URI template +with a resource. Falcon then maps incoming requests to resources +based on these templates. + +Falcon's default router uses Python classes to represent resources. In +practice, these classes act as controllers in your application. They +convert an incoming request into one or more internal actions, and then +compose a response back to the client based on the results of those +actions. (See also: +:ref:`Tutorial: Creating Resources `) + +.. code:: + + ┌────────────┐ + request → │ │ + │ Resource │ ↻ Orchestrate the requested action + │ Controller │ ↻ Compose the result + response ← │ │ + └────────────┘ + +Each resource class defines various "responder" methods, one for each +HTTP method the resource allows. Responder names start with ``on_`` and +are named according to which HTTP method they handle, as in ``on_get()``, +``on_post()``, ``on_put()``, etc. + +.. note:: + If your resource does not support a particular + HTTP method, simply omit the corresponding responder and + Falcon will use a default responder that raises + an instance of :class:`~.HTTPMethodNotAllowed` when that + method is requested. Normally this results in sending a + 405 response back to the client. + +Responders must always define at least two arguments to receive +:class:`~.Request` and :class:`~.Response` objects, respectively:: + + def on_post(self, req, resp): + pass + +The :class:`~.Request` object represents the incoming HTTP +request. It exposes properties and methods for examining headers, +query string parameters, and other metadata associated with +the request. A file-like stream object is also provided for reading +any data that was included in the body of the request. + +The :class:`~.Response` object represents the application's +HTTP response to the above request. It provides properties +and methods for setting status, header and body data. The +:class:`~.Response` object also exposes a dict-like +:attr:`~.Response.context` property for passing arbitrary +data to hooks and middleware methods. + +.. note:: + Rather than directly manipulate the :class:`~.Response` + object, a responder may raise an instance of either + :class:`~.HTTPError` or :class:`~.HTTPStatus`. Falcon will + convert these exceptions to appropriate HTTP responses. + Alternatively, you can handle them youself via + :meth:`~.API.add_error_handler`. + +In addition to the standard `req` and `resp` parameters, if the +route's template contains field expressions, any responder that +desires to receive requests for that route must accept arguments +named after the respective field names defined in the template. + +A field expression consists of a bracketed field name. For +example, given the following template:: + + /user/{name} + +A PUT request to "/user/kgriffs" would be routed to: + +.. code:: python + + def on_put(self, req, resp, name): + pass + +Because field names correspond to argument names in responder +methods, they must be valid Python identifiers. + +Individual path segments may contain one or more field +expressions, and fields need not span the entire path +segment. For example:: + + /repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1} + /serviceRoot/People('{name}') + +(See also the :ref:`Falcon tutorial ` for additional examples +and a walkthough of setting up routes within the context of a sample +application.) + +.. _routing_field_converters: + +Field Converters +---------------- + +Falcon's default router supports the use of field converters to +transform a URI template field value. Field converters may also perform +simple input validation. For example, the following URI template uses +the `int` converter to convert the value of `tid` to a Python ``int``, +but only if it has exactly eight digits:: + + /teams/{tid:int(8)} + +If the value is malformed and can not be converted, Falcon will reject +the request with a 404 response to the client. + +Converters are instantiated with the argument specification given in the +field expression. These specifications follow the standard Python syntax +for passing arguments. For example, the comments in the following code +show how a converter would be instantiated given different argument +specifications in the URI template: + +.. code:: python + + # IntConverter() + api.add_route( + '/a/{some_field:int}', + some_resource + ) + + # IntConverter(8) + api.add_route( + '/b/{some_field:int(8)}', + some_resource + ) + + # IntConverter(8, min=10000000) + api.add_route( + '/c/{some_field:int(8, min=10000000)}', + some_resource + ) + +Built-in Converters +------------------- + +============ ================================= ================================================================== + Identifier Class Example +============ ================================= ================================================================== + ``int`` :class:`~.IntConverter` ``/teams/{tid:int(8)}`` + ``uuid`` :class:`~.UUIDConverter` ``/diff/{left:uuid}...{right:uuid}`` + ``dt`` :class:`~.DateTimeConverter` ``/logs/{day:dt("%Y-%m-%d")}`` +============ ================================= ================================================================== + +| + +.. autoclass:: falcon.routing.IntConverter + :members: + +.. autoclass:: falcon.routing.UUIDConverter + :members: + +.. autoclass:: falcon.routing.DateTimeConverter + :members: + +Custom Converters +----------------- + +Custom converters can be registered via the +:attr:`~.CompiledRouterOptions.converters` router option. A converter is +simply a class that implements the ``BaseConverter`` interface: + +.. autoclass:: falcon.routing.BaseConverter + :members: + +.. _routing_custom: + +Custom Routers +-------------- + +A custom routing engine may be specified when instantiating +:py:meth:`falcon.API`. For example: + +.. code:: python + + router = MyRouter() + api = API(router=router) + +Custom routers may derive from the default :py:class:`~.CompiledRouter` +engine, or implement a completely different routing strategy (such as +object-based routing). + +A custom router is any class that implements the following interface: + +.. code:: python + + class MyRouter(object): + def add_route(self, uri_template, method_map, resource): + """Adds a route between URI path template and resource. + + Args: + uri_template (str): The URI template to add. + method_map (dict): A method map obtained by calling + falcon.routing.create_http_method_map. + resource (object): Instance of the resource class that + will handle requests for the given URI. + """ + + def find(self, uri, req=None): + """Search for a route that matches the given partial URI. + + Args: + uri(str): The requested path to route. + + Keyword Args: + req(Request): The Request object that will be passed to + the routed responder. The router may use `req` to + further differentiate the requested route. For + example, a header may be used to determine the + desired API version and route the request + accordingly. + + Note: + The `req` keyword argument was added in version + 1.2. To ensure backwards-compatibility, routers + that do not implement this argument are still + supported. + + Returns: + tuple: A 4-member tuple composed of (resource, method_map, + params, uri_template), or ``None`` if no route matches + the requested path. + + """ + +Routing Utilities +----------------- + +The *falcon.routing* module contains the following utilities that may +be used by custom routing engines. + +.. autofunction:: falcon.routing.map_http_methods + +.. autofunction:: falcon.routing.set_default_responders + +.. autofunction:: falcon.routing.create_http_method_map + +.. autofunction:: falcon.routing.compile_uri_template diff -Nru python-falcon-1.0.0/docs/api/status.rst python-falcon-1.4.1/docs/api/status.rst --- python-falcon-1.0.0/docs/api/status.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/status.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,183 @@ +.. _status: + +Status Codes +============ + +Falcon provides a list of constants for common +`HTTP response status codes `_. + +For example: + +.. code:: python + + # Override the default "200 OK" response status + resp.status = falcon.HTTP_409 + +Or, using the more verbose name: + +.. code:: python + + resp.status = falcon.HTTP_CONFLICT + +Using these constants helps avoid typos and cuts down on the number of +string objects that must be created when preparing responses. + +Falcon also provides a generic `HTTPStatus` class. Raise this class from a hook, +middleware, or a responder to stop handling the request and skip to the response +handling. It takes status, additional headers and body as input arguments. + +HTTPStatus +---------- + +.. autoclass:: falcon.HTTPStatus + :members: + + +1xx Informational +----------------- + +.. code:: python + + HTTP_CONTINUE = HTTP_100 + HTTP_SWITCHING_PROTOCOLS = HTTP_101 + HTTP_PROCESSING = HTTP_102 + + HTTP_100 = '100 Continue' + HTTP_101 = '101 Switching Protocols' + HTTP_102 = '102 Processing' + +2xx Success +----------- + +.. code:: python + + HTTP_OK = HTTP_200 + HTTP_CREATED = HTTP_201 + HTTP_ACCEPTED = HTTP_202 + HTTP_NON_AUTHORITATIVE_INFORMATION = HTTP_203 + HTTP_NO_CONTENT = HTTP_204 + HTTP_RESET_CONTENT = HTTP_205 + HTTP_PARTIAL_CONTENT = HTTP_206 + HTTP_MULTI_STATUS = HTTP_207 + HTTP_ALREADY_REPORTED = HTTP_208 + HTTP_IM_USED = HTTP_226 + + HTTP_200 = '200 OK' + HTTP_201 = '201 Created' + HTTP_202 = '202 Accepted' + HTTP_203 = '203 Non-Authoritative Information' + HTTP_204 = '204 No Content' + HTTP_205 = '205 Reset Content' + HTTP_206 = '206 Partial Content' + HTTP_207 = '207 Multi-Status' + HTTP_208 = '208 Already Reported' + HTTP_226 = '226 IM Used' + +3xx Redirection +--------------- + +.. code:: python + + HTTP_MULTIPLE_CHOICES = HTTP_300 + HTTP_MOVED_PERMANENTLY = HTTP_301 + HTTP_FOUND = HTTP_302 + HTTP_SEE_OTHER = HTTP_303 + HTTP_NOT_MODIFIED = HTTP_304 + HTTP_USE_PROXY = HTTP_305 + HTTP_TEMPORARY_REDIRECT = HTTP_307 + HTTP_PERMANENT_REDIRECT = HTTP_308 + + HTTP_300 = '300 Multiple Choices' + HTTP_301 = '301 Moved Permanently' + HTTP_302 = '302 Found' + HTTP_303 = '303 See Other' + HTTP_304 = '304 Not Modified' + HTTP_305 = '305 Use Proxy' + HTTP_307 = '307 Temporary Redirect' + HTTP_308 = '308 Permanent Redirect' + +4xx Client Error +---------------- + +.. code:: python + + HTTP_BAD_REQUEST = HTTP_400 + HTTP_UNAUTHORIZED = HTTP_401 # <-- Really means "unauthenticated" + HTTP_PAYMENT_REQUIRED = HTTP_402 + HTTP_FORBIDDEN = HTTP_403 # <-- Really means "unauthorized" + HTTP_NOT_FOUND = HTTP_404 + HTTP_METHOD_NOT_ALLOWED = HTTP_405 + HTTP_NOT_ACCEPTABLE = HTTP_406 + HTTP_PROXY_AUTHENTICATION_REQUIRED = HTTP_407 + HTTP_REQUEST_TIMEOUT = HTTP_408 + HTTP_CONFLICT = HTTP_409 + HTTP_GONE = HTTP_410 + HTTP_LENGTH_REQUIRED = HTTP_411 + HTTP_PRECONDITION_FAILED = HTTP_412 + HTTP_REQUEST_ENTITY_TOO_LARGE = HTTP_413 + HTTP_REQUEST_URI_TOO_LONG = HTTP_414 + HTTP_UNSUPPORTED_MEDIA_TYPE = HTTP_415 + HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = HTTP_416 + HTTP_EXPECTATION_FAILED = HTTP_417 + HTTP_IM_A_TEAPOT = HTTP_418 + HTTP_UNPROCESSABLE_ENTITY = HTTP_422 + HTTP_LOCKED = HTTP_423 + HTTP_FAILED_DEPENDENCY = HTTP_424 + HTTP_UPGRADE_REQUIRED = HTTP_426 + HTTP_PRECONDITION_REQUIRED = HTTP_428 + HTTP_TOO_MANY_REQUESTS = HTTP_429 + HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = HTTP_431 + HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = HTTP_451 + + HTTP_400 = '400 Bad Request' + HTTP_401 = '401 Unauthorized' # <-- Really means "unauthenticated" + HTTP_402 = '402 Payment Required' + HTTP_403 = '403 Forbidden' # <-- Really means "unauthorized" + HTTP_404 = '404 Not Found' + HTTP_405 = '405 Method Not Allowed' + HTTP_406 = '406 Not Acceptable' + HTTP_407 = '407 Proxy Authentication Required' + HTTP_408 = '408 Request Time-out' + HTTP_409 = '409 Conflict' + HTTP_410 = '410 Gone' + HTTP_411 = '411 Length Required' + HTTP_412 = '412 Precondition Failed' + HTTP_413 = '413 Payload Too Large' + HTTP_414 = '414 URI Too Long' + HTTP_415 = '415 Unsupported Media Type' + HTTP_416 = '416 Range Not Satisfiable' + HTTP_417 = '417 Expectation Failed' + HTTP_418 = "418 I'm a teapot" + HTTP_422 = "422 Unprocessable Entity" + HTTP_423 = '423 Locked' + HTTP_424 = '424 Failed Dependency' + HTTP_426 = '426 Upgrade Required' + HTTP_428 = '428 Precondition Required' + HTTP_429 = '429 Too Many Requests' + HTTP_431 = '431 Request Header Fields Too Large' + HTTP_451 = '451 Unavailable For Legal Reasons' + +5xx Server Error +---------------- + +.. code:: python + + HTTP_INTERNAL_SERVER_ERROR = HTTP_500 + HTTP_NOT_IMPLEMENTED = HTTP_501 + HTTP_BAD_GATEWAY = HTTP_502 + HTTP_SERVICE_UNAVAILABLE = HTTP_503 + HTTP_GATEWAY_TIMEOUT = HTTP_504 + HTTP_HTTP_VERSION_NOT_SUPPORTED = HTTP_505 + HTTP_INSUFFICIENT_STORAGE = HTTP_507 + HTTP_LOOP_DETECTED = HTTP_508 + HTTP_NETWORK_AUTHENTICATION_REQUIRED = HTTP_511 + + HTTP_500 = '500 Internal Server Error' + HTTP_501 = '501 Not Implemented' + HTTP_502 = '502 Bad Gateway' + HTTP_503 = '503 Service Unavailable' + HTTP_504 = '504 Gateway Time-out' + HTTP_505 = '505 HTTP Version not supported' + HTTP_507 = '507 Insufficient Storage' + HTTP_508 = '508 Loop Detected' + HTTP_511 = '511 Network Authentication Required' diff -Nru python-falcon-1.0.0/docs/api/testing.rst python-falcon-1.4.1/docs/api/testing.rst --- python-falcon-1.0.0/docs/api/testing.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/testing.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,23 @@ +.. _testing: + +Testing +======= + +Reference +--------- + +.. automodule:: falcon.testing + :members: Result, Cookie, + simulate_request, simulate_get, simulate_head, simulate_post, + simulate_put, simulate_options, simulate_patch, simulate_delete, + TestClient, TestCase, SimpleTestResource, StartResponseMock, + capture_responder_args, rand_string, create_environ, redirected + +Deprecated +---------- + +.. autoclass:: falcon.testing.TestBase + :members: + +.. autoclass:: falcon.testing.TestResource + :members: diff -Nru python-falcon-1.0.0/docs/api/util.rst python-falcon-1.4.1/docs/api/util.rst --- python-falcon-1.0.0/docs/api/util.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/api/util.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,21 @@ +.. _util: + +Utilities +========= + +URI Functions +------------- + +.. automodule:: falcon.uri + :members: encode, encode_value, decode, parse_host, + parse_query_string, unquote_string + +Miscellaneous +------------- + +.. automodule:: falcon + :members: deprecated, http_now, dt_to_http, http_date_to_dt, + to_query_str, get_http_status, get_bound_method + +.. autoclass:: falcon.TimezoneGMT + :members: diff -Nru python-falcon-1.0.0/docs/changes/0.2.0.rst python-falcon-1.4.1/docs/changes/0.2.0.rst --- python-falcon-1.0.0/docs/changes/0.2.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/0.2.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,100 @@ +Changelog for Falcon 0.2.0 +========================== + +Breaking Changes +---------------- + +- The deprecated util.misc.percent\_escape and + util.misc.percent\_unescape functions were removed. Please use the + functions in the util.uri module instead. +- The deprecated function, API.set\_default\_route, was removed. Please + use sinks instead. +- HTTPRangeNotSatisfiable no longer accepts a media\_type parameter. +- When using the comma-delimited list convention, + req.get\_param\_as\_list(...) will no longer insert placeholders, + using the None type, for empty elements. For example, where + previously the query string "foo=1,,3" would result in ['1', None, + '3'], it will now result in ['1', '3']. + +New & Improved +-------------- + +- Since 0.1 we've added proper RTD docs to make it easier for everyone + to get started with the framework. Over time we will continue adding + content, and we would love your help! +- Falcon now supports "wsgi.filewrapper". You can assign any file-like + object to resp.stream and Falcon will use "wsgi.filewrapper" to more + efficiently pipe the data to the WSGI server. +- Support was added for automatically parsing requests containing + "application/x-www-form-urlencoded" content. Form fields are now + folded into req.params. +- Custom Request and Response classes are now supported. You can + specify custom types when instantiating falcon.API. +- A new middleware feature was added to the framework. Middleware + deprecates global hooks, and we encourage everyone to migrate as soon + as possible. +- A general-purpose dict attribute was added to Request. Middleware, + hooks, and responders can now use req.context to share contextual + information about the current request. +- A new method, append\_header, was added to falcon.API to allow + setting multiple values for the same header using comma separation. + Note that this will not work for setting cookies, but we plan to + address this in the next release (0.3). +- A new "resource" attribute was added to hooks. Old hooks that do not + accept this new attribute are shimmed so that they will continue to + function. While we have worked hard to minimize the performance + impact, we recommend migrating to the new function signature to avoid + any overhead. +- Error response bodies now support XML in addition to JSON. In + addition, the HTTPError serialization code was refactored to make it + easier to implement a custom error serializer. +- A new method, "set\_error\_serializer" was added to falcon.API. You + can use this method to override Falcon's default HTTPError serializer + if you need to support custom media types. +- Falcon's testing base class, testing.TestBase was improved to + facilitate Py3k testing. Notably, TestBase.simulate\_request now + takes an additional "decode" kwarg that can be used to automatically + decode byte-string PEP-3333 response bodies. +- An "add\_link" method was added to the Response class. Apps can use + this method to add one or more Link header values to a response. +- Added two new properties, req.host and req.subdomain, to make it + easier to get at the hostname info in the request. +- Allow a wider variety of characters to be used in query string + params. +- Internal APIs have been refactored to allow overriding the default + routing mechanism. Further modularization is planned for the next + release (0.3). +- Changed req.get\_param so that it behaves the same whether a list was + specified in the query string using the HTML form style (in which + each element is listed in a separate 'key=val' field) or in the more + compact API style (in which each element is comma-separated and + assigned to a single param instance, as in 'key=val1,val2,val3') +- Added a convenience method, set\_stream(...), to the Response class + for setting the stream and its length at the same time, which should + help people not forget to set both (and save a few keystrokes along + the way). +- Added several new error classes, including HTTPRequestEntityTooLarge, + HTTPInvalidParam, HTTPMissingParam, HTTPInvalidHeader and + HTTPMissingHeader. +- Python 3.4 is now fully supported. +- Various minor performance improvements + +Fixed +----- + +- Ensure 100% test coverage and fix any bugs identified in the process. +- Fix not recognizing the "bytes=" prefix in Range headers. +- Make HTTPNotFound and HTTPMethodNotAllowed fully compliant, according + to RFC 7231. +- Fixed the default on\_options responder causing a Cython type error. +- URI template strings can now be of type unicode under Python 2. +- When SCRIPT\_NAME is not present in the WSGI environ, return an empty + string for the req.app property. +- Global "after" hooks will now be executed even when a responder + raises an error. +- Fixed several minor issues regarding testing.create\_environ(...) +- Work around a wsgiref quirk, where if no content-length header is + submitted by the client, wsgiref will set the value of that header to + an empty string in the WSGI environ. +- Resolved an issue causing several source files to not be Cythonized. +- Docstrings have been edited for clarity and correctness. diff -Nru python-falcon-1.0.0/docs/changes/0.3.0.rst python-falcon-1.4.1/docs/changes/0.3.0.rst --- python-falcon-1.0.0/docs/changes/0.3.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/0.3.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,41 @@ +Changelog for Falcon 0.3.0 +========================== + +Breaking Changes +---------------- +- Date headers are now returned as :py:class:`datetime.datetime` objects + instead of strings. + +New & Improved +-------------- + +- This release includes a new router architecture for improved performance + and flexibility. +- A custom router can now be specified when instantiating the + :py:class:`API` class. +- URI templates can now include multiple parameterized fields within a + single path segment. +- Falcon now supports reading and writing cookies. +- Falcon now supports Jython 2.7. +- A method for getting a query param as a date was added to the + :py:class:`Request` class. +- Date headers are now returned as :py:class:`datetime.datetime` objects. +- A default value can now be specified when calling + :py:meth:`Request.get_param`. This provides an alternative to using the + pattern:: + + value = req.get_param(name) or default_value + +- Friendly constants for status codes were added (e.g., + :py:attr:`falcon.HTTP_NO_CONTENT` vs. :py:attr:`falcon.HTTP_204`.) +- Several minor performance optimizations were made to the code base. + +Fixed +----- + +- The query string parser was modified to improve handling of percent-encoded + data. +- Several errors in the documentation were corrected. +- The :py:mod:`six` package was pinned to 1.4.0 or better. + :py:attr:`six.PY2` is required by Falcon, but that wasn't added to + :py:mod:`six` until version 1.4.0. diff -Nru python-falcon-1.0.0/docs/changes/1.0.0.rst python-falcon-1.4.1/docs/changes/1.0.0.rst --- python-falcon-1.0.0/docs/changes/1.0.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.0.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,152 @@ +Changelog for Falcon 1.0.0 +========================== + +Breaking Changes +---------------- +- The deprecated global hooks feature has been removed. + :class:`~falcon.API` no longer accepts `before` and `after` + kwargs. Applications can work around this by migrating any logic + contained in global hooks to reside in middleware components instead. +- The middleware method :meth:`process_resource` must now accept + an additional `params` argument. This gives the middleware method an + opportunity to interact with the values for any fields defined in a + route's URI template. +- The middleware method :meth:`process_resource` is now skipped when + no route is found for the incoming request. This avoids having to + include an ``if resource is not None`` check when implementing this + method. A sink may be used instead to execute logic in the case that + no route is found. +- An option was added to toggle automatic parsing of form params. Falcon + will no longer automatically parse, by default, requests that have the + content type "application/x-www-form-urlencoded". This was done to + avoid unintended side-effects that may arise from consuming the + request stream. It also makes it more straightforward for applications + to customize and extend the handling of form submissions. Applications + that require this functionality must re-enable it explicitly, by + setting a new request option that was added for that purpose, per the + example below:: + + app = falcon.API() + app.req_options.auto_parse_form_urlencoded = True + +- The :class:`~falcon.HTTPUnauthorized` initializer now requires an + additional argument, `challenges`. Per RFC 7235, a server returning a + 401 must include a WWW-Authenticate header field containing at least + one challenge. +- The performance of composing the response body was + improved. As part of this work, the :attr:`Response.body_encoded` + attribute was removed. This property was only intended to be used by + the framework itself, but any dependent code can be migrated per + the example below:: + + # Before + body = resp.body_encoded + + # After + if resp.body: + body = resp.body.encode('utf-8') + else: + body = b'' + +New & Improved +-------------- + +- A `code of conduct `_ + was added to solidify our community's commitment to sustaining a + welcoming, respectful culture. +- CPython 3.5 is now fully supported. +- The constants HTTP_422, HTTP_428, HTTP_429, HTTP_431, HTTP_451, and + HTTP_511 were added. +- The :class:`~falcon.HTTPUnprocessableEntity`, + :class:`~falcon.HTTPTooManyRequests`, and + :class:`~falcon.HTTPUnavailableForLegalReasons` error classes were + added. +- The :any:`HTTPStatus` class is now available directly under + the `falcon` module, and has been properly documented. +- Support for HTTP redirections was added via a set of + :any:`HTTPStatus` subclasses. This should avoid the problem + of hooks and responder methods possibly overriding the redirect. + Raising an instance of one of these new redirection classes will + short-circuit request processing, similar to raising an instance of + :class:`~falcon.HTTPError`. +- The default 404 responder now raises an instance of + :class:`~falcon.HTTPError` instead of manipulating the + response object directly. This makes it possible to customize the + response body using a custom error handler or serializer. +- A new method, :meth:`~falcon.Response.get_header`, was added to + :class:`~falcon.Response`. Previously there was no way to check if a + header had been set. The new :meth:`~falcon.Response.get_header` + method facilitates this and other use cases. +- :meth:`falcon.Request.client_accepts_msgpack` now recognizes + "application/msgpack", in addition to "application/x-msgpack". +- New :any:`access_route` and :any:`remote_addr` properties were added + to :class:`~falcon.Request` for getting upstream IP addresses. +- :class:`~falcon.Request` and :class:`~falcon.Response` now support + range units other than bytes. +- The :class:`~falcon.API` and + :class:`~falcon.testing.StartResponseMock` class types can now be + customized by inheriting from :class:`~falcon.testing.TestBase` and + overriding the `api_class` and `srmock_class` class attributes. +- Path segments with multiple field expressions may now be defined at + the same level as path segments having only a single field + expression. For example:: + + api.add_route('/files/{file_id}', resource_1) + api.add_route('/files/{file_id}.{ext}', resource_2) + +- Support was added to :any:`API.add_route()` for passing through + additional args and kwargs to custom routers. +- Digits and the underscore character are now allowed in the + :meth:`falcon.routing.compile_uri_template` helper, for use in custom + router implementations. +- A new testing framework was added that should be more intuitive to + use than the old one. Several of Falcon's own tests were ported to use + the new framework (the remainder to be ported in a + subsequent release.) The new testing framework performs wsgiref + validation on all requests. +- The performance of setting :attr:`Response.content_range` was + improved by ~50%. +- A new param, `obs_date`, was added to + :meth:`falcon.Request.get_header_as_datetime`, and defaults to + ``False``. This improves the method's performance when obsolete date + formats do not need to be supported. + +Fixed +----- + +- Field expressions at a given level in the routing tree no longer + mask alternative branches. When a single segment in a requested path + can match more than one node at that branch in the routing tree, and + the first branch taken happens to be the wrong one (i.e., the + subsequent nodes do not match, but they would have under a different + branch), the other branches that could result in a + successful resolution of the requested path will now be subsequently + tried, whereas previously the framework would behave as if no route + could be found. +- The user agent is now instructed to expire the cookie when it is + cleared via :meth:`~falcon.Response.unset_cookie`. +- Support was added for hooks that have been defined via + :meth:`functools.partial`. +- Tunneled UTF-8 characters in the request path are now properly + decoded, and a placeholder character is substituted for any invalid + code points. +- The instantiation of :any:`Request.context_type` is now + delayed until after all other properties of the + :class:`~falcon.Request` class have been initialized, in case the + context type's own initialization depends on any of + :class:`~falcon.Request`'s properties. +- A case was fixed in which reading from :any:`Request.stream` + could hang when using :mod:`wsgiref` to host the app. +- The default error serializer now sets the Vary header in responses. + Implementing this required passing the :class:`~falcon.Response` + object to the serializer, which would normally be a breaking change. + However, the framework was modified to detect old-style error + serializers and wrap them with a shim to make them compatible with + the new interface. +- A query string containing malformed percent-encoding no longer causes + the framework to raise an error. +- Additional tests were added for a few lines of code that were + previously not covered, due to deficiencies in code coverage reporting + that have since been corrected. +- The Cython note is no longer displayed when installing under Jython. +- Several errors and ambiguities in the documentation were corrected. diff -Nru python-falcon-1.0.0/docs/changes/1.1.0.rst python-falcon-1.4.1/docs/changes/1.1.0.rst --- python-falcon-1.0.0/docs/changes/1.1.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.1.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,107 @@ +Changelog for Falcon 1.1.0 +========================== + +Breaking Changes +---------------- + +(None) + +New & Improved +-------------- + +- A new `bounded_stream` property was added to :class:`falcon.Request` + that can be used in place of the `stream` property to mitigate + the blocking behavior of input objects used by some WSGI servers. +- A new `uri_template` property was added to :class:`~falcon.Request` + to expose the template for the route corresponding to the + path requested by the user agent. +- A `context` property was added to :class:`~falcon.Response` to mirror + the same property that is already available for + :class:`~falcon.Request`. +- JSON-encoded query parameter values can now be retrieved and decoded + in a single step via :meth:`~falcon.Request.get_param_as_dict`. +- CSV-style parsing of query parameter values can now be disabled. +- :meth:`~falcon.Request.get_param_as_bool` now recognizes "on" and + "off" in support of IE's default checkbox values. +- An `accept_ranges` property was added to :class:`~falcon.Response` to + facilitate setting the Accept-Ranges header. +- Added the :class:`~falcon.HTTPUriTooLong` and + :class:`~falcon.HTTPGone` error classes. +- When a title is not specified for :class:`~falcon.HTTPError`, it now + defaults to the HTTP status text. +- All parameters are now optional for most error classes. +- Cookie-related documentation has been clarified and expanded +- The :class:`falcon.testing.Cookie` class was added to represent a + cookie returned by a simulated request. :class:`falcon.testing.Result` + now exposes a `cookies` attribute for examining returned cookies. +- pytest support was added to Falcon's testing framework. Apps can now + choose to either write unittest- or pytest-style tests. +- The test runner for Falcon's own tests was switched from nose + to pytest. +- When simulating a request using Falcon's testing framework, query + string parameters can now be specified as a :class:`dict`, as + an alternative to passing a raw query string. +- A flag is now passed to the `process_request` middleware method to + signal whether or not an exception was raised while processing the + request. A shim was added to avoid breaking existing middleware + methods that do not yet accept this new parameter. +- A new CLI utility, `falcon-print-routes`, was added that takes in a + module:callable, introspects the routes, and prints the + results to stdout. This utility is automatically installed along + with the framework:: + + $ falcon-print-routes commissaire:api + -> /api/v0/status + -> /api/v0/cluster/{name} + -> /api/v0/cluster/{name}/hosts + -> /api/v0/cluster/{name}/hosts/{address} + +- Custom attributes can now be attached to instances of + :class:`~falcon.Request` and :class:`~falcon.Response`. This can be + used as an alternative to adding values to the `context` property, + or implementing custom subclasses. +- :meth:`~falcon.get_http_status` was implemented to provide a way to + look up a full HTTP status line, given just a status code. + +Fixed +----- + +- When :any:`auto_parse_form_urlencoded` is + set to ``True``, the framework now checks the HTTP method before + attempting to consume and parse the body. +- Before attempting to read the body of a form-encoded request, the + framework now checks the Content-Length header to ensure that a + non-empty body is expected. This helps prevent bad requests from + causing a blocking read when running behind certain WSGI servers. +- When the requested method is not implemented for the target resource, + the framework now raises :class:`~falcon.HTTPMethodNotAllowed`, rather + than modifying the :class:`~falcon.Request` object directly. This + improves visibility for custom error handlers and for middleware + methods. +- Error class docstrings have been updated to reflect the latest RFCs. +- When an error is raised by a resource method or a hook, the error + will now always be processed (including setting the appropriate + properties of the :class:`~falcon.Response` object) before middleware + methods are called. +- A case was fixed in which middleware processing did not + continue when an instance of :class:`~falcon.HTTPError` or + :class:`~falcon.HTTPStatus` was raised. +- The :meth:`~falcon.uri.encode` method will now attempt to detect + whether the specified string has already been encoded, and return + it unchanged if that is the case. +- The default OPTIONS responder now explicitly sets Content-Length + to zero in the response. +- :class:`falcon.testing.Result` now assumes that the response body + is encoded as UTF-8 when the character set is not specified, rather + than raising an error when attempting to decode the response body. +- When simulating requests, Falcon's testing framework now properly + tunnels Unicode characters through the WSGI interface. +- ``import falcon.uri`` now works, in addition to + ``from falcon import uri``. +- URI template fields are now validated up front, when the route is + added, to ensure they are valid Python identifiers. This prevents + cryptic errors from being raised later on when requests are routed. +- When running under Python 3, :meth:`inspect.signature()` is used + instead of :meth:`inspect.getargspec()` to provide compatibility with + annotated functions. + diff -Nru python-falcon-1.0.0/docs/changes/1.2.0.rst python-falcon-1.4.1/docs/changes/1.2.0.rst --- python-falcon-1.0.0/docs/changes/1.2.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.2.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,77 @@ +Changelog for Falcon 1.2.0 +========================== + +Breaking Changes +---------------- + +(None) + +New & Improved +-------------- + +- A new `default` kwarg was added to :meth:`~falcon.Request.get_header`. +- A :meth:`~falcon.Response.delete_header` method was added to + :class:`falcon.Response`. +- Several new HTTP status codes and error classes were added, such as + :class:`falcon.HTTPFailedDependency`. +- If `ujson` is installed it will be used in lieu of `json` to speed up + error serialization and query string parsing under CPython. PyPy users + should continue to use `json`. +- The `independent_middleware` kwarg was added to :class:`falcon.API` to + enable the execution of `process_response()` middleware methods, even + when `process_request()` raises an error. +- Single-character field names are now allowed in URL templates when + specifying a route. +- A detailed error message is now returned when an attempt is made to + add a route that conflicts with one that has already been added. +- The HTTP protocol version can now be specified when simulating + requests with the testing framework. +- The :class:`falcon.ResponseOptions` class was added, along with a + `secure_cookies_by_default` option to control the default value of + the "secure" attribute when setting cookies. This can make testing + easier by providing a way to toggle whether or not HTTPS is required. +- `port`, `netloc` and `scheme` properties were added to the + :class:`falcon.Request` class. The `protocol` property is now + deprecated and will be removed in a future release. +- The `strip_url_path_trailing_slash` was added + to :class:`falcon.RequestOptions` to control whether or not to retain + the trailing slash in the URL path, if one is present. When this + option is enabled (the default), the URL path is normalized by + stripping the trailing slash character. This lets the application + define a single route to a resource for a path that may or may not end + in a forward slash. However, this behavior can be problematic in + certain cases, such as when working with authentication schemes that + employ URL-based signatures. Therefore, the + `strip_url_path_trailing_slash` option was introduced to make this + behavior configurable. +- Improved the documentation for :class:`falcon.HTTPError`, particularly + around customizing error serialization. +- Misc. improvements to the look and feel of Falcon's documentation. +- The tutorial in the docs was revamped, and now includes guidance on + testing Falcon applications. + +Fixed +----- + +- Certain non-alphanumeric characters, such as parenthesis, are not + handled properly in complex URI template path segments that are + comprised of both literal text and field definitions. +- When the WSGI server does not provide a `wsgi.file_wrapper` object, + Falcon wraps :attr:`Response.stream` in a simple iterator + object that does not implement `close()`. The iterator should be + modified to implement a `close()` method that calls the underlying + stream's `close()` to free system resources. +- The testing framework does not correctly parse cookies under Jython. +- Whitespace is not stripped when parsing cookies in the testing + framework. +- The Vary header is not always set by the default error serializer. +- While not specified in PEP-3333 that the status returned to the WSGI + server must be of type `str`, setting the status on the response to a + `unicode` string under Python 2.6 or 2.7 can cause WSGI servers to + raise an error. Therefore, the status string must first be converted + if it is of the wrong type. +- The default OPTIONS responder returns 204, when it should return + 200. RFC 7231 specifically states that Content-Length should be zero + in the response to an OPTIONS request, which implies a status code of + 200 since RFC 7230 states that Content-Length must not be set in any + response with a status code of 204. diff -Nru python-falcon-1.0.0/docs/changes/1.3.0.rst python-falcon-1.4.1/docs/changes/1.3.0.rst --- python-falcon-1.0.0/docs/changes/1.3.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.3.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,55 @@ +Changelog for Falcon 1.3.0 +========================== + +Breaking Changes +---------------- + +(None) + +Changes to Supported Platforms +------------------------------ + +- CPython 3.6 is now fully supported. +- Falcon appears to work well on PyPy3.5, but we are waiting until + that platform is out of beta before officially supporting it. +- Support for both CPython 2.6 and Jython 2.7 is now deprecated and + will be discontinued in Falcon 2.0. + +New & Improved +-------------- + +- We added built-in resource representation serialization and + deserialization, including input validation based on JSON Schema. + (See also: :ref:`Media `) +- URI template field converters are now supported. We expect to expand + this feature over time. (See also: + :ref:`Field Converters `) +- A new method, :meth:`~.Request.get_param_as_datetime`, was added to + :class:`~.Request`. +- A number of attributes were added to :class:`~.Request` to + make proxy information easier to consume. These include the + :attr:`~.Request.forwarded`, :attr:`~.Request.forwarded_uri`, + :attr:`~.Request.forwarded_scheme`, :attr:`~.Request.forwarded_host`, + and :attr:`~.Request.forwarded_prefix` attributes. The + :attr:`~.Request.prefix` attribute was also added as part of this + work. +- A :attr:`~.Request.referer` attribute was added to + :class:`~.Request`. +- We implemented ``__repr__()`` for :class:`~.Request`, + :class:`~.Response`, and :class:`~.HTTPError` to aid in + debugging. +- A number of Internet media type constants were defined to make it + easier to check and set content type headers. (See also: + :ref:`Media Type Constants `) +- Several new 5xx error classes were implemented. (See also: + :ref:`Error Handling `) + + +Fixed +----- + +- If even a single cookie in the request to the server is malformed, + none of the cookies will be parsed (all-or-nothing). Change the + parser to simply skip bad cookies (best-effort). +- :class:`~.API` instances are not pickleable. Modify the default router + to fix this. diff -Nru python-falcon-1.0.0/docs/changes/1.4.0.rst python-falcon-1.4.1/docs/changes/1.4.0.rst --- python-falcon-1.0.0/docs/changes/1.4.0.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.4.0.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,86 @@ +Changelog for Falcon 1.4.0 +========================== + +Breaking Changes +---------------- + +- :attr:`falcon.testing.Result.json` now returns None when the response body is + empty, rather than raising an error. + +Changes to Supported Platforms +------------------------------ + +- Python 3 is now supported on PyPy as of PyPy3.5 v5.10. +- Support for CPython 3.3 is now deprecated and will be removed in + Falcon 2.0. +- As with the previous release, Python 2.6 and Jython 2.7 remain deprecated and + will no longer be supported in Falcon 2.0. + +New & Improved +-------------- + +- We added a new method, :meth:`~.API.add_static_route`, that makes it easy to + serve files from a local directory. This feature provides an alternative to + serving files from the web server when you don't have that option, when + authorization is required, or for testing purposes. +- Arguments can now be passed to hooks (see :ref:`Hooks `). +- The default JSON media type handler will now use + `ujson `_, if available, to + speed up JSON (de)serialization under CPython. +- Semantic validation via the *format* keyword is now enabled for the + :meth:`~falcon.media.validators.jsonschema.validate` JSON Schema decorator. +- We added a new helper, :meth:`~falcon.Request.get_param_as_uuid`, to the + :class:`~.Request` class. +- Falcon now supports WebDAV methods + (`RFC 3253 `_), such as UPDATE and + REPORT. +- We added a new property, :attr:`~falcon.Response.downloadable_as`, to the + :class:`~.Response` class for setting the Content-Disposition header. +- :meth:`~falcon.routing.create_http_method_map` has been refactored into two + new methods, :meth:`~falcon.routing.map_http_methods` and :meth:`~falcon.routing.set_default_responders`, so that + custom routers can better pick and choose the functionality they need. The + original method is still available for backwards-compatibility, but will + be removed in a future release. +- We added a new `json` param to :meth:`~falcon.testing.simulate_request` et al. + to automatically serialize the request body from a JSON serializable object + or type (for a complete list of serializable types, see + `json.JSONEncoder `_). +- :class:`~.TestClient`'s ``simulate_*()`` methods now call + :meth:`~.TestClient.simulate_request` to make it easier for subclasses to + override :class:`~.TestClient`'s behavior. +- :class:`~.TestClient` can now be configured with a default set of headers to + send with every request. +- The :ref:`FAQ ` has been reorganized and greatly expanded. +- We restyled the docs to match https://falconframework.org + +Fixed +----- + +- Forwarded headers containing quoted strings with commas were not being parsed + correctly. This has been fixed, and the parser generally made more robust. +- :class:`~falcon.media.JSONHandler` was raising an error under Python 2.x when + serializing strings containing Unicode code points. This issue has been + fixed. +- Overriding a resource class and calling its responders via ``super()`` did + not work when passing URI template params as positional arguments. This has + now been fixed. +- Python 3.6 was generating warnings for strings containing ``'\s'`` within + Falcon. These strings have been converted to raw strings to mitigate the + warning. +- Several syntax errors were found and fixed in the code examples used in the + docs. + +Contributors to this Release +---------------------------- + +Many thanks to all of our talented and stylish contributors for this release! + +- GriffGeorge +- hynek +- kgriffs +- rhemz +- santeyio +- timc13 +- tyronegroves +- vytas7 +- zhanghanyun diff -Nru python-falcon-1.0.0/docs/changes/1.4.1.rst python-falcon-1.4.1/docs/changes/1.4.1.rst --- python-falcon-1.0.0/docs/changes/1.4.1.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/1.4.1.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,25 @@ +Changelog for Falcon 1.4.1 +========================== + +Breaking Changes +---------------- + +(None) + +Changes to Supported Platforms +------------------------------ + +(None) + +New & Improved +-------------- + +(None) + +Fixed +----- + +- Reverted the breaking change in 1.4.0 to :attr:`falcon.testing.Result.json`. + Minor releases should have no breaking changes. +- The README was not rendering properly on PyPI. This was fixed and a validation + step was added to the build process. diff -Nru python-falcon-1.0.0/docs/changes/index.rst python-falcon-1.4.1/docs/changes/index.rst --- python-falcon-1.0.0/docs/changes/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/changes/index.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,13 @@ +Changelogs +========== + +.. toctree:: + + 1.4.1 <1.4.1> + 1.4.0 <1.4.0> + 1.3.0 <1.3.0> + 1.2.0 <1.2.0> + 1.1.0 <1.1.0> + 1.0.0 <1.0.0> + 0.3.0 <0.3.0> + 0.2.0 <0.2.0> diff -Nru python-falcon-1.0.0/docs/community/contribute.rst python-falcon-1.4.1/docs/community/contribute.rst --- python-falcon-1.0.0/docs/community/contribute.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/community/contribute.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,25 @@ +.. _contribute: + +Contribute to Falcon +==================== + +Thanks for your interest in the project! We welcome pull requests from +developers of all skill levels. To get started, simply fork the master branch +on GitHub to your personal account and then clone the fork into your +development environment. + +Kurt Griffiths (**kgriffs** on GH, Gitter, and Twitter) is the original +creator of the Falcon framework, and currently co-maintains the project +along with John Vrbanac (**jmvrbanac** on GH and Gitter, and +**jvrbanac** on Twitter). Falcon is developed by a growing community of +users and contributors just like you. + +Please don't hesitate to reach out if you have any questions, or just need a +little help getting started. You can find us in +`falconry/dev `_ on Gitter. + +Please check out our +`Contributor's Guide `_ +for more information. + +Thanks! diff -Nru python-falcon-1.0.0/docs/community/help.rst python-falcon-1.4.1/docs/community/help.rst --- python-falcon-1.0.0/docs/community/help.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/community/help.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,61 @@ +.. _help: + +Get Help +======== + +Welcome to the Falcon community! We are a pragmatic group of HTTP enthusiasts +working on the next generation of web apps and cloud services. We would love +to have you join us and share your ideas. + +Please help us spread the word and grow the community! + +FAQ +--- +First, :ref:`take a quick look at our FAQ ` to see if your question has +already been addressed. If not, or if the answer is unclear, please don't +hesitate to reach out via one of the channels below. + +Chat +---- +The Falconry community on Gitter is a great place to ask questions and +share your ideas. You can find us in `falconry/user +`_. We also have a +`falconry/dev `_ room for discussing +the design and development of the framework itself. + +Per our +`Code of Conduct `_, +we expect everyone who participates in community discussions to act +professionally, and lead by example in encouraging constructive +discussions. Each individual in the community is responsible for +creating a positive, constructive, and productive culture. + +Submit Issues +------------- +If you have an idea for a feature, run into something that is harder to +use than it should be, or find a bug, please let the crew know +in `falconry/dev `_ or by +`submitting an issue `_. We +need your help to make Falcon awesome! + +Pay it Forward +-------------- +We'd like to invite you to help other community members with their +questions in `falconry/user +`_, and to help peer-review +`pull requests `_. If you use the +Chrome browser, we recommend installing the +`NotHub extension `_ to stay up to date with PRs. + +If you would like to contribute a new feature or fix a bug in the +framework, please check out our +`Contributor's Guide `_ +for more information. + +We'd love to have your help! + +Code of Conduct +--------------- +All contributors and maintainers of this project are subject to our `Code +of Conduct `_. + diff -Nru python-falcon-1.0.0/docs/community/index.rst python-falcon-1.4.1/docs/community/index.rst --- python-falcon-1.0.0/docs/community/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/community/index.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,9 @@ +Community Guide +=============== + +.. toctree:: + :maxdepth: 1 + + help + contribute + ../user/faq diff -Nru python-falcon-1.0.0/docs/conf.py python-falcon-1.4.1/docs/conf.py --- python-falcon-1.0.0/docs/conf.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/conf.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +# +# Falcon documentation build configuration file, created by +# sphinx-quickstart on Wed Mar 12 14:14:02 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +from datetime import datetime +from collections import OrderedDict +import sys +import os + +try: + import configparser +except ImportError: + import ConfigParser as configparser + +import falcon + +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath('.')) + +# Path to custom themes +sys.path.append(os.path.abspath('_themes')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + + # Falcon-specific extensions + 'ext.rfc', + 'ext.doorway', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.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'Falcon' +copyright = u"{year} Falcon Contributors".format( + year=datetime.utcnow().year +) + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. + +cfg = configparser.SafeConfigParser() +cfg.read('../setup.cfg') +tag = cfg.get('egg_info', 'tag_build') + +html_context = { + 'prerelease': bool(tag), # True if tag is not the empty string +} + +# The short X.Y version. +version = '.'.join(falcon.__version__.split('.')[0:2]) + tag + +# The full version, including alpha/beta/rc tags. +release = falcon.__version__ + tag + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'github' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = ['_themes'] +# html_theme = '' + +html_theme = 'alabaster' + +# if not on_rtd: +# # Use the RTD theme explicitly if it is available +# try: +# import sphinx_rtd_theme + +# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +# html_theme = "sphinx_rtd_theme" +# except ImportError: +# pass + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + 'github_user': 'falconry', + 'github_repo': 'falcon', + 'github_button': False, + 'github_banner': True, + 'fixed_sidebar': False, + 'show_powered_by': False, + 'extra_nav_links': OrderedDict([ + ('Falcon Home', 'https://falconframework.org/'), + ('Falcon Wiki', 'https://github.com/falconry/falcon/wiki'), + ('Get Help', '/community/help.html'), + ('Support Falcon', 'https://falconframework.org/#sectionSupportFalconDevelopment'), + ]), +} + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = '../falcon.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = '_static/img/favicon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = { +# 'index': ['side-primary.html', 'searchbox.html'], +# '**': ['side-secondary.html', 'localtoc.html', +# 'relations.html', 'searchbox.html'] +# } + +html_sidebars = { + '**': [ + 'sidebar-top.html', + 'sidebar-sponsors.html', + 'about.html', + 'navigation.html', + 'relations.html', + 'searchbox.html', + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Falcondoc' + + +# -- 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': '', +} + +# 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 = [ + ('index', 'Falcon.tex', u'Falcon Documentation', + u'Kurt Griffiths et al.', '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 = [ + ('index', 'falcon', u'Falcon Documentation', + [u'Kurt Griffiths et al.'], 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 = [ + ('index', 'Falcon', u'Falcon Documentation', + u'Kurt Griffiths et al.', 'Falcon', '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 + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/2': None} Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/docs/_content/corinne-kutz-211251.jpg and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/docs/_content/corinne-kutz-211251.jpg differ diff -Nru python-falcon-1.0.0/docs/_content/sidebar-sponsors.html python-falcon-1.4.1/docs/_content/sidebar-sponsors.html --- python-falcon-1.0.0/docs/_content/sidebar-sponsors.html 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/_content/sidebar-sponsors.html 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,8 @@ +

Sponsors

+
+ LIKALO + Luhnar Site Accelerator +
+

+ Use Falcon at work? Talk with your team about supporting Falcon development. +

diff -Nru python-falcon-1.0.0/docs/ext/doorway.py python-falcon-1.4.1/docs/ext/doorway.py --- python-falcon-1.0.0/docs/ext/doorway.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/ext/doorway.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,36 @@ +# Copyright 2016 by Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Doorway module extension for Sphinx. + +This extension modifies the way the top-level "falcon" doorway module +is documented. +""" + + +def _on_process_docstring(app, what, name, obj, options, lines): + """Process the docstring for a given python object.""" + + # NOTE(kgriffs): Suppress the top-level docstring since it is + # typically used with autodoc on rst pages that already have their + # own introductory texts, tailored to a specific subset of + # things that have been hoisted into the 'falcon' doorway module. + if what == 'module' and name == 'falcon': + lines[:] = [] + + +def setup(app): + app.connect('autodoc-process-docstring', _on_process_docstring) + + return {'parallel_read_safe': True} diff -Nru python-falcon-1.0.0/docs/ext/rfc.py python-falcon-1.4.1/docs/ext/rfc.py --- python-falcon-1.0.0/docs/ext/rfc.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/ext/rfc.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,66 @@ +# Copyright 2016 by Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RFC extension for Sphinx. + +This extensions adds hyperlinking for any RFC references that are +formatted like this:: + + RFC 7231; Section 6.5.3 + +""" + +import re + + +RFC_PATTERN = re.compile('RFC (\d{4}), Section ([\d\.]+)') + + +def _render_section(section_number, rfc_number): + template = '`{0} `_' + return template.format(section_number, rfc_number) + + +def _process_line(line): + m = RFC_PATTERN.search(line) + if not m: + return line + + rfc = m.group(1) + section = m.group(2) + + template = ( + '`RFC {rfc}, Section {section} ' + '`_' + ) + + rendered_text = template.format(rfc=rfc, section=section) + + return line[:m.start()] + rendered_text + line[m.end():] + + +def _on_process_docstring(app, what, name, obj, options, lines): + """Process the docstring for a given python object.""" + + if what == 'module' and name == 'falcon': + lines[:] = [] + return + + lines[:] = [_process_line(line) for line in lines] + + +def setup(app): + app.connect('autodoc-process-docstring', _on_process_docstring) + + return {'parallel_read_safe': True} diff -Nru python-falcon-1.0.0/docs/index.rst python-falcon-1.4.1/docs/index.rst --- python-falcon-1.0.0/docs/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/index.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,114 @@ +.. Falcon documentation master file, created by + sphinx-quickstart on Mon Feb 17 18:21:12 2014. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +The Falcon Web Framework +================================= + +Release v\ |version| (:ref:`Installation `) + +Falcon is a minimalist WSGI library for building speedy web APIs and app +backends. We like to think of Falcon as the `Dieter Rams` of web frameworks. + +When it comes to building HTTP APIs, other frameworks weigh you down with tons +of dependencies and unnecessary abstractions. Falcon cuts to the chase with a +clean design that embraces HTTP and the REST architectural style. + +.. code:: python + + class CatalogItem(object): + + # ... + + @falcon.before(hooks.to_oid) + def on_get(self, id): + return self._collection.find_one(id) + + app = falcon.API(after=[hooks.serialize]) + app.add_route('/items/{id}', CatalogItem()) + +What People are Saying +---------------------- + +"We have been using Falcon as a replacement for [framework] and we simply love the performance (three times faster) and code base size (easily half of our original [framework] code)." + +"Falcon looks great so far. I hacked together a quick test for a +tiny server of mine and was ~40% faster with only 20 minutes of +work." + +"Falcon is rock solid and it's fast." + +"I'm loving #falconframework! Super clean and simple, I finally +have the speed and flexibility I need!" + +"I feel like I'm just talking HTTP at last, with nothing in the +middle. Falcon seems like the requests of backend." + +"The source code for Falcon is so good, I almost prefer it to +documentation. It basically can't be wrong." + +"What other framework has integrated support for 786 TRY IT NOW ?" + +Quick Links +----------- + +* `Read the docs `_ +* `Falcon add-ons and complementary packages `_ +* `Falcon talks, podcasts, and blog posts `_ +* `falconry/user for Falcon users `_ @ Gitter +* `falconry/dev for Falcon contributors `_ @ Gitter + +Features +-------- + +Falcon tries to do as little as possible while remaining highly effective. + +- Routes based on URI templates RFC +- REST-inspired mapping of URIs to resources +- Global, resource, and method hooks +- Idiomatic HTTP error responses +- Full Unicode support +- Intuitive request and response objects +- Works great with async libraries like gevent +- Minimal attack surface for writing secure APIs +- 100% code coverage with a comprehensive test suite +- Only depends on six and mimeparse +- Supports Python 2.6, 2.7, 3.3, 3.4 and 3.6 +- Compatible with PyPy and Jython + +Who's Using Falcon? +------------------- + +Falcon is used around the world by a growing number of organizations, +including: + +- 7ideas +- Cronitor +- EMC +- Hurricane Electric +- Leadpages +- OpenStack +- Rackspace +- Shiftgig +- tempfil.es +- Opera Software + +If you are using the Falcon framework for a community or commercial +project, please consider adding your information to our wiki under +`Who's Using Falcon? `_ + +You might also like to view our +`Add-on Catalog `_, +where you can find a list of add-ons maintained by the community. + +Documentation +------------- + +.. toctree:: + :maxdepth: 2 + + user/index + api/index + community/index + changes/index diff -Nru python-falcon-1.0.0/docs/Makefile python-falcon-1.4.1/docs/Makefile --- python-falcon-1.0.0/docs/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/Makefile 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,177 @@ +# 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 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 " 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)" + +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/Falcon.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Falcon.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Falcon" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Falcon" + @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." + +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." diff -Nru python-falcon-1.0.0/docs/_static/custom.css python-falcon-1.4.1/docs/_static/custom.css --- python-falcon-1.0.0/docs/_static/custom.css 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/_static/custom.css 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,215 @@ +body { + font-family: Oxygen, 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif; +} + + +.field-name { + /* Fix for https://github.com/bitprophet/alabaster/issues/95 */ + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; + + width: 110px; /* Prevent "Return type:" from wrapping. */ +} + +a { + text-decoration: none; +} + +h1 a:hover, h2 a:hover, h3 a:hover { + text-decoration: none; + border: none; +} + +div.footer { + text-align: center; +} + +div.footer a:hover { + border-bottom: none; +} + +dd ul, dd table { + margin-bottom: 1.4em; +} + +table.field-list th, table.field-list td { + padding-top: 1em; +} + +table.field-list tbody tr:first-child th, table.field-list tbody tr:first-child td { + padding-top: 0; +} + +code.docutils.literal { + background-color: rgba(0, 0, 0, 0.06); + padding: 2px 5px 1px 5px; + font-size: 0.88em; +} + +code.xref.docutils.literal { + background-color: transparent; + padding: 0; + font-size: 0.9em; +} + +div.viewcode-block:target { + background: inherit; +} + +a:hover, div.sphinxsidebar a:hover, a.reference:hover, a.reference.internal:hover code { + color: #f0ad4e; + border-bottom: 1px solid #f0ad4e; +} + +a, div.sphinxsidebar a, a.reference, a code.literal { + color: #c77c11; + border: none; +} + +.highlight pre span { + line-height: 1.5em; +} + +.field-body cite { + font-style: normal; + font-weight: bold; +} + +/* Hide theme's default logo section */ +.logo a { + display: none; +} + +#logo { + position: relative; + left: -13px; +} + +#logo a, +#logo a:hover { + border-bottom: none; +} + +#logo img { + margin: 0; + padding: 0; +} + +#gh-buttons { + margin-top: 2em; +} + +#dev-warning { + background-color: #fdfbe8; + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 1em; + } + +div.warning { + background-color: #fdfbe8; + border: 1px solid #ccc; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6, +div.sphinxsidebar h3 { + font-family: Oxygen, 'goudy old style', serif; + font-weight: bold; + color: #444; +} + +div.sphinxsidebar h3 { + margin: 1.5em 0 0 0; +} + +div.sphinxsidebar h4 { + font-family: Oxygen, Garamond, Georgia, serif; +} + +div.sphinxsidebar ul { + margin-top: 5px; +} + +div.sphinxsidebarwrapper { + padding-top: 0; +} + +div.admonition p.admonition-title { + display: block; + line-height: 1.4em; + font-family: Oxygen, Garamond, Georgia, serif; +} + +div.admonition .last.highlight-default { + display: inline-block; + font-size: smaller; +} + +pre { + /*background-color: #212529;*/ + padding: 1.25em 30px; +} + +.highlight pre { + font-size: smaller; +} + +/* Fix drifting to the left in some parameters lists. */ +li .highlight pre { + float: right; +} + + +div.admonition { + padding-bottom: 15px; +} + +div input[type="text"] { + padding: 5px; + margin: 0 0 0.5em 0; + font-family: Oxygen, Garamond, Georgia, serif; +} + +div input[type="submit"] { + font-size: 14px; + width: 72px; + height: 27px; + font-weight: 600; + font-family: Oxygen, Garamond, Georgia, serif; + + border: 1px solid #d5d5d5; + border-radius: 3px; + padding: 0 10px; + + color: #333; + background-color: #eee; + background-image: linear-gradient(to bottom,#fcfcfc,#eee); +} + +div input[type="submit"]:hover { + background-color: #ddd; + background-image: linear-gradient(to bottom,#eee,#ddd); + border-color: #ccc; +} + +div input[type="submit"]:active, #searchbox input[type="submit"]:active { + background-color: #dcdcdc; + background-image: none; + border-color: #b5b5b5; + box-shadow: inset 0 2px 4px rgba(0,0,0,.15); +} + +div input[type="submit"]:focus { + outline: none; +} + +input[type=text]:focus { + outline: 1px solid #999; +} Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/docs/_static/img/favicon.ico and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/docs/_static/img/favicon.ico differ diff -Nru python-falcon-1.0.0/docs/_static/img/logo.svg python-falcon-1.4.1/docs/_static/img/logo.svg --- python-falcon-1.0.0/docs/_static/img/logo.svg 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/_static/img/logo.svg 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,50 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/docs/_static/img/my-web-app.gif and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/docs/_static/img/my-web-app.gif differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/docs/_static/img/my-web-app.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/docs/_static/img/my-web-app.png differ diff -Nru python-falcon-1.0.0/docs/_templates/sidebar-sponsors.html python-falcon-1.4.1/docs/_templates/sidebar-sponsors.html --- python-falcon-1.0.0/docs/_templates/sidebar-sponsors.html 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/_templates/sidebar-sponsors.html 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,18 @@ + + +
+

Sponsors

+

+ Use Falcon at work? Talk with your team about supporting Falcon development. +

+
diff -Nru python-falcon-1.0.0/docs/_templates/sidebar-top.html python-falcon-1.4.1/docs/_templates/sidebar-top.html --- python-falcon-1.0.0/docs/_templates/sidebar-top.html 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/_templates/sidebar-top.html 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,19 @@ + + + +{% if prerelease %} + +{% endif %} + + + +
+ Star + Issue +
diff -Nru python-falcon-1.0.0/docs/user/faq.rst python-falcon-1.4.1/docs/user/faq.rst --- python-falcon-1.0.0/docs/user/faq.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/faq.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,682 @@ +.. _faq: + +FAQ +=== + +.. contents:: :local: + +Design Philosophy +~~~~~~~~~~~~~~~~~ + +Why doesn't Falcon come with batteries included? +------------------------------------------------ +Falcon is designed for applications that require a high level of +customization or performance tuning. The framework's minimalist design +frees the developer to select the best strategies and 3rd-party +packages for the task at hand. + +The Python ecosystem offers a number of great packages that you can +use from within your responders, hooks, and middleware components. As +a starting point, the community maintains a list of `Falcon add-ons +and complementary packages `_. + +Why doesn't Falcon create a new Resource instance for every request? +-------------------------------------------------------------------- +Falcon generally tries to minimize the number of objects that it +instantiates. It does this for two reasons: first, to avoid the expense of +creating the object, and second to reduce memory usage. Therefore, when +adding a route, Falcon requires an *instance* of your resource class, rather +than the class type. That same instance will be used to serve all requests +coming in on that route. + +Why does raising an error inside a resource crash my app? +--------------------------------------------------------- +Generally speaking, Falcon assumes that resource responders (such as +``on_get()``, ``on_post()``, etc.) will, for the most part, do the right thing. +In other words, Falcon doesn't try very hard to protect responder code from +itself. + +This approach reduces the number of checks that Falcon +would otherwise have to perform, making the framework more efficient. With +that in mind, writing a high-quality API based on Falcon requires that: + +#. Resource responders set response variables to sane values. +#. Your code is well-tested, with high code coverage. +#. Errors are anticipated, detected, and handled appropriately within + each responder and with the aid of custom error handlers. + +.. tip:: Falcon will re-raise errors that do not inherit from + :class:`~falcon.HTTPError` unless you have registered a custom error + handler for that type (see also: :ref:`falcon.API `). + +How do I generate API documentation for my Falcon API? +------------------------------------------------------ +When it comes to API documentation, some developers prefer to use the API +implementation as the user contract or source of truth (taking an +implementation-first approach), while other developers prefer to use the API +spec itself as the contract, implementing and testing the API against that spec +(taking a design-first approach). + +At the risk of erring on the side of flexiblity, Falcon does not provide API +spec support out of the box. However, there are several community projects +available in this vein. Our +`Add on Catalog `_ lists +a couple of these projects, but you may also wish to search +`PyPI `_ for additional packages. + +If you are interested in the design-first approach mentioned above, you may +also want to check out API design and gateway services such as Tyk, Apiary, +Amazon API Gateway, or Google Cloud Endpoints. + +Performance +~~~~~~~~~~~ + +Does Falcon work with HTTP/2? +----------------------------- + +Falcon is a WSGI framework and as such does not serve HTTP requests directly. +However, you can get most of the benefits of HTTP/2 by simply deploying any +HTTP/2-compliant web server or load balancer in front of your app to translate +between HTTP/2 and HTTP/1.1. Eventually we expect that Python web servers (such +as uWSGI) will support HTTP/2 natively, eliminating the need for a translation +layer. + +Is Falcon thread-safe? +---------------------- + +New :class:`~falcon.Request` and :class:`~falcon.Response` objects are created +for each incoming HTTP request. However, a single instance of each resource +class attached to a route is shared among all requests. Therefore, as long as +you are careful about the way responders access class member variables to avoid +conflicts, your WSGI app should be thread-safe. + +That being said, IO-bound Falcon APIs are usually scaled via green +threads (courtesy of the `gevent `_ library or similar) +which aren't truly running concurrently, so there may be some edge cases where +Falcon is not thread-safe that haven't been discovered yet. + +*Caveat emptor!* + +Does Falcon support asyncio? +------------------------------ + +Due to the limitations of WSGI, Falcon is unable to support ``asyncio`` at this +time. However, we are exploring alternatives to WSGI (such +as `ASGI `_) +that will allow us to support asyncio natively in the future. + +In the meantime, we recommend using `gevent `_ via +Gunicorn or uWSGI in order to scale IO-bound services. + +Does Falcon support WebSocket? +------------------------------ + +Due to the limitations of WSGI, Falcon is unable to support the WebSocket +protocol as stated above. + +In the meantime, you might try leveraging +`uWSGI's native WebSocket support `_, +or implementing a standalone service via Aymeric Augustin's +handy `websockets `_ library. + +Routing +~~~~~~~ + +How do I implement CORS with Falcon? +------------------------------------ + +In order for a website or SPA to access an API hosted under a different +domain name, that API must implement +`Cross-Origin Resource Sharing (CORS) `_. +For a public API, implementing CORS in Falcon can be as simple as implementing +a middleware component similar to the following: + +.. code:: python + + class CORSComponent(object): + def process_response(self, req, resp, resource, req_succeeded): + resp.set_header('Access-Control-Allow-Origin', '*') + + if (req_succeeded + and req.method == 'OPTIONS' + and req.get_header('Access-Control-Request-Method') + ): + # NOTE(kgriffs): This is a CORS preflight request. Patch the + # response accordingly. + + allow = resp.get_header('Allow') + resp.delete_header('Allow') + + allow_headers = req.get_header( + 'Access-Control-Request-Headers', + default='*' + ) + + resp.set_headers(( + ('Access-Control-Allow-Methods', allow), + ('Access-Control-Allow-Headers', allow_headers), + ('Access-Control-Max-Age', '86400'), # 24 hours + )) + +When using the above approach, OPTIONS requests must also be special-cased in +any other middleware or hooks you use for auth, content-negotiation, etc. For +example, you will typically skip auth for preflight requests because it is +simply unnecessary; note that such request do not include the Authorization +header in any case. + +For more sophisticated use cases, have a look at Falcon add-ons from the +community, such as `falcon-cors `_, or +try one of the generic +`WSGI CORS libraries available on PyPI `_. +If you use an API gateway, you might also look into what CORS functionaly +it provides at that level. + +How do I implement redirects within Falcon? +------------------------------------------- + +Falcon provides a number of exception classes that can be raised to redirect the +client to a different location (see also :ref:`Redirection `). + +Note, however, that it is more efficient to handle permanent redirects +directly with your web server, if possible, rather than placing additional load +on your app for such requests. + +How do I implement both POSTing and GETing items for the same resource? +----------------------------------------------------------------------- +Suppose you have the following routes:: + + # Resource Collection + POST /resources + GET /resources{?marker, limit} + + # Resource Item + GET /resources/{id} + PATCH /resources/{id} + DELETE /resources/{id} + +You can implement this sort of API by simply using two Python classes, one +to represent a single resource, and another to represent the collection of +said resources. It is common to place both classes in the same module. + +A proposal has been made to add a new routing feature that will afford +mapping related routes to a single resource class, if so desired. To learn +more, see `#584 on GitHub `_. + +(See also :ref:`this section of the tutorial `.) + +What is the recommended way to map related routes to resource classes? +---------------------------------------------------------------------- + +Let's say we have the following URL schema:: + + GET /game/ping + GET /game/{game_id} + POST /game/{game_id} + GET /game/{game_id}/state + POST /game/{game_id}/state + +We can break this down into three resources:: + + Ping: + + GET /game/ping + + Game: + + GET /game/{game_id} + POST /game/{game_id} + + GameState: + + GET /game/{game_id}/state + POST /game/{game_id}/state + +GameState may be thought of as a sub-resource of Game. It is +a distinct logical entity encapsulated within a more general +Game concept. + +In Falcon, these resources would be implemented with standard +classes: + +.. code:: python + + class Ping(object): + + def on_get(self, req, resp): + resp.body = '{"message": "pong"}' + + + class Game(object): + + def __init__(self, dal): + self._dal = dal + + def on_get(self, req, resp, game_id): + pass + + def on_post(self, req, resp, game_id): + pass + + + class GameState(object): + + def __init__(self, dal): + self._dal = dal + + def on_get(self, req, resp, game_id): + pass + + def on_post(self, req, resp, game_id): + pass + + + api = falcon.API() + + # Game and GameState are closely related, and so it + # probably makes sense for them to share an object + # in the Data Access Layer. This could just as + # easily use a DB object or ORM layer. + # + # Note how the resources classes provide a layer + # of abstraction or indirection which makes your + # app more flexible since the data layer can + # evolve somewhat independently from the presentation + # layer. + game_dal = myapp.DAL.Game(myconfig) + + api.add_route('/game/ping', Ping()) + api.add_route('/game/{game_id}', Game(game_dal)) + api.add_route('/game/{game_id}/state', GameState(game_dal)) + +In the future, we hope to support an alternative approach, using the proposal +from `#584 on GitHub `_, +that will afford combining all of these resources into a single class like so: + +.. code:: python + + class Ping(object): + + + class Game(object): + + def __init__(self, dal): + self._dal = dal + + def on_get(self, req, resp, game_id): + pass + + def on_post(self, req, resp, game_id): + pass + + def on_get_ping(self, req, resp): + resp.body = '{"message": "pong"}' + + def on_get_state(self, req, resp, game_id): + pass + + def on_post_state(self, req, resp, game_id): + pass + + + api = falcon.API() + + game = Game(myapp.DAL.Game(myconfig)) + + api.add_route('/game/ping', game, 'ping') + api.add_route('/game/{game_id}', game) + api.add_route('/game/{game_id}/state', game, 'state') + +Extensibility +~~~~~~~~~~~~~ + +How do I use WSGI middleware with Falcon? +----------------------------------------- +Instances of :class:`falcon.API` are first-class WSGI apps, so you can use the +standard pattern outlined in PEP-3333. In your main "app" file, you would +simply wrap your api instance with a middleware app. For example: + +.. code:: python + + import my_restful_service + import some_middleware + + app = some_middleware.DoSomethingFancy(my_restful_service.api) + +See also the `WSGI middleware example `_ given in PEP-3333. + +How can I pass data from a hook to a responder, and between hooks? +------------------------------------------------------------------ +You can inject extra responder kwargs from a hook by adding them +to the *params* dict passed into the hook. You can also add custom data to +the ``req.context`` dict, as a way of passing contextual information around. + +How can I write a custom handler for 404 and 500 pages in falcon? +------------------------------------------------------------------ +When a route can not be found for an incoming request, Falcon uses a default +responder that simply raises an instance of :attr:`falcon.HTTPNotFound`. You +can use :meth:`falcon.API.add_error_handler` to register a custom error handler +for this exception type. Alternatively, you may be able to configure your web +server to transform the response for you (e.g., using Nginx's ``error_page`` +directive). + +500 errors are typically the result of an unhandled exception making its way +up to the web server. To handle these errors more gracefully, you can add a +custom error handler for Python's base :class:`Exception` type. + +Request Handling +~~~~~~~~~~~~~~~~ + +How do I authenticate requests? +------------------------------- +Hooks and middleware components can be used together to authenticate and +authorize requests. For example, a middleware component could be used to +parse incoming credentials and place the results in ``req.context``. +Downstream components or hooks could then use this information to +authorize the request, taking into account the user's role and the requested +resource. + +Why does req.stream.read() hang for certain requests? +----------------------------------------------------- + +This behavior is an unfortunate artifact of the request body mechanics not +being fully defined by the WSGI spec (PEP-3333). This is discussed in the +reference documentation for :attr:`~falcon.Request.stream`, and a workaround +is provided in the form of :attr:`~falcon.Request.bounded_stream`. + +Why are trailing slashes trimmed from req.path? +----------------------------------------------- +By default, Falcon normalizes incoming URI paths to simplify later processing +and improve the predictability of application logic. This behavior can be +disabled via the :attr:`~falcon.RequestOptions.strip_url_path_trailing_slash` +request option. + +Note also that routing is also normalized, so adding a route for "/foo/bar" +also implicitly adds a route for "/foo/bar/". Requests coming in for either +path will be sent to the same resource. + +Why is my query parameter missing from the req object? +------------------------------------------------------ +If a query param does not have a value, Falcon will by default ignore that +parameter. For example, passing ``'foo'`` or ``'foo='`` will result in the +parameter being ignored. + +If you would like to recognize such parameters, you must set the +`keep_blank_qs_values` request option to ``True``. Request options are set +globally for each instance of :class:`falcon.API` via the +:attr:`~falcon.API.req_options` property. For example: + +.. code:: python + + api.req_options.keep_blank_qs_values = True + +Why are '+' characters in my params being converted to spaces? +-------------------------------------------------------------- +The ``+`` character is often used instead of ``%20`` to represent spaces in +query string params, due to the historical conflation of form parameter encoding +(``application/x-www-form-urlencoded``) and URI percent-encoding. Therefore, +Falcon, converts ``+`` to a space when decoding strings. + +To work around this, RFC 3986 specifies ``+`` as a reserved character, +and recommends percent-encoding any such characters when their literal value is +desired (``%2B`` in the case of ``+``). + +How can I access POSTed form params? +------------------------------------ +By default, Falcon does not consume request bodies. However, setting +the :attr:`~RequestOptions.auto_parse_form_urlencoded` to ``True`` +on an instance of ``falcon.API`` +will cause the framework to consume the request body when the +content type is ``application/x-www-form-urlencoded``, making +the form parameters accessible via :attr:`~.Request.params`, +:meth:`~.Request.get_param`, etc. + +.. code:: python + + api.req_options.auto_parse_form_urlencoded = True + +Alternatively, POSTed form parameters may be read directly from +:attr:`~.Request.stream` and parsed via +:meth:`falcon.uri.parse_query_string` or +`urllib.parse.parse_qs() `_. + +How can I access POSTed files? +------------------------------ +Falcon does not currently support parsing files submitted by +an HTTP form (``multipart/form-data``), although we do plan +to add this feature in a future version. In the meantime, +you can use the standard ``cgi.FieldStorage`` class to +parse the request: + +.. code:: python + + # TODO: Either validate that content type is multipart/form-data + # here, or in another hook before allowing execution to proceed. + + # This must be done to avoid a bug in cgi.FieldStorage + env = req.env + env.setdefault('QUERY_STRING', '') + + # TODO: Add error handling, when the request is not formatted + # correctly or does not contain the desired field... + + # TODO: Consider overriding make_file, so that you can + # stream directly to the destination rather than + # buffering using TemporaryFile (see http://goo.gl/Yo8h3P) + form = cgi.FieldStorage(fp=req.stream, environ=env) + + file_item = form[name] + if file_item.file: + # TODO: It's an uploaded file... read it in + else: + # TODO: Raise an error + +You might also try this +`streaming_form_data `_ +package by Siddhant Goel, or searching PyPI for additional options from the +community. + +How do I consume a query string that has a JSON value? +------------------------------------------------------ +Falcon defaults to treating commas in a query string as literal characters +delimiting a comma separated list. For example, given +the query string ``?c=1,2,3``, Falcon defaults to adding this to your +``request.params`` dictionary as ``{'c': ['1', '2', '3']}``. If you attempt +to use JSON in the value of the query string, for example ``?c={'a':1,'b':2}``, +the value will get added to your ``request.params`` in a way that you probably +don't expect: ``{'c': ["{'a':1", "'b':2}"]}``. + +Commas are a reserved character that can be escaped according to +`RFC 3986 - 2.2. Reserved Characters `_, +so one possible solution is to percent encode any commas that appear in your +JSON query string. The other option is to switch the way Falcon +handles commas in a query string by setting the +:attr:`~falcon.RequestOptions.auto_parse_qs_csv` to ``False`` on an instance of +:class:`falcon.API`: + +.. code:: python + + api.auto_parse_qs_csv = False + +When :attr:`~falcon.RequestOptions.auto_parse_qs_csv` is set to ``False``, the +value of the query string ``?c={'a':1,'b':2}`` will be added to +the ``req.params`` dictionary as ``{'c': "{'a':1,'b':2}"}``. +This lets you consume JSON whether or not the client chooses to escape +commas in the request. + +How can I handle forward slashes within a route template field? +--------------------------------------------------------------- + +In Falcon 1.3 we shipped initial support for +`field converters `_. +We’ve discussed building on this feature to support consuming multiple path +segments ala Flask. This work is currently planned for 2.0. + +In the meantime, the workaround is to percent-encode the forward slash. If you +don’t control the clients and can't enforce this, you can implement a Falcon +middleware component to rewrite the path before it is routed. + +Response Handling +~~~~~~~~~~~~~~~~~ + +How can I use resp.media with types like datetime? +-------------------------------------------------- + +The default JSON handler for ``resp.media`` only supports the objects and types +listed in the table documented under +`json.JSONEncoder `_. +To handle additional types, you can either serialize them beforehand, or create +a custom JSON media handler that sets the `default` param for ``json.dumps()``. +When deserializing an incoming request body, you may also wish to implement +`object_hook` for ``json.loads()``. Note, however, that setting the `default` or +`object_hook` params can negatively impact the performance of (de)serialization. + +Does Falcon set Content-Length or do I need to do that explicitly? +------------------------------------------------------------------ +Falcon will try to do this for you, based on the value of ``resp.body``, +``resp.data``, or ``resp.stream_len`` (whichever is set in the response, +checked in that order.) + +For dynamically-generated content, you can choose to not set ``stream_len``, +in which case Falcon will then leave off the Content-Length header, and +hopefully your WSGI server will do the Right Thing™ (assuming you've told +it to enable keep-alive). + +.. note:: PEP-3333 prohibits apps from setting hop-by-hop headers itself, + such as Transfer-Encoding. + +Why is an empty response body returned when I raise an instance of HTTPError? +----------------------------------------------------------------------------- + +Falcon attempts to serialize the :class:`~falcon.HTTPError` instance using its +:meth:`~falcon.HTTPError.to_json` or :meth:`~falcon.HTTPError.to_xml` methods, +according to the Accept header in the request. If neither JSON nor XML is +acceptable, no response body will be generated. You can override this behavior +if needed via :meth:`~falcon.API.set_error_serializer`. + +I'm setting a response body, but it isn't getting returned. What's going on? +---------------------------------------------------------------------------- +Falcon skips processing the response body when, according to the HTTP +spec, no body should be returned. If the client +sends a HEAD request, the framework will always return an empty body. +Falcon will also return an empty body whenever the response status is any +of the following:: + + falcon.HTTP_100 + falcon.HTTP_204 + falcon.HTTP_416 + falcon.HTTP_304 + +If you have another case where the body isn't being returned, it's probably a +bug! :ref:`Let us know ` so we can help. + +I'm setting a cookie, but it isn't being returned in subsequent requests. +------------------------------------------------------------------------- +By default, Falcon enables the `secure` cookie attribute. Therefore, if you are +testing your app over HTTP (instead of HTTPS), the client will not send the +cookie in subsequent requests. + +(See also the :ref:`cookie documentation `.) + +How can I serve a downloadable file with falcon? +------------------------------------------------ +In the ``on_get()`` responder method for the resource, you can tell the user +agent to download the file by setting the Content-Disposition header. Falcon +includes the :attr:`~falcon.Request.downloadable_as` property to make this +easy: + +.. code:: python + + resp.downloadable_as = 'report.pdf' + +Can Falcon serve static files? +------------------------------ + +Falcon makes it easy to efficiently serve static files by simply assigning an +open file to ``resp.stream`` :ref:`as demonstrated in the tutorial +`. You can also serve an entire directory of files via +:meth:`falcon.API.add_static_route`. However, if possible, it is best to serve +static files directly from a web server like Nginx, or from a CDN. + +Misc. +~~~~~ + +How do I manage my database connections? +---------------------------------------- + +Assuming your database library manages its own connection pool, all you need +to do is initialize the client and pass an instance of it into your resource +classes. For example, using SQLAlchemy Core: + +.. code:: python + + engine = create_engine('sqlite:///:memory:') + resource = SomeResource(engine) + +Then, within ``SomeResource``: + +.. code:: python + + # Read from the DB + result = self._engine.execute(some_table.select()) + for row in result: + # .... + result.close() + + # ... + + # Write to the DB within a transaction + with self._engine.begin() as connection: + r1 = connection.execute(some_table.select()) + # ... + connection.execute( + some_table.insert(), + col1=7, + col2='this is some data' + ) + +When using a data access layer, simply pass the engine into your data +access objects instead. See also +`this sample Falcon project `_ +that demonstrates using an ORM with Falcon. + +You can also create a middleware component to automatically check out +database connections for each request, but this can make it harder to track +down errors, or to tune for the needs of individual requests. + +If you need to transparently handle reconnecting after an error, or for other +use cases that may not be supported by your client library, simply encapsulate +the client library within a management class that handles all the tricky bits, +and pass that around instead. + +What is the recommended approach for making configuration variables available to multiple resource classes? +----------------------------------------------------------------------------------------------------------- + +People usually fall into two camps when it comes to this question. The first +camp likes to instantiate a config object and pass that around to the +initializers of the resource classes so the data sharing is explicit. The second +camp likes to create a config module and import that wherever it’s needed. + +With the latter approach, to control when the config is actually loaded, +it’s best not to instantiate it at +the top level of the config module’s namespace. This avoids any problematic +side-effects that may be caused by loading the config whenever Python happens +to process the first import of the config module. Instead, +consider implementing a function in the module that returns a new or cached +config object on demand. + +Other than that, it’s pretty much up to you if you want to use the standard +library config library or something like ``aumbry`` as demonstrated by this +`falcon example app `_ + +(See also the **Configuration** section of our +`Complementary Packages wiki page `_. +You may also wish to search PyPI for other options). + +How do I test my Falcon app? Can I use pytest? +---------------------------------------------- + +Falcon's testing framework supports both ``unittest`` and ``pytest``. In fact, +the tutorial in the docs provides an excellent introduction to +`testing Falcon apps with pytest `_. + +(See also: `Testing `_) diff -Nru python-falcon-1.0.0/docs/user/index.rst python-falcon-1.4.1/docs/user/index.rst --- python-falcon-1.0.0/docs/user/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/index.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,11 @@ +User Guide +========== + +.. toctree:: + :maxdepth: 2 + + intro + install + quickstart + tutorial + faq diff -Nru python-falcon-1.0.0/docs/user/install.rst python-falcon-1.4.1/docs/user/install.rst --- python-falcon-1.0.0/docs/user/install.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/install.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,178 @@ +.. _install: + +Installation +============ + +PyPy +---- + +`PyPy `__ is the fastest way to run your Falcon app. +Both PyPy2.7 and PyPy3.5 are supported as of PyPy v5.10. + +.. code:: bash + + $ pip install falcon + +Or, to install the latest beta or release candidate, if any: + +.. code:: bash + + $ pip install --pre falcon + +CPython +------- + +Falcon also fully supports +`CPython `__ 2.6-2.7, and 3.3+. + +.. warning:: + + Support for CPython 2.6 is deprecated and will be removed in + Falcon 2.0. + +A universal wheel is available on PyPI for the the Falcon framework. +Installing it is as simple as: + +.. code:: bash + + $ pip install falcon + +If `ujson `__ is available, Falcon +will use it to speed up media (de)serialization, error serialization, +and query string parsing. Note that ``ujson`` can actually be slower +on PyPy than the standard ``json`` module due to ctypes overhead, and +so we recommend only using ``ujson`` with CPython deployments: + +.. code:: bash + + $ pip install ujson + +Installing the Falcon wheel is a great way to get up and running +quickly in a development environment, but for an extra speed boost when +deploying your application in production, Falcon can compile itself with +Cython. + +The following commands tell pip to install Cython, and then to invoke +Falcon's ``setup.py``, which will in turn detect the presence of Cython +and then compile (AKA cythonize) the Falcon framework with the system's +default C compiler. + +.. code:: bash + + $ pip install cython + $ pip install --no-binary :all: falcon + +If you want to verify that Cython is being invoked, simply +pass `-v` to pip in order to echo the compilation commands: + +.. code:: bash + + $ pip install -v --no-binary :all: falcon + +**Installing on OS X** + +Xcode Command Line Tools are required to compile Cython. Install them +with this command: + +.. code:: bash + + $ xcode-select --install + +The Clang compiler treats unrecognized command-line options as +errors; this can cause problems under Python 2.6, for example: + +.. code:: bash + + clang: error: unknown argument: '-mno-fused-madd' [-Wunused-command-line-argument-hard-error-in-future] + +You might also see warnings about unused functions. You can work around +these issues by setting additional Clang C compiler flags as follows: + +.. code:: bash + + $ export CFLAGS="-Qunused-arguments -Wno-unused-function" + +Dependencies +------------ + +Falcon depends on `six` and `python-mimeparse`. `python-mimeparse` is a +better-maintained fork of the similarly named `mimeparse` project. +Normally the correct package will be selected by Falcon's ``setup.py``. +However, if you are using an alternate strategy to manage dependencies, +please take care to install the correct package in order to avoid +errors. + +WSGI Server +----------- + +Falcon speaks WSGI, and so in order to serve a Falcon app, you will +need a WSGI server. Gunicorn and uWSGI are some of the more popular +ones out there, but anything that can load a WSGI app will do. + +All Windows developers can use Waitress production-quality pure-Python WSGI server with very acceptable performance. +Unfortunately Gunicorn is still not working on Windows and uWSGI need to have Cygwin on Windows installed. +Waitress can be good alternative for Windows users if they want quick start using Falcon on it. + +.. code:: bash + + $ pip install [gunicorn|uwsgi|waitress] + +Source Code +----------- + +Falcon `lives on GitHub `_, making the +code easy to browse, download, fork, etc. Pull requests are always welcome! Also, +please remember to star the project if it makes you happy. :) + +Once you have cloned the repo or downloaded a tarball from GitHub, you +can install Falcon like this: + +.. code:: bash + + $ cd falcon + $ pip install . + +Or, if you want to edit the code, first fork the main repo, clone the fork +to your desktop, and then run the following to install it using symbolic +linking, so that when you change your code, the changes will be automagically +available to your app without having to reinstall the package: + +.. code:: bash + + $ cd falcon + $ pip install -e . + +You can manually test changes to the Falcon framework by switching to the +directory of the cloned repo and then running pytest: + +.. code:: bash + + $ cd falcon + $ pip install -r requirements/tests + $ pytest tests + +Or, to run the default set of tests: + +.. code:: bash + + $ pip install tox && tox + +.. tip:: + + See also the `tox.ini `_ + file for a full list of available environments. + +Finally, to build Falcon's docs from source, simply run: + +.. code:: bash + + $ pip install tox && tox -e docs + +Once the docs have been built, you can view them by opening the following +index page in your browser. On OS X it's as simple as:: + + $ open docs/_build/html/index.html + +Or on Linux:: + + $ xdg-open docs/_build/html/index.html diff -Nru python-falcon-1.0.0/docs/user/intro.rst python-falcon-1.4.1/docs/user/intro.rst --- python-falcon-1.0.0/docs/user/intro.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/intro.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,84 @@ +.. _introduction: + +Introduction +============ + + Perfection is finally attained not when there is no longer anything + to add, but when there is no longer anything to take away. + + *- Antoine de Saint-Exupéry* + +`Falcon `__ is a reliable, +high-performance Python web framework for building +large-scale app backends and microservices. It encourages the REST +architectural style, and tries to do as little as possible while +remaining highly effective. + +Falcon apps work with any WSGI server, and run great under +CPython 2.7, PyPy, and CPython 3.3+. + +How is Falcon different? +------------------------ + +We designed Falcon to support the demanding needs of large-scale +microservices and responsive app backends. Falcon complements more +general Python web frameworks by providing bare-metal performance, +reliability, and flexibility wherever you need it. + +**Fast.** Same hardware, more requests. Falcon turns around +requests several times faster than most other Python frameworks. For +an extra speed boost, Falcon compiles itself with Cython when +available, and also works well with `PyPy `__. +Considering a move to another programming language? Benchmark with +Falcon + PyPy first. + +**Reliable.** We go to great lengths to avoid introducing +breaking changes, and when we do they are fully documented and only +introduced (in the spirit of +`SemVer `__) with a major version +increment. The code is rigorously tested with numerous inputs and we +require 100% coverage at all times. Six and mimeparse are the only +third-party dependencies. + +**Flexible.** Falcon leaves a lot of decisions and implementation +details to you, the API developer. This gives you a lot of freedom to +customize and tune your implementation. Due to Falcon's minimalist +design, Python community members are free to independently innovate on +`Falcon add-ons and complementary packages `__. + +**Debuggable.** Falcon eschews magic. It's easy to tell which inputs +lead to which outputs. Unhandled exceptions are never encapsulated or +masked. Potentially surprising behaviors, such as automatic request body +parsing, are well-documented and disabled by default. Finally, when it +comes to the framework itself, we take care to keep logic paths simple +and understandable. All this makes it easier to reason about the code +and to debug edge cases in large-scale deployments. + +Features +-------- + +- Highly-optimized, extensible code base +- Intuitive routing via URI templates and REST-inspired resource + classes +- Easy access to headers and bodies through request and response + classes +- DRY request processing via middleware components and hooks +- Idiomatic HTTP error responses +- Straightforward exception handling +- Snappy unit testing through WSGI helpers and mocks +- CPython 2.6-2.7, PyPy 2.7, Jython 2.7, and + +About Apache 2.0 +---------------- + +Falcon is released under the terms of the `Apache 2.0 License`_. This means that you can use it in your commercial applications without having to also open-source your own code. It also means that if someone happens to contribute code that is associated with a patent, you are granted a free license to use said patent. That's a pretty sweet deal. + +Now, if you do make changes to Falcon itself, please consider contributing your awesome work back to the community. + +.. _`Apache 2.0 License`: http://opensource.org/licenses/Apache-2.0 + + +Falcon License +-------------- + +.. include:: ../../LICENSE diff -Nru python-falcon-1.0.0/docs/user/quickstart.rst python-falcon-1.4.1/docs/user/quickstart.rst --- python-falcon-1.0.0/docs/user/quickstart.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/quickstart.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,310 @@ +.. _quickstart: + +Quickstart +========== + +If you haven't done so already, please take a moment to +:ref:`install ` the Falcon web framework before +continuing. + +Learning by Example +------------------- + +Here is a simple example from Falcon's README, showing how to get +started writing an API: + +.. code:: python + + # things.py + + # Let's get this party started! + import falcon + + + # Falcon follows the REST architectural style, meaning (among + # other things) that you think in terms of resources and state + # transitions, which map to HTTP verbs. + class ThingsResource(object): + def on_get(self, req, resp): + """Handles GET requests""" + resp.status = falcon.HTTP_200 # This is the default status + resp.body = ('\nTwo things awe me most, the starry sky ' + 'above me and the moral law within me.\n' + '\n' + ' ~ Immanuel Kant\n\n') + + # falcon.API instances are callable WSGI apps + app = falcon.API() + + # Resources are represented by long-lived class instances + things = ThingsResource() + + # things will handle all requests to the '/things' URL path + app.add_route('/things', things) + +You can run the above example using any WSGI server, such as uWSGI +or Gunicorn. For example: + +.. code:: bash + + $ pip install gunicorn + $ gunicorn things:app + +On Windows where Gunicorn and uWSGI don't work yet you can use Waitress server + +.. code:: bash + + $ pip install waitress + $ waitress-serve --port=8000 things:app + +Then, in another terminal: + +.. code:: bash + + $ curl localhost:8000/things + +Curl is a bit of a pain to use, so let's install +`HTTPie `_ and use it from now on. + +.. code:: bash + + $ pip install --upgrade httpie + $ http localhost:8000/things + +.. _quickstart-more-features: + +More Features +------------- + +Here is a more involved example that demonstrates reading headers and query +parameters, handling errors, and working with request and response bodies. + +.. code:: python + + import json + import logging + import uuid + from wsgiref import simple_server + + import falcon + import requests + + + class StorageEngine(object): + + def get_things(self, marker, limit): + return [{'id': str(uuid.uuid4()), 'color': 'green'}] + + def add_thing(self, thing): + thing['id'] = str(uuid.uuid4()) + return thing + + + class StorageError(Exception): + + @staticmethod + def handle(ex, req, resp, params): + description = ('Sorry, couldn\'t write your thing to the ' + 'database. It worked on my box.') + + raise falcon.HTTPError(falcon.HTTP_725, + 'Database Error', + description) + + + class SinkAdapter(object): + + engines = { + 'ddg': 'https://duckduckgo.com', + 'y': 'https://search.yahoo.com/search', + } + + def __call__(self, req, resp, engine): + url = self.engines[engine] + params = {'q': req.get_param('q', True)} + result = requests.get(url, params=params) + + resp.status = str(result.status_code) + ' ' + result.reason + resp.content_type = result.headers['content-type'] + resp.body = result.text + + + class AuthMiddleware(object): + + def process_request(self, req, resp): + token = req.get_header('Authorization') + account_id = req.get_header('Account-ID') + + challenges = ['Token type="Fernet"'] + + if token is None: + description = ('Please provide an auth token ' + 'as part of the request.') + + raise falcon.HTTPUnauthorized('Auth token required', + description, + challenges, + href='http://docs.example.com/auth') + + if not self._token_is_valid(token, account_id): + description = ('The provided auth token is not valid. ' + 'Please request a new token and try again.') + + raise falcon.HTTPUnauthorized('Authentication required', + description, + challenges, + href='http://docs.example.com/auth') + + def _token_is_valid(self, token, account_id): + return True # Suuuuuure it's valid... + + + class RequireJSON(object): + + def process_request(self, req, resp): + if not req.client_accepts_json: + raise falcon.HTTPNotAcceptable( + 'This API only supports responses encoded as JSON.', + href='http://docs.examples.com/api/json') + + if req.method in ('POST', 'PUT'): + if 'application/json' not in req.content_type: + raise falcon.HTTPUnsupportedMediaType( + 'This API only supports requests encoded as JSON.', + href='http://docs.examples.com/api/json') + + + class JSONTranslator(object): + # NOTE: Starting with Falcon 1.3, you can simply + # use req.media and resp.media for this instead. + + def process_request(self, req, resp): + # req.stream corresponds to the WSGI wsgi.input environ variable, + # and allows you to read bytes from the request body. + # + # See also: PEP 3333 + if req.content_length in (None, 0): + # Nothing to do + return + + body = req.stream.read() + if not body: + raise falcon.HTTPBadRequest('Empty request body', + 'A valid JSON document is required.') + + try: + req.context['doc'] = json.loads(body.decode('utf-8')) + + except (ValueError, UnicodeDecodeError): + raise falcon.HTTPError(falcon.HTTP_753, + 'Malformed JSON', + 'Could not decode the request body. The ' + 'JSON was incorrect or not encoded as ' + 'UTF-8.') + + def process_response(self, req, resp, resource): + if 'result' not in resp.context: + return + + resp.body = json.dumps(resp.context['result']) + + + def max_body(limit): + + def hook(req, resp, resource, params): + length = req.content_length + if length is not None and length > limit: + msg = ('The size of the request is too large. The body must not ' + 'exceed ' + str(limit) + ' bytes in length.') + + raise falcon.HTTPRequestEntityTooLarge( + 'Request body is too large', msg) + + return hook + + + class ThingsResource(object): + + def __init__(self, db): + self.db = db + self.logger = logging.getLogger('thingsapp.' + __name__) + + def on_get(self, req, resp, user_id): + marker = req.get_param('marker') or '' + limit = req.get_param_as_int('limit') or 50 + + try: + result = self.db.get_things(marker, limit) + except Exception as ex: + self.logger.error(ex) + + description = ('Aliens have attacked our base! We will ' + 'be back as soon as we fight them off. ' + 'We appreciate your patience.') + + raise falcon.HTTPServiceUnavailable( + 'Service Outage', + description, + 30) + + # An alternative way of doing DRY serialization would be to + # create a custom class that inherits from falcon.Request. This + # class could, for example, have an additional 'doc' property + # that would serialize to JSON under the covers. + # + # NOTE: Starting with Falcon 1.3, you can simply + # use resp.media for this instead. + resp.context['result'] = result + + resp.set_header('Powered-By', 'Falcon') + resp.status = falcon.HTTP_200 + + @falcon.before(max_body(64 * 1024)) + def on_post(self, req, resp, user_id): + try: + doc = req.context['doc'] + except KeyError: + raise falcon.HTTPBadRequest( + 'Missing thing', + 'A thing must be submitted in the request body.') + + proper_thing = self.db.add_thing(doc) + + resp.status = falcon.HTTP_201 + resp.location = '/%s/things/%s' % (user_id, proper_thing['id']) + + + # Configure your WSGI server to load "things.app" (app is a WSGI callable) + app = falcon.API(middleware=[ + AuthMiddleware(), + RequireJSON(), + JSONTranslator(), + ]) + + db = StorageEngine() + things = ThingsResource(db) + app.add_route('/{user_id}/things', things) + + # If a responder ever raised an instance of StorageError, pass control to + # the given handler. + app.add_error_handler(StorageError, StorageError.handle) + + # Proxy some things to another service; this example shows how you might + # send parts of an API off to a legacy system that hasn't been upgraded + # yet, or perhaps is a single cluster that all data centers have to share. + sink = SinkAdapter() + app.add_sink(sink, r'/search/(?Pddg|y)\Z') + + # Useful for debugging problems in your API; works with pdb.set_trace(). You + # can also use Gunicorn to host your app. Gunicorn can be configured to + # auto-restart workers when it detects a code change, and it also works + # with pdb. + if __name__ == '__main__': + httpd = simple_server.make_server('127.0.0.1', 8000, app) + httpd.serve_forever() + +To test this example go to the another terminal and run: + +.. code:: bash + + $ http localhost:8000/1/things authorization:custom-token diff -Nru python-falcon-1.0.0/docs/user/tutorial.rst python-falcon-1.4.1/docs/user/tutorial.rst --- python-falcon-1.0.0/docs/user/tutorial.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/docs/user/tutorial.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,1467 @@ +.. _tutorial: + +Tutorial +======== + +In this tutorial we'll walk through building an API for a simple image sharing +service. Along the way, we'll discuss Falcon's major features and introduce +the terminology used by the framework. + +First Steps +----------- + +The first thing we'll do is :ref:`install ` Falcon +inside a fresh +`virtualenv `_. +To that end, let's create a new project folder called "look", and set +up a virtual environment within it that we can use for the tutorial: + +.. code:: bash + + $ mkdir look + $ cd look + $ virtualenv .venv + $ source .venv/bin/activate + $ pip install falcon + +It's customary for the project's top-level module to be called the same as the +project, so let's create another "look" folder inside the first one and mark +it as a python module by creating an empty ``__init__.py`` file in it: + +.. code:: bash + + $ mkdir look + $ touch look/__init__.py + +Next, let's create a new file that will be the entry point into your app: + +.. code:: bash + + $ touch look/app.py + +The file hierarchy should now look like this: + +.. code:: bash + + look + ├── .venv + └── look + ├── __init__.py + └── app.py + +Now, open ``app.py`` in your favorite text editor and add the following lines: + +.. code:: python + + import falcon + + api = application = falcon.API() + +This code creates your WSGI application and aliases it as ``api``. You can use any +variable names you like, but we'll use ``application`` since that is what +Gunicorn, by default, expects it to be called (we'll see how this works +in the next section of the tutorial). + +.. note:: + A WSGI application is just a callable with a well-defined signature so that + you can host the application with any web server that understands the `WSGI + protocol `_. + +Next let's take a look at the :class:`falcon.API` class. Install +`IPython `_ and fire it up: + +.. code:: bash + + $ pip install ipython + $ ipython + +Now, type the following to introspect the :class:`falcon.API` callable: + +.. code:: bash + + In [1]: import falcon + + In [2]: falcon.API.__call__? + +Alternatively, you can use the standard Python ``help()`` function: + +.. code:: bash + + In [3]: help(falcon.API.__call__) + +Note the method signature. ``env`` and ``start_response`` are standard +WSGI params. Falcon adds a thin abstraction on top of these params +so you don't have to interact with them directly. + +The Falcon framework contains extensive inline documentation that you +can query using the above technique. + +.. tip:: + + In addition to `IPython `_, the Python + community maintains several other super-powered REPLs + that you may wish to try, including + `bpython `_ + and + `ptpython `_. + +Hosting Your App +---------------- + +Now that you have a simple Falcon app, you can take it for a spin with +a WSGI server. Python includes a reference server for self-hosting, but +let's use something more robust that you might use in production. + +Open a new terminal and run the following: + +.. code:: bash + + $ source .venv/bin/activate + $ pip install gunicorn + $ gunicorn --reload look.app + +(Note the use of the ``--reload`` option to tell Gunicorn to reload the +app whenever its code changes.) + +If you are a Windows user, Waitress can be used in lieu of Gunicorn, +since the latter doesn't work under Windows: + +.. code:: bash + + $ pip install waitress + $ waitress-serve --port=8000 look:app + +Now, in a different terminal, try querying the running app with curl: + +.. code:: bash + + $ curl -v localhost:8000 + +You should get a 404. That's actually OK, because we haven't specified +any routes yet. Falcon includes a default 404 response handler that +will fire for any requested path for which a route does not exist. + +While curl certainly gets the job done, it can be a bit crufty to use. +`HTTPie `_ is a modern, +user-friendly alternative. Let's install HTTPie and use it from now on: + +.. code:: bash + + $ source .venv/bin/activate + $ pip install httpie + $ http localhost:8000 + + +.. _tutorial_resources: + +Creating Resources +------------------ + +Falcon's design borrows several key concepts from the REST architectural +style. + +Central to both REST and the Falcon framework is the concept of a +"resource". Resources are simply all the things in your API or +application that can be accessed by a URL. For example, an event booking +application may have resources such as "ticket" and "venue", while a +video game backend may have resources such as "achievements" and +"player". + +URLs provide a way for the client to uniquely identify resources. For +example, ``/players`` might identify the "list of all players" resource, +while ``/players/45301f54`` might identify the "individual player with +ID 45301f54", and ``/players/45301f54/achievements`` the +"list of all achievements for the player resource with ID 45301f54". + +.. code:: + + POST /players/45301f54/achievements + └──────┘ └────────────────────────────────┘ + Action Resource Identifier + +In the REST architectural style, the URL only +identifies the resource; it does not specify what action to take on +that resource. Instead, users choose from a set of standard methods. +For HTTP, these are the familiar GET, POST, HEAD, etc. Clients can +query a resource to discover which methods it supports. + +.. note:: + + This is one of the key differences between the REST and RPC + architectural styles. REST applies a standard set of + verbs across any number of resources, as opposed to + having each application define its own unique set of methods. + +Depending on the requested action, the server may or may not return a +representation to the client. Representations may be encoded in +any one of a number of Internet media types, such as JSON and HTML. + +Falcon uses Python classes to represent resources. In practice, these +classes act as controllers in your application. They convert an +incoming request into one or more internal actions, and then compose a +response back to the client based on the results of those actions. + +.. code:: + + ┌────────────┐ + request → │ │ + │ Resource │ ↻ Orchestrate the requested action + │ Controller │ ↻ Compose the result + response ← │ │ + └────────────┘ + +A resource in Falcon is just a regular Python class that includes +one or more methods representing the standard HTTP verbs supported by +that resource. Each requested URL is mapped to a specific resource. + +Since we are building an image-sharing API, let's start by creating an +"images" resource. Create a new module, ``images.py`` next to ``app.py``, +and add the following code to it: + +.. code:: python + + import json + + import falcon + + + class Resource(object): + + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + # Create a JSON representation of the resource + resp.body = json.dumps(doc, ensure_ascii=False) + + # The following line can be omitted because 200 is the default + # status returned by the framework, but it is included here to + # illustrate how this may be overridden as needed. + resp.status = falcon.HTTP_200 + +As you can see, ``Resource`` is just a regular class. You can name the +class anything you like. Falcon uses duck-typing, so you don't need to +inherit from any sort of special base class. + +The image resource above defines a single method, ``on_get()``. For any +HTTP method you want your resource to support, simply add an ``on_*()`` +method to the class, where ``*`` is any one of the standard +HTTP methods, lowercased (e.g., ``on_get()``, ``on_put()``, +``on_head()``, etc.). + +We call these well-known methods "responders". Each responder takes (at +least) two params, one representing the HTTP request, and one representing +the HTTP response to that request. By convention, these are called +``req`` and ``resp``, respectively. Route templates and hooks can inject extra +params, as we shall see later on. + +Right now, the image resource responds to GET requests with a simple +``200 OK`` and a JSON body. Falcon's Internet media type defaults to +``application/json`` but you can set it to whatever you like. +Noteworthy JSON alternatives include +`YAML `_ and `MessagePack `_. + +Next let's wire up this resource and see it in action. Go back to +``app.py`` and modify it so that it looks something like this: + +.. code:: python + + import falcon + + from .images import Resource + + + api = application = falcon.API() + + images = Resource() + api.add_route('/images', images) + +Now, when a request comes in for ``/images``, Falcon will call the +responder on the images resource that corresponds to the requested +HTTP method. + +Let's try it. Restart Gunicorn (unless you're using ``--reload``), and +send a GET request to the resource: + +.. code:: bash + + $ http localhost:8000/images + +You should receive a ``200 OK`` response, including a JSON-encoded +representation of the "images" resource. + +.. note:: + + ``add_route()`` expects an instance of the + resource class, not the class itself. The same instance is used for + all requests. This strategy improves performance and reduces memory + usage, but this also means that if you host your application with a + threaded web server, resources and their dependencies must be + thread-safe. + +So far we have only implemented a responder for GET. Let's see what +happens when a different method is requested: + +.. code:: bash + + $ http PUT localhost:8000/images + +This time you should get back ``405 Method Not Allowed``, +since the resource does not support the ``PUT`` method. Note the +value of the Allow header: + +.. code:: bash + + allow: GET, OPTIONS + +This is generated automatically by Falcon based on the set of +methods implemented by the target resource. If a resource does not +include its own OPTIONS responder, the framework provides a +default implementation. Therefore, OPTIONS is always included in the +list of allowable methods. + +.. note:: + + If you have a lot of experience with other Python web frameworks, + you may be used to using decorators to set up your routes. Falcon's + particular approach provides the following benefits: + + * The URL structure of the application is centralized. This makes + it easier to reason about and maintain the API over time. + * The use of resource classes maps somewhat naturally to the REST + architectural style, in which a URL is used to identify a resource + only, not the action to perform on that resource. + * Resource class methods provide a uniform interface that does not + have to be reinvented (and maintained) from class to class and + application to application. + +Next, just for fun, let's modify our resource to use +`MessagePack `_ instead of JSON. Start by +installing the relevant package: + +.. code:: bash + + $ pip install msgpack-python + +Then, update the responder to use the new media type: + +.. code:: python + + import falcon + + import msgpack + + + class Resource(object): + + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + resp.data = msgpack.packb(doc, use_bin_type=True) + resp.content_type = falcon.MEDIA_MSGPACK + resp.status = falcon.HTTP_200 + +Note the use of ``resp.data`` in lieu of ``resp.body``. If you assign a +bytestring to the latter, Falcon will figure it out, but you can +realize a small performance gain by assigning directly to ``resp.data``. + +Also note the use of ``falcon.MEDIA_MSGPACK``. The ``falcon`` module +provides a number of constants for common media types, including +``falcon.MEDIA_JSON``, ``falcon.MEDIA_MSGPACK``, ``falcon.MEDIA_YAML``, +``falcon.MEDIA_XML``, ``falcon.MEDIA_HTML``, ``falcon.MEDIA_JS``, +``falcon.MEDIA_TEXT``, ``falcon.MEDIA_JPEG``, ``falcon.MEDIA_PNG``, +and ``falcon.MEDIA_GIF``. + +Restart Gunicorn (unless you're using ``--reload``), and then try +sending a GET request to the revised resource: + +.. code:: bash + + $ http localhost:8000/images + +.. _testing_tutorial: + +Testing your application +------------------------ + +Fully exercising your code is critical to creating a robust application. +Let's take a moment to write a test for what's been implemented so +far. + +First, create a ``tests`` directory with ``__init__.py`` and a test +module (``test_app.py``) inside it. The project's structure should +now look like this: + +.. code:: bash + + look + ├── .venv + ├── look + │   ├── __init__.py + │   ├── app.py + │   └── images.py + └── tests + ├── __init__.py + └── test_app.py + +Falcon supports :ref:`testing ` its :class:`~.API` object by +simulating HTTP requests. + +Tests can either be written using Python's standard :mod:`unittest` +module, or with any of a number of third-party testing +frameworks, such as `pytest `_. For +this tutorial we'll use `pytest `_ +since it allows for more pythonic test code as compared to the +JUnit-inspired :mod:`unittest` module. + +Let's start by installing the +`pytest `_ package: + +.. code:: bash + + $ pip install pytest + +Next, edit ``test_app.py`` to look like this: + +.. code:: python + + import falcon + from falcon import testing + import msgpack + import pytest + + from look.app import api + + + @pytest.fixture + def client(): + return testing.TestClient(api) + + + # pytest will inject the object returned by the "client" function + # as an additional parameter. + def test_list_images(client): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + response = client.simulate_get('/images') + result_doc = msgpack.unpackb(response.content, encoding='utf-8') + + assert result_doc == doc + assert response.status == falcon.HTTP_OK + +From the main project directory, exercise your new test by running +pytest against the ``tests`` directory: + +.. code:: bash + + $ pytest tests + +If pytest reports any errors, take a moment to fix them up before +proceeding to the next section of the tutorial. + +Request and Response Objects +---------------------------- + +Each responder in a resource receives a ``Request`` object that can be +used to read the headers, query parameters, and body of the request. You +can use the standard ``help()`` function or IPython's magic ``?`` +function to list the attributes and methods of Falcon's ``Request`` +class: + +.. code:: bash + + In [1]: import falcon + + In [2]: falcon.Request? + +Each responder also receives a ``Response`` object that can be used for +setting the status code, headers, and body of the response: + +.. code:: bash + + In [3]: falcon.Response? + +This will be useful when creating a POST endpoint in the application +that can add new image resources to our collection. We'll tackle this +functionality next. + +We'll use TDD this time around, to demonstrate how to apply this +particular testing strategy when developing a Falcon application. Via +tests, we'll first define precisely what we want the application to do, +and then code until the tests tell us that we're done. + +.. note:: + To learn more about TDD, you may wish to check out one of the many + books on the topic, such as + `Test Driven Development with Python `_. + The examples in this particular book use the Django framework and + even JavaScript, but the author covers a number of testing + principles that are widely applicable. + +Let's start by adding an additional import statement to ``test_app.py``. +We need to import two modules from ``unittest.mock`` +if you are using Python 3, or from ``mock`` if you are using Python 2. + +.. code:: python + + # Python 3 + from unittest.mock import mock_open, call + + # Python 2 + from mock import mock_open, call + +For Python 2, you will also need to install the ``mock`` package: + +.. code:: bash + + $ pip install mock + +Now add the following test: + +.. code:: python + + # "monkeypatch" is a special built-in pytest fixture that can be + # used to install mocks. + def test_posted_image_gets_saved(client, monkeypatch): + mock_file_open = mock_open() + monkeypatch.setattr('io.open', mock_file_open) + + fake_uuid = '123e4567-e89b-12d3-a456-426655440000' + monkeypatch.setattr('uuid.uuid4', lambda: fake_uuid) + + # When the service receives an image through POST... + fake_image_bytes = b'fake-image-bytes' + response = client.simulate_post( + '/images', + body=fake_image_bytes, + headers={'content-type': 'image/png'} + ) + + # ...it must return a 201 code, save the file, and return the + # image's resource location. + assert response.status == falcon.HTTP_CREATED + assert call().write(fake_image_bytes) in mock_file_open.mock_calls + assert response.headers['location'] == '/images/{}.png'.format(fake_uuid) + +As you can see, this test relies heavily on mocking, making it +somewhat fragile in the face of implementation changes. We'll revisit +this later. For now, run the tests again and watch to make sure +they fail. A key step in the TDD workflow is verifying that +your tests **do not** pass before moving on to the implementation: + +.. code:: bash + + $ pytest tests + +To make the new test pass, we need to add a new method for handling +POSTs. Open ``images.py`` and add a POST responder to the +``Resource`` class as follows: + +.. code:: python + + import io + import os + import uuid + import mimetypes + + import falcon + import msgpack + + + class Resource(object): + + _CHUNK_SIZE_BYTES = 4096 + + # The resource object must now be initialized with a path used during POST + def __init__(self, storage_path): + self._storage_path = storage_path + + # This is the method we implemented before + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + resp.data = msgpack.packb(doc, use_bin_type=True) + resp.content_type = falcon.MEDIA_MSGPACK + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + ext = mimetypes.guess_extension(req.content_type) + name = '{uuid}{ext}'.format(uuid=uuid.uuid4(), ext=ext) + image_path = os.path.join(self._storage_path, name) + + with io.open(image_path, 'wb') as image_file: + while True: + chunk = req.stream.read(self._CHUNK_SIZE_BYTES) + if not chunk: + break + + image_file.write(chunk) + + resp.status = falcon.HTTP_201 + resp.location = '/images/' + name + +As you can see, we generate a unique name for the image, and then write +it out by reading from ``req.stream``. It's called ``stream`` instead +of ``body`` to emphasize the fact that you are really reading from an input +stream; by default Falcon does not spool or decode request data, instead +giving you direct access to the incoming binary stream provided by the +WSGI server. + +Note the use of ``falcon.HTTP_201`` for setting the response status to +"201 Created". We could have also used the ``falcon.HTTP_CREATED`` +alias. For a full list of predefined status strings, simply +call ``help()`` on ``falcon.status_codes``: + +.. code:: bash + + In [4]: help(falcon.status_codes) + +The last line in the ``on_post()`` responder sets the Location header +for the newly created resource. (We will create a route for that path in +just a minute.) The :class:`~.Request` and :class:`~.Response` classes +contain convent attributes for reading and setting common headers, but +you can always access any header by name with the ``req.get_header()`` +and ``resp.set_header()`` methods. + +Take a moment to run pytest again to check your progress: + +.. code:: bash + + $ pytest tests + +You should see a ``TypeError`` as a consequence of adding the +``storage_path`` parameter to ``Resource.__init__()``. + +To fix this, simply edit ``app.py`` and pass in a path to the +initializer. For now, just use the working directory from which you +started the service: + +.. code:: python + + images = Resource(storage_path='.') + +Try running the tests again. This time, they should pass with flying +colors! + +.. code:: bash + + $ pytest tests + +Finally, restart Gunicorn and then try +sending a POST request to the resource from the command line +(substituting ``test.png`` for a path to any PNG you like.) + +.. code:: bash + + $ http POST localhost:8000/images Content-Type:image/png < test.png + +Now, if you check your storage directory, it should contain a copy of the +image you just POSTed. + +Upward and onward! + +Refactoring for testability +--------------------------- + +Earlier we pointed out that our POST test relied heavily on mocking, +relying on assumptions that may or may not hold true as the code +evolves. To mitigate this problem, we'll not only have to refactor the +tests, but also the application itself. + +We'll start by factoring out the business logic from the resource's +POST responder in ``images.py`` so that it can be tested independently. +In this case, the resource's "business logic" is simply the image-saving +operation: + +.. code:: python + + import io + import mimetypes + import os + import uuid + + import falcon + import msgpack + + + class Resource(object): + + def __init__(self, image_store): + self._image_store = image_store + + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + resp.data = msgpack.packb(doc, use_bin_type=True) + resp.content_type = falcon.MEDIA_MSGPACK + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + name = self._image_store.save(req.stream, req.content_type) + resp.status = falcon.HTTP_201 + resp.location = '/images/' + name + + + class ImageStore(object): + + _CHUNK_SIZE_BYTES = 4096 + + # Note the use of dependency injection for standard library + # methods. We'll use these later to avoid monkey-patching. + def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open): + self._storage_path = storage_path + self._uuidgen = uuidgen + self._fopen = fopen + + def save(self, image_stream, image_content_type): + ext = mimetypes.guess_extension(image_content_type) + name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext) + image_path = os.path.join(self._storage_path, name) + + with self._fopen(image_path, 'wb') as image_file: + while True: + chunk = image_stream.read(self._CHUNK_SIZE_BYTES) + if not chunk: + break + + image_file.write(chunk) + + return name + +Let's check to see if we broke anything with the changes above: + +.. code:: bash + + $ pytest tests + +Hmm, it looks like we forgot to update ``app.py``. Let's do that now: + +.. code:: python + + import falcon + + from .images import ImageStore, Resource + + + api = application = falcon.API() + + image_store = ImageStore('.') + images = Resource(image_store) + api.add_route('/images', images) + +Let's try again: + +.. code:: bash + + $ pytest tests + +Now you should see a failed test assertion regarding ``mock_file_open``. +To fix this, we need to switch our strategy from monkey-patching to +dependency injection. Return to ``app.py`` and modify it to look +similar to the following: + +.. code:: python + + import falcon + + from .images import ImageStore, Resource + + + def create_app(image_store): + image_resource = Resource(image_store) + api = falcon.API() + api.add_route('/images', image_resource) + return api + + + def get_app(): + image_store = ImageStore('.') + return create_app(image_store) + +As you can see, the bulk of the setup logic has been moved to +``create_app()``, which can be used to obtain an API object either +for testing or for hosting in production. +``get_app()`` takes care of instantiating additional resources and +configuring the application for hosting. + +The command to run the application is now: + +.. code:: bash + + $ gunicorn --reload 'look.app:get_app()' + +Finally, we need to update the test code. Modify ``test_app.py`` to +look similar to this: + +.. code:: python + + import io + + # Python 3 + from unittest.mock import call, MagicMock, mock_open + + # Python 2 + # from mock import call, MagicMock, mock_open + + import falcon + from falcon import testing + import msgpack + import pytest + + import look.app + import look.images + + + @pytest.fixture + def mock_store(): + return MagicMock() + + + @pytest.fixture + def client(mock_store): + api = look.app.create_app(mock_store) + return testing.TestClient(api) + + + def test_list_images(client): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + response = client.simulate_get('/images') + result_doc = msgpack.unpackb(response.content, encoding='utf-8') + + assert result_doc == doc + assert response.status == falcon.HTTP_OK + + + # With clever composition of fixtures, we can observe what happens with + # the mock injected into the image resource. + def test_post_image(client, mock_store): + file_name = 'fake-image-name.xyz' + + # We need to know what ImageStore method will be used + mock_store.save.return_value = file_name + image_content_type = 'image/xyz' + + response = client.simulate_post( + '/images', + body=b'some-fake-bytes', + headers={'content-type': image_content_type} + ) + + assert response.status == falcon.HTTP_CREATED + assert response.headers['location'] == '/images/{}'.format(file_name) + saver_call = mock_store.save.call_args + + # saver_call is a unittest.mock.call tuple. It's first element is a + # tuple of positional arguments supplied when calling the mock. + assert isinstance(saver_call[0][0], falcon.request_helpers.BoundedStream) + assert saver_call[0][1] == image_content_type + +As you can see, we've redone the POST. While there are fewer mocks, the assertions +have gotten more elaborate to properly check interactions at the interface boundaries. + +Let's check our progress: + +.. code:: bash + + $ pytest tests + +All green! But since we used a mock, we're no longer covering the actual +saving of the image. Let's add a test for that: + +.. code:: python + + def test_saving_image(monkeypatch): + # This still has some mocks, but they are more localized and do not + # have to be monkey-patched into standard library modules (always a + # risky business). + mock_file_open = mock_open() + + fake_uuid = '123e4567-e89b-12d3-a456-426655440000' + def mock_uuidgen(): + return fake_uuid + + fake_image_bytes = b'fake-image-bytes' + fake_request_stream = io.BytesIO(fake_image_bytes) + storage_path = 'fake-storage-path' + store = look.images.ImageStore( + storage_path, + uuidgen=mock_uuidgen, + fopen=mock_file_open + ) + + assert store.save(fake_request_stream, 'image/png') == fake_uuid + '.png' + assert call().write(fake_image_bytes) in mock_file_open.mock_calls + +Now give it a try: + +.. code:: bash + + $ pytest tests -k test_saving_image + +Like the former test, this one still uses mocks. But the +structure of the code has been improved through the techniques of +componentization and dependency inversion, making the application +more flexible and testable. + +.. tip:: + Checking code `coverage `_ would + have helped us detect the missing test above; it's always a good + idea to include coverage testing in your workflow to ensure you + don't have any bugs hiding off somewhere in an unexercised code + path. + +Functional tests +---------------- + +Functional tests define the application's behavior from the outside. +When using TDD, this can be a more natural place to start as opposed +to lower-level unit testing, since it is difficult to anticipate +what internal interfaces and components are needed in advance of +defining the application's user-facing functionality. + +In the case of the refactoring work from the last section, we could have +inadvertently introduced a functional bug into the application that our +unit tests would not have caught. This can happen when a bug is a result +of an unexpected interaction between multiple units, between +the application and the web server, or between the application and +any external services it depends on. + +With test helpers such as ``simulate_get()`` and ``simulate_post()``, +we can create tests that span multiple units. But we can also go one +step further and run the application as a normal, separate process +(e.g. with Gunicorn). We can then write tests that interact with the running +process through HTTP, behaving like a normal client. + +Let's see this in action. Create a new test module, +``tests/test_integration.py`` with the following contents: + +.. code:: python + + import os + + import requests + + + def test_posted_image_gets_saved(): + file_save_prefix = '/tmp/' + location_prefix = '/images/' + fake_image_bytes = b'fake-image-bytes' + + response = requests.post( + 'http://localhost:8000/images', + data=fake_image_bytes, + headers={'content-type': 'image/png'} + ) + + assert response.status_code == 201 + location = response.headers['location'] + assert location.startswith(location_prefix) + image_name = location.replace(location_prefix, '') + + file_path = file_save_prefix + image_name + with open(file_path, 'rb') as image_file: + assert image_file.read() == fake_image_bytes + + os.remove(file_path) + +Next, install the ``requests`` package (as required by the new test) +and make sure Gunicorn is up and running: + +.. code:: bash + + $ pip install requests + $ gunicorn 'look.app:get_app()' + +Then, in another terminal, try running the new test: + +.. code:: bash + + $ pytest tests -k test_posted_image_gets_saved + +The test will fail since it expects the image file to reside under +``/tmp``. To fix this, modify ``app.py`` to add the ability to configure +the image storage directory with an environment variable: + +.. code:: python + + import os + + import falcon + + from .images import ImageStore, Resource + + + def create_app(image_store): + image_resource = Resource(image_store) + api = falcon.API() + api.add_route('/images', image_resource) + return api + + + def get_app(): + storage_path = os.environ.get('LOOK_STORAGE_PATH', '.') + image_store = ImageStore(storage_path) + return create_app(image_store) + +Now you can re-run the app against the desired storage directory: + +.. code:: bash + + $ LOOK_STORAGE_PATH=/tmp gunicorn --reload 'look.app:get_app()' + +You should now be able to re-run the test and see it succeed: + +.. code:: bash + + $ pytest tests -k test_posted_image_gets_saved + +.. note:: + The above process of starting, testing, stopping, and cleaning + up after each test run can (and really should be) automated. + Depending on your needs, you can develop your own automation + fixtures, or use a library such as + `mountepy `_. + +Many developers choose to write tests like the above to sanity-check +their application's primary functionality, while leaving the bulk of +testing to simulated requests and unit tests. These latter types +of tests generally execute much faster and facilitate more fine-grained +test assertions as compared to higher-level functional and system +tests. That being said, testing strategies vary widely and you should +choose the one that best suits your needs. + +At this point, you should have a good grip on how to apply +common testing strategies to your Falcon application. For the +sake of brevity we'll omit further testing instructions from the +following sections, focusing instead on showcasing more of Falcon's +features. + +.. _tutorial-serving-images: + +Serving Images +-------------- + +Now that we have a way of getting images into the service, we of course +need a way to get them back out. What we want to do is return an image +when it is requested, using the path that came back in the Location +header. + +Try executing the following: + +.. code:: bash + + $ http localhost:8000/images/db79e518-c8d3-4a87-93fe-38b620f9d410.png + +In response, you should get a ``404 Not Found``. This is the default +response given by Falcon when it can not find a resource that matches +the requested URL path. + +Let's address this by creating a separate class to represent a single +image resource. We will then add an ``on_get()`` method to respond to +the path above. + +Go ahead and edit your ``images.py`` file to look something like this: + +.. code:: python + + import io + import os + import re + import uuid + import mimetypes + + import falcon + import msgpack + + + class Collection(object): + + def __init__(self, image_store): + self._image_store = image_store + + def on_get(self, req, resp): + # TODO: Modify this to return a list of href's based on + # what images are actually available. + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + resp.data = msgpack.packb(doc, use_bin_type=True) + resp.content_type = falcon.MEDIA_MSGPACK + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + name = self._image_store.save(req.stream, req.content_type) + resp.status = falcon.HTTP_201 + resp.location = '/images/' + name + + + class Item(object): + + def __init__(self, image_store): + self._image_store = image_store + + def on_get(self, req, resp, name): + resp.content_type = mimetypes.guess_type(name)[0] + resp.stream, resp.stream_len = self._image_store.open(name) + + + class ImageStore(object): + + _CHUNK_SIZE_BYTES = 4096 + _IMAGE_NAME_PATTERN = re.compile( + '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[a-z]{2,4}$' + ) + + def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open): + self._storage_path = storage_path + self._uuidgen = uuidgen + self._fopen = fopen + + def save(self, image_stream, image_content_type): + ext = mimetypes.guess_extension(image_content_type) + name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext) + image_path = os.path.join(self._storage_path, name) + + with self._fopen(image_path, 'wb') as image_file: + while True: + chunk = image_stream.read(self._CHUNK_SIZE_BYTES) + if not chunk: + break + + image_file.write(chunk) + + return name + + def open(self, name): + # Always validate untrusted input! + if not self._IMAGE_NAME_PATTERN.match(name): + raise IOError('File not found') + + image_path = os.path.join(self._storage_path, name) + stream = self._fopen(image_path, 'rb') + stream_len = os.path.getsize(image_path) + + return stream, stream_len + +As you can see, we renamed ``Resource`` to ``Collection`` and added a new ``Item`` +class to represent a single image resource. Also, note the ``name`` parameter +for the ``on_get()`` responder. Any URI parameters that you specify in your routes +will be turned into corresponding kwargs and passed into the target responder as +such. We'll see how to specify URI parameters in a moment. + +Inside the ``on_get()`` responder, +we set the Content-Type header based on the filename extension, and then +stream out the image directly from an open file handle. Note the use of +``resp.stream_len``. Whenever using ``resp.stream`` instead of ``resp.body`` or +``resp.data``, you typically also specify the expected length of the stream so +that the web client knows how much data to read from the response. + +.. note:: If you do not know the size of the stream in advance, you can work around + that by using chunked encoding, but that's beyond the scope of this + tutorial. + +If ``resp.status`` is not set explicitly, it defaults to ``200 OK``, which is +exactly what we want ``on_get()`` to do. + +Now let's wire everything up and give it a try. Edit ``app.py`` to look +similar to the following: + +.. code:: python + + import os + + import falcon + + import images + + + def create_app(image_store): + api = falcon.API() + api.add_route('/images', images.Collection(image_store)) + api.add_route('/images/{name}', images.Item(image_store)) + return api + + + def get_app(): + storage_path = os.environ.get('LOOK_STORAGE_PATH', '.') + image_store = images.ImageStore(storage_path) + return create_app(image_store) + +As you can see, we specified a new route, ``/images/{name}``. This causes +Falcon to expect all associated responders to accept a ``name`` +argument. + +.. note:: + + Falcon also supports more complex parameterized path segments that + contain multiple values. For example, a version control API might + use the following route template for diffing two code branches:: + + /repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1} + +Now re-run your app and try to POST another picture: + +.. code:: bash + + $ http POST localhost:8000/images Content-Type:image/png < test.png + +Make a note of the path returned in the Location header, and use it to +GET the image: + +.. code:: bash + + $ http localhost:8000/images/dddff30e-d2a6-4b57-be6a-b985ee67fa87.png + +HTTPie won't display the image, but you can see that the +response headers were set correctly. Just for fun, go ahead and paste +the above URI into your browser. The image should display correctly. + + +.. Query Strings +.. ------------- + +.. *Coming soon...* + +Introducing Hooks +----------------- + +At this point you should have a pretty good understanding of the basic parts +that make up a Falcon-based API. Before we finish up, let's just take a few +minutes to clean up the code and add some error handling. + +First, let's check the incoming media type when something is posted +to make sure it is a common image type. We'll implement this with a +``before`` hook. + +Start by defining a list of media types the service will accept. Place +this constant near the top, just after the import statements in +``images.py``: + +.. code:: python + + ALLOWED_IMAGE_TYPES = ( + 'image/gif', + 'image/jpeg', + 'image/png', + ) + +The idea here is to only accept GIF, JPEG, and PNG images. You can add others +to the list if you like. + +Next, let's create a hook that will run before each request to post a +message. Add this method below the definition of ``ALLOWED_IMAGE_TYPES``: + +.. code:: python + + def validate_image_type(req, resp, resource, params): + if req.content_type not in ALLOWED_IMAGE_TYPES: + msg = 'Image type not allowed. Must be PNG, JPEG, or GIF' + raise falcon.HTTPBadRequest('Bad request', msg) + +And then attach the hook to the ``on_post()`` responder: + +.. code:: python + + @falcon.before(validate_image_type) + def on_post(self, req, resp): + # ... + +Now, before every call to that responder, Falcon will first invoke +``validate_image_type()``. There isn't anything special about this +function, other than it must accept four arguments. Every hook takes, as its +first two arguments, a reference to the same ``req`` and ``resp`` objects +that are passed into responders. The ``resource`` argument is a Resource +instance associated with the request. The fourth argument, named ``params`` +by convention, is a reference to the kwarg dictionary Falcon creates for +each request. ``params`` will contain the route's URI template params and +their values, if any. + +As you can see in the example above, you can use ``req`` to get information +about the incoming request. However, you can also use ``resp`` to play with +the HTTP response as needed, and you can even use hooks to inject extra +kwargs: + +.. code:: python + + def extract_project_id(req, resp, resource, params): + """Adds `project_id` to the list of params for all responders. + + Meant to be used as a `before` hook. + """ + params['project_id'] = req.get_header('X-PROJECT-ID') + +Now, you might imagine that such a hook should apply to all responders +for a resource. In fact, hooks can be applied to an entire resource +by simply decorating the class: + +.. code:: python + + @falcon.before(extract_project_id) + class Message(object): + + # ... + +Similar logic can be applied globally with middleware. +(See also: :ref:`falcon.middleware `) + +Now that you've added a hook to validate the media type, you can see it +in action by attempting to POST something nefarious: + +.. code:: bash + + $ http POST localhost:8000/images Content-Type:image/jpx + +You should get back a ``400 Bad Request`` status and a nicely structured +error body. + +.. tip:: + When something goes wrong, you usually want to give your users + some info to help them resolve the issue. The exception to this rule + is when an error occurs because the user is requested something they + are not authorized to access. In that case, you may wish to simply + return ``404 Not Found`` with an empty body, in case a malicious + user is fishing for information that will help them crack your app. + +Check out the :ref:`hooks reference ` to learn more. + +Error Handling +-------------- + +Generally speaking, Falcon assumes that resource responders +(``on_get()``, ``on_post()``, etc.) will, for the most part, do the +right thing. In other words, Falcon doesn't try very hard to protect +responder code from itself. + +This approach reduces the number of (often) extraneous checks that Falcon +would otherwise have to perform, making the framework more efficient. With +that in mind, writing a high-quality API based on Falcon requires that: + +1. Resource responders set response variables to sane values. +2. Untrusted input (i.e., input from an external client or service) is + validated. +3. Your code is well-tested, with high code coverage. +4. Errors are anticipated, detected, logged, and handled appropriately + within each responder or by global error handling hooks. + +When it comes to error handling, you can always directly set the error +status, appropriate response headers, and error body using the ``resp`` +object. However, Falcon tries to make things a little easier by +providing a :ref:`set of error classes ` you can +raise when something goes wrong. Falcon will convert any instance or +subclass of :class:`falcon.HTTPError` raised by a responder, hook, or +middleware component into an appropriate HTTP response. + +You may raise an instance of :class:`falcon.HTTPError` directly, or use +any one of a number of :ref:`predefined errors ` +that are designed to set the response headers and body appropriately +for each error type. + +.. tip:: + Falcon will re-raise errors that do not inherit from + :class:`falcon.HTTPError` + unless you have registered a custom error handler for that type. + + Error handlers may be registered for any type, including + :class:`~.HTTPError`. This feature provides a central location + for logging and otherwise handling exceptions raised by + responders, hooks, and middleware components. + + See also: :meth:`~.API.add_error_handler`. + +Let's see a quick example of how this works. Try requesting an invalid +image name from your application: + +.. code:: bash + + $ http localhost:8000/images/voltron.png + +As you can see, the result isn't exactly graceful. To fix this, we'll +need to add some exception handling. Modify your ``Item`` class +as follows: + +.. code:: python + + class Item(object): + + def __init__(self, image_store): + self._image_store = image_store + + def on_get(self, req, resp, name): + resp.content_type = mimetypes.guess_type(name)[0] + + try: + resp.stream, resp.stream_len = self._image_store.open(name) + except IOError: + # Normally you would also log the error. + raise falcon.HTTPNotFound() + +Now let's try that request again: + +.. code:: bash + + $ http localhost:8000/images/voltron.png + +Additional information about error handling is available in the +:ref:`error handling reference `. + +What Now? +--------- + +Our friendly community is available to answer your questions and help you +work through sticky problems. See also: :ref:`Getting Help `. + +As mentioned previously, Falcon's docstrings are quite extensive, and so you +can learn a lot just by poking around Falcon's modules from a Python REPL, +such as `IPython `_ or +`bpython `_. + +Also, don't be shy about pulling up Falcon's source code on GitHub or in your +favorite text editor. The team has tried to make the code as straightforward +and readable as possible; where other documentation may fall short, the code +basically can't be wrong. + +A number of Falcon add-ons, templates, and complementary packages are +available for use in your projects. We've listed several of these on the +`Falcon wiki `_ as a starting +point, but you may also wish to search PyPI for additional resources. diff -Nru python-falcon-1.0.0/examples/look/look/app.py python-falcon-1.4.1/examples/look/look/app.py --- python-falcon-1.0.0/examples/look/look/app.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/look/app.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,18 @@ +import os + +import falcon + +from .images import ImageStore, Resource + + +def create_app(image_store): + image_resource = Resource(image_store) + api = falcon.API() + api.add_route('/images', image_resource) + return api + + +def get_app(): + storage_path = os.environ.get('LOOK_STORAGE_PATH', '.') + image_store = ImageStore(storage_path) + return create_app(image_store) diff -Nru python-falcon-1.0.0/examples/look/look/images.py python-falcon-1.4.1/examples/look/look/images.py --- python-falcon-1.0.0/examples/look/look/images.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/look/images.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,58 @@ +import io +import mimetypes +import os +import uuid + +import falcon +import msgpack + + +class Resource(object): + + def __init__(self, image_store): + self._image_store = image_store + + def on_get(self, req, resp): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + resp.data = msgpack.packb(doc, use_bin_type=True) + resp.content_type = 'application/msgpack' + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + name = self._image_store.save(req.stream, req.content_type) + resp.status = falcon.HTTP_201 + resp.location = '/images/' + name + + +class ImageStore(object): + + _CHUNK_SIZE_BYTES = 4096 + + # Note the use of dependency injection for standard library + # methods. We'll use these later to avoid monkey-patching. + def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open): + self._storage_path = storage_path + self._uuidgen = uuidgen + self._fopen = fopen + + def save(self, image_stream, image_content_type): + ext = mimetypes.guess_extension(image_content_type) + name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext) + image_path = os.path.join(self._storage_path, name) + + with self._fopen(image_path, 'wb') as image_file: + while True: + chunk = image_stream.read(self._CHUNK_SIZE_BYTES) + if not chunk: + break + + image_file.write(chunk) + + return name diff -Nru python-falcon-1.0.0/examples/look/README.rst python-falcon-1.4.1/examples/look/README.rst --- python-falcon-1.0.0/examples/look/README.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/README.rst 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,5 @@ +Tutorial application +==================== + +This is the application that you can build by going along with the tutorial. + diff -Nru python-falcon-1.0.0/examples/look/requirements/look python-falcon-1.4.1/examples/look/requirements/look --- python-falcon-1.0.0/examples/look/requirements/look 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/requirements/look 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,2 @@ +falcon>=1.1.0 +msgpack-python>=0.4.8 diff -Nru python-falcon-1.0.0/examples/look/requirements/test python-falcon-1.4.1/examples/look/requirements/test --- python-falcon-1.0.0/examples/look/requirements/test 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/requirements/test 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,3 @@ +mock>=2.0.0 +pytest>=3.0.6 +requests>=2.13.0 diff -Nru python-falcon-1.0.0/examples/look/tests/test_app.py python-falcon-1.4.1/examples/look/tests/test_app.py --- python-falcon-1.0.0/examples/look/tests/test_app.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/tests/test_app.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,86 @@ +import io + +import falcon +from falcon import testing +from mock import call, MagicMock, mock_open +import msgpack +import pytest + +import look.app +import look.images + + +@pytest.fixture +def mock_store(): + return MagicMock() + + +@pytest.fixture +def client(mock_store): + api = look.app.create_app(mock_store) + return testing.TestClient(api) + + +def test_list_images(client): + doc = { + 'images': [ + { + 'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png' + } + ] + } + + response = client.simulate_get('/images') + result_doc = msgpack.unpackb(response.content, encoding='utf-8') + + assert result_doc == doc + assert response.status == falcon.HTTP_OK + + +# With clever composition of fixtures, we can observe what happens with +# the mock injected into the image resource. +def test_post_image(client, mock_store): + file_name = 'fake-image-name.xyz' + + # We need to know what ImageStore method will be used + mock_store.save.return_value = file_name + image_content_type = 'image/xyz' + + response = client.simulate_post( + '/images', + body=b'some-fake-bytes', + headers={'content-type': image_content_type} + ) + + assert response.status == falcon.HTTP_CREATED + assert response.headers['location'] == '/images/{}'.format(file_name) + saver_call = mock_store.save.call_args + + # saver_call is a unittest.mock.call tuple. It's first element is a + # tuple of positional arguments supplied when calling the mock. + assert isinstance(saver_call[0][0], falcon.request_helpers.BoundedStream) + assert saver_call[0][1] == image_content_type + + +def test_saving_image(monkeypatch): + # This still has some mocks, but they are more localized and do not + # have to be monkey-patched into standard library modules (always a + # risky business). + mock_file_open = mock_open() + + fake_uuid = '123e4567-e89b-12d3-a456-426655440000' + + def mock_uuidgen(): + return fake_uuid + + fake_image_bytes = b'fake-image-bytes' + fake_request_stream = io.BytesIO(fake_image_bytes) + storage_path = 'fake-storage-path' + store = look.images.ImageStore( + storage_path, + uuidgen=mock_uuidgen, + fopen=mock_file_open + ) + + assert store.save(fake_request_stream, 'image/png') == fake_uuid + '.png' + assert call().write(fake_image_bytes) in mock_file_open.mock_calls diff -Nru python-falcon-1.0.0/examples/look/tests/test_integration.py python-falcon-1.4.1/examples/look/tests/test_integration.py --- python-falcon-1.0.0/examples/look/tests/test_integration.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/examples/look/tests/test_integration.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,26 @@ +import os + +import requests + + +def test_posted_image_gets_saved(): + file_save_prefix = '/tmp/' + location_prefix = '/images/' + fake_image_bytes = b'fake-image-bytes' + + response = requests.post( + 'http://localhost:8000/images', + data=fake_image_bytes, + headers={'content-type': 'image/png'} + ) + + assert response.status_code == 201 + location = response.headers['location'] + assert location.startswith(location_prefix) + image_name = location.replace(location_prefix, '') + + file_path = file_save_prefix + image_name + with open(file_path, 'rb') as image_file: + assert image_file.read() == fake_image_bytes + + os.remove(file_path) diff -Nru python-falcon-1.0.0/falcon/api_helpers.py python-falcon-1.4.1/falcon/api_helpers.py --- python-falcon-1.0.0/falcon/api_helpers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/api_helpers.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,22 +12,56 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Utilities for the API class.""" + +from functools import wraps + +import six + from falcon import util -def prepare_middleware(middleware=None): +def make_router_search(router): + """Create a search function for routing requests. + + Args: + router(object): An object that implements the routing engine + interface. + + Returns: + callable: A function that accepts a request and invokes the + router's find method. + """ + + arg_names = util.get_argnames(router.find) + supports_req = 'req' in arg_names and len(arg_names) > 1 + + if supports_req: + return router.find + + def search_shim(path, req): + return router.find(path) + + return search_shim + + +def prepare_middleware(middleware=None, independent_middleware=False): """Check middleware interface and prepare it to iterate. Args: - middleware: list (or object) of input middleware + middleware: list (or object) of input middleware + independent_middleware: bool whether should prepare request and + response middleware independently Returns: - A middleware list + list: A tuple of prepared middleware tuples """ # PERF(kgriffs): do getattr calls once, in advance, so we don't # have to do them every time in the request path. - prepared_middleware = [] + request_mw = [] + resource_mw = [] + response_mw = [] if middleware is None: middleware = [] @@ -47,10 +81,37 @@ msg = '{0} does not implement the middleware interface' raise TypeError(msg.format(component)) - prepared_middleware.append((process_request, process_resource, - process_response)) + if process_response: + # NOTE(kgriffs): Shim older implementations to ensure + # backwards-compatibility. + args = util.get_argnames(process_response) + + if len(args) == 3: # (req, resp, resource) + def let(process_response=process_response): + @wraps(process_response) + def shim(req, resp, resource, req_succeeded): + process_response(req, resp, resource) + + return shim + + process_response = let() + + # NOTE: depending on whether we want to execute middleware + # independently, we group response and request middleware either + # together or separately. + if independent_middleware: + if process_request: + request_mw.append(process_request) + if process_response: + response_mw.insert(0, process_response) + else: + if process_request or process_response: + request_mw.append((process_request, process_response)) + + if process_resource: + resource_mw.append(process_resource) - return prepared_middleware + return (tuple(request_mw), tuple(resource_mw), tuple(response_mw)) def default_serialize_error(req, resp, exception): @@ -104,7 +165,6 @@ preferred = 'application/xml' if preferred is not None: - resp.append_header('Vary', 'Accept') if preferred == 'application/json': representation = exception.to_json() else: @@ -113,9 +173,11 @@ resp.body = representation resp.content_type = preferred + '; charset=UTF-8' + resp.append_header('Vary', 'Accept') + def wrap_old_error_serializer(old_fn): - """Wraps an old-style error serializer to add body/content_type setting. + """Wrap an old-style error serializer to add body/content_type setting. Args: old_fn: Old-style error serializer @@ -131,3 +193,39 @@ resp.content_type = media_type return new_fn + + +class CloseableStreamIterator(six.Iterator): + """Iterator that wraps a file-like stream with support for close(). + + This iterator can be used to read from an underlying file-like stream + in block_size-chunks until the response from the stream is an empty + byte string. + + This class is used to wrap WSGI response streams when a + wsgi_file_wrapper is not provided by the server. The fact that it + also supports closing the underlying stream allows use of (e.g.) + Python tempfile resources that would be deleted upon close. + + Args: + stream (object): Readable file-like stream object. + block_size (int): Number of bytes to read per iteration. + """ + + def __init__(self, stream, block_size): + self.stream = stream + self.block_size = block_size + + def __iter__(self): + return self + + def __next__(self): + data = self.stream.read(self.block_size) + if data == b'': + raise StopIteration + else: + return data + + def close(self): + if hasattr(self.stream, 'close') and callable(self.stream.close): + self.stream.close() diff -Nru python-falcon-1.0.0/falcon/api.py python-falcon-1.4.1/falcon/api.py --- python-falcon-1.0.0/falcon/api.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/api.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,46 +12,52 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect +"""Falcon API class.""" + import re + import six -from falcon import api_helpers as helpers -from falcon import DEFAULT_MEDIA_TYPE +from falcon import api_helpers as helpers, DEFAULT_MEDIA_TYPE, routing from falcon.http_error import HTTPError from falcon.http_status import HTTPStatus from falcon.request import Request, RequestOptions -from falcon.response import Response import falcon.responders -from falcon import routing +from falcon.response import Response, ResponseOptions import falcon.status_codes as status +from falcon.util.misc import get_argnames class API(object): """This class is the main entry point into a Falcon-based app. - Each API instance provides a callable WSGI interface and a routing engine. + Each API instance provides a callable WSGI interface and a routing + engine. - Args: - media_type (str, optional): Default media type to use as the value for - the Content-Type header on responses (default 'application/json'). - middleware(object or list, optional): One or more objects - (instantiated classes) that implement the following middleware - component interface:: + Keyword Arguments: + media_type (str): Default media type to use as the + value for the Content-Type header on responses (default + 'application/json'). The ``falcon`` module provides a + number of constants for common media types, such as + ``falcon.MEDIA_MSGPACK``, ``falcon.MEDIA_YAML``, + ``falcon.MEDIA_XML``, etc. + middleware(object or list): Either a single object or a list + of objects (instantiated classes) that implement the + following middleware component interface:: class ExampleComponent(object): def process_request(self, req, resp): - \"""Process the request before routing it. + \"\"\"Process the request before routing it. Args: req: Request object that will eventually be routed to an on_* responder method. resp: Response object that will be routed to the on_* responder. - \""" + \"\"\" def process_resource(self, req, resp, resource, params): - \"""Process the request and resource *after* routing. + \"\"\"Process the request and resource *after* routing. Note: This method is only called when the request matches @@ -70,10 +76,10 @@ template fields, that will be passed to the resource's responder method as keyword arguments. - \""" + \"\"\" - def process_response(self, req, resp, resource) - \"""Post-processing of the response (after routing). + def process_response(self, req, resp, resource, req_succeeded) + \"\"\"Post-processing of the response (after routing). Args: req: Request object. @@ -81,27 +87,43 @@ resource: Resource object to which the request was routed. May be None if no route was found for the request. - \""" + req_succeeded: True if no exceptions were raised + while the framework processed and routed the + request; otherwise False. + \"\"\" - See also :ref:`Middleware `. + (See also: :ref:`Middleware `) - request_type (Request, optional): ``Request``-like class to use instead + request_type (Request): ``Request``-like class to use instead of Falcon's default class. Among other things, this feature affords inheriting from ``falcon.request.Request`` in order to override the ``context_type`` class variable. (default ``falcon.request.Request``) - response_type (Response, optional): ``Response``-like class to use + response_type (Response): ``Response``-like class to use instead of Falcon's default class. (default ``falcon.response.Response``) - router (object, optional): An instance of a custom router + router (object): An instance of a custom router to use in lieu of the default engine. - See also: :ref:`Routing `. + (See also: :ref:`Custom Routers `) + + independent_middleware (bool): Set to ``True`` if response + middleware should be executed independently of whether or + not request middleware raises an exception (default + ``False``). Attributes: - req_options (RequestOptions): A set of behavioral options related to - incoming requests. + req_options: A set of behavioral options related to incoming + requests. (See also: :py:class:`~.RequestOptions`) + resp_options: A set of behavioral options related to outgoing + responses. (See also: :py:class:`~.ResponseOptions`) + router_options: Configuration options for the router. If a + custom router is in use, and it does not expose any + configurable options, referencing this attribute will raise + an instance of ``AttributeError``. + + (See also: :ref:`CompiledRouterOptions `) """ # PERF(kgriffs): Reference via self since that is faster than @@ -117,34 +139,50 @@ __slots__ = ('_request_type', '_response_type', '_error_handlers', '_media_type', '_router', '_sinks', - '_serialize_error', 'req_options', '_middleware') + '_serialize_error', 'req_options', 'resp_options', + '_middleware', '_independent_middleware', '_router_search', + '_static_routes') def __init__(self, media_type=DEFAULT_MEDIA_TYPE, request_type=Request, response_type=Response, - middleware=None, router=None): + middleware=None, router=None, + independent_middleware=False): self._sinks = [] self._media_type = media_type + self._static_routes = [] # set middleware - self._middleware = helpers.prepare_middleware(middleware) + self._middleware = helpers.prepare_middleware( + middleware, independent_middleware=independent_middleware) + self._independent_middleware = independent_middleware self._router = router or routing.DefaultRouter() + self._router_search = helpers.make_router_search(self._router) self._request_type = request_type self._response_type = response_type self._error_handlers = [] self._serialize_error = helpers.default_serialize_error + self.req_options = RequestOptions() + self.resp_options = ResponseOptions() + + self.req_options.default_media_type = media_type + self.resp_options.default_media_type = media_type - def __call__(self, env, start_response): + # NOTE(kgriffs): Add default error handlers + self.add_error_handler(falcon.HTTPError, self._http_error_handler) + self.add_error_handler(falcon.HTTPStatus, self._http_status_handler) + + def __call__(self, env, start_response): # noqa: C901 """WSGI `app` method. Makes instances of API callable from a WSGI server. May be used to host an API or called directly in order to simulate requests when testing the API. - See also PEP 3333. + (See also: PEP 3333) Args: env (dict): A WSGI environment dictionary @@ -154,78 +192,88 @@ """ req = self._request_type(env, options=self.req_options) - resp = self._response_type() + resp = self._response_type(options=self.resp_options) resource = None - middleware_stack = [] # Keep track of executed components params = {} - try: - # NOTE(kgriffs): Using an inner try..except in order to - # address the case when err_handler raises HTTPError. - - # NOTE(kgriffs): Coverage is giving false negatives, - # so disabled on relevant lines. All paths are tested - # afaict. - try: - # NOTE(ealogar): The execution of request middleware should be - # before routing. This will allow request mw to modify path. - self._call_req_mw(middleware_stack, req, resp) - # NOTE(warsaw): Moved this to inside the try except because it - # is possible when using object-based traversal for - # _get_responder() to fail. An example is a case where an - # object does not have the requested next-hop child resource. - # In that case, the object being asked to dispatch to its - # child will raise an HTTP exception signalling the problem, - # e.g. a 404. - responder, params, resource = self._get_responder(req) - - # NOTE(kgriffs): If the request did not match any route, - # a default responder is returned and the resource is - # None. - if resource is not None: - self._call_rsrc_mw(middleware_stack, req, resp, resource, - params) - - responder(req, resp, **params) - self._call_resp_mw(middleware_stack, req, resp, resource) - - except Exception as ex: - for err_type, err_handler in self._error_handlers: - if isinstance(ex, err_type): - err_handler(ex, req, resp, params) - self._call_resp_mw(middleware_stack, req, resp, - resource) + dependent_mw_resp_stack = [] + mw_req_stack, mw_rsrc_stack, mw_resp_stack = self._middleware - break + req_succeeded = False + try: + try: + # NOTE(ealogar): The execution of request middleware + # should be before routing. This will allow request mw + # to modify the path. + # NOTE: if flag set to use independent middleware, execute + # request middleware independently. Otherwise, only queue + # response middleware after request middleware succeeds. + if self._independent_middleware: + for process_request in mw_req_stack: + process_request(req, resp) else: - # PERF(kgriffs): This will propagate HTTPError to - # the handler below. It makes handling HTTPError - # less efficient, but that is OK since error cases - # don't need to be as fast as the happy path, and - # indeed, should perhaps be slower to create - # backpressure on clients that are issuing bad - # requests. - - # NOTE(ealogar): This will executed remaining - # process_response when no error_handler is given - # and for whatever exception. If an HTTPError is raised - # remaining process_response will be executed later. - self._call_resp_mw(middleware_stack, req, resp, resource) + for process_request, process_response in mw_req_stack: + if process_request: + process_request(req, resp) + if process_response: + dependent_mw_resp_stack.insert(0, process_response) + + # NOTE(warsaw): Moved this to inside the try except + # because it is possible when using object-based + # traversal for _get_responder() to fail. An example is + # a case where an object does not have the requested + # next-hop child resource. In that case, the object + # being asked to dispatch to its child will raise an + # HTTP exception signalling the problem, e.g. a 404. + responder, params, resource, req.uri_template = self._get_responder(req) + except Exception as ex: + if not self._handle_exception(ex, req, resp, params): raise + else: + try: + # NOTE(kgriffs): If the request did not match any + # route, a default responder is returned and the + # resource is None. In that case, we skip the + # resource middleware methods. + if resource is not None: + # Call process_resource middleware methods. + for process_resource in mw_rsrc_stack: + process_resource(req, resp, resource, params) + + responder(req, resp, **params) + req_succeeded = True + except Exception as ex: + if not self._handle_exception(ex, req, resp, params): + raise + finally: + # NOTE(kgriffs): It may not be useful to still execute + # response middleware methods in the case of an unhandled + # exception, but this is done for the sake of backwards + # compatibility, since it was incidentally the behavior in + # the 1.0 release before this section of the code was + # reworked. + + # Call process_response middleware methods. + for process_response in mw_resp_stack or dependent_mw_resp_stack: + try: + process_response(req, resp, resource, req_succeeded) + except Exception as ex: + if not self._handle_exception(ex, req, resp, params): + raise - except HTTPStatus as ex: - self._compose_status_response(req, resp, ex) - self._call_resp_mw(middleware_stack, req, resp, resource) - - except HTTPError as ex: - self._compose_error_response(req, resp, ex) - self._call_resp_mw(middleware_stack, req, resp, resource) + req_succeeded = False # # Set status and headers # - if req.method == 'HEAD' or resp.status in self._BODILESS_STATUS_CODES: + + # NOTE(kgriffs): While not specified in the spec that the status + # must be of type str (not unicode on Py27), some WSGI servers + # can complain when it is not. + resp_status = str(resp.status) if six.PY2 else resp.status + + if req.method == 'HEAD' or resp_status in self._BODILESS_STATUS_CODES: body = [] else: body, length = self._get_body(resp, env.get('wsgi.file_wrapper')) @@ -236,61 +284,42 @@ # RFC 2616, as commented in that module's source code. The # presence of the Content-Length header is not similarly # enforced. - if resp.status in (status.HTTP_204, status.HTTP_304): + if resp_status in (status.HTTP_204, status.HTTP_304): media_type = None else: media_type = self._media_type headers = resp._wsgi_headers(media_type) - # Return the response per the WSGI spec - start_response(resp.status, headers) + # Return the response per the WSGI spec. + start_response(resp_status, headers) return body - def add_route(self, uri_template, resource, *args, **kwargs): - """Associates a templatized URI path with a resource. - - A resource is an instance of a class that defines various - "responder" methods, one for each HTTP method the resource - allows. Responder names start with `on_` and are named according to - which HTTP method they handle, as in `on_get`, `on_post`, `on_put`, - etc. - - If your resource does not support a particular - HTTP method, simply omit the corresponding responder and - Falcon will reply with "405 Method not allowed" if that - method is ever requested. - - Responders must always define at least two arguments to receive - request and response objects, respectively. For example:: + @property + def router_options(self): + return self._router.options - def on_post(self, req, resp): - pass - - In addition, if the route's template contains field - expressions, any responder that desires to receive requests - for that route must accept arguments named after the respective - field names defined in the template. A field expression consists - of a bracketed field name. - - For example, given the following template:: - - /user/{name} - - A PUT request to "/user/kgriffs" would be routed to:: - - def on_put(self, req, resp, name): - pass + def add_route(self, uri_template, resource, *args, **kwargs): + """Associate a templatized URI path with a resource. - Individual path segments may contain one or more field expressions. - For example:: + Falcon routes incoming requests to resources based on a set of + URI templates. If the path requested by the client matches the + template for a given route, the request is then passed on to the + associated resource for processing. + + If no route matches the request, control then passes to a + default responder that simply raises an instance of + :class:`~.HTTPNotFound`. - /repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1} + (See also: :ref:`Routing `) Args: uri_template (str): A templatized URI. Care must be taken to ensure the template does not mask any sink - patterns, if any are registered (see also `add_sink`). + patterns, if any are registered. + + (See also: :meth:`~.add_sink`) + resource (instance): Object which represents a REST resource. Falcon will pass "GET" requests to on_get, "PUT" requests to on_put, etc. If any HTTP methods are not @@ -318,12 +347,60 @@ if '//' in uri_template: raise ValueError("uri_template may not contain '//'") - method_map = routing.create_http_method_map(resource) + method_map = routing.map_http_methods(resource) + routing.set_default_responders(method_map) self._router.add_route(uri_template, method_map, resource, *args, **kwargs) + def add_static_route(self, prefix, directory, downloadable=False): + """Add a route to a directory of static files. + + Static routes provide a way to serve files directly. This + feature provides an alternative to serving files at the web server + level when you don't have that option, when authorization is + required, or for testing purposes. + + Warning: + Serving files directly from the web server, + rather than through the Python app, will always be more efficient, + and therefore should be preferred in production deployments. + + Static routes are matched in LIFO order. Therefore, if the same + prefix is used for two routes, the second one will override the + first. This also means that more specific routes should be added + *after* less specific ones. For example, the following sequence + would result in ``'/foo/bar/thing.js'`` being mapped to the + ``'/foo/bar'`` route, and ``'/foo/xyz/thing.js'`` being mapped to the + ``'/foo'`` route:: + + api.add_static_route('/foo', foo_path) + api.add_static_route('/foo/bar', foobar_path) + + Args: + prefix (str): The path prefix to match for this route. If the + path in the requested URI starts with this string, the remainder + of the path will be appended to the source directory to + determine the file to serve. This is done in a secure manner + to prevent an attacker from requesting a file outside the + specified directory. + + Note that static routes are matched in LIFO order, and are only + attempted after checking dynamic routes and sinks. + + directory (str): The source directory from which to serve files. + downloadable (bool): Set to ``True`` to include a + Content-Disposition header in the response. The "filename" + directive is simply set to the name of the requested file. + + """ + + self._static_routes.insert( + 0, + routing.StaticRoute(prefix, directory, downloadable=downloadable) + ) + def add_sink(self, sink, prefix=r'/'): - """Registers a sink method for the API. + """Register a sink method for the API. If no route matches a request, but the path in the requested URI matches a sink prefix, Falcon will pass control to the @@ -349,8 +426,9 @@ Warning: If the prefix overlaps a registered route template, - the route will take precedence and mask the sink - (see also `add_route`). + the route will take precedence and mask the sink. + + (See also: :meth:`~.add_route`) """ @@ -364,7 +442,25 @@ self._sinks.insert(0, (prefix, sink)) def add_error_handler(self, exception, handler=None): - """Registers a handler for a given exception error type. + """Register a handler for a given exception error type. + + Error handlers may be registered for any type, including + :class:`~.HTTPError`. This feature provides a central location + for logging and otherwise handling exceptions raised by + responders, hooks, and middleware components. + + A handler can raise an instance of :class:`~.HTTPError` or + :class:`~.HTTPStatus` to communicate information about the issue to + the client. Alternatively, a handler may modify `resp` + directly. + + Error handlers are matched in LIFO order. In other words, when + searching for an error handler to match a raised exception, and + more than one handler matches the exception type, the framework + will choose the one that was most recently registered. + Therefore, more general error handlers (e.g., for the + standard ``Exception`` type) should be added first, to avoid + masking more specific handlers for subclassed types. Args: exception (type): Whenever an error occurs when handling a request @@ -387,10 +483,6 @@ # Convert to an instance of falcon.HTTPError raise falcon.HTTPError(falcon.HTTP_792) - Note: - A handler can either raise an instance of ``HTTPError`` - or modify `resp` manually in order to communicate - information about the issue to the client. """ @@ -408,16 +500,22 @@ self._error_handlers.insert(0, (exception, handler)) def set_error_serializer(self, serializer): - """Override the default serializer for instances of HTTPError. + """Override the default serializer for instances of :class:`~.HTTPError`. + + When a responder raises an instance of :class:`~.HTTPError`, + Falcon converts it to an HTTP response automatically. The + default serializer supports JSON and XML, but may be overridden + by this method to use a custom serializer in order to support + other media types. + + Note: + If a custom media type is used and the type includes a + "+json" or "+xml" suffix, the default serializer will + convert the error to JSON or XML, respectively. - When a responder raises an instance of HTTPError, Falcon converts - it to an HTTP response automatically. The default serializer - supports JSON and XML, but may be overridden by this method to - use a custom serializer in order to support other media types. - - The ``falcon.HTTPError`` class contains helper methods, such as - `to_json()` and `to_dict()`, that can be used from within - custom serializers. For example:: + The :class:`~.HTTPError` class contains helper methods, + such as `to_json()` and `to_dict()`, that can be used from + within custom serializers. For example:: def my_serializer(req, resp, exception): representation = None @@ -434,12 +532,7 @@ resp.body = representation resp.content_type = preferred - Note: - If a custom media type is used and the type includes a - "+json" or "+xml" suffix, the default serializer will - convert the error to JSON or XML, respectively. If this - is not desirable, a custom error serializer may be used - to override this behavior. + resp.append_header('Vary', 'Accept') Args: serializer (callable): A function taking the form @@ -450,8 +543,9 @@ """ - if len(inspect.getargspec(serializer).args) == 2: + if len(get_argnames(serializer)) == 2: serializer = helpers.wrap_old_error_serializer(serializer) + self._serialize_error = serializer # ------------------------------------------------------------------------ @@ -459,13 +553,13 @@ # ------------------------------------------------------------------------ def _get_responder(self, req): - """Searches routes for a matching responder. + """Search routes for a matching responder. Args: req: The request object. Returns: - A 3-member tuple consisting of a responder callable, + tuple: A 3-member tuple consisting of a responder callable, a ``dict`` containing parsed path fields (if any were specified in the matching route's URI template), and a reference to the responder's resource instance. @@ -483,11 +577,18 @@ path = req.path method = req.method + uri_template = None - route = self._router.find(path) + route = self._router_search(path, req=req) if route is not None: - resource, method_map, params = route + try: + resource, method_map, params, uri_template = route + except ValueError: + # NOTE(kgriffs): Older routers may not return the + # template. But for performance reasons they should at + # least return None if they don't support it. + resource, method_map, params = route else: # NOTE(kgriffs): Older routers may indicate that no route # was found by returning (None, None, None). Therefore, we @@ -511,12 +612,18 @@ break else: - responder = falcon.responders.path_not_found - return (responder, params, resource) + for sr in self._static_routes: + if sr.match(path): + responder = sr + break + else: + responder = falcon.responders.path_not_found + + return (responder, params, resource, uri_template) def _compose_status_response(self, req, resp, http_status): - """Composes a response for the given HTTPStatus instance.""" + """Compose a response for the given HTTPStatus instance.""" # PERF(kgriffs): The code to set the status and headers is identical # to that used in _compose_error_response(), but refactoring in the @@ -531,7 +638,7 @@ resp.body = http_status.body def _compose_error_response(self, req, resp, error): - """Composes a response for the given HTTPError instance.""" + """Compose a response for the given HTTPError instance.""" resp.status = error.status @@ -541,38 +648,51 @@ if error.has_representation: self._serialize_error(req, resp, error) - def _call_req_mw(self, stack, req, resp): - """Run process_request middleware methods.""" + def _http_status_handler(self, status, req, resp, params): + self._compose_status_response(req, resp, status) + + def _http_error_handler(self, error, req, resp, params): + self._compose_error_response(req, resp, error) + + def _handle_exception(self, ex, req, resp, params): + """Handle an exception raised from mw or a responder. + + Args: + ex: Exception to handle + req: Current request object to pass to the handler + registered for the given exception type + resp: Current response object to pass to the handler + registered for the given exception type + params: Responder params to pass to the handler + registered for the given exception type + + Returns: + bool: ``True`` if a handler was found and called for the + exception, ``False`` otherwise. + """ - for component in self._middleware: - process_request, _, _ = component - if process_request is not None: - process_request(req, resp) - - # Put executed component on the stack - stack.append(component) # keep track from outside - - def _call_rsrc_mw(self, stack, req, resp, resource, params): - """Run process_resource middleware methods.""" - - for component in self._middleware: - _, process_resource, _ = component - if process_resource is not None: - process_resource(req, resp, resource, params) - - def _call_resp_mw(self, stack, req, resp, resource): - """Run process_response middleware.""" - - while stack: - _, _, process_response = stack.pop() - if process_response is not None: - process_response(req, resp, resource) + for err_type, err_handler in self._error_handlers: + if isinstance(ex, err_type): + try: + err_handler(ex, req, resp, params) + except HTTPStatus as status: + self._compose_status_response(req, resp, status) + except HTTPError as error: + self._compose_error_response(req, resp, error) + + return True + + # NOTE(kgriffs): No error handlers are defined for ex + # and it is not one of (HTTPStatus, HTTPError), since it + # would have matched one of the corresponding default + # handlers. + return False # PERF(kgriffs): Moved from api_helpers since it is slightly faster # to call using self, and this function is called for most # requests. def _get_body(self, resp, wsgi_file_wrapper=None): - """Converts resp content into an iterable as required by PEP 333 + """Convert resp content into an iterable as required by PEP 333 Args: resp: Instance of falcon.Response @@ -581,7 +701,7 @@ when resp.stream is a file-like object (default None). Returns: - A two-member tuple of the form (iterable, content_length). + tuple: A two-member tuple of the form (iterable, content_length). The length is returned as ``None`` when unknown. The iterable is determined as follows: @@ -620,10 +740,7 @@ iterable = wsgi_file_wrapper(stream, self._STREAM_BLOCK_SIZE) else: - iterable = iter( - lambda: stream.read(self._STREAM_BLOCK_SIZE), - b'' - ) + iterable = helpers.CloseableStreamIterator(stream, self._STREAM_BLOCK_SIZE) else: iterable = stream diff -Nru python-falcon-1.0.0/falcon/bench/bench.py python-falcon-1.4.1/falcon/bench/bench.py --- python-falcon-1.0.0/falcon/bench/bench.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/bench.py 2018-08-08 23:08:36.000000000 +0000 @@ -18,11 +18,14 @@ from __future__ import print_function import argparse -from collections import defaultdict +from collections import defaultdict, deque from decimal import Decimal import gc +import inspect +import platform import random import sys +import tempfile import timeit try: @@ -42,13 +45,58 @@ except ImportError: pprofile = None +try: + import vmprof + from vmshare.service import Service +except ImportError: + vmprof = None + from falcon.bench import create # NOQA import falcon.testing as helpers -def bench(name, iterations, env, stat_memory): - func = create_bench(name, env) +# NOTE(kgriffs): Based on testing, these values provide a ceiling that's +# several times higher than fast x86 hardware can achieve today. +ITER_DETECTION_MAX_ATTEMPTS = 27 +ITER_DETECTION_MULTIPLIER = 1.7 +ITER_DETECTION_STARTING = 3000 + +# NOTE(kgriffs): Benchmark duration range, in seconds, to target +ITER_DETECTION_DURATION_MIN = 1.0 +ITER_DETECTION_DURATION_MAX = 5.0 + +JIT_WARMING_MULTIPLIER = 30 + +PYPY = platform.python_implementation() == 'PyPy' + +BODY = helpers.rand_string(10240, 10240).encode('utf-8') # NOQA +HEADERS = {'X-Test': 'Funky Chicken'} # NOQA + + +class StartResponseMockLite(object): + """Mock object representing a WSGI `start_response` callable.""" + + def __init__(self): + self._called = 0 + self.status = None + self.headers = None + self.exc_info = None + + def __call__(self, status, headers, exc_info=None): + """Implements the PEP-3333 `start_response` protocol.""" + + self._called += 1 + + self.status = status + self.headers = headers + self.exc_info = exc_info + + @property + def call_count(self): + return self._called + +def bench(func, iterations, stat_memory): gc.collect() heap_diff = None @@ -62,10 +110,29 @@ sec_per_req = Decimal(str(total_sec)) / Decimal(str(iterations)) - sys.stdout.write('.') - sys.stdout.flush() + return (sec_per_req, heap_diff) - return (name, sec_per_req, heap_diff) + +def determine_iterations(func): + # NOTE(kgriffs): Algorithm adapted from IPython's magic timeit + # function to determine iterations so that 0.2 <= total time < 2.0 + iterations = ITER_DETECTION_STARTING + for __ in range(1, ITER_DETECTION_MAX_ATTEMPTS): + gc.collect() + + total_sec = timeit.timeit( + func, + setup=gc.enable, + number=int(iterations) + ) + + if total_sec >= ITER_DETECTION_DURATION_MIN: + assert total_sec < ITER_DETECTION_DURATION_MAX + break + + iterations *= ITER_DETECTION_MULTIPLIER + + return int(iterations) def profile(name, env, filename=None, verbose=False): @@ -83,9 +150,21 @@ print('=' * len(title)) func = create_bench(name, env) - gc.collect() - code = 'for x in range(10000): func()' + + num_iterations = 100000 + + if PYPY: + print('JIT warmup...') + + # TODO(kgriffs): Measure initial time, and keep iterating until + # performance increases and then steadies + for x in range(num_iterations * JIT_WARMING_MULTIPLIER): + func() + + print('Ready.') + + code = 'for x in range({0}): func()'.format(num_iterations) if verbose: if pprofile is None: @@ -99,22 +178,67 @@ sort='tottime', filename=filename) -BODY = helpers.rand_string(10240, 10240) # NOQA -HEADERS = {'X-Test': 'Funky Chicken'} # NOQA +def profile_vmprof(name, env): + if vmprof is None: + print('vmprof not found. Please install vmprof and try again.') + return + + func = create_bench(name, env) + gc.collect() + + # + # Based on: https://github.com/vmprof/vmprof-python/blob/master/vmprof/__main__.py + # + + prof_file = tempfile.NamedTemporaryFile(delete=False) + filename = prof_file.name + + vmprof.enable(prof_file.fileno()) + + try: + for __ in range(1000000): + func() + + except BaseException as e: + if not isinstance(e, (KeyboardInterrupt, SystemExit)): + raise + + vmprof.disable() + + service = Service('vmprof.com') + service.post({ + Service.FILE_CPU_PROFILE: filename, + Service.FILE_JIT_PROFILE: filename + '.jit', + 'argv': ' '.join(sys.argv[:]), + 'VM': platform.python_implementation(), + }) + + prof_file.close() + + +def exhaust(iterator_or_generator): + # from https://docs.python.org/dev/library/itertools.html#itertools-recipes + deque(iterator_or_generator, maxlen=0) def create_bench(name, env): - srmock = helpers.StartResponseMock() + srmock = StartResponseMockLite() function = name.lower().replace('-', '_') app = eval('create.{0}(BODY, HEADERS)'.format(function)) def bench(): app(env, srmock) - if srmock.status != '200 OK': - raise AssertionError(srmock.status + ' != 200 OK') + assert srmock.status == '200 OK' - return bench + def bench_generator(): + exhaust(app(env, srmock)) + assert srmock.status == '200 OK' + + if inspect.isgeneratorfunction(app): + return bench_generator + else: + return bench def consolidate_datasets(datasets): @@ -146,7 +270,7 @@ path = ('/v1/852809/queues/0fd4c8c6-bd72-11e2-8e47-db5ebd4c8125' '/claims/db5ebd4c8125') - qs = 'limit=10&thing=a%20b&x=%23%24' + qs = 'limit=10&thing=a+b&x=%23%24' return helpers.create_environ(path, query_string=qs, headers=request_headers) @@ -167,11 +291,32 @@ print() + datasets = [] + if not frameworks: print('Nothing to do.\n') - return + return datasets + + benchmarks = [] + for name in frameworks: + bm = create_bench(name, get_env(name)) + + bm_iterations = iterations if iterations else determine_iterations(bm) + + if PYPY: + print('{}: JIT warmup'.format(name)) + + # TODO(kgriffs): Measure initial time, and keep iterating until + # performance increases and then steadies + bench(bm, bm_iterations * JIT_WARMING_MULTIPLIER, False) + + bm_iterations = iterations if iterations else determine_iterations(bm) + + benchmarks.append((name, bm_iterations, bm)) + print('{}: {} iterations'.format(name, bm_iterations)) + + print() - datasets = [] for r in range(trials): random.shuffle(frameworks) @@ -179,9 +324,18 @@ (r + 1, trials)) sys.stdout.flush() - dataset = [bench(framework, iterations, - get_env(framework), stat_memory) - for framework in frameworks] + dataset = [] + for name, bm_iterations, bm in benchmarks: + sec_per_req, heap_diff = bench( + bm, + bm_iterations, + stat_memory + ) + + dataset.append((name, sec_per_req, heap_diff)) + + sys.stdout.write('.') + sys.stdout.flush() datasets.append(dataset) print('done.') @@ -192,20 +346,21 @@ def main(): frameworks = [ 'bottle', + 'django', 'falcon', 'falcon-ext', 'flask', 'pecan', - 'werkzeug' + 'werkzeug', ] parser = argparse.ArgumentParser(description='Falcon benchmark runner') parser.add_argument('-b', '--benchmark', type=str, action='append', choices=frameworks, dest='frameworks', nargs='+') - parser.add_argument('-i', '--iterations', type=int, default=50000) - parser.add_argument('-t', '--trials', type=int, default=3) + parser.add_argument('-i', '--iterations', type=int, default=0) + parser.add_argument('-t', '--trials', type=int, default=10) parser.add_argument('-p', '--profile', type=str, - choices=['standard', 'verbose']) + choices=['standard', 'verbose', 'vmprof']) parser.add_argument('-o', '--profile-output', type=str, default=None) parser.add_argument('-m', '--stat-memory', action='store_true') args = parser.parse_args() @@ -228,8 +383,12 @@ # Profile? if args.profile: - for name in frameworks: - profile(name, get_env(name), + framework = 'falcon-ext' + + if args.profile == 'vmprof': + profile_vmprof(framework, get_env(framework)) + else: + profile(framework, get_env(framework), filename=args.profile_output, verbose=(args.profile == 'verbose')) @@ -240,6 +399,9 @@ datasets = run(frameworks, args.trials, args.iterations, args.stat_memory) + if not datasets: + return + dataset = consolidate_datasets(datasets) dataset = sorted(dataset, key=lambda r: r[1]) baseline = dataset[-1][1] @@ -251,7 +413,7 @@ us_per_req = (sec_per_req * Decimal(10 ** 6)) factor = round_to_int(baseline / sec_per_req) - print('{3}. {0:.<15s}{1:.>06d} req/sec or {2: >3.2f} μs/req ({4}x)'. + print('{3}. {0:.<20s}{1:.>06d} req/sec or {2: >3.2f} μs/req ({4}x)'. format(name, req_per_sec, us_per_req, i + 1, factor)) if heapy and args.stat_memory: @@ -267,5 +429,6 @@ print() + if __name__ == '__main__': main() diff -Nru python-falcon-1.0.0/falcon/bench/create.py python-falcon-1.4.1/falcon/bench/create.py --- python-falcon-1.0.0/falcon/bench/create.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/create.py 2018-08-08 23:08:36.000000000 +0000 @@ -70,7 +70,10 @@ user_agent = bottle.request.headers['User-Agent'] # NOQA limit = bottle.request.query.limit or '10' # NOQA - return bottle.Response(body, headers=headers) + for header in headers.items(): + bottle.response.set_header(*header) + + return body return bottle.default_app() @@ -96,59 +99,26 @@ return hello -def cherrypy(body, headers): - import cherrypy - - # Disable logging - cherrypy.config.update({'environment': 'embedded'}) - - class HelloResource(object): - - exposed = True - - def GET(self, account_id, test, limit=8): - user_agent = cherrypy.request.headers['User-Agent'] # NOQA - for name, value in headers.items(): - cherrypy.response.headers[name] = value - - return body - - class Root(object): - pass - - root = Root() - root.hello = HelloResource() - - conf = { - '/': { - 'request.dispatch': cherrypy.dispatch.MethodDispatcher(), - } - } - - app = cherrypy.tree.mount(root, '/', conf) - return app - - -# def wsme(body, headers): -# import wsme - -# class HelloService(wsme.WSRoot): - -# @wsme.expose(str, str) -# def hello(self, limit='10'): -# import pdb -# pdb.set_trace() -# return body - -# ws = HelloService(protocols=['restjson']) -# return ws.wsgiapp() - - def pecan(body, headers): - import falcon.bench.nuts.nuts.app as nuts + import pecan + pecan.x_test_body = body + pecan.x_test_headers = headers + import falcon.bench.nuts.nuts.app as nuts sys.path.append(os.path.dirname(nuts.__file__)) app = nuts.create() del sys.path[-1] return app + + +def django(body, headers): + import django + django.x_test_body = body + django.x_test_headers = headers + + from falcon.bench import dj + sys.path.append(os.path.dirname(dj.__file__)) + + from falcon.bench.dj.dj import wsgi + return wsgi.application diff -Nru python-falcon-1.0.0/falcon/bench/dj/dj/settings.py python-falcon-1.4.1/falcon/bench/dj/dj/settings.py --- python-falcon-1.0.0/falcon/bench/dj/dj/settings.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/dj/settings.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,123 @@ +""" +Django settings for dj project. + +Generated by 'django-admin startproject' using Django 1.11.3. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +import os + +from django.core.management import utils + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = utils.get_random_secret_key() + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'hello', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'dj.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'dj.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' diff -Nru python-falcon-1.0.0/falcon/bench/dj/dj/urls.py python-falcon-1.4.1/falcon/bench/dj/dj/urls.py --- python-falcon-1.0.0/falcon/bench/dj/dj/urls.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/dj/urls.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +from django.conf.urls import url +from hello import views + + +urlpatterns = [ + url(r'^hello/(?P[0-9]+)/test$', views.hello) +] diff -Nru python-falcon-1.0.0/falcon/bench/dj/dj/wsgi.py python-falcon-1.4.1/falcon/bench/dj/dj/wsgi.py --- python-falcon-1.0.0/falcon/bench/dj/dj/wsgi.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/dj/wsgi.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,16 @@ +""" +WSGI config for dj project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj.settings') + +application = get_wsgi_application() diff -Nru python-falcon-1.0.0/falcon/bench/dj/hello/admin.py python-falcon-1.4.1/falcon/bench/dj/hello/admin.py --- python-falcon-1.0.0/falcon/bench/dj/hello/admin.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/hello/admin.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# from django.contrib import admin + +# Register your models here. diff -Nru python-falcon-1.0.0/falcon/bench/dj/hello/apps.py python-falcon-1.4.1/falcon/bench/dj/hello/apps.py --- python-falcon-1.0.0/falcon/bench/dj/hello/apps.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/hello/apps.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class HelloConfig(AppConfig): + name = 'hello' diff -Nru python-falcon-1.0.0/falcon/bench/dj/hello/models.py python-falcon-1.4.1/falcon/bench/dj/hello/models.py --- python-falcon-1.0.0/falcon/bench/dj/hello/models.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/hello/models.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# from django.db import models + +# Create your models here. diff -Nru python-falcon-1.0.0/falcon/bench/dj/hello/tests.py python-falcon-1.4.1/falcon/bench/dj/hello/tests.py --- python-falcon-1.0.0/falcon/bench/dj/hello/tests.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/hello/tests.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# from django.test import TestCase + +# Create your tests here. diff -Nru python-falcon-1.0.0/falcon/bench/dj/hello/views.py python-falcon-1.4.1/falcon/bench/dj/hello/views.py --- python-falcon-1.0.0/falcon/bench/dj/hello/views.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/hello/views.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,17 @@ +import django +from django.http import HttpResponse + + +_body = django.x_test_body +_headers = django.x_test_headers + + +def hello(request, account_id): + user_agent = request.META['HTTP_USER_AGENT'] # NOQA + limit = request.GET.get('limit', '10') # NOQA + response = HttpResponse(_body) + + for name, value in _headers.items(): + response[name] = value + + return response diff -Nru python-falcon-1.0.0/falcon/bench/dj/manage.py python-falcon-1.4.1/falcon/bench/dj/manage.py --- python-falcon-1.0.0/falcon/bench/dj/manage.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/dj/manage.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dj.settings') + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django # NOQA + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' + ) + raise + execute_from_command_line(sys.argv) diff -Nru python-falcon-1.0.0/falcon/bench/nuts/nuts/controllers/root.py python-falcon-1.4.1/falcon/bench/nuts/nuts/controllers/root.py --- python-falcon-1.0.0/falcon/bench/nuts/nuts/controllers/root.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/nuts/nuts/controllers/root.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,16 +1,11 @@ import random +import pecan from pecan import expose, response, request -def rand_string(min, max): - int_gen = random.randint - string_length = int_gen(min, max) - return ''.join([chr(int_gen(ord('\t'), ord('~'))) - for i in range(string_length)]) - - -body = rand_string(10240, 10240) +_body = pecan.x_test_body +_headers = pecan.x_test_headers class TestController(object): @@ -20,10 +15,10 @@ @expose(content_type='text/plain') def test(self): user_agent = request.headers['User-Agent'] # NOQA - limit = request.params['limit'] # NOQA - response.headers['X-Test'] = 'Funky Chicken' + limit = request.params.get('limit', '10') # NOQA + response.headers.update(_headers) - return body + return _body class HelloController(object): @@ -36,7 +31,7 @@ @expose(content_type='text/plain') def index(self): - response.headers['X-Test'] = 'Funky Chicken' - return body + response.headers.update(_headers) + return _body hello = HelloController() diff -Nru python-falcon-1.0.0/falcon/bench/nuts/nuts/model/__init__.py python-falcon-1.4.1/falcon/bench/nuts/nuts/model/__init__.py --- python-falcon-1.0.0/falcon/bench/nuts/nuts/model/__init__.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/nuts/nuts/model/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -10,6 +10,6 @@ recommended place to do it. For more information working with databases, and some common recipes, - see http://pecan.readthedocs.org/en/latest/databases.html + see https://pecan.readthedocs.io/en/latest/databases.html """ pass diff -Nru python-falcon-1.0.0/falcon/bench/nuts/nuts/tests/test_functional.py python-falcon-1.4.1/falcon/bench/nuts/nuts/tests/test_functional.py --- python-falcon-1.0.0/falcon/bench/nuts/nuts/tests/test_functional.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/nuts/nuts/tests/test_functional.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,7 +12,7 @@ def test_search(self): response = self.app.post('/', params={'q': 'RestController'}) assert response.status_int == 302 - assert response.headers['Location'] == ('http://pecan.readthedocs.org' + assert response.headers['Location'] == ('https://pecan.readthedocs.io' '/en/latest/search.html' '?q=RestController') diff -Nru python-falcon-1.0.0/falcon/bench/queues/api.py python-falcon-1.4.1/falcon/bench/queues/api.py --- python-falcon-1.0.0/falcon/bench/queues/api.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/bench/queues/api.py 2018-08-08 23:08:36.000000000 +0000 @@ -14,7 +14,6 @@ # limitations under the License. import falcon - from falcon.bench.queues import claims from falcon.bench.queues import messages from falcon.bench.queues import queues @@ -35,8 +34,11 @@ self._headers = headers def process_response(self, req, resp, resource): + user_agent = req.user_agent # NOQA + limit = req.get_param('limit') or '10' # NOQA + resp.status = falcon.HTTP_200 - resp.body = self._body + resp.data = self._body resp.set_headers(self._headers) resp.vary = ('X-Auth-Token', 'Accept-Encoding') resp.content_range = (0, len(self._body), len(self._body) + 100) diff -Nru python-falcon-1.0.0/falcon/cmd/print_routes.py python-falcon-1.4.1/falcon/cmd/print_routes.py --- python-falcon-1.0.0/falcon/cmd/print_routes.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/cmd/print_routes.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# Copyright 2013 by Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Script that prints out the routes of an API instance. +""" + +from __future__ import print_function + +from functools import partial +import inspect + +import falcon + + +def print_routes(api, verbose=False): # pragma: no cover + """ + Initial call. + + :param api: The falcon.API or callable that returns an instance to look at. + :type api: falcon.API or callable + :param verbose: If the output should be verbose. + :type verbose: bool + """ + traverse(api._router._roots, verbose=verbose) + + +def traverse(roots, parent='', verbose=False): + """ + Recursive call which also handles printing output. + + :param api: The falcon.API or callable that returns an instance to look at. + :type api: falcon.API or callable + :param parent: The parent uri path to the current iteration. + :type parent: str + :param verbose: If the output should be verbose. + :type verbose: bool + """ + for root in roots: + if root.method_map: + print('->', parent + '/' + root.raw_segment) + if verbose: + for method, func in root.method_map.items(): + if func.__name__ != 'method_not_allowed': + if isinstance(func, partial): + real_func = func.func + else: + real_func = func + + source_file = inspect.getsourcefile(real_func) + + print('-->{0} {1}:{2}'.format( + method, + source_file, + source_file[1] + )) + + if root.children: + traverse(root.children, parent + '/' + root.raw_segment, verbose) + + +def main(): + """ + Main entrypoint. + """ + import argparse + + parser = argparse.ArgumentParser( + description='Example: print-api-routes myprogram:app') + parser.add_argument( + '-v', '--verbose', action='store_true', + help='Prints out information for each method.') + parser.add_argument( + 'api_module', + help='The module and api to inspect. Example: myapp.somemodule:api', + ) + args = parser.parse_args() + + try: + module, instance = args.api_module.split(':', 1) + except ValueError: + parser.error( + 'The api_module must include a colon between ' + 'the module and instnace') + api = getattr(__import__(module, fromlist=[True]), instance) + if not isinstance(api, falcon.API): + if callable(api): + api = api() + if not isinstance(api, falcon.API): + parser.error( + '{0} did not return a falcon.API instance'.format( + args.api_module)) + else: + parser.error( + 'The instance must be of falcon.API or be ' + 'a callable without args that returns falcon.API') + print_routes(api, verbose=args.verbose) + + +if __name__ == '__main__': + main() diff -Nru python-falcon-1.0.0/falcon/constants.py python-falcon-1.4.1/falcon/constants.py --- python-falcon-1.0.0/falcon/constants.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/constants.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,79 @@ +# RFC 7231, 5789 methods +HTTP_METHODS = ( + 'CONNECT', + 'DELETE', + 'GET', + 'HEAD', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + 'TRACE', +) + +# RFC 3253 methods +WEBDAV_METHODS = ( + 'CHECKIN', + 'CHECKOUT', + 'REPORT', + 'UNCHECKIN', + 'UPDATE', + 'VERSION-CONTROL', +) + +COMBINED_METHODS = HTTP_METHODS + WEBDAV_METHODS + + +# NOTE(kgriffs): According to RFC 7159, most JSON parsers assume +# UTF-8 and so it is the recommended default charset going forward, +# and indeed, other charsets should not be specified to ensure +# maximum interoperability. +# +# TODO(kgriffs) In Falcon 2.0, omit the charset parameter +# below. +MEDIA_JSON = 'application/json; charset=UTF-8' + +# NOTE(kgriffs): An internet media type for MessagePack has not +# yet been registered. 'application/x-msgpack' is commonly used, +# but the use of the 'x-' prefix is discouraged by RFC 6838. +MEDIA_MSGPACK = 'application/msgpack' + +# NOTE(kgriffs): An internet media type for YAML has not been +# registered. RoR uses 'application/x-yaml', but since use of +# 'x-' is discouraged by RFC 6838, we don't use it in Falcon. +# +# The YAML specification requires that parsers deduce the character +# encoding by examining the first few bytes of the document itself. +# Therefore, it does not make sense to include the charset in the +# media type string. +MEDIA_YAML = 'application/yaml' + +# NOTE(kgriffs): According to RFC 7303, when the charset is +# omitted, preference is given to the encoding specified in the +# document itself (either via a BOM, or via the XML declaration). If +# the document does not explicitly specify the encoding, UTF-8 is +# assumed. We do not specify the charset here, because many parsers +# ignore it anyway and just use what is specified in the document, +# contrary to the RFCs. +MEDIA_XML = 'application/xml' + + +# NOTE(kgriffs): RFC 4329 recommends application/* over text/. +# futhermore, parsers are required to respect the Unicode +# encoding signature, if present in the document, and to default +# to UTF-8 when not present. Note, however, that implementations +# are not required to support anything besides UTF-8, so it is +# unclear how much utility an encoding signature (or the charset +# parameter for that matter) has in practice. +MEDIA_JS = 'application/javascript' + +# NOTE(kgriffs): According to RFC 6838, most text media types should +# include the charset parameter. +MEDIA_HTML = 'text/html; charset=utf-8' +MEDIA_TEXT = 'text/plain; charset=utf-8' + +MEDIA_JPEG = 'image/jpeg' +MEDIA_PNG = 'image/png' +MEDIA_GIF = 'image/gif' + +DEFAULT_MEDIA_TYPE = MEDIA_JSON diff -Nru python-falcon-1.0.0/falcon/errors.py python-falcon-1.4.1/falcon/errors.py --- python-falcon-1.0.0/falcon/errors.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/errors.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,30 +12,78 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""HTTP error classes. + +This module implements a collection of `falcon.HTTPError` +specializations that can be raised to generate a 4xx or 5xx HTTP +response. All classes are available directly from the `falcon` +package namespace:: + + import falcon + + class MessageResource(object): + def on_get(self, req, resp): + + # ... + + raise falcon.HTTPBadRequest( + 'TTL Out of Range', + 'The message's TTL must be between 60 and 300 seconds, inclusive.' + ) + + # ... + +""" + from datetime import datetime +from falcon import util from falcon.http_error import HTTPError, NoRepresentation, \ OptionalRepresentation import falcon.status_codes as status -from falcon import util class HTTPBadRequest(HTTPError): """400 Bad Request. - The request could not be understood by the server due to malformed - syntax. The client SHOULD NOT repeat the request without - modifications. (RFC 2616) + The server cannot or will not process the request due to something + that is perceived to be a client error (e.g., malformed request + syntax, invalid request message framing, or deceptive request + routing). - Args: - title (str): Error title (e.g., 'TTL Out of Range'). + (See also: RFC 7231, Section 6.5.1) + + Keyword Args: + title (str): Error title (default '400 Bad Request'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPBadRequest, self).__init__(status.HTTP_400, title, description, **kwargs) @@ -43,22 +91,62 @@ class HTTPUnauthorized(HTTPError): """401 Unauthorized. - Use when authentication is required, and the provided credentials are - not valid, or no credentials were provided in the first place. + The request has not been applied because it lacks valid + authentication credentials for the target resource. - Args: - title (str): Error title (e.g., 'Authentication Required'). + The server generating a 401 response MUST send a WWW-Authenticate + header field containing at least one challenge applicable to the + target resource. + + If the request included authentication credentials, then the 401 + response indicates that authorization has been refused for those + credentials. The user agent MAY repeat the request with a new or + replaced Authorization header field. If the 401 response contains + the same challenge as the prior response, and the user agent has + already attempted authentication at least once, then the user agent + SHOULD present the enclosed representation to the user, since it + usually contains relevant diagnostic information. + + (See also: RFC 7235, Section 3.1) + + Keyword Args: + title (str): Error title (default '401 Unauthorized'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. challenges (iterable of str): One or more authentication challenges to use as the value of the WWW-Authenticate header in - the response. See also: - http://tools.ietf.org/html/rfc7235#section-2.1 - kwargs (optional): Same as for ``HTTPError``. + the response. + + (See also: RFC 7235, Section 2.1) + + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, challenges, **kwargs): + def __init__(self, title=None, description=None, challenges=None, **kwargs): headers = kwargs.setdefault('headers', {}) if challenges: @@ -71,24 +159,55 @@ class HTTPForbidden(HTTPError): """403 Forbidden. - Use when the client's credentials are good, but they do not have permission - to access the requested resource. + The server understood the request but refuses to authorize it. - If the request method was not HEAD and the server wishes to make - public why the request has not been fulfilled, it SHOULD describe the - reason for the refusal in the entity. If the server does not wish to - make this information available to the client, the status code 404 - (Not Found) can be used instead. (RFC 2616) + A server that wishes to make public why the request has been + forbidden can describe that reason in the response payload (if any). - Args: - title (str): Error title (e.g., 'Permission Denied'). + If authentication credentials were provided in the request, the + server considers them insufficient to grant access. The client + SHOULD NOT automatically repeat the request with the same + credentials. The client MAY repeat the request with new or different + credentials. However, a request might be forbidden for reasons + unrelated to the credentials. + + An origin server that wishes to "hide" the current existence of a + forbidden target resource MAY instead respond with a status code of + 404 Not Found. + + (See also: RFC 7231, Section 6.5.4) + + Keyword Args: + title (str): Error title (default '403 Forbidden'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPForbidden, self).__init__(status.HTTP_403, title, description, **kwargs) @@ -96,9 +215,50 @@ class HTTPNotFound(OptionalRepresentation, HTTPError): """404 Not Found. - Use this when the URL path does not map to an existing resource, or you - do not wish to disclose exactly why a request was refused. + The origin server did not find a current representation for the + target resource or is not willing to disclose that one exists. + A 404 status code does not indicate whether this lack of + representation is temporary or permanent; the 410 Gone status code + is preferred over 404 if the origin server knows, presumably through + some configurable means, that the condition is likely to be + permanent. + + A 404 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls. + + (See also: RFC 7231, Section 6.5.3) + + Keyword Args: + title (str): Human-friendly error title. If not provided, and + `description` is also not provided, no body will be included + in the response. + description (str): Human-friendly description of the error, along with + a helpful suggestion or two (default ``None``). + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, **kwargs): @@ -108,15 +268,52 @@ class HTTPMethodNotAllowed(OptionalRepresentation, HTTPError): """405 Method Not Allowed. - The method specified in the Request-Line is not allowed for the - resource identified by the Request-URI. The response MUST include an - Allow header containing a list of valid methods for the requested - resource. (RFC 2616) + The method received in the request-line is known by the origin + server but not supported by the target resource. + + The origin server MUST generate an Allow header field in a 405 + response containing a list of the target resource's currently + supported methods. + + A 405 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls. + + (See also: RFC 7231, Section 6.5.5) Args: allowed_methods (list of str): Allowed HTTP methods for this resource (e.g., ``['GET', 'POST', 'HEAD']``). + Keyword Args: + title (str): Human-friendly error title. If not provided, and + `description` is also not provided, no body will be included + in the response. + description (str): Human-friendly description of the error, along with + a helpful suggestion or two (default ``None``). + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, allowed_methods, **kwargs): @@ -132,22 +329,51 @@ class HTTPNotAcceptable(HTTPError): """406 Not Acceptable. - The client requested a resource in a representation that is not - supported by the server. The client must indicate a supported - media type in the Accept header. - - 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. (RFC 2616) + The target resource does not have a current representation that + would be acceptable to the user agent, according to the proactive + negotiation header fields received in the request, and the server + is unwilling to supply a default representation. + + The server SHOULD generate a payload containing a list of available + representation characteristics and corresponding resource + identifiers from which the user or user agent can choose the one + most appropriate. A user agent MAY automatically select the most + appropriate choice from that list. However, this specification does + not define any standard for such automatic selection, as described + in RFC 7231, Section 6.4.1 - Args: + (See also: RFC 7231, Section 6.5.6) + + Keyword Args: description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, description, **kwargs): + def __init__(self, description=None, **kwargs): super(HTTPNotAcceptable, self).__init__(status.HTTP_406, 'Media type not acceptable', description, **kwargs) @@ -156,55 +382,162 @@ class HTTPConflict(HTTPError): """409 Conflict. - The request could not be completed due to a conflict with the current - state of the resource. This code is only allowed in situations where - it is expected that the user might be able to resolve the conflict - and resubmit the request. The response body SHOULD include enough - information for the user to recognize the source of the conflict. - Ideally, the response entity would include enough information for the - user or user agent to fix the problem; however, that might not be - possible and is not required. + The request could not be completed due to a conflict with the + current state of the target resource. This code is used in + situations where the user might be able to resolve the conflict and + resubmit the request. + + The server SHOULD generate a payload that includes enough + information for a user to recognize the source of the conflict. Conflicts are most likely to occur in response to a PUT request. For - example, if versioning were being used and the entity being PUT - included changes to a resource which conflict with those made by an - earlier (third-party) request, the server might use the 409 response - to indicate that it can't complete the request. In this case, the - response entity would likely contain a list of the differences - between the two versions in a format defined by the response - Content-Type. + example, if versioning were being used and the representation being + PUT included changes to a resource that conflict with those made by + an earlier (third-party) request, the origin server might use a 409 + response to indicate that it can't complete the request. In this + case, the response representation would likely contain information + useful for merging the differences based on the revision history. - (RFC 2616) + (See also: RFC 7231, Section 6.5.8) - Args: - title (str): Error title (e.g., 'Editing Conflict'). + Keyword Args: + title (str): Error title (default '409 Conflict'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPConflict, self).__init__(status.HTTP_409, title, description, **kwargs) +class HTTPGone(OptionalRepresentation, HTTPError): + """410 Gone. + + The target resource is no longer available at the origin server and + this condition is likely to be permanent. + + If the origin server does not know, or has no facility to determine, + whether or not the condition is permanent, the status code 404 Not + Found ought to be used instead. + + The 410 response is primarily intended to assist the task of web + maintenance by notifying the recipient that the resource is + intentionally unavailable and that the server owners desire that + remote links to that resource be removed. Such an event is common + for limited-time, promotional services and for resources belonging + to individuals no longer associated with the origin server's site. + It is not necessary to mark all permanently unavailable resources as + "gone" or to keep the mark for any length of time -- that is left to + the discretion of the server owner. + + A 410 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls. + + (See also: RFC 7231, Section 6.5.9) + + Keyword Args: + title (str): Human-friendly error title. If not provided, and + `description` is also not provided, no body will be included + in the response. + description (str): Human-friendly description of the error, along with + a helpful suggestion or two (default ``None``). + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, **kwargs): + super(HTTPGone, self).__init__(status.HTTP_410, **kwargs) + + class HTTPLengthRequired(HTTPError): """411 Length Required. - The server refuses to accept the request without a defined - Content-Length. The client MAY repeat the request if it adds a - valid Content-Length header field containing the length of the - message-body in the request message. (RFC 2616) + The server refuses to accept the request without a defined Content- + Length. - Args: - title (str): Error title (e.g., 'Missing Content-Length'). + The client MAY repeat the request if it adds a valid Content-Length + header field containing the length of the message body in the + request message. + + (See also: RFC 7231, Section 6.5.10) + + Keyword Args: + title (str): Error title (default '411 Length Required'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPLengthRequired, self).__init__(status.HTTP_411, title, description, **kwargs) @@ -212,22 +545,47 @@ class HTTPPreconditionFailed(HTTPError): """412 Precondition Failed. - The precondition given in one or more of the request-header fields - evaluated to false when it was tested on the server. This response - code allows the client to place preconditions on the current resource - metainformation (header field data) and thus prevent the requested - method from being applied to a resource other than the one intended. - (RFC 2616) + One or more conditions given in the request header fields evaluated + to false when tested on the server. - Args: - title (str): Error title (e.g., 'Image Not Modified'). + This response code allows the client to place preconditions on the + current resource state (its current representations and metadata) + and, thus, prevent the request method from being applied if the + target resource is in an unexpected state. + + (See also: RFC 7232, Section 4.2) + + Keyword Args: + title (str): Error title (default '412 Precondition Failed'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPPreconditionFailed, self).__init__(status.HTTP_412, title, description, **kwargs) @@ -236,29 +594,54 @@ """413 Request Entity Too Large. The server is refusing to process a request because the request - entity is larger than the server is willing or able to process. The - server MAY close the connection to prevent the client from continuing - the request. + payload is larger than the server is willing or able to process. + + The server MAY close the connection to prevent the client from + continuing the request. - If the condition is temporary, the server SHOULD include a Retry- + If the condition is temporary, the server SHOULD generate a Retry- After header field to indicate that it is temporary and after what time the client MAY try again. - (RFC 2616) + (See also: RFC 7231, Section 6.5.11) + + Keyword Args: + title (str): Error title (default '413 Request Entity Too Large'). - Args: - title (str): Error title (e.g., 'Request Body Limit Exceeded'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - retry_after (datetime or int, optional): Value for the Retry-After + + retry_after (datetime or int): Value for the Retry-After header. If a ``datetime`` object, will serialize as an HTTP date. Otherwise, a non-negative ``int`` is expected, representing the number of seconds to wait. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, retry_after=None, **kwargs): + def __init__(self, title=None, description=None, retry_after=None, **kwargs): headers = kwargs.setdefault('headers', {}) if isinstance(retry_after, datetime): @@ -272,20 +655,101 @@ **kwargs) +class HTTPUriTooLong(HTTPError): + """414 URI Too Long. + + The server is refusing to service the request because the request- + target is longer than the server is willing to interpret. + + This rare condition is only likely to occur when a client has + improperly converted a POST request to a GET request with long query + information, when the client has descended into a "black hole" of + redirection (e.g., a redirected URI prefix that points to a suffix + of itself) or when the server is under attack by a client attempting + to exploit potential security holes. + + A 414 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls. + + (See also: RFC 7231, Section 6.5.12) + + Keyword Args: + title (str): Error title (default '414 URI Too Long'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two (default ``None``). + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPUriTooLong, self).__init__(status.HTTP_414, title, description, **kwargs) + + class HTTPUnsupportedMediaType(HTTPError): """415 Unsupported Media Type. - The client is trying to submit a resource encoded as an Internet media - type that the server does not support. + The origin server is refusing to service the request because the + payload is in a format not supported by this method on the target + resource. - Args: + The format problem might be due to the request's indicated Content- + Type or Content-Encoding, or as a result of inspecting the data + directly. + + (See also: RFC 7231, Section 6.5.13) + + Keyword Args: description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, description, **kwargs): + def __init__(self, description=None, **kwargs): super(HTTPUnsupportedMediaType, self).__init__( status.HTTP_415, 'Unsupported media type', description, **kwargs) @@ -293,7 +757,19 @@ class HTTPRangeNotSatisfiable(NoRepresentation, HTTPError): """416 Range Not Satisfiable. - The requested range is not valid. See also: http://goo.gl/Qsa4EF + None of the ranges in the request's Range header field overlap the + current extent of the selected resource or that the set of ranges + requested has been rejected due to invalid ranges or an excessive + request of small or overlapping ranges. + + For byte ranges, failing to overlap the current extent means that + the first-byte-pos of all of the byte-range-spec values were greater + than the current length of the selected representation. When this + status code is generated in response to a byte-range request, the + sender SHOULD generate a Content-Range header field specifying the + current length of the selected representation. + + (See also: RFC 7233, Section 4.4) Args: resource_length: The maximum value for the last-byte-pos of a range @@ -309,46 +785,242 @@ class HTTPUnprocessableEntity(HTTPError): """422 Unprocessable Entity. - The request was well-formed but was unable to be followed due to semantic - errors. See also: http://www.ietf.org/rfc/rfc4918. + The server understands the content type of the request entity (hence + a 415 Unsupported Media Type status code is inappropriate), and the + syntax of the request entity is correct (thus a 400 Bad Request + status code is inappropriate) but was unable to process the + contained instructions. + + For example, this error condition may occur if an XML request body + contains well-formed (i.e., syntactically correct), but semantically + erroneous, XML instructions. - Args: - title (str): Error title (e.g., 'Missing title field'). + (See also: RFC 4918, Section 11.2) + + Keyword Args: + title (str): Error title (default '422 Unprocessable Entity'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPUnprocessableEntity, self).__init__(status.HTTP_422, title, description, **kwargs) +class HTTPLocked(OptionalRepresentation, HTTPError): + """423 Locked. + + The 423 (Locked) status code means the source or destination resource + of a method is locked. This response SHOULD contain an appropriate + precondition or postcondition code, such as 'lock-token-submitted' or + 'no-conflicting-lock'. + + (See also: RFC 4918, Section 11.3) + + Keyword Args: + title (str): Error title (default '423 Locked'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPLocked, self).__init__(status.HTTP_423, title, + description, **kwargs) + + +class HTTPFailedDependency(OptionalRepresentation, HTTPError): + """424 Failed Dependency. + + The 424 (Failed Dependency) status code means that the method could + not be performed on the resource because the requested action + depended on another action and that action failed. + + (See also: RFC 4918, Section 11.4) + + Keyword Args: + title (str): Error title (default '424 Failed Dependency'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPFailedDependency, self).__init__(status.HTTP_424, title, + description, **kwargs) + + +class HTTPPreconditionRequired(HTTPError): + """428 Precondition Required. + + The 428 status code indicates that the origin server requires the + request to be conditional. + + Its typical use is to avoid the "lost update" problem, where a client + GETs a resource's state, modifies it, and PUTs it back to the server, + when meanwhile a third party has modified the state on the server, + leading to a conflict. By requiring requests to be conditional, the + server can assure that clients are working with the correct copies. + + Responses using this status code SHOULD explain how to resubmit the + request successfully. + + (See also: RFC 6585, Section 3) + + Keyword Args: + title (str): Error title (default '428 Precondition Required'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + def __init__(self, title=None, description=None, **kwargs): + super(HTTPPreconditionRequired, self).__init__(status.HTTP_428, title, + description, **kwargs) + + class HTTPTooManyRequests(HTTPError): """429 Too Many Requests. - The user has sent too many requests in a given amount of time - ("rate limiting"). + The user has sent too many requests in a given amount of time ("rate + limiting"). The response representations SHOULD include details explaining the condition, and MAY include a Retry-After header indicating how long to wait before making a new request. - (RFC 6585) + Responses with the 429 status code MUST NOT be stored by a cache. - Args: - title (str): Error title (e.g., 'Too Many Requests'). + (See also: RFC 6585, Section 4) + + Keyword Args: + title (str): Error title (default '429 Too Many Requests'). description (str): Human-friendly description of the rate limit that was exceeded. - retry_after (datetime or int, optional): Value for the Retry-After + retry_after (datetime or int): Value for the Retry-After header. If a ``datetime`` object, will serialize as an HTTP date. Otherwise, a non-negative ``int`` is expected, representing the number of seconds to wait. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, retry_after=None, **kwargs): + def __init__(self, title=None, description=None, retry_after=None, **kwargs): headers = kwargs.setdefault('headers', {}) if isinstance(retry_after, datetime): @@ -362,22 +1034,113 @@ **kwargs) +class HTTPRequestHeaderFieldsTooLarge(HTTPError): + """431 Request Header Fields Too Large. + + The 431 status code 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. + + It can be used both when the set of request header fields in total is + too large, and when a single header field is at fault. In the latter + case, the response representation SHOULD specify which header field + was too large. + + Responses with the 431 status code MUST NOT be stored by a cache. + + (See also: RFC 6585, Section 5) + + Keyword Args: + title (str): Error title (default '431 Request Header Fields Too Large'). + description (str): Human-friendly description of the rate limit that + was exceeded. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPRequestHeaderFieldsTooLarge, self).__init__(status.HTTP_431, + title, + description, + **kwargs) + + class HTTPUnavailableForLegalReasons(OptionalRepresentation, HTTPError): """451 Unavailable For Legal Reasons. - This status code indicates that the server is denying access to the - resource as a consequence of a legal demand. + The server is denying access to the resource as a consequence of a + legal demand. - See also: - https://datatracker.ietf.org/doc/draft-ietf-httpbis-legally-restricted-status/ + The server in question might not be an origin server. This type of + legal demand typically most directly affects the operations of ISPs + and search engines. - Args: - title (str): Error title (e.g., 'Legal reason: '). - kwargs (optional): Same as for ``HTTPError``. + Responses using this status code SHOULD include an explanation, in + the response body, of the details of the legal demand: the party + making it, the applicable legislation or regulation, and what + classes of person and resource it applies to. + + Note that in many cases clients can still access the denied resource + by using technical countermeasures such as a VPN or the Tor network. + + A 451 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls. + (See also: RFC 7725, Section 3) + + Keyword Args: + title (str): Error title (default '451 Unavailable For Legal Reasons'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two (default ``None``). + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, **kwargs): + def __init__(self, title=None, **kwargs): super(HTTPUnavailableForLegalReasons, self).__init__(status.HTTP_451, title, **kwargs) @@ -385,32 +1148,138 @@ class HTTPInternalServerError(HTTPError): """500 Internal Server Error. - Args: - title (str): Error title (e.g., 'This Should Never Happen'). + The server encountered an unexpected condition that prevented it + from fulfilling the request. + + (See also: RFC 7231, Section 6.6.1) + + Keyword Args: + title (str): Error title (default '500 Internal Server Error'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPInternalServerError, self).__init__(status.HTTP_500, title, description, **kwargs) +class HTTPNotImplemented(HTTPError): + """501 Not Implemented. + + The 501 (Not Implemented) status code indicates that the server does + not support the functionality required to fulfill the request. This + is the appropriate response when the server does not recognize the + request method and is not capable of supporting it for any resource. + + A 501 response is cacheable by default; i.e., unless otherwise + indicated by the method definition or explicit cache controls + as described in RFC 7234, Section 4.2.2. + + (See also: RFC 7231, Section 6.6.2) + + Keyword Args: + title (str): Error title (default '500 Internal Server Error'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPNotImplemented, self).__init__(status.HTTP_501, title, + description, **kwargs) + + class HTTPBadGateway(HTTPError): """502 Bad Gateway. - Args: - title (str): Error title, for - example: 'Upstream Server is Unavailable'. + The server, while acting as a gateway or proxy, received an invalid + response from an inbound server it accessed while attempting to + fulfill the request. + + (See also: RFC 7231, Section 6.6.3) + + Keyword Args: + title (str): Error title (default '502 Bad Gateway'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, **kwargs): + def __init__(self, title=None, description=None, **kwargs): super(HTTPBadGateway, self).__init__(status.HTTP_502, title, description, **kwargs) @@ -418,24 +1287,60 @@ class HTTPServiceUnavailable(HTTPError): """503 Service Unavailable. - Args: - title (str): Error title (e.g., 'Temporarily Unavailable'). + The server is currently unable to handle the request due to a + temporary overload or scheduled maintenance, which will likely be + alleviated after some delay. + + The server MAY send a Retry-After header field to suggest an + appropriate amount of time for the client to wait before retrying + the request. + + Note: The existence of the 503 status code does not imply that a + server has to use it when becoming overloaded. Some servers might + simply refuse the connection. + + (See also: RFC 7231, Section 6.6.4) + + Keyword Args: + title (str): Error title (default '503 Service Unavailable'). description (str): Human-friendly description of the error, along with a helpful suggestion or two. retry_after (datetime or int): Value for the Retry-After header. If a ``datetime`` object, will serialize as an HTTP date. Otherwise, a non-negative ``int`` is expected, representing the number of seconds to wait. - kwargs (optional): Same as for ``HTTPError``. - + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ - def __init__(self, title, description, retry_after, **kwargs): + def __init__(self, title=None, description=None, retry_after=None, **kwargs): headers = kwargs.setdefault('headers', {}) if isinstance(retry_after, datetime): headers['Retry-After'] = util.dt_to_http(retry_after) - else: + elif retry_after is not None: headers['Retry-After'] = str(retry_after) super(HTTPServiceUnavailable, self).__init__(status.HTTP_503, @@ -444,14 +1349,289 @@ **kwargs) +class HTTPGatewayTimeout(HTTPError): + """504 Gateway Timeout. + + The 504 (Gateway Timeout) status code indicates that the server, + while acting as a gateway or proxy, did not receive a timely response + from an upstream server it needed to access in order to complete the + request. + + (See also: RFC 7231, Section 6.6.5) + + Keyword Args: + title (str): Error title (default '503 Service Unavailable'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPGatewayTimeout, self).__init__(status.HTTP_504, title, + description, **kwargs) + + +class HTTPVersionNotSupported(HTTPError): + """505 HTTP Version Not Supported + + The 505 (HTTP Version Not Supported) status code indicates that the + server does not support, or refuses to support, the major version of + HTTP that was used in the request message. The server is indicating + that it is unable or unwilling to complete the request using the same + major version as the client (as described in RFC 7230, Section 2.6), + other than with this error message. The server SHOULD + generate a representation for the 505 response that describes why + that version is not supported and what other protocols are supported + by that server. + + (See also: RFC 7231, Section 6.6.6) + + Keyword Args: + title (str): Error title (default '503 Service Unavailable'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPVersionNotSupported, self).__init__(status.HTTP_505, title, + description, **kwargs) + + +class HTTPInsufficientStorage(HTTPError): + """507 Insufficient Storage. + + The 507 (Insufficient Storage) status code means the method could not + be performed on the resource because the server is unable to store + the representation needed to successfully complete the request. This + condition is considered to be temporary. If the request that + received this status code was the result of a user action, the + request MUST NOT be repeated until it is requested by a separate user + action. + + (See also: RFC 4918, Section 11.5) + + Keyword Args: + title (str): Error title (default '507 Insufficient Storage'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPInsufficientStorage, self).__init__(status.HTTP_507, title, + description, **kwargs) + + +class HTTPLoopDetected(HTTPError): + """508 Loop Detected. + + The 508 (Loop Detected) status code indicates that the server + terminated an operation because it encountered an infinite loop while + processing a request with "Depth: infinity". This status indicates + that the entire operation failed. + + (See also: RFC 5842, Section 7.2) + + Keyword Args: + title (str): Error title (default '508 Loop Detected'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPLoopDetected, self).__init__(status.HTTP_508, title, + description, **kwargs) + + +class HTTPNetworkAuthenticationRequired(HTTPError): + """511 Network Authentication Required. + + The 511 status code indicates that the client needs to authenticate + to gain network access. + + The response representation SHOULD contain a link to a resource that + allows the user to submit credentials. + + Note that the 511 response SHOULD NOT contain a challenge or the + authentication interface itself, because clients would show the + interface as being associated with the originally requested URL, + which may cause confusion. + + The 511 status SHOULD NOT be generated by origin servers; it is + intended for use by intercepting proxies that are interposed as a + means of controlling access to the network. + + Responses with the 511 status code MUST NOT be stored by a cache. + + (See also: RFC 6585, Section 6) + + Keyword Args: + title (str): Error title (default '511 Network Authentication Required'). + description (str): Human-friendly description of the error, along with + a helpful suggestion or two. + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). + """ + + def __init__(self, title=None, description=None, **kwargs): + super(HTTPNetworkAuthenticationRequired, self).__init__(status.HTTP_511, + title, + description, + **kwargs) + + class HTTPInvalidHeader(HTTPBadRequest): - """A header in the request is invalid. Inherits from ``HTTPBadRequest``. + """400 Bad Request. + + One of the headers in the request is invalid. Args: msg (str): A description of why the value is invalid. - header_name (str): The name of the header. - kwargs (optional): Same as for ``HTTPError``. + header_name (str): The name of the invalid header. + Keyword Args: + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, msg, header_name, **kwargs): @@ -464,16 +1644,42 @@ class HTTPMissingHeader(HTTPBadRequest): - """A header is missing from the request. Inherits from ``HTTPBadRequest``. + """400 Bad Request + + A header is missing from the request. Args: - header_name (str): The name of the header. - kwargs (optional): Same as for ``HTTPError``. + header_name (str): The name of the missing header. + Keyword Args: + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, header_name, **kwargs): - description = ('The {0} header is required.') + description = 'The {0} header is required.' description = description.format(header_name) super(HTTPMissingHeader, self).__init__('Missing header value', @@ -481,16 +1687,41 @@ class HTTPInvalidParam(HTTPBadRequest): - """A parameter in the request is invalid. Inherits from ``HTTPBadRequest``. + """400 Bad Request - This error may refer to a parameter in a query string, form, or - document that was submitted with the request. + A parameter in the request is invalid. This error may refer to a + parameter in a query string, form, or document that was submitted + with the request. Args: msg (str): A description of the invalid parameter. param_name (str): The name of the parameter. - kwargs (optional): Same as for ``HTTPError``. + Keyword Args: + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, msg, param_name, **kwargs): @@ -502,15 +1733,40 @@ class HTTPMissingParam(HTTPBadRequest): - """A parameter is missing from the request. Inherits from ``HTTPBadRequest``. - - This error may refer to a parameter in a query string, form, or - document that was submitted with the request. - - Args: - param_name (str): The name of the parameter. - kwargs (optional): Same as for ``HTTPError``. + """400 Bad Request + A parameter is missing from the request. This error may refer to a + parameter in a query string, form, or document that was submitted + with the request. + + Args: + param_name (str): The name of the missing parameter. + + Keyword Args: + headers (dict or list): A ``dict`` of header names and values + to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and + *value* must be of type ``str`` or ``StringType``, and only + character values 0x00 through 0xFF may be used on platforms that + use wide characters. + + Note: + The Content-Type header, if present, will be overridden. If + you wish to return custom error messages, you can create + your own HTTP error class, and install an error handler + to convert it into an appropriate HTTP response for the + client + + Note: + Falcon can process a list of ``tuple`` slightly faster + than a ``dict``. + href (str): A URL someone can visit to find out more information + (default ``None``). Unicode characters are percent-encoded. + href_text (str): If href is given, use this as the friendly + title/description for the link (default 'API documentation + for this error'). + code (int): An internal code that customers can reference in their + support request or to help them when searching for knowledge + base articles related to this error (default ``None``). """ def __init__(self, param_name, **kwargs): diff -Nru python-falcon-1.0.0/falcon/forwarded.py python-falcon-1.4.1/falcon/forwarded.py --- python-falcon-1.0.0/falcon/forwarded.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/forwarded.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,184 @@ +# Based on code from the aiohttp project, Copyright 2013-2017 by Nikolay Kim and +# Andrew Svetlov, with modifications for the Falcon project by Kurt Griffiths. +# +# See also: +# +# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/web_request.py +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import string + +from falcon.util.uri import unquote_string + + +# '-' at the end to prevent interpretation as range in a char class +_TCHAR = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" + +_TOKEN = r'[{tchar}]+'.format(tchar=_TCHAR) + +# qdtext includes 0x5C to escape 0x5D ('\]') +# qdtext excludes obs-text (because obsoleted, and encoding not specified) +_QDTEXT = r'[{0}]'.format( + r''.join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))) + +_QUOTED_PAIR = r'\\[\t !-~]' + +_QUOTED_STRING = r'"(?:{quoted_pair}|{qdtext})*"'.format( + qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR) + +_FORWARDED_PAIR = ( + r'({token})=({token}|{quoted_string})'.format( + token=_TOKEN, + quoted_string=_QUOTED_STRING)) + +# same pattern as _QUOTED_PAIR but contains a capture group +_QUOTED_PAIR_REPLACE_RE = re.compile(r'\\([\t !-~])') + +_FORWARDED_PAIR_RE = re.compile(_FORWARDED_PAIR) + + +class Forwarded(object): + """Represents a parsed Forwarded header. + + (See also: RFC 7239, Section 4) + + Attributes: + src (str): The value of the "for" parameter, or + ``None`` if the parameter is absent. Identifies the + node making the request to the proxy. + dest (str): The value of the "by" parameter, or + ``None`` if the parameter is absent. Identifies the + client-facing interface of the proxy. + host (str): The value of the "host" parameter, or + ``None`` if the parameter is absent. Provides the host + request header field as received by the proxy. + scheme (str): The value of the "proto" parameter, or + ``None`` if the parameter is absent. Indicates the + protocol that was used to make the request to + the proxy. + """ + + # NOTE(kgriffs): Use "client" since "for" is a keyword, and + # "scheme" instead of "proto" to be consistent with the + # falcon.Request interface. + __slots__ = ('src', 'dest', 'host', 'scheme') + + def __init__(self): + self.src = None + self.dest = None + self.host = None + self.scheme = None + + +def _parse_forwarded_header(forwarded): + """Parses the value of a Forwarded header. + + Makes an effort to parse Forwarded headers as specified by RFC 7239: + + - It checks that every value has valid syntax in general as specified + in section 4: either a 'token' or a 'quoted-string'. + - It un-escapes found escape sequences. + - It does NOT validate 'by' and 'for' contents as specified in section + 6. + - It does NOT validate 'host' contents (Host ABNF). + - It does NOT validate 'proto' contents for valid URI scheme names. + + Arguments: + forwarded (str): Value of a Forwarded header + + Returns: + list: Sequence of Forwarded instances, representing each forwarded-element + in the header, in the same order as they appeared in the header. + """ + + elements = [] + + pos = 0 + end = len(forwarded) + need_separator = False + parsed_element = None + + while 0 <= pos < end: + match = _FORWARDED_PAIR_RE.match(forwarded, pos) + + if match is not None: # got a valid forwarded-pair + if need_separator: + # bad syntax here, skip to next comma + pos = forwarded.find(',', pos) + + else: + pos += len(match.group(0)) + need_separator = True + + name, value = match.groups() + + # NOTE(kgriffs): According to RFC 7239, parameter + # names are case-insensitive. + name = name.lower() + + if value[0] == '"': + value = unquote_string(value) + + # NOTE(kgriffs): If this is the first pair we've encountered + # for this forwarded-element, initialize a new object. + if not parsed_element: + parsed_element = Forwarded() + + if name == 'by': + parsed_element.dest = value + elif name == 'for': + parsed_element.src = value + elif name == 'host': + parsed_element.host = value + elif name == 'proto': + # NOTE(kgriffs): RFC 7239 only requires that + # the "proto" value conform to the Host ABNF + # described in RFC 7230. The Host ABNF, in turn, + # does not require that the scheme be in any + # particular case, so we normalize it here to be + # consistent with the WSGI spec that *does* + # require the value of 'wsgi.url_scheme' to be + # either 'http' or 'https' (case-sensitive). + parsed_element.scheme = value.lower() + + elif forwarded[pos] == ',': # next forwarded-element + need_separator = False + pos += 1 + + # NOTE(kgriffs): It's possible that we arrive here without a + # parsed element if the header is malformed. + if parsed_element: + elements.append(parsed_element) + parsed_element = None + + elif forwarded[pos] == ';': # next forwarded-pair + need_separator = False + pos += 1 + + elif forwarded[pos] in ' \t': + # Allow whitespace even between forwarded-pairs, though + # RFC 7239 doesn't. This simplifies code and is in line + # with Postel's law. + pos += 1 + + else: + # bad syntax here, skip to next comma + pos = forwarded.find(',', pos) + + # NOTE(kgriffs): Add the last forwarded-element, if any + if parsed_element: + elements.append(parsed_element) + + return elements diff -Nru python-falcon-1.0.0/falcon/hooks.py python-falcon-1.4.1/falcon/hooks.py --- python-falcon-1.0.0/falcon/hooks.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/hooks.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools +"""Hook decorators.""" + from functools import wraps -import inspect import six -from falcon import HTTP_METHODS +from falcon import COMBINED_METHODS +from falcon.util.misc import get_argnames -def before(action): +def before(action, *args, **kwargs): """Decorator to execute the given action function *before* the responder. Args: @@ -44,13 +45,19 @@ params['answer'] = 42 + *args: Any additional arguments will be passed to *action* in the + order given, immediately following the *req*, *resp*, *resource*, + and *params* arguments. + + **kwargs: Any additional keyword arguments will be passed through to + *action*. """ def _before(responder_or_resource): if isinstance(responder_or_resource, six.class_types): resource = responder_or_resource - for method in HTTP_METHODS: + for method in COMBINED_METHODS: responder_name = 'on_' + method.lower() try: @@ -67,7 +74,7 @@ # variable that is shared between iterations of the # for loop, above. def let(responder=responder): - do_before_all = _wrap_with_before(action, responder) + do_before_all = _wrap_with_before(responder, action, args, kwargs) setattr(resource, responder_name, do_before_all) @@ -77,14 +84,14 @@ else: responder = responder_or_resource - do_before_one = _wrap_with_before(action, responder) + do_before_one = _wrap_with_before(responder, action, args, kwargs) return do_before_one return _before -def after(action): +def after(action, *args, **kwargs): """Decorator to execute the given action function *after* the responder. Args: @@ -93,13 +100,19 @@ reference to the resource class instance associated with the request + *args: Any additional arguments will be passed to *action* in the + order given, immediately following the *req*, *resp*, *resource*, + and *params* arguments. + + **kwargs: Any additional keyword arguments will be passed through to + *action*. """ def _after(responder_or_resource): if isinstance(responder_or_resource, six.class_types): resource = responder_or_resource - for method in HTTP_METHODS: + for method in COMBINED_METHODS: responder_name = 'on_' + method.lower() try: @@ -112,7 +125,7 @@ if callable(responder): def let(responder=responder): - do_after_all = _wrap_with_after(action, responder) + do_after_all = _wrap_with_after(responder, action, args, kwargs) setattr(resource, responder_name, do_after_all) @@ -122,7 +135,7 @@ else: responder = responder_or_resource - do_after_one = _wrap_with_after(action, responder) + do_after_one = _wrap_with_after(responder, action, args, kwargs) return do_after_one @@ -134,83 +147,106 @@ # ----------------------------------------------------------------------------- -def _has_resource_arg(action): - """Check if the given action function accepts a resource arg.""" - - if isinstance(action, functools.partial): - # NOTE(kgriffs): We special-case this, since versions of - # Python prior to 3.4 raise an error when trying to get the - # spec for a partial. - spec = inspect.getargspec(action.func) - - elif inspect.isroutine(action): - # NOTE(kgriffs): We have to distinguish between instances of a - # callable class vs. a routine, since Python versions prior to - # 3.4 raise an error when trying to get the spec from - # a callable class instance. - spec = inspect.getargspec(action) - - else: - spec = inspect.getargspec(action.__call__) - - return 'resource' in spec.args - - -def _wrap_with_after(action, responder): +def _wrap_with_after(responder, action, action_args, action_kwargs): """Execute the given action function after a responder method. Args: + responder: The responder method to wrap. action: A function with a signature similar to a resource responder method, taking the form ``func(req, resp, resource)``. - responder: The responder method to wrap. + action_args: Additiona positional agruments to pass to *action*. + action_kwargs: Additional keyword arguments to pass to *action*. """ # NOTE(swistakm): create shim before checking what will be actually # decorated. This helps to avoid excessive nesting - if _has_resource_arg(action): + if 'resource' in get_argnames(action): shim = action else: # TODO(kgriffs): This decorator does not work on callable # classes in Python vesions prior to 3.4. # # @wraps(action) - def shim(req, resp, resource): - action(req, resp) + def shim(req, resp, resource, *args, **kwargs): + action(req, resp, *args, **kwargs) + + responder_argnames = get_argnames(responder) + extra_argnames = responder_argnames[2:] # Skip req, resp @wraps(responder) - def do_after(self, req, resp, **kwargs): + def do_after(self, req, resp, *args, **kwargs): + if args: + _merge_responder_args(args, kwargs, extra_argnames) + responder(self, req, resp, **kwargs) - shim(req, resp, self) + shim(req, resp, self, *action_args, **action_kwargs) return do_after -def _wrap_with_before(action, responder): +def _wrap_with_before(responder, action, action_args, action_kwargs): """Execute the given action function before a responder method. Args: + responder: The responder method to wrap. action: A function with a similar signature to a resource responder method, taking the form ``func(req, resp, resource, params)``. - responder: The responder method to wrap + action_args: Additiona positional agruments to pass to *action*. + action_kwargs: Additional keyword arguments to pass to *action*. """ # NOTE(swistakm): create shim before checking what will be actually # decorated. This allows to avoid excessive nesting - if _has_resource_arg(action): + if 'resource' in get_argnames(action): shim = action else: # TODO(kgriffs): This decorator does not work on callable - # classes in Python vesions prior to 3.4. + # classes in Python versions prior to 3.4. # # @wraps(action) - def shim(req, resp, resource, kwargs): + def shim(req, resp, resource, params, *args, **kwargs): # NOTE(kgriffs): Don't have to pass "self" even if has_self, # since method is assumed to be bound. - action(req, resp, kwargs) + action(req, resp, params, *args, **kwargs) + + responder_argnames = get_argnames(responder) + extra_argnames = responder_argnames[2:] # Skip req, resp @wraps(responder) - def do_before(self, req, resp, **kwargs): - shim(req, resp, self, kwargs) + def do_before(self, req, resp, *args, **kwargs): + if args: + _merge_responder_args(args, kwargs, extra_argnames) + + shim(req, resp, self, kwargs, *action_args, **action_kwargs) responder(self, req, resp, **kwargs) return do_before + + +def _merge_responder_args(args, kwargs, argnames): + """Merge responder args into kwargs. + + The framework always passes extra args as keyword arguments. + However, when the app calls the responder directly, it might use + positional arguments instead, so we need to handle that case. This + might happen, for example, when overriding a resource and calling + a responder via super(). + + Args: + args (tuple): Extra args passed into the responder + kwargs (dict): Keyword args passed into the responder + argnames (list): Extra argnames from the responder's + signature, ordered as defined + """ + + # NOTE(kgriffs): Merge positional args into kwargs by matching + # them up to the responder's signature. To do that, we must + # find out the names of the positional arguments by matching + # them in the order of the arguments named in the responder's + # signature. + for i, argname in enumerate(argnames): + # NOTE(kgriffs): extra_argnames may contain keyword arguments, + # which wont be in the args list, and are already in the kwargs + # dict anyway, so detect and skip them. + if argname not in kwargs: + kwargs[argname] = args[i] diff -Nru python-falcon-1.0.0/falcon/http_error.py python-falcon-1.4.1/falcon/http_error.py --- python-falcon-1.0.0/falcon/http_error.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/http_error.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json +"""HTTPError exception class.""" + import xml.etree.ElementTree as et try: @@ -20,15 +21,25 @@ except ImportError: OrderedDict = dict -from falcon.util import uri +from falcon.util import json, uri class HTTPError(Exception): """Represents a generic HTTP error. - Raise this or a child class to have Falcon automagically return pretty - error responses (with an appropriate HTTP status code) to the client - when something goes wrong. + Raise an instance or subclass of ``HTTPError`` to have Falcon return + a formatted error response and an appropriate HTTP status code + to the client when something goes wrong. JSON and XML media types + are supported by default. + + To customize the error presentation, implement a custom error + serializer and set it on the :class:`~.API` instance via + :meth:`~.API.set_error_serializer`. + + To customize what data is passed to the serializer, subclass + ``HTTPError`` and override the ``to_dict()`` method (``to_json()`` + is implemented via ``to_dict()``). To also support XML, override + the ``to_xml()`` method. Attributes: status (str): HTTP status line, e.g. '748 Confounded by Ponies'. @@ -37,9 +48,10 @@ the HTTP response. In ``HTTPError`` this property always returns ``True``, but child classes may override it in order to return ``False`` when an empty HTTP body is desired. - See also the ``falcon.http_error.NoRepresentation`` mixin. - title (str): Error title to send to the client. Will be ``None`` if - the error should result in an HTTP response with an empty body. + + (See also: :class:`falcon.http_error.NoRepresentation`) + + title (str): Error title to send to the client. description (str): Description of the error to send to the client. headers (dict): Extra headers to add to the response. link (str): An href that the client can provide to the user for @@ -51,7 +63,8 @@ status (str): HTTP status code and text, such as "400 Bad Request" Keyword Args: - title (str): Human-friendly error title (default ``None``). + title (str): Human-friendly error title. If not provided, defaults + to the HTTP status line as determined by the ``status`` argument. description (str): Human-friendly description of the error, along with a helpful suggestion or two (default ``None``). headers (dict or list): A ``dict`` of header names and values @@ -71,13 +84,11 @@ Falcon can process a list of ``tuple`` slightly faster than a ``dict``. - headers (dict): Extra headers to return in the - response to the client (default ``None``). href (str): A URL someone can visit to find out more information (default ``None``). Unicode characters are percent-encoded. href_text (str): If href is given, use this as the friendly - title/description for the link (defaults to "API documentation - for this error"). + title/description for the link (default 'API documentation + for this error'). code (int): An internal code that customers can reference in their support request or to help them when searching for knowledge base articles related to this error (default ``None``). @@ -95,7 +106,13 @@ def __init__(self, status, title=None, description=None, headers=None, href=None, href_text=None, code=None): self.status = status - self.title = title + + # TODO(kgriffs): HTTP/2 does away with the "reason phrase". Eventually + # we'll probably switch over to making everything code-based to more + # easily support HTTP/2. When that happens, should we continue to + # include the reason phrase in the title? + self.title = title or status + self.description = description self.headers = headers self.code = code @@ -108,12 +125,15 @@ else: self.link = None + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.status) + @property def has_representation(self): return True def to_dict(self, obj_type=dict): - """Returns a basic dictionary representing the error. + """Return a basic dictionary representing the error. This method can be useful when serializing the error to hash-like media types, such as YAML, JSON, and MessagePack. @@ -123,16 +143,14 @@ error information (default ``dict``). Returns: - A dictionary populated with the error's title, description, etc. + dict: A dictionary populated with the error's title, + description, etc. """ - assert self.has_representation - obj = obj_type() - if self.title is not None: - obj['title'] = self.title + obj['title'] = self.title if self.description is not None: obj['description'] = self.description @@ -146,31 +164,27 @@ return obj def to_json(self): - """Returns a pretty-printed JSON representation of the error. + """Return a pretty-printed JSON representation of the error. Returns: - A JSON document for the error. + str: A JSON document for the error. """ obj = self.to_dict(OrderedDict) - return json.dumps(obj, indent=4, separators=(',', ': '), - ensure_ascii=False) + return json.dumps(obj, ensure_ascii=False) def to_xml(self): - """Returns an XML-encoded representation of the error. + """Return an XML-encoded representation of the error. Returns: - An XML document for the error. + str: An XML document for the error. """ - assert self.has_representation - error_element = et.Element('error') - if self.title is not None: - et.SubElement(error_element, 'title').text = self.title + et.SubElement(error_element, 'title').text = self.title if self.description is not None: et.SubElement(error_element, 'description').text = self.description diff -Nru python-falcon-1.0.0/falcon/http_status.py python-falcon-1.4.1/falcon/http_status.py --- python-falcon-1.0.0/falcon/http_status.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/http_status.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""HTTPStatus exception class.""" + class HTTPStatus(Exception): """Represents a generic HTTP status. diff -Nru python-falcon-1.0.0/falcon/__init__.py python-falcon-1.4.1/falcon/__init__.py --- python-falcon-1.0.0/falcon/__init__.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,30 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -HTTP_METHODS = ( - 'CONNECT', - 'DELETE', - 'GET', - 'HEAD', - 'OPTIONS', - 'PATCH', - 'POST', - 'PUT', - 'TRACE', -) +"""Primary package for Falcon, the minimalist WSGI library. -DEFAULT_MEDIA_TYPE = 'application/json; charset=UTF-8' +Falcon is a minimalist WSGI library for building speedy web APIs and app +backends. The `falcon` package can be used to directly access most of +the framework's classes, functions, and variables:: + import falcon + + app = falcon.API() + +""" # Hoist classes and functions into the falcon namespace from falcon.version import __version__ # NOQA -from falcon.api import API, DEFAULT_MEDIA_TYPE # NOQA +from falcon.constants import * # NOQA +from falcon.api import API # NOQA from falcon.status_codes import * # NOQA from falcon.errors import * # NOQA from falcon.redirects import * # NOQA from falcon.http_error import HTTPError # NOQA from falcon.http_status import HTTPStatus # NOQA + +# NOTE(kgriffs): Ensure that "from falcon import uri" will import +# the same front-door module as "import falcon.uri". This works by +# priming the import cache with the one we want. +import falcon.uri # NOQA + from falcon.util import * # NOQA + from falcon.hooks import before, after # NOQA -from falcon.request import Request, RequestOptions # NOQA -from falcon.response import Response # NOQA +from falcon.request import Request, RequestOptions, Forwarded # NOQA +from falcon.response import Response, ResponseOptions # NOQA diff -Nru python-falcon-1.0.0/falcon/media/base.py python-falcon-1.4.1/falcon/media/base.py --- python-falcon-1.0.0/falcon/media/base.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/base.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,30 @@ +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class BaseHandler(object): + """Abstract Base Class for an internet media type handler""" + + @abc.abstractmethod # pragma: no cover + def serialize(self, obj): + """Serialize the media object on a :any:`falcon.Response` + + Args: + obj (object): A serializable object. + + Returns: + bytes: The resulting serialized bytes from the input object. + """ + + @abc.abstractmethod # pragma: no cover + def deserialize(self, raw): + """Deserialize the :any:`falcon.Request` body. + + Args: + raw (bytes): Input bytes to deserialize + + Returns: + object: A deserialized object. + """ diff -Nru python-falcon-1.0.0/falcon/media/handlers.py python-falcon-1.4.1/falcon/media/handlers.py --- python-falcon-1.0.0/falcon/media/handlers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/handlers.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,53 @@ +import mimeparse +from six.moves import UserDict + +from falcon import errors +from falcon.media import JSONHandler + + +class Handlers(UserDict): + """A dictionary like object that manages internet media type handlers.""" + def __init__(self, initial=None): + handlers = initial or { + 'application/json': JSONHandler(), + 'application/json; charset=UTF-8': JSONHandler(), + } + + # NOTE(jmvrbanac): Directly calling UserDict as it's not inheritable. + # Also, this results in self.update(...) being called. + UserDict.__init__(self, handlers) + + def _resolve_media_type(self, media_type, all_media_types): + resolved = None + + try: + # NOTE(jmvrbanac): Mimeparse will return an empty string if it can + # parse the media type, but cannot find a suitable type. + resolved = mimeparse.best_match( + all_media_types, + media_type + ) + except ValueError: + pass + + return resolved + + def find_by_media_type(self, media_type, default): + # PERF(jmvrbanac): Check via a quick methods first for performance + if media_type == '*/*' or not media_type: + return self.data[default] + + try: + return self.data[media_type] + except KeyError: + pass + + # PERF(jmvrbanac): Fallback to the slower method + resolved = self._resolve_media_type(media_type, self.data.keys()) + + if not resolved: + raise errors.HTTPUnsupportedMediaType( + '{0} is an unsupported media type.'.format(media_type) + ) + + return self.data[resolved] diff -Nru python-falcon-1.0.0/falcon/media/__init__.py python-falcon-1.4.1/falcon/media/__init__.py --- python-falcon-1.0.0/falcon/media/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,5 @@ +from .base import BaseHandler # NOQA +from .json import JSONHandler # NOQA +from .msgpack import MessagePackHandler # NOQA + +from .handlers import Handlers # NOQA diff -Nru python-falcon-1.0.0/falcon/media/json.py python-falcon-1.4.1/falcon/media/json.py --- python-falcon-1.0.0/falcon/media/json.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/json.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,27 @@ +from __future__ import absolute_import + +import six + +from falcon import errors +from falcon.media import BaseHandler +from falcon.util import json + + +class JSONHandler(BaseHandler): + """Handler built using Python's :py:mod:`json` module.""" + + def deserialize(self, raw): + try: + return json.loads(raw.decode('utf-8')) + except ValueError as err: + raise errors.HTTPBadRequest( + 'Invalid JSON', + 'Could not parse JSON body - {0}'.format(err) + ) + + def serialize(self, media): + result = json.dumps(media, ensure_ascii=False) + if six.PY3 or not isinstance(result, bytes): + return result.encode('utf-8') + + return result diff -Nru python-falcon-1.0.0/falcon/media/msgpack.py python-falcon-1.4.1/falcon/media/msgpack.py --- python-falcon-1.0.0/falcon/media/msgpack.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/msgpack.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,49 @@ +from __future__ import absolute_import + +from falcon import errors +from falcon.media import BaseHandler + + +class MessagePackHandler(BaseHandler): + """Handler built using the :py:mod:`msgpack` module. + + This handler uses ``msgpack.unpackb()`` and ``msgpack.packb()``. The + MessagePack ``bin`` type is used to distinguish between Unicode strings + (``str`` on Python 3, ``unicode`` on Python 2) and byte strings + (``bytes`` on Python 2/3, or ``str`` on Python 2). + + Note: + This handler requires the extra ``msgpack`` package, which must be + installed in addition to ``falcon`` from PyPI: + + .. code:: + + $ pip install msgpack + + Python 2.6 users will need to use the deprecated ``msgpack-python`` + package instead, pinned to version 0.4.8. + """ + + def __init__(self): + import msgpack + + self.msgpack = msgpack + self.packer = msgpack.Packer( + encoding='utf-8', + autoreset=True, + use_bin_type=True, + ) + + def deserialize(self, raw): + try: + # NOTE(jmvrbanac): Using unpackb since we would need to manage + # a buffer for Unpacker() which wouldn't gain us much. + return self.msgpack.unpackb(raw, encoding='utf-8') + except ValueError as err: + raise errors.HTTPBadRequest( + 'Invalid MessagePack', + 'Could not parse MessagePack body - {0}'.format(err) + ) + + def serialize(self, media): + return self.packer.pack(media) diff -Nru python-falcon-1.0.0/falcon/media/validators/jsonschema.py python-falcon-1.4.1/falcon/media/validators/jsonschema.py --- python-falcon-1.0.0/falcon/media/validators/jsonschema.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/media/validators/jsonschema.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,54 @@ +from __future__ import absolute_import + +import falcon + +try: + import jsonschema +except ImportError: + pass + + +def validate(schema): + """Decorator for validating ``req.media`` using JSON Schema. + + This decorator provides standard JSON Schema validation via the + ``jsonschema`` package available from PyPI. Semantic validation via + the *format* keyword is enabled for the default checkers implemented + by ``jsonschema.FormatChecker``. + + Note: + The `jsonschema`` package must be installed separately in order to use + this decorator, as Falcon does not install it by default. + + Args: + schema (dict): A dictionary that follows the JSON Schema specification. + See `json-schema.org `_ for more + information on defining a compatible dictionary. + + Example: + .. code:: python + + from falcon.media.validators import jsonschema + + # -- snip -- + + @jsonschema.validate(my_post_schema) + def on_post(self, req, resp): + + # -- snip -- + + """ + + def decorator(func): + def wrapper(self, req, resp, *args, **kwargs): + try: + jsonschema.validate(req.media, schema, format_checker=jsonschema.FormatChecker()) + except jsonschema.ValidationError as e: + raise falcon.HTTPBadRequest( + 'Failed data validation', + description=e.message + ) + + return func(self, req, resp, *args, **kwargs) + return wrapper + return decorator diff -Nru python-falcon-1.0.0/falcon/redirects.py python-falcon-1.4.1/falcon/redirects.py --- python-falcon-1.0.0/falcon/redirects.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/redirects.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""HTTPStatus specializations for 3xx redirects.""" + import falcon from falcon.http_status import HTTPStatus @@ -28,7 +30,7 @@ behavior is undesired, the 308 (Permanent Redirect) status code can be used instead. - See also: https://tools.ietf.org/html/rfc7231#section-6.4.2 + (See also: RFC 7231, Section 6.4.2) Args: location (str): URI to provide as the Location header in the @@ -54,7 +56,7 @@ behavior is undesired, the 307 (Temporary Redirect) status code can be used instead. - See also: https://tools.ietf.org/html/rfc7231#section-6.4.3 + (See also: RFC 7231, Section 6.4.3) Args: location (str): URI to provide as the Location header in the @@ -85,7 +87,7 @@ The new URI in the Location header field is not considered equivalent to the effective request URI. - See also: https://tools.ietf.org/html/rfc7231#section-6.4.4 + (See also: RFC 7231, Section 6.4.4) Args: location (str): URI to provide as the Location header in the @@ -111,7 +113,7 @@ This status code is similar to 302 (Found), except that it does not allow changing the request method from POST to GET. - See also: https://tools.ietf.org/html/rfc7231#section-6.4.7 + (See also: RFC 7231, Section 6.4.7) Args: location (str): URI to provide as the Location header in the @@ -134,7 +136,7 @@ that it does not allow changing the request method from POST to GET. - See also: https://tools.ietf.org/html/rfc7238#section-3 + (See also: RFC 7238, Section 3) Args: location (str): URI to provide as the Location header in the diff -Nru python-falcon-1.0.0/falcon/request_helpers.py python-falcon-1.4.1/falcon/request_helpers.py --- python-falcon-1.0.0/falcon/request_helpers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/request_helpers.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,9 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Utilities for the Request class.""" + +import io + def header_property(wsgi_name): - """Creates a read-only header property. + """Create a read-only header property. Args: wsgi_name (str): Case-sensitive name of the header as it would @@ -34,7 +38,7 @@ return property(fget) -class Body(object): +class BoundedStream(io.IOBase): """Wrap *wsgi.input* streams to make them more robust. ``socket._fileobject`` and ``io.BufferedReader`` are sometimes used @@ -72,16 +76,16 @@ """Helper function for proxing reads to the underlying stream. Args: - size (int): Maximum number of bytes/characters to read. - Will be coerced, if None or -1, to `self.stream_len`. Will - likewise be coerced if greater than `self.stream_len`, so - that if the stream doesn't follow standard io semantics, - the read won't block. + size (int): Maximum number of bytes to read. Will be + coerced, if None or -1, to the number of remaining bytes + in the stream. Will likewise be coerced if greater than + the number of remaining bytes, to avoid making a + blocking call to the wrapped stream. target (callable): Once `size` has been fixed up, this function will be called to actually do the work. Returns: - Data read from the stream, as returned by `target`. + bytes: Data read from the stream, as returned by `target`. """ @@ -94,6 +98,18 @@ self._bytes_remaining -= size return target(size) + def readable(self): + """Always returns ``True``.""" + return True + + def seekable(self): + """Always returns ``False``.""" + return False + + def writeable(self): + """Always returns ``False``.""" + return False + def read(self, size=None): """Read from the stream. @@ -102,7 +118,7 @@ Defaults to reading until EOF. Returns: - Data read from the stream. + bytes: Data read from the stream. """ @@ -116,7 +132,7 @@ Defaults to reading until EOF. Returns: - Data read from the stream. + bytes: Data read from the stream. """ @@ -130,8 +146,17 @@ Defaults to reading until EOF. Returns: - Data read from the stream. + bytes: Data read from the stream. """ return self._read(hint, self.stream.readlines) + + def write(self, data): + """Always raises IOError; writing is not supported.""" + + raise IOError('Stream is not writeable') + + +# NOTE(kgriffs): Alias for backwards-compat +Body = BoundedStream diff -Nru python-falcon-1.0.0/falcon/request.py python-falcon-1.4.1/falcon/request.py --- python-falcon-1.0.0/falcon/request.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/request.py 2018-08-08 23:08:36.000000000 +0000 @@ -10,6 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Request class.""" + from datetime import datetime try: @@ -24,33 +26,35 @@ import io NativeStream = io.BufferedReader +from uuid import UUID # NOQA: I202 +from wsgiref.validate import InputWrapper + import mimeparse import six -from wsgiref.validate import InputWrapper +from six.moves import http_cookies -from falcon.errors import * # NOQA -from falcon import util -from falcon.util.uri import parse_query_string, parse_host, unquote_string +from falcon import DEFAULT_MEDIA_TYPE +from falcon import errors from falcon import request_helpers as helpers +from falcon import util +from falcon.forwarded import _parse_forwarded_header +from falcon.forwarded import Forwarded # NOQA +from falcon.media import Handlers +from falcon.util import json +from falcon.util.uri import parse_host, parse_query_string # NOTE(tbug): In some cases, http_cookies is not a module # but a dict-like structure. This fixes that issue. # See issue https://github.com/falconry/falcon/issues/556 -from six.moves import http_cookies SimpleCookie = http_cookies.SimpleCookie - DEFAULT_ERROR_LOG_FORMAT = (u'{0:%Y-%m-%d %H:%M:%S} [FALCON] [ERROR]' u' {1} {2}{3} => ') -TRUE_STRINGS = ('true', 'True', 'yes', '1') -FALSE_STRINGS = ('false', 'False', 'no', '0') +TRUE_STRINGS = ('true', 'True', 'yes', '1', 'on') +FALSE_STRINGS = ('false', 'False', 'no', '0', 'off') WSGI_CONTENT_HEADERS = ('CONTENT_TYPE', 'CONTENT_LENGTH') - -_maybe_wrap_wsgi_stream = True - - # PERF(kgriffs): Avoid an extra namespace lookup when using these functions strptime = datetime.strptime now = datetime.now @@ -65,12 +69,86 @@ Args: env (dict): A WSGI environment dict passed in from the server. See also PEP-3333. + + Keyword Arguments: options (dict): Set of global options passed from the API handler. Attributes: - protocol (str): Either 'http' or 'https'. + env (dict): Reference to the WSGI environ ``dict`` passed in from the + server. (See also PEP-3333.) + context (dict): Dictionary to hold any data about the request which is + specific to your app (e.g. session object). Falcon itself will + not interact with this attribute after it has been initialized. + context_type (class): Class variable that determines the factory or + type to use for initializing the `context` attribute. By default, + the framework will instantiate standard ``dict`` objects. However, + you may override this behavior by creating a custom child class of + ``falcon.Request``, and then passing that new class to + `falcon.API()` by way of the latter's `request_type` parameter. + + Note: + When overriding `context_type` with a factory function (as + opposed to a class), the function is called like a method of + the current Request instance. Therefore the first argument is + the Request instance itself (self). + scheme (str): URL scheme used for the request. Either 'http' or + 'https'. + + Note: + If the request was proxied, the scheme may not + match what was originally requested by the client. + :py:attr:`forwarded_scheme` can be used, instead, + to handle such cases. + + forwarded_scheme (str): Original URL scheme requested by the + user agent, if the request was proxied. Typical values are + 'http' or 'https'. + + The following request headers are checked, in order of + preference, to determine the forwarded scheme: + + - ``Forwarded`` + - ``X-Forwarded-For`` + + If none of these headers are available, or if the + Forwarded header is available but does not contain a + "proto" parameter in the first hop, the value of + :attr:`scheme` is returned instead. + + (See also: RFC 7239, Section 1) + + protocol (str): Deprecated alias for `scheme`. Will be removed + in a future release. method (str): HTTP method requested (e.g., 'GET', 'POST', etc.) - host (str): Hostname requested by the client + host (str): Host request header field + forwarded_host (str): Original host request header as received + by the first proxy in front of the application server. + + The following request headers are checked, in order of + preference, to determine the forwarded scheme: + + - ``Forwarded`` + - ``X-Forwarded-Host`` + + If none of the above headers are available, or if the + Forwarded header is available but the "host" + parameter is not included in the first hop, the value of + :attr:`host` is returned instead. + + Note: + Reverse proxies are often configured to set the Host + header directly to the one that was originally + requested by the user agent; in that case, using + :attr:`host` is sufficient. + + (See also: RFC 7239, Section 4) + + port (int): Port used for the request. If the request URI does + not specify a port, the default one for the given schema is + returned (80 for HTTP and 443 for HTTPS). + netloc (str): Returns the 'host:port' portion of the request + URL. The port may be ommitted if it is the default one for + the URL's schema (80 for HTTP and 443 for HTTPS). subdomain (str): Leftmost (i.e., most specific) subdomain from the hostname. If only a single domain name is given, `subdomain` will be ``None``. @@ -79,10 +157,54 @@ If the hostname in the request is an IP address, the value for `subdomain` is undefined. - env (dict): Reference to the WSGI environ ``dict`` passed in from the - server. See also PEP-3333. - app (str): Name of the WSGI app (if using WSGI's notion of virtual - hosting). + app (str): The initial portion of the request URI's path that + corresponds to the application object, so that the + application knows its virtual "location". This may be an + empty string, if the application corresponds to the "root" + of the server. + + (Corresponds to the "SCRIPT_NAME" environ variable defined + by PEP-3333.) + uri (str): The fully-qualified URI for the request. + url (str): Alias for `uri`. + forwarded_uri (str): Original URI for proxied requests. Uses + :attr:`forwarded_scheme` and :attr:`forwarded_host` in + order to reconstruct the original URI requested by the user + agent. + relative_uri (str): The path and query string portion of the + request URI, omitting the scheme and host. + prefix (str): The prefix of the request URI, including scheme, + host, and WSGI app (if any). + forwarded_prefix (str): The prefix of the original URI for + proxied requests. Uses :attr:`forwarded_scheme` and + :attr:`forwarded_host` in order to reconstruct the + original URI. + path (str): Path portion of the request URI (not including query + string). + + Note: + `req.path` may be set to a new value by a `process_request()` + middleware method in order to influence routing. + query_string (str): Query string portion of the request URI, without + the preceding '?' character. + uri_template (str): The template for the route that was matched for + this request. May be ``None`` if the request has not yet been + routed, as would be the case for `process_request()` middleware + methods. May also be ``None`` if your app uses a custom routing + engine and the engine does not provide the URI template when + resolving a route. + remote_addr(str): IP address of the closest client or proxy to + the WSGI server. + + This property is determined by the value of ``REMOTE_ADDR`` + in the WSGI environment dict. Since this address is not + derived from an HTTP header, clients and proxies can not + forge it. + + Note: + If your application is behind one or more reverse + proxies, you can use :py:attr:`~.access_route` + to retrieve the real IP address of the client. access_route(list): IP address of the original client, as well as any known addresses of proxies fronting the WSGI server. @@ -109,50 +231,23 @@ using them. Do not rely on the access route to authorize requests. - remote_addr(str): IP address of the closest client or proxy to - the WSGI server. - - This property is determined by the value of ``REMOTE_ADDR`` - in the WSGI environment dict. Since this address is not - derived from an HTTP header, clients and proxies can not - forge it. - - Note: - If your application is behind one or more reverse - proxies, you can use :py:attr:`~.access_route` - to retrieve the real IP address of the client. - - context (dict): Dictionary to hold any data about the request which is - specific to your app (e.g. session object). Falcon itself will - not interact with this attribute after it has been initialized. - context_type (class): Class variable that determines the - factory or type to use for initializing the - `context` attribute. By default, the framework will - instantiate standard - ``dict`` objects. However, You may override this behavior - by creating a custom child class of ``falcon.Request``, and - then passing that new class to `falcon.API()` by way of the - latter's `request_type` parameter. - - Note: - When overriding `context_type` with a factory function (as - opposed to a class), the function is called like a method of - the current Request instance. Therefore the first argument is - the Request instance itself (self). + forwarded (list): Value of the Forwarded header, as a parsed list + of :class:`falcon.Forwarded` objects, or ``None`` if the header + is missing. If the header value is malformed, Falcon will + make a best effort to parse what it can. - uri (str): The fully-qualified URI for the request. - url (str): alias for `uri`. - relative_uri (str): The path + query string portion of the full URI. - path (str): Path portion of the request URL (not including query - string). - query_string (str): Query string portion of the request URL, without - the preceding '?' character. + (See also: RFC 7239, Section 4) + date (datetime): Value of the Date header, converted to a + ``datetime`` instance. The header value is assumed to + conform to RFC 1123. + auth (str): Value of the Authorization header, or ``None`` if the + header is missing. user_agent (str): Value of the User-Agent header, or ``None`` if the header is missing. + referer (str): Value of the Referer header, or ``None`` if + the header is missing. accept (str): Value of the Accept header, or '*/*' if the header is missing. - auth (str): Value of the Authorization header, or ``None`` if the - header is missing. client_accepts_json (bool): ``True`` if the Accept header indicates that the client is willing to receive JSON, otherwise ``False``. client_accepts_msgpack (bool): ``True`` if the Accept header indicates @@ -160,11 +255,51 @@ ``False``. client_accepts_xml (bool): ``True`` if the Accept header indicates that the client is willing to receive XML, otherwise ``False``. + cookies (dict): + A dict of name/value cookie pairs. (See also: + :ref:`Getting Cookies `) content_type (str): Value of the Content-Type header, or ``None`` if the header is missing. content_length (int): Value of the Content-Length header converted to an ``int``, or ``None`` if the header is missing. - stream: File-like object for reading the body of the request, if any. + stream: File-like input object for reading the body of the + request, if any. This object provides direct access to the + server's data stream and is non-seekable. In order to + avoid unintended side effects, and to provide maximum + flexibility to the application, Falcon itself does not + buffer or spool the data in any way. + + Since this object is provided by the WSGI + server itself, rather than by Falcon, it may behave + differently depending on how you host your app. For example, + attempting to read more bytes than are expected (as + determined by the Content-Length header) may or may not + block indefinitely. It's a good idea to test your WSGI + server to find out how it behaves. + + This can be particulary problematic when a request body is + expected, but none is given. In this case, the following + call blocks under certain WSGI servers:: + + # Blocks if Content-Length is 0 + data = req.stream.read() + + The workaround is fairly straightforward, if verbose:: + + # If Content-Length happens to be 0, or the header is + # missing altogether, this will not block. + data = req.stream.read(req.content_length or 0) + + Alternatively, when passing the stream directly to a + consumer, it may be necessary to branch off the + value of the Content-Length header:: + + if req.content_length: + doc = json.load(req.stream) + + For a slight performance cost, you may instead wish to use + :py:attr:`bounded_stream`, which wraps the native WSGI + input object to normalize its behavior. Note: If an HTML form is POSTed to the API using the @@ -175,11 +310,37 @@ and merge them into the query string parameters. In this case, the stream will be left at EOF. - date (datetime): Value of the Date header, converted to a - ``datetime`` instance. The header value is assumed to - conform to RFC 1123. + bounded_stream: File-like wrapper around `stream` to normalize + certain differences between the native input objects + employed by different WSGI servers. In particular, + `bounded_stream` is aware of the expected Content-Length of + the body, and will never block on out-of-bounds reads, + assuming the client does not stall while transmitting the + data to the server. + + For example, the following will not block when + Content-Length is 0 or the header is missing altogether:: + + data = req.bounded_stream.read() + + This is also safe:: + + doc = json.load(req.bounded_stream) + expect (str): Value of the Expect header, or ``None`` if the header is missing. + media (object): Returns a deserialized form of the request stream. + When called, it will attempt to deserialize the request stream + using the Content-Type header as well as the media-type handlers + configured via :class:`falcon.RequestOptions`. + + See :ref:`media` for more information regarding media handling. + + Warning: + This operation will consume the request stream the first time + it's called and cache the results. Follow-up calls will just + retrieve a cached version of the object. + range (tuple of int): A 2-member ``tuple`` parsed from the value of the Range header. @@ -219,82 +380,92 @@ all the values in the order seen. options (dict): Set of global options passed from the API handler. - - cookies (dict): - A dict of name/value cookie pairs. - See also: :ref:`Getting Cookies ` - """ __slots__ = ( + '__dict__', + '_bounded_stream', + '_cached_access_route', + '_cached_forwarded', + '_cached_forwarded_prefix', + '_cached_forwarded_uri', '_cached_headers', - '_cached_uri', + '_cached_prefix', '_cached_relative_uri', + '_cached_uri', + '_cookies', + '_params', + '_wsgierrors', 'content_type', + 'context', 'env', 'method', - '_params', + 'options', 'path', 'query_string', 'stream', - 'context', - '_wsgierrors', - 'options', - '_cookies', - '_cached_access_route', + 'uri_template', + '_media', ) - # Allow child classes to override this - context_type = None + # Child classes may override this + context_type = dict - def __init__(self, env, options=None): - global _maybe_wrap_wsgi_stream + _wsgi_input_type_known = False + _always_wrap_wsgi_input = False + def __init__(self, env, options=None): self.env = env self.options = options if options else RequestOptions() self._wsgierrors = env['wsgi.errors'] - self.stream = env['wsgi.input'] self.method = env['REQUEST_METHOD'] - # Normalize path - path = env['PATH_INFO'] - if path: - if six.PY3: - # PEP 3333 specifies that PATH_INFO variable are always - # "bytes tunneled as latin-1" and must be encoded back - path = path.encode('latin1').decode('utf-8', 'replace') + self.uri_template = None + self._media = None - if len(path) != 1 and path.endswith('/'): - self.path = path[:-1] - else: - self.path = path + # NOTE(kgriffs): PEP 3333 specifies that PATH_INFO may be the + # empty string, so normalize it in that case. + path = env['PATH_INFO'] or '/' + + if six.PY3: + # PEP 3333 specifies that PATH_INFO variable are always + # "bytes tunneled as latin-1" and must be encoded back + path = path.encode('latin1').decode('utf-8', 'replace') + + if (self.options.strip_url_path_trailing_slash and + len(path) != 1 and path.endswith('/')): + self.path = path[:-1] else: - self.path = '/' + self.path = path - # PERF(kgriffs): if...in is faster than using env.get(...) - if 'QUERY_STRING' in env: + # PERF(ueg1990): try/catch cheaper and faster (and more Pythonic) + try: self.query_string = env['QUERY_STRING'] - + except KeyError: + self.query_string = '' + self._params = {} + else: if self.query_string: self._params = parse_query_string( self.query_string, keep_blank_qs_values=self.options.keep_blank_qs_values, + parse_qs_csv=self.options.auto_parse_qs_csv, ) else: self._params = {} - else: - self.query_string = '' - self._params = {} - self._cookies = None + self._cached_access_route = None + self._cached_forwarded = None + self._cached_forwarded_prefix = None + self._cached_forwarded_uri = None self._cached_headers = None - self._cached_uri = None + self._cached_prefix = None self._cached_relative_uri = None - self._cached_access_route = None + self._cached_uri = None try: self.content_type = self.env['CONTENT_TYPE'] @@ -303,28 +474,48 @@ # NOTE(kgriffs): Wrap wsgi.input if needed to make read() more robust, # normalizing semantics between, e.g., gunicorn and wsgiref. - if _maybe_wrap_wsgi_stream: - if isinstance(self.stream, (NativeStream, InputWrapper,)): - self._wrap_stream() - else: - # PERF(kgriffs): If self.stream does not need to be wrapped - # this time, it never needs to be wrapped since the server - # will continue using the same type for wsgi.input. - _maybe_wrap_wsgi_stream = False + # + # PERF(kgriffs): Accessing via self when reading is faster than + # via the class name. But we must set the variables using the + # class name so they are picked up by all future instantiations + # of the class. + if not self._wsgi_input_type_known: + Request._always_wrap_wsgi_input = isinstance( + env['wsgi.input'], + (NativeStream, InputWrapper) + ) + + Request._wsgi_input_type_known = True + + if self._always_wrap_wsgi_input: + # TODO(kgriffs): In Falcon 2.0, stop wrapping stream since it is + # less useful now that we have bounded_stream. + self.stream = self._get_wrapped_wsgi_input() + self._bounded_stream = self.stream + else: + self.stream = env['wsgi.input'] + self._bounded_stream = None # Lazy wrapping # PERF(kgriffs): Technically, we should spend a few more # cycles and parse the content type for real, but # this heuristic will work virtually all the time. - if (self.options.auto_parse_form_urlencoded and - self.content_type is not None and - 'application/x-www-form-urlencoded' in self.content_type): + if ( + self.options.auto_parse_form_urlencoded and + self.content_type is not None and + 'application/x-www-form-urlencoded' in self.content_type and + + # NOTE(kgriffs): Within HTTP, a payload for a GET or HEAD + # request has no defined semantics, so we don't expect a + # body in those cases. We would normally not expect a body + # for OPTIONS either, but RFC 7231 does allow for it. + self.method not in ('GET', 'HEAD') + ): self._parse_form_urlencoded() - if self.context_type is None: - # Literal syntax is more efficient than using dict() - self.context = {} - else: - self.context = self.context_type() + self.context = self.context_type() + + def __repr__(self): + return '<%s: %s %r>' % (self.__class__.__name__, self.method, self.url) # ------------------------------------------------------------------------ # Properties @@ -339,6 +530,32 @@ if_none_match = helpers.header_property('HTTP_IF_NONE_MATCH') if_range = helpers.header_property('HTTP_IF_RANGE') + referer = helpers.header_property('HTTP_REFERER') + + @property + def forwarded(self): + # PERF(kgriffs): We could DRY up this memoization pattern using + # a decorator, but that would incur additional overhead without + # resorting to some trickery to rewrite the body of the method + # itself (vs. simply wrapping it with some memoization logic). + # At some point we might look into this but I don't think + # it's worth it right now. + if self._cached_forwarded is None: + # PERF(kgriffs): If someone is calling this, they are probably + # confident that the header exists, so most of the time we + # expect this call to succeed. Therefore, we won't need to + # pay the penalty of a raised exception in most cases, and + # there is no need to spend extra cycles calling get() or + # checking beforehand whether the key is in the dict. + try: + forwarded = self.env['HTTP_FORWARDED'] + except KeyError: + return None + + self._cached_forwarded = _parse_forwarded_header(forwarded) + + return self._cached_forwarded + @property def client_accepts_json(self): return self.client_accepts('application/json') @@ -381,15 +598,22 @@ value_as_int = int(value) except ValueError: msg = 'The value of the header must be a number.' - raise HTTPInvalidHeader(msg, 'Content-Length') + raise errors.HTTPInvalidHeader(msg, 'Content-Length') if value_as_int < 0: msg = 'The value of the header must be a positive number.' - raise HTTPInvalidHeader(msg, 'Content-Length') + raise errors.HTTPInvalidHeader(msg, 'Content-Length') return value_as_int @property + def bounded_stream(self): + if self._bounded_stream is None: + self._bounded_stream = self._get_wrapped_wsgi_input() + + return self._bounded_stream + + @property def date(self): return self.get_header_as_datetime('Date') @@ -409,13 +633,13 @@ unit, sep, req_range = value.partition('=') else: msg = "The value must be prefixed with a range unit, e.g. 'bytes='" - raise HTTPInvalidHeader(msg, 'Range') + raise errors.HTTPInvalidHeader(msg, 'Range') except KeyError: return None if ',' in req_range: msg = 'The value must be a continuous range.' - raise HTTPInvalidHeader(msg, 'Range') + raise errors.HTTPInvalidHeader(msg, 'Range') try: first, sep, last = req_range.partition('-') @@ -429,14 +653,14 @@ return (-int(last), -1) else: msg = 'The range offsets are missing.' - raise HTTPInvalidHeader(msg, 'Range') + raise errors.HTTPInvalidHeader(msg, 'Range') except ValueError: href = 'http://goo.gl/zZ6Ey' href_text = 'HTTP/1.1 Range Requests' msg = ('It must be a range formatted according to RFC 7233.') - raise HTTPInvalidHeader(msg, 'Range', href=href, - href_text=href_text) + raise errors.HTTPInvalidHeader(msg, 'Range', href=href, + href_text=href_text) @property def range_unit(self): @@ -448,51 +672,62 @@ return unit else: msg = "The value must be prefixed with a range unit, e.g. 'bytes='" - raise HTTPInvalidHeader(msg, 'Range') + raise errors.HTTPInvalidHeader(msg, 'Range') except KeyError: return None @property def app(self): - return self.env.get('SCRIPT_NAME', '') + # PERF(kgriffs): try..except is faster than get() assuming that + # we normally expect the key to exist. Even though PEP-3333 + # allows WSGI servers to omit the key when the value is an + # empty string, uwsgi, gunicorn, waitress, and wsgiref all + # include it even in that case. + try: + return self.env['SCRIPT_NAME'] + except KeyError: + return '' @property - def protocol(self): + def scheme(self): return self.env['wsgi.url_scheme'] @property - def uri(self): - if self._cached_uri is None: - env = self.env - protocol = env['wsgi.url_scheme'] - - # NOTE(kgriffs): According to PEP-3333 we should first - # try to use the Host header if present. - # - # PERF(kgriffs): try..except is faster than .get + def forwarded_scheme(self): + # PERF(kgriffs): Since the Forwarded header is still relatively + # new, we expect X-Forwarded-Proto to be more common, so + # try to avoid calling self.forwarded if we can, since it uses a + # try...catch that will usually result in a relatively expensive + # raised exception. + if 'HTTP_FORWARDED' in self.env: + first_hop = self.forwarded[0] + scheme = first_hop.scheme or self.scheme + else: + # PERF(kgriffs): This call should normally succeed, so + # just go for it without wasting time checking it + # first. Note also that the indexing operator is + # slightly faster than using get(). try: - host = env['HTTP_HOST'] + scheme = self.env['HTTP_X_FORWARDED_PROTO'].lower() except KeyError: - host = env['SERVER_NAME'] - port = env['SERVER_PORT'] + scheme = self.env['wsgi.url_scheme'] - if protocol == 'https': - if port != '443': - host += ':' + port - else: - if port != '80': - host += ':' + port + return scheme + + # TODO(kgriffs): Remove this deprecated alias in Falcon 2.0 + protocol = scheme + + @property + def uri(self): + if self._cached_uri is None: + scheme = self.env['wsgi.url_scheme'] # PERF: For small numbers of items, '+' is faster # than ''.join(...). Concatenation is also generally # faster than formatting. - value = (protocol + '://' + - host + - self.app + - self.path) - - if self.query_string: - value = value + '?' + self.query_string + value = (scheme + '://' + + self.netloc + + self.relative_uri) self._cached_uri = value @@ -501,6 +736,53 @@ url = uri @property + def forwarded_uri(self): + if self._cached_forwarded_uri is None: + # PERF: For small numbers of items, '+' is faster + # than ''.join(...). Concatenation is also generally + # faster than formatting. + value = (self.forwarded_scheme + '://' + + self.forwarded_host + + self.relative_uri) + + self._cached_forwarded_uri = value + + return self._cached_forwarded_uri + + @property + def relative_uri(self): + if self._cached_relative_uri is None: + if self.query_string: + self._cached_relative_uri = (self.app + self.path + '?' + + self.query_string) + else: + self._cached_relative_uri = self.app + self.path + + return self._cached_relative_uri + + @property + def prefix(self): + if self._cached_prefix is None: + self._cached_prefix = ( + self.env['wsgi.url_scheme'] + '://' + + self.netloc + + self.app + ) + + return self._cached_prefix + + @property + def forwarded_prefix(self): + if self._cached_forwarded_prefix is None: + self._cached_forwarded_prefix = ( + self.forwarded_scheme + '://' + + self.forwarded_host + + self.app + ) + + return self._cached_forwarded_prefix + + @property def host(self): try: # NOTE(kgriffs): Prefer the host header; the web server @@ -516,23 +798,34 @@ return host @property + def forwarded_host(self): + # PERF(kgriffs): Since the Forwarded header is still relatively + # new, we expect X-Forwarded-Host to be more common, so + # try to avoid calling self.forwarded if we can, since it uses a + # try...catch that will usually result in a relatively expensive + # raised exception. + if 'HTTP_FORWARDED' in self.env: + first_hop = self.forwarded[0] + host = first_hop.host or self.host + else: + # PERF(kgriffs): This call should normally succeed, assuming + # that the caller is expecting a forwarded header, so + # just go for it without wasting time checking it + # first. + try: + host = self.env['HTTP_X_FORWARDED_HOST'] + except KeyError: + host = self.host + + return host + + @property def subdomain(self): # PERF(kgriffs): .partition is slightly faster than .split subdomain, sep, remainder = self.host.partition('.') return subdomain if sep else None @property - def relative_uri(self): - if self._cached_relative_uri is None: - if self.query_string: - self._cached_relative_uri = (self.app + self.path + '?' + - self.query_string) - else: - self._cached_relative_uri = self.app + self.path - - return self._cached_relative_uri - - @property def headers(self): # NOTE(kgriffs: First time here will cache the dict so all we # have to do is clone it in the future. @@ -562,7 +855,13 @@ # NOTE(tbug): We might want to look into parsing # cookies ourselves. The SimpleCookie is doing a # lot if stuff only required to SEND cookies. - parser = SimpleCookie(self.get_header('Cookie')) + cookie_header = self.get_header('Cookie', default='') + parser = SimpleCookie() + for cookie_part in cookie_header.split('; '): + try: + parser.load(cookie_part) + except http_cookies.CookieError: + pass cookies = {} for morsel in parser.values(): cookies[morsel.key] = morsel.value @@ -586,7 +885,11 @@ # aware that an upstream proxy is malfunctioning. if 'HTTP_FORWARDED' in self.env: - self._cached_access_route = self._parse_rfc_forwarded() + self._cached_access_route = [] + for hop in self.forwarded: + if hop.src is not None: + host, __ = parse_host(hop.src) + self._cached_access_route.append(host) elif 'HTTP_X_FORWARDED_FOR' in self.env: addresses = self.env['HTTP_X_FORWARDED_FOR'].split(',') self._cached_access_route = [ip.strip() for ip in addresses] @@ -603,20 +906,79 @@ def remote_addr(self): return self.env.get('REMOTE_ADDR') + @property + def port(self): + try: + host_header = self.env['HTTP_HOST'] + + default_port = 80 if self.env['wsgi.url_scheme'] == 'http' else 443 + host, port = parse_host(host_header, default_port=default_port) + except KeyError: + # NOTE(kgriffs): Normalize to an int, since that is the type + # returned by parse_host(). + # + # NOTE(kgriffs): In the case that SERVER_PORT was used, + # PEP-3333 requires that the port never be an empty string. + port = int(self.env['SERVER_PORT']) + + return port + + @property + def netloc(self): + env = self.env + protocol = env['wsgi.url_scheme'] + + # NOTE(kgriffs): According to PEP-3333 we should first + # try to use the Host header if present. + # + # PERF(kgriffs): try..except is faster than get() when we + # expect the key to be present most of the time. + try: + netloc_value = env['HTTP_HOST'] + except KeyError: + netloc_value = env['SERVER_NAME'] + + port = env['SERVER_PORT'] + if protocol == 'https': + if port != '443': + netloc_value += ':' + port + else: + if port != '80': + netloc_value += ':' + port + + return netloc_value + + @property + def media(self): + if self._media: + return self._media + + handler = self.options.media_handlers.find_by_media_type( + self.content_type, + self.options.default_media_type + ) + + # Consume the stream + raw = self.bounded_stream.read() + + # Deserialize and Return + self._media = handler.deserialize(raw) + return self._media + # ------------------------------------------------------------------------ # Methods # ------------------------------------------------------------------------ def client_accepts(self, media_type): - """Determines whether or not the client accepts a given media type. + """Determine whether or not the client accepts a given media type. Args: media_type (str): An Internet media type to check. Returns: bool: ``True`` if the client has indicated in the Accept header - that it accepts the specified media type. Otherwise, returns - ``False``. + that it accepts the specified media type. Otherwise, returns + ``False``. """ accept = self.accept @@ -633,7 +995,7 @@ return False def client_prefers(self, media_types): - """Returns the client's preferred media type, given several choices. + """Return the client's preferred media type, given several choices. Args: media_types (iterable of str): One or more Internet media types @@ -642,8 +1004,8 @@ Returns: str: The client's preferred media type, based on the Accept - header. Returns ``None`` if the client does not accept any - of the given types. + header. Returns ``None`` if the client does not accept any + of the given types. """ try: @@ -655,18 +1017,23 @@ return (preferred_type if preferred_type else None) - def get_header(self, name, required=False): + def get_header(self, name, required=False, default=None): """Retrieve the raw string value for the given header. Args: name (str): Header name, case-insensitive (e.g., 'Content-Type') - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning gracefully when the header is not found (default ``False``). + default (any): Value to return if the header + is not found (default ``None``). Returns: - str: The value of the specified header if it exists, or ``None`` if - the header is not found and is not required. + str: The value of the specified header if it exists, or + the default value if the header is not found and is not + required. Raises: HTTPBadRequest: The header was not found in the request, but @@ -695,25 +1062,27 @@ pass if not required: - return None + return default - raise HTTPMissingHeader(name) + raise errors.HTTPMissingHeader(name) def get_header_as_datetime(self, header, required=False, obs_date=False): """Return an HTTP header with HTTP-Date values as a datetime. Args: name (str): Header name, case-insensitive (e.g., 'Date') - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning gracefully when the header is not found (default ``False``). - obs_date (bool, optional): Support obs-date formats according to + obs_date (bool): Support obs-date formats according to RFC 7231, e.g.: "Sunday, 06-Nov-94 08:49:37 GMT" (default ``False``). Returns: datetime: The value of the specified header if it exists, - or ``None`` if the header is not found and is not required. + or ``None`` if the header is not found and is not required. Raises: HTTPBadRequest: The header was not found in the request, but @@ -730,41 +1099,42 @@ except ValueError: msg = ('It must be formatted according to RFC 7231, ' 'Section 7.1.1.1') - raise HTTPInvalidHeader(msg, header) + raise errors.HTTPInvalidHeader(msg, header) def get_param(self, name, required=False, store=None, default=None): """Return the raw value of a query string parameter as a string. Note: If an HTML form is POSTed to the API using the - *application/x-www-form-urlencoded* media type, the - parameters from the request body will be merged into - the query string parameters. - - If a key appears more than once in the form data, one of the - values will be returned as a string, but it is undefined which - one. Use `req.get_param_as_list()` to retrieve all the values. + *application/x-www-form-urlencoded* media type, Falcon can + automatically parse the parameters from the request body + and merge them into the query string parameters. To enable + this functionality, set + :py:attr:`~.RequestOptions.auto_parse_form_urlencoded` to + ``True`` via :any:`API.req_options`. Note: Similar to the way multiple keys in form data is handled, if a query parameter is assigned a comma-separated list of - values (e.g., 'foo=a,b,c'), only one of those values will be + values (e.g., ``foo=a,b,c``), only one of those values will be returned, and it is undefined which one. Use - `req.get_param_as_list()` to retrieve all the values. + :meth:`~.get_param_as_list` to retrieve all the values. Args: name (str): Parameter name, case-sensitive (e.g., 'sort'). - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning ``None`` when the parameter is not found (default ``False``). - store (dict, optional): A ``dict``-like object in which to place + store (dict): A ``dict``-like object in which to place the value of the param, but only if the param is present. - default (any, optional): If the param is not found returns the + default (any): If the param is not found returns the given value instead of None Returns: str: The value of the param as a string, or ``None`` if param is - not found and is not required. + not found and is not required. Raises: HTTPBadRequest: A required param is missing from the request. @@ -791,7 +1161,7 @@ if not required: return default - raise HTTPMissingParam(name) + raise errors.HTTPMissingParam(name) def get_param_as_int(self, name, required=False, min=None, max=None, store=None): @@ -799,28 +1169,31 @@ Args: name (str): Parameter name, case-sensitive (e.g., 'limit'). - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning ``None`` when the parameter is not found or is not an integer (default ``False``). - min (int, optional): Set to the minimum value allowed for this + min (int): Set to the minimum value allowed for this param. If the param is found and it is less than min, an ``HTTPError`` is raised. - max (int, optional): Set to the maximum value allowed for this + max (int): Set to the maximum value allowed for this param. If the param is found and its value is greater than max, an ``HTTPError`` is raised. - store (dict, optional): A ``dict``-like object in which to place + store (dict): A ``dict``-like object in which to place the value of the param, but only if the param is found (default ``None``). Returns: int: The value of the param if it is found and can be converted to - an integer. If the param is not found, returns ``None``, unless - `required` is ``True``. + an ``int``. If the param is not found, returns ``None``, unless + `required` is ``True``. Raises HTTPBadRequest: The param was not found in the request, even though - it was required to be there. Also raised if the param's value + it was required to be there, or it was found but could not + be converted to an ``int``. Also raised if the param's value falls outside the given interval, i.e., the value must be in the interval: min <= value <= max to avoid triggering an error. @@ -839,15 +1212,15 @@ val = int(val) except ValueError: msg = 'The value must be an integer.' - raise HTTPInvalidParam(msg, name) + raise errors.HTTPInvalidParam(msg, name) if min is not None and val < min: msg = 'The value must be at least ' + str(min) - raise HTTPInvalidParam(msg, name) + raise errors.HTTPInvalidParam(msg, name) if max is not None and max < val: msg = 'The value may not exceed ' + str(max) - raise HTTPInvalidParam(msg, name) + raise errors.HTTPInvalidParam(msg, name) if store is not None: store[name] = val @@ -857,7 +1230,71 @@ if not required: return None - raise HTTPMissingParam(name) + raise errors.HTTPMissingParam(name) + + def get_param_as_uuid(self, name, required=False, store=None): + """Return the value of a query string parameter as an UUID. + + The value to convert must conform to the standard UUID string + representation per RFC 4122. For example, the following + strings are all valid:: + + # Lowercase + '64be949b-3433-4d36-a4a8-9f19d352fee8' + + # Uppercase + 'BE71ECAA-F719-4D42-87FD-32613C2EEB60' + + # Mixed + '81c8155C-D6de-443B-9495-39Fa8FB239b5' + + Args: + name (str): Parameter name, case-sensitive (e.g., 'id'). + + Keyword Args: + required (bool): Set to ``True`` to raise + ``HTTPBadRequest`` instead of returning ``None`` when the + parameter is not found or is not a UUID (default + ``False``). + store (dict): A ``dict``-like object in which to place + the value of the param, but only if the param is found + (default ``None``). + + Returns: + UUID: The value of the param if it is found and can be converted to + a ``UUID``. If the param is not found, returns ``None``, unless + `required` is ``True``. + + Raises + HTTPBadRequest: The param was not found in the request, even though + it was required to be there, or it was found but could not + be converted to a ``UUID``. + """ + + params = self._params + + # PERF: Use if..in since it is a good all-around performer; we don't + # know how likely params are to be specified by clients. + if name in params: + val = params[name] + if isinstance(val, list): + val = val[-1] + + try: + val = UUID(val) + except ValueError: + msg = 'The value must be a UUID string.' + raise errors.HTTPInvalidParam(msg, name) + + if store is not None: + store[name] = val + + return val + + if not required: + return None + + raise errors.HTTPMissingParam(name) def get_param_as_bool(self, name, required=False, store=None, blank_as_true=False): @@ -865,32 +1302,35 @@ The following boolean strings are supported:: - TRUE_STRINGS = ('true', 'True', 'yes', '1') - FALSE_STRINGS = ('false', 'False', 'no', '0') + TRUE_STRINGS = ('true', 'True', 'yes', '1', 'on') + FALSE_STRINGS = ('false', 'False', 'no', '0', 'off') Args: name (str): Parameter name, case-sensitive (e.g., 'detailed'). - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning ``None`` when the parameter is not found or is not a recognized boolean string (default ``False``). - store (dict, optional): A ``dict``-like object in which to place + store (dict): A ``dict``-like object in which to place the value of the param, but only if the param is found (default ``None``). blank_as_true (bool): If ``True``, an empty string value will be - treated as ``True``. Normally empty strings are ignored; if - you would like to recognize such parameters, you must set the - `keep_blank_qs_values` request option to ``True``. Request - options are set globally for each instance of ``falcon.API`` - through the `req_options` attribute. + treated as ``True`` (default ``False``). Normally empty strings + are ignored; if you would like to recognize such parameters, you + must set the `keep_blank_qs_values` request option to ``True``. + Request options are set globally for each instance of + ``falcon.API`` through the `req_options` attribute. Returns: bool: The value of the param if it is found and can be converted - to a ``bool``. If the param is not found, returns ``None`` - unless required is ``True``. + to a ``bool``. If the param is not found, returns ``None`` + unless required is ``True``. Raises: - HTTPBadRequest: A required param is missing from the request. + HTTPBadRequest: A required param is missing from the request, or + can not be converted to a ``bool``. """ @@ -911,7 +1351,7 @@ val = True else: msg = 'The value of the parameter must be "true" or "false".' - raise HTTPInvalidParam(msg, name) + raise errors.HTTPInvalidParam(msg, name) if store is not None: store[name] = val @@ -921,7 +1361,7 @@ if not required: return None - raise HTTPMissingParam(name) + raise errors.HTTPMissingParam(name) def get_param_as_list(self, name, transform=None, required=False, store=None): @@ -933,31 +1373,32 @@ Args: name (str): Parameter name, case-sensitive (e.g., 'ids'). - transform (callable, optional): An optional transform function + + Keyword Args: + transform (callable): An optional transform function that takes as input each element in the list as a ``str`` and outputs a transformed element for inclusion in the list that will be returned. For example, passing ``int`` will transform list items into numbers. - required (bool, optional): Set to ``True`` to raise - ``HTTPBadRequest`` instead of returning ``None`` when the - parameter is not found (default ``False``). - store (dict, optional): A ``dict``-like object in which to place + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` + instead of returning ``None`` when the parameter is not + found (default ``False``). + store (dict): A ``dict``-like object in which to place the value of the param, but only if the param is found (default ``None``). Returns: list: The value of the param if it is found. Otherwise, returns - ``None`` unless required is True. Empty list elements will be - discarded. For example, the following query strings would - both result in `['1', '3']`:: + ``None`` unless required is True. Empty list elements will be + discarded. For example, the following query strings would + both result in `['1', '3']`:: - things=1,,3 - things=1&things=&things=3 + things=1,,3 + things=1&things=&things=3 Raises: - HTTPBadRequest: A required param is missing from the request. - HTTPInvalidParam: A transform function raised an instance of - ``ValueError``. + HTTPBadRequest: A required param is missing from the request, or + a transform function raised an instance of ``ValueError``. """ @@ -983,7 +1424,7 @@ except ValueError: msg = 'The value is not formatted correctly.' - raise HTTPInvalidParam(msg, name) + raise errors.HTTPInvalidParam(msg, name) if store is not None: store[name] = items @@ -993,7 +1434,51 @@ if not required: return None - raise HTTPMissingParam(name) + raise errors.HTTPMissingParam(name) + + def get_param_as_datetime(self, name, format_string='%Y-%m-%dT%H:%M:%SZ', + required=False, store=None): + """Return the value of a query string parameter as a datetime. + + Args: + name (str): Parameter name, case-sensitive (e.g., 'ids'). + + Keyword Args: + format_string (str): String used to parse the param value + into a ``datetime``. Any format recognized by strptime() is + supported (default ``'%Y-%m-%dT%H:%M:%SZ'``). + required (bool): Set to ``True`` to raise + ``HTTPBadRequest`` instead of returning ``None`` when the + parameter is not found (default ``False``). + store (dict): A ``dict``-like object in which to place + the value of the param, but only if the param is found (default + ``None``). + Returns: + datetime.datetime: The value of the param if it is found and can be + converted to a ``datetime`` according to the supplied format + string. If the param is not found, returns ``None`` unless + required is ``True``. + + Raises: + HTTPBadRequest: A required param is missing from the request, or + the value could not be converted to a ``datetime``. + """ + + param_value = self.get_param(name, required=required) + + if param_value is None: + return None + + try: + date_time = strptime(param_value, format_string) + except ValueError: + msg = 'The date value does not match the required format.' + raise errors.HTTPInvalidParam(msg, name) + + if store is not None: + store[name] = date_time + + return date_time def get_param_as_date(self, name, format_string='%Y-%m-%d', required=False, store=None): @@ -1001,26 +1486,63 @@ Args: name (str): Parameter name, case-sensitive (e.g., 'ids'). - format_string (str): String used to parse the param value into a - date. - Any format recognized by strptime() is supported. - (default ``"%Y-%m-%d"``) - required (bool, optional): Set to ``True`` to raise + + Keyword Args: + format_string (str): String used to parse the param value + into a date. Any format recognized by strptime() is + supported (default ``"%Y-%m-%d"``). + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning ``None`` when the parameter is not found (default ``False``). - store (dict, optional): A ``dict``-like object in which to place + store (dict): A ``dict``-like object in which to place the value of the param, but only if the param is found (default ``None``). Returns: datetime.date: The value of the param if it is found and can be - converted to a ``date`` according to the supplied format - string. If the param is not found, returns ``None`` unless - required is ``True``. + converted to a ``date`` according to the supplied format + string. If the param is not found, returns ``None`` unless + required is ``True``. Raises: - HTTPBadRequest: A required param is missing from the request. - HTTPInvalidParam: A transform function raised an instance of - ``ValueError``. + HTTPBadRequest: A required param is missing from the request, or + the value could not be converted to a ``date``. + """ + + date_time = self.get_param_as_datetime(name, format_string, required) + if date_time: + date = date_time.date() + else: + return None + + if store is not None: + store[name] = date + + return date + + def get_param_as_json(self, name, required=False, store=None): + """Return the decoded JSON value of a query string parameter. + + Given a JSON value, decode it to an appropriate Python type, + (e.g., ``dict``, ``list``, ``str``, ``int``, ``bool``, etc.) + + Args: + name (str): Parameter name, case-sensitive (e.g., 'payload'). + + Keyword Args: + required (bool): Set to ``True`` to raise ``HTTPBadRequest`` + instead of returning ``None`` when the parameter is not + found (default ``False``). + store (dict): A ``dict``-like object in which to place the + value of the param, but only if the param is found + (default ``None``). + + Returns: + dict: The value of the param if it is found. Otherwise, returns + ``None`` unless required is ``True``. + + Raises: + HTTPBadRequest: A required param is missing from the request, or + the value could not be parsed as JSON. """ param_value = self.get_param(name, required=required) @@ -1029,15 +1551,23 @@ return None try: - date = strptime(param_value, format_string).date() + val = json.loads(param_value) except ValueError: - msg = 'The date value does not match the required format' - raise HTTPInvalidParam(msg, name) + msg = 'It could not be parsed as JSON.' + raise errors.HTTPInvalidParam(msg, name) if store is not None: - store[name] = date + store[name] = val - return date + return val + + get_param_as_dict = get_param_as_json + """Deprecated alias of :meth:`~get_param_as_json`. + + Warning: + + This method has been deprecated and will be removed in a future release. + """ def log_error(self, message): """Write an error message to the server's log. @@ -1074,29 +1604,25 @@ # Helpers # ------------------------------------------------------------------------ - def _wrap_stream(self): + def _get_wrapped_wsgi_input(self): try: content_length = self.content_length or 0 # NOTE(kgriffs): This branch is indeed covered in test_wsgi.py # even though coverage isn't able to detect it. - except HTTPInvalidHeader: # pragma: no cover + except errors.HTTPInvalidHeader: # pragma: no cover # NOTE(kgriffs): The content-length header was specified, # but it had an invalid value. Assume no content. content_length = 0 - self.stream = helpers.Body(self.stream, content_length) + return helpers.BoundedStream(self.env['wsgi.input'], content_length) def _parse_form_urlencoded(self): - # NOTE(kgriffs): This assumes self.stream has been patched - # above in the case of wsgiref, so that self.content_length - # is not needed. Normally we just avoid accessing - # self.content_length, because it is a little expensive - # to call. We could cache self.content_length, but the - # overhead to do that won't usually be helpful, since - # content length will only ever be read once per - # request in most cases. - body = self.stream.read() + content_length = self.content_length + if not content_length: + return + + body = self.stream.read(content_length) # NOTE(kgriffs): According to http://goo.gl/6rlcux the # body should be US-ASCII. Enforcing this also helps @@ -1114,67 +1640,93 @@ extra_params = parse_query_string( body, keep_blank_qs_values=self.options.keep_blank_qs_values, + parse_qs_csv=self.options.auto_parse_qs_csv, ) self._params.update(extra_params) - def _parse_rfc_forwarded(self): - """Parse RFC 7239 "Forwarded" header. - - Returns: - list: addresses derived from "for" parameters. - """ - - addr = [] - - for forwarded in self.env['HTTP_FORWARDED'].split(','): - for param in forwarded.split(';'): - # PERF(kgriffs): Partition() is faster than split(). - key, _, val = param.strip().partition('=') - if not val: - # NOTE(kgriffs): The '=' separator was not found or - # it was, but the value was missing. - continue - - if key.lower() != 'for': - # We only want "for" params - continue - - host, _ = parse_host(unquote_string(val)) - addr.append(host) - - return addr - # PERF: To avoid typos and improve storage space and speed over a dict. class RequestOptions(object): - """This class is a container for ``Request`` options. + """Defines a set of configurable request options. + + An instance of this class is exposed via :any:`API.req_options` for + configuring certain :py:class:`~.Request` behaviors. Attributes: - keep_blank_qs_values (bool): Set to ``True`` in order to retain - blank values in query string parameters (default ``False``). + keep_blank_qs_values (bool): Set to ``True`` to keep query string + fields even if they do not have a value (default ``False``). + For comma-separated values, this option also determines + whether or not empty elements in the parsed list are + retained. + auto_parse_form_urlencoded: Set to ``True`` in order to automatically consume the request stream and merge the results into the request's query string params when the request's content type is - *application/x-www-form-urlencoded* (default ``False``). In - this case, the request's content stream will be left at EOF. + *application/x-www-form-urlencoded* (default ``False``). + + Enabling this option makes the form parameters accessible + via :attr:`~.params`, :meth:`~.get_param`, etc. + + Warning: + When this option is enabled, the request's body + stream will be left at EOF. The original data is + not retained by the framework. Note: The character encoding for fields, before percent-encoding non-ASCII bytes, is assumed to be - UTF-8. The special `_charset_` field is ignored if present. + UTF-8. The special `_charset_` field is ignored if + present. Falcon expects form-encoded request bodies to be encoded according to the standard W3C algorithm (see also http://goo.gl/6rlcux). + auto_parse_qs_csv: Set to ``False`` to treat commas in a query + string value as literal characters, rather than as a comma- + separated list (default ``True``). When this option is + enabled, the value will be split on any non-percent-encoded + commas. Disable this option when encoding lists as multiple + occurrences of the same parameter, and when values may be + encoded in alternative formats in which the comma character + is significant. + + strip_url_path_trailing_slash: Set to ``False`` in order to + retain a trailing slash, if present, at the end of the URL + path (default ``True``). When this option is enabled, + the URL path is normalized by stripping the trailing slash + character. This lets the application define a single route + to a resource for a path that may or may not end in a + forward slash. However, this behavior can be problematic in + certain cases, such as when working with authentication + schemes that employ URL-based signatures. + + default_media_type (str): The default media-type to use when + deserializing a response. This value is normally set to the media + type provided when a :class:`falcon.API` is initialized; however, + if created independently, this will default to the + ``DEFAULT_MEDIA_TYPE`` specified by Falcon. + + media_handlers (Handlers): A dict-like object that allows you to + configure the media-types that you would like to handle. + By default, a handler is provided for the ``application/json`` + media type. """ __slots__ = ( 'keep_blank_qs_values', 'auto_parse_form_urlencoded', + 'auto_parse_qs_csv', + 'strip_url_path_trailing_slash', + 'default_media_type', + 'media_handlers', ) def __init__(self): self.keep_blank_qs_values = False self.auto_parse_form_urlencoded = False + self.auto_parse_qs_csv = True + self.strip_url_path_trailing_slash = True + self.default_media_type = DEFAULT_MEDIA_TYPE + self.media_handlers = Handlers() diff -Nru python-falcon-1.0.0/falcon/responders.py python-falcon-1.4.1/falcon/responders.py --- python-falcon-1.0.0/falcon/responders.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/responders.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Default responder implementations.""" + +from functools import partial, update_wrapper + from falcon.errors import HTTPBadRequest +from falcon.errors import HTTPMethodNotAllowed from falcon.errors import HTTPNotFound -from falcon.status_codes import HTTP_204 -from falcon.status_codes import HTTP_405 +from falcon.status_codes import HTTP_200 def path_not_found(req, resp, **kwargs): @@ -28,25 +32,33 @@ raise HTTPBadRequest('Bad request', 'Invalid HTTP method') +def method_not_allowed(allowed_methods, req, resp, **kwargs): + """Raise 405 HTTPMethodNotAllowed error""" + raise HTTPMethodNotAllowed(allowed_methods) + + def create_method_not_allowed(allowed_methods): - """Creates a responder for "405 Method Not Allowed" + """Create a responder for "405 Method Not Allowed" Args: allowed_methods: A list of HTTP methods (uppercase) that should be returned in the Allow header. """ - allowed = ', '.join(allowed_methods) + partial_method_not_allowed = partial(method_not_allowed, allowed_methods) + update_wrapper(partial_method_not_allowed, method_not_allowed) + return partial_method_not_allowed - def method_not_allowed(req, resp, **kwargs): - resp.status = HTTP_405 - resp.set_header('Allow', allowed) - return method_not_allowed +def on_options(allowed, req, resp, **kwargs): + """Default options responder.""" + resp.status = HTTP_200 + resp.set_header('Allow', allowed) + resp.set_header('Content-Length', '0') def create_default_options(allowed_methods): - """Creates a default responder for the OPTIONS method + """Create a default responder for the OPTIONS method Args: allowed_methods: A list of HTTP methods (uppercase) that should be @@ -54,9 +66,6 @@ """ allowed = ', '.join(allowed_methods) - - def on_options(req, resp, **kwargs): - resp.status = HTTP_204 - resp.set_header('Allow', allowed) - - return on_options + partial_on_options = partial(on_options, allowed) + update_wrapper(partial_on_options, on_options) + return partial_on_options diff -Nru python-falcon-1.0.0/falcon/response_helpers.py python-falcon-1.4.1/falcon/response_helpers.py --- python-falcon-1.0.0/falcon/response_helpers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/response_helpers.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,9 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Utilities for the Response class.""" + +import six + def header_property(name, doc, transform=None): - """Creates a header getter/setter. + """Create a header getter/setter. Args: name: Header name, e.g., "Content-Type" @@ -35,10 +39,22 @@ if transform is None: def fset(self, value): - self._headers[normalized_name] = value + if value is None: + try: + del self._headers[normalized_name] + except KeyError: + pass + else: + self._headers[normalized_name] = str(value) else: def fset(self, value): - self._headers[normalized_name] = transform(value) + if value is None: + try: + del self._headers[normalized_name] + except KeyError: + pass + else: + self._headers[normalized_name] = transform(value) def fdel(self): del self._headers[normalized_name] @@ -47,7 +63,7 @@ def format_range(value): - """Formats a range header tuple per the HTTP spec. + """Format a range header tuple per the HTTP spec. Args: value: ``tuple`` passed to `req.range` @@ -57,9 +73,32 @@ # string concatenation, and str.join() in this case. if len(value) == 4: - return '%s %s-%s/%s' % (value[3], value[0], value[1], value[2]) + result = '%s %s-%s/%s' % (value[3], value[0], value[1], value[2]) + else: + result = 'bytes %s-%s/%s' % (value[0], value[1], value[2]) + + if six.PY2: + # NOTE(kgriffs): In case one of the values was a unicode + # string, convert back to str + result = str(result) + + return result + + +def format_content_disposition(value): + """Formats a Content-Disposition header given a filename.""" + + return 'attachment; filename="' + value + '"' + - return 'bytes %s-%s/%s' % (value[0], value[1], value[2]) +if six.PY2: + def format_header_value_list(iterable): + """Join an iterable of strings with commas.""" + return str(', '.join(iterable)) +else: + def format_header_value_list(iterable): + """Join an iterable of strings with commas.""" + return ', '.join(iterable) def is_ascii_encodable(s): diff -Nru python-falcon-1.0.0/falcon/response.py python-falcon-1.4.1/falcon/response.py --- python-falcon-1.0.0/falcon/response.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/response.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,20 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Response class.""" + +import mimetypes + from six import PY2 from six import string_types as STRING_TYPES # NOTE(tbug): In some cases, http_cookies is not a module # but a dict-like structure. This fixes that issue. # See issue https://github.com/falconry/falcon/issues/556 -from six.moves import http_cookies +from six.moves import http_cookies # NOQA: I202 -from falcon.response_helpers import header_property, format_range -from falcon.response_helpers import is_ascii_encodable +from falcon import DEFAULT_MEDIA_TYPE +from falcon.media import Handlers +from falcon.response_helpers import ( + format_content_disposition, + format_header_value_list, + format_range, + header_property, + is_ascii_encodable, +) from falcon.util import dt_to_http, TimezoneGMT from falcon.util.uri import encode as uri_encode from falcon.util.uri import encode_value as uri_encode_value + SimpleCookie = http_cookies.SimpleCookie CookieError = http_cookies.CookieError @@ -38,6 +50,9 @@ Note: `Response` is not meant to be instantiated directly by responders. + Keyword Arguments: + options (dict): Set of global options passed from the API handler. + Attributes: status (str): HTTP status line (e.g., '200 OK'). Falcon requires the full status line, not just the code (e.g., 200). This design @@ -52,10 +67,18 @@ codes. They all start with the ``HTTP_`` prefix, as in: ``falcon.HTTP_204``. - body (str or unicode): String representing response content. If - Unicode, Falcon will encode as UTF-8 in the response. If - data is already a byte string, use the data attribute - instead (it's faster). + media (object): A serializable object supported by the media handlers + configured via :class:`falcon.RequestOptions`. + + See :ref:`media` for more information regarding media handling. + + body (str or unicode): String representing response content. + + If set to a Unicode type (``unicode`` in Python 2, or + ``str`` in Python 3), Falcon will encode the text as UTF-8 + in the response. If the content is already a byte string, + use the :attr:`data` attribute instead (it's faster). + data (bytes): Byte string representing response content. Use this attribute in lieu of `body` when your content is @@ -83,12 +106,29 @@ blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if provided by the WSGI server, in order to efficiently serve file-like objects. - stream_len (int): Expected length of `stream`. If `stream` is set, but `stream_len` is not, Falcon will not supply a Content-Length header to the WSGI server. Consequently, the server may choose to use chunked encoding or one of the other strategies suggested by PEP-3333. + + context (dict): Dictionary to hold any data about the response which is + specific to your app. Falcon itself will not interact with this + attribute after it has been initialized. + context_type (class): Class variable that determines the factory or + type to use for initializing the `context` attribute. By default, + the framework will instantiate standard ``dict`` objects. However, + you may override this behavior by creating a custom child class of + ``falcon.Response``, and then passing that new class to + `falcon.API()` by way of the latter's `response_type` parameter. + + Note: + When overriding `context_type` with a factory function (as + opposed to a class), the function is called like a method of + the current Response instance. Therefore the first argument is + the Response instance itself (self). + + options (dict): Set of global options passed from the API handler. """ __slots__ = ( @@ -98,22 +138,53 @@ '_cookies', 'status', 'stream', - 'stream_len' + 'stream_len', + 'context', + 'options', + '__dict__', ) - def __init__(self): + # Child classes may override this + context_type = dict + + def __init__(self, options=None): self.status = '200 OK' self._headers = {} + self.options = options if options else ResponseOptions() + # NOTE(tbug): will be set to a SimpleCookie object # when cookie is set via set_cookie self._cookies = None + self._media = None self.body = None self.data = None self.stream = None self.stream_len = None + self.context = self.context_type() + + @property + def media(self): + return self._media + + @media.setter + def media(self, obj): + self._media = obj + + if not self.content_type: + self.content_type = self.options.default_media_type + + handler = self.options.media_handlers.find_by_media_type( + self.content_type, + self.options.default_media_type + ) + self.data = handler.serialize(self._media) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.status) + def set_stream(self, stream, stream_len): """Convenience method for setting both `stream` and `stream_len`. @@ -133,7 +204,7 @@ self.stream_len = stream_len def set_cookie(self, name, value, expires=None, max_age=None, - domain=None, path=None, secure=True, http_only=True): + domain=None, path=None, secure=None, http_only=True): """Set a response cookie. Note: @@ -142,34 +213,74 @@ See Also: To learn more about setting cookies, see - :ref:`Setting Cookies `. The parameters listed - below correspond to those defined in `RFC 6265`_. + :ref:`Setting Cookies `. The parameters + listed below correspond to those defined in `RFC 6265`_. Args: - name (str): - Cookie name - value (str): - Cookie value - expires (datetime): Specifies when the cookie should expire. By - default, cookies expire when the user agent exits. - max_age (int): Defines the lifetime of the cookie in seconds. - After the specified number of seconds elapse, the client - should discard the cookie. Coercion to `int` is attempted - if provided with `float` or `str`. - domain (str): Specifies the domain for which the cookie is valid. - An explicitly specified domain must always start with a dot. - A value of 0 means the cookie should be discarded immediately. - path (str): Specifies the subset of URLs to - which this cookie applies. - secure (bool): Direct the client to only return the cookie in - subsequent requests if they are made over HTTPS - (default: ``True``). This prevents attackers from reading - sensitive cookie data. Note that for the `secure` cookie - attribute to be effective, your application will need to - enforce HTTPS. See also: `RFC 6265, Section 4.1.2.5`_. - http_only (bool): Direct the client to only transfer the cookie - with unscripted HTTP requests (default: ``True``). This is - intended to mitigate some forms of cross-site scripting. + name (str): Cookie name + value (str): Cookie value + + Keyword Args: + expires (datetime): Specifies when the cookie should expire. + By default, cookies expire when the user agent exits. + + (See also: RFC 6265, Section 4.1.2.1) + max_age (int): Defines the lifetime of the cookie in + seconds. By default, cookies expire when the user agent + exits. If both `max_age` and `expires` are set, the + latter is ignored by the user agent. + + Note: + Coercion to ``int`` is attempted if provided with + ``float`` or ``str``. + + (See also: RFC 6265, Section 4.1.2.2) + + domain (str): Restricts the cookie to a specific domain and + any subdomains of that domain. By default, the user + agent will return the cookie only to the origin server. + When overriding this default behavior, the specified + domain must include the origin server. Otherwise, the + user agent will reject the cookie. + + (See also: RFC 6265, Section 4.1.2.3) + + path (str): Scopes the cookie to the given path plus any + subdirectories under that path (the "/" character is + interpreted as a directory separator). If the cookie + does not specify a path, the user agent defaults to the + path component of the requested URI. + + Warning: + User agent interfaces do not always isolate + cookies by path, and so this should not be + considered an effective security measure. + + (See also: RFC 6265, Section 4.1.2.4) + + secure (bool): Direct the client to only return the cookie + in subsequent requests if they are made over HTTPS + (default: ``True``). This prevents attackers from + reading sensitive cookie data. + + Note: + The default value for this argument is normally + ``True``, but can be modified by setting + :py:attr:`~.ResponseOptions.secure_cookies_by_default` + via :any:`API.resp_options`. + + Warning: + For the `secure` cookie attribute to be effective, + your application will need to enforce HTTPS. + + (See also: RFC 6265, Section 4.1.2.5) + + http_only (bool): Direct the client to only transfer the + cookie with unscripted HTTP requests + (default: ``True``). This is intended to mitigate some + forms of cross-site scripting. + + (See also: RFC 6265, Section 4.1.2.6) Raises: KeyError: `name` is not a valid cookie name. @@ -178,9 +289,6 @@ .. _RFC 6265: http://tools.ietf.org/html/rfc6265 - .. _RFC 6265, Section 4.1.2.5: - https://tools.ietf.org/html/rfc6265#section-4.1.2.5 - """ if not is_ascii_encodable(name): @@ -188,9 +296,8 @@ if not is_ascii_encodable(value): raise ValueError('"value" is not ascii encodable') - if PY2: - name = str(name) - value = str(value) + name = str(name) + value = str(value) if self._cookies is None: self._cookies = SimpleCookie() @@ -233,8 +340,13 @@ if path: self._cookies[name]['path'] = path - if secure: - self._cookies[name]['secure'] = secure + if secure is None: + is_secure = self.options.secure_cookies_by_default + else: + is_secure = secure + + if is_secure: + self._cookies[name]['secure'] = True if http_only: self._cookies[name]['httponly'] = http_only @@ -242,10 +354,13 @@ def unset_cookie(self, name): """Unset a cookie in the response - Note: - This will clear the contents of the cookie, and instruct - the browser to immediately expire its own copy of the - cookie, if any. + Clears the contents of the cookie, and instructs the user + agent to immediately expire its own copy of the cookie. + + Warning: + In order to successfully remove a cookie, both the + path and the domain must match the values that were + used when the cookie was created. """ if self._cookies is None: self._cookies = SimpleCookie() @@ -284,16 +399,45 @@ Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. - value (str): Value for the header. Must be of type ``str`` or - ``StringType`` and contain only ISO-8859-1 characters. + value (str): Value for the header. Must be convertable to + ``str`` or be of type ``str`` or + ``StringType``. Strings must contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, - although such strings are also limited to ISO-8859-1. + although such strings are also limited to US-ASCII. """ - name, value = self._encode_header(name, value) + + # NOTE(kgriffs): uwsgi fails with a TypeError if any header + # is not a str, so do the conversion here. It's actually + # faster to not do an isinstance check. str() will encode + # to US-ASCII. + name = str(name) + value = str(value) # NOTE(kgriffs): normalize name by lowercasing it self._headers[name.lower()] = value + def delete_header(self, name): + """Delete a header that was previously set for this response. + + If the header was not previously set, nothing is done (no error is + raised). + + Note that calling this method is equivalent to setting the corresponding + header property (when said property is available) to ``None``. For + example:: + + resp.etag = None + + Args: + name (str): Header name (case-insensitive). Must be of type + ``str`` or ``StringType`` and contain only US-ASCII characters. + Under Python 2.x, the ``unicode`` type is also accepted, + although such strings are also limited to US-ASCII. + """ + + # NOTE(kgriffs): normalize name by lowercasing it + self._headers.pop(name.lower(), None) + def append_header(self, name, value): """Set or append a header for this response. @@ -308,13 +452,19 @@ Args: name (str): Header name (case-insensitive). The restrictions noted below for the header's value also apply here. - value (str): Value for the header. Must be of type ``str`` or - ``StringType`` and contain only ISO-8859-1 characters. + value (str): Value for the header. Must be convertable to + ``str`` or be of type ``str`` or + ``StringType``. Strings must contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, - although such strings are also limited to ISO-8859-1. - + although such strings are also limited to US-ASCII. """ - name, value = self._encode_header(name, value) + + # NOTE(kgriffs): uwsgi fails with a TypeError if any header + # is not a str, so do the conversion here. It's actually + # faster to not do an isinstance check. str() will encode + # to US-ASCII. + name = str(name) + value = str(value) name = name.lower() if name in self._headers: @@ -332,9 +482,9 @@ headers (dict or list): A dictionary of header names and values to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and *value* must be of type ``str`` or ``StringType`` and - contain only ISO-8859-1 characters. Under Python 2.x, the + contain only US-ASCII characters. Under Python 2.x, the ``unicode`` type is also accepted, although such strings are - also limited to ISO-8859-1. + also limited to US-ASCII. Note: Falcon can process a list of tuples slightly faster @@ -351,16 +501,22 @@ # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers + for name, value in headers: - name, value = self._encode_header(name, value) + # NOTE(kgriffs): uwsgi fails with a TypeError if any header + # is not a str, so do the conversion here. It's actually + # faster to not do an isinstance check. str() will encode + # to US-ASCII. + name = str(name) + value = str(value) + _headers[name.lower()] = value def add_link(self, target, rel, title=None, title_star=None, anchor=None, hreflang=None, type_hint=None): - """ - Add a link header to the response. + """Add a link header to the response. - See also: https://tools.ietf.org/html/rfc5988 + (See also: RFC 5988, Section 1) Note: Calling this method repeatedly will cause each link to be @@ -375,10 +531,11 @@ link. Will be converted to a URI, if necessary, per RFC 3987, Section 3.1. rel (str): Relation type of the link, such as "next" or - "bookmark". See also http://goo.gl/618GHr for a list - of registered link relation types. + "bookmark". + + (See also: http://www.iana.org/assignments/link-relations/link-relations.xhtml) - Kwargs: + Keyword Args: title (str): Human-readable label for the destination of the link (default ``None``). If the title includes non-ASCII characters, you will need to use `title_star` instead, or @@ -462,6 +619,12 @@ if anchor is not None: value += '; anchor="' + uri_encode(anchor) + '"' + # NOTE(kgriffs): uwsgi fails with a TypeError if any header + # is not a str, so do the conversion here. It's actually + # faster to not do an isinstance check. str() will encode + # to US-ASCII. + value = str(value) + _headers = self._headers if 'link' in _headers: _headers['link'] += ', ' + value @@ -470,18 +633,23 @@ cache_control = header_property( 'Cache-Control', - """Sets the Cache-Control header. + """Set the Cache-Control header. Used to set a list of cache directives to use as the value of the Cache-Control header. The list will be joined with ", " to produce the value for the header. """, - lambda v: ', '.join(v)) + format_header_value_list) content_location = header_property( 'Content-Location', - 'Sets the Content-Location header.', + """Set the Content-Location header. + + This value will be URI encoded per RFC 3986. If the value that is + being set is already URI encoded it should be decoded first or the + header should be set manually using the set_header method. + """, uri_encode) content_range = header_property( @@ -500,21 +668,40 @@ case, raising ``falcon.HTTPRangeNotSatisfiable`` will do the right thing. - See also: http://goo.gl/Iglhp + (See also: RFC 7233, Section 4.2) """, format_range) content_type = header_property( 'Content-Type', - 'Sets the Content-Type header.') + """Sets the Content-Type header. + + The ``falcon`` module provides a number of constants for + common media types, including ``falcon.MEDIA_JSON``, + ``falcon.MEDIA_MSGPACK``, ``falcon.MEDIA_YAML``, + ``falcon.MEDIA_XML``, ``falcon.MEDIA_HTML``, + ``falcon.MEDIA_JS``, ``falcon.MEDIA_TEXT``, + ``falcon.MEDIA_JPEG``, ``falcon.MEDIA_PNG``, + and ``falcon.MEDIA_GIF``. + """) + + downloadable_as = header_property( + 'Content-Disposition', + """Set the Content-Disposition header using the given filename. + + The value will be used for the *filename* directive. For example, + given ``'report.pdf'``, the Content-Disposition header would be set + to: ``'attachment; filename="report.pdf"'``. + """, + format_content_disposition) etag = header_property( 'ETag', - 'Sets the ETag header.') + 'Set the ETag header.') last_modified = header_property( 'Last-Modified', - """Sets the Last-Modified header. Set to a ``datetime`` (UTC) instance. + """Set the Last-Modified header. Set to a ``datetime`` (UTC) instance. Note: Falcon will format the ``datetime`` as an HTTP date string. @@ -523,12 +710,17 @@ location = header_property( 'Location', - 'Sets the Location header.', + """Set the Location header. + + This value will be URI encoded per RFC 3986. If the value that is + being set is already URI encoded it should be decoded first or the + header should be set manually using the set_header method. + """, uri_encode) retry_after = header_property( 'Retry-After', - """Sets the Retry-After header. + """Set the Retry-After header. The expected value is an integral number of seconds to use as the value for the header. The HTTP-date syntax is not supported. @@ -540,29 +732,54 @@ """Value to use for the Vary header. Set this property to an iterable of header names. For a single - asterisk or field value, simply pass a single-element ``list`` or - ``tuple``. + asterisk or field value, simply pass a single-element ``list`` + or ``tuple``. - "Tells downstream proxies how to match future request headers - to decide whether the cached response can be used rather than - requesting a fresh one from the origin server." + The "Vary" header field in a response describes what parts of + a request message, aside from the method, Host header field, + and request target, might influence the origin server's + process for selecting and representing this response. The + value consists of either a single asterisk ("*") or a list of + header field names (case-insensitive). - (Wikipedia) + (See also: RFC 7231, Section 7.1.4) + """, + format_header_value_list) - See also: http://goo.gl/NGHdL + accept_ranges = header_property( + 'Accept-Ranges', + """Set the Accept-Ranges header. + + The Accept-Ranges header field indicates to the client which + range units are supported (e.g. "bytes") for the target + resource. + + If range requests are not supported for the target resource, + the header may be set to "none" to advise the client not to + attempt any such requests. - """, - lambda v: ', '.join(v)) + Note: + "none" is the literal string, not Python's built-in ``None`` + type. - def _encode_header(self, name, value, py2=PY2): - if py2: - if isinstance(name, unicode): - name = name.encode('ISO-8859-1') + """) - if isinstance(value, unicode): - value = value.encode('ISO-8859-1') + def _set_media_type(self, media_type=None): + """Wrapper around set_header to set a content-type. - return name, value + Args: + media_type: Media type to use for the Content-Type + header. + + """ + + # PERF(kgriffs): Using "in" like this is faster than using + # dict.setdefault (tested on py27). + set_content_type = (media_type is not None and + 'content-type' not in self._headers) + + if set_content_type: + self.set_header('content-type', media_type) def _wsgi_headers(self, media_type=None, py2=PY2): """Convert headers into the format expected by WSGI servers. @@ -574,14 +791,7 @@ """ headers = self._headers - - # PERF(kgriffs): Using "in" like this is faster than using - # dict.setdefault (tested on py27). - set_content_type = (media_type is not None and - 'content-type' not in headers) - - if set_content_type: - headers['content-type'] = media_type + self._set_media_type(media_type) if py2: # PERF(kgriffs): Don't create an extra list object if @@ -602,3 +812,47 @@ items += [('set-cookie', c.OutputString()) for c in self._cookies.values()] return items + + +class ResponseOptions(object): + """Defines a set of configurable response options. + + An instance of this class is exposed via :any:`API.resp_options` for + configuring certain :py:class:`~.Response` behaviors. + + Attributes: + secure_cookies_by_default (bool): Set to ``False`` in development + environments to make the `secure` attribute for all cookies + default to ``False``. This can make testing easier by + not requiring HTTPS. Note, however, that this setting can + be overridden via `set_cookie()`'s `secure` kwarg. + + default_media_type (str): The default Internet media type (RFC 2046) to + use when deserializing a response. This value is normally set to the + media type provided when a :class:`falcon.API` is initialized; + however, if created independently, this will default to the + ``DEFAULT_MEDIA_TYPE`` specified by Falcon. + + media_handlers (Handlers): A dict-like object that allows you to + configure the media-types that you would like to handle. + By default, a handler is provided for the ``application/json`` + media type. + + static_media_types (dict): A mapping of dot-prefixed file extensions to + Internet media types (RFC 2046). Defaults to ``mimetypes.types_map`` + after calling ``mimetypes.init()``. + """ + __slots__ = ( + 'secure_cookies_by_default', + 'default_media_type', + 'media_handlers', + 'static_media_types', + ) + + def __init__(self): + self.secure_cookies_by_default = True + self.default_media_type = DEFAULT_MEDIA_TYPE + self.media_handlers = Handlers() + + mimetypes.init() + self.static_media_types = mimetypes.types_map diff -Nru python-falcon-1.0.0/falcon/routing/compiled.py python-falcon-1.4.1/falcon/routing/compiled.py --- python-falcon-1.0.0/falcon/routing/compiled.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/routing/compiled.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,10 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Default routing engine.""" + +import keyword import re +import textwrap + +from six.moves import UserDict +from falcon.routing import converters -TAB_STR = ' ' * 4 + +_TAB_STR = ' ' * 4 +_FIELD_PATTERN = re.compile( + # NOTE(kgriffs): This disallows the use of the '}' character within + # an argstr. However, we don't really have a way of escaping + # curly brackets in URI templates at the moment, so users should + # see this as a similar restriction and so somewhat unsurprising. + # + # We may want to create a contextual parser at some point to + # work around this problem. + '{((?P[^}:]*)((?P:(?P[^}\(]*))(\((?P[^}]*)\))?)?)}' +) +_IDENTIFIER_PATTERN = re.compile('[A-Za-z_][A-Za-z0-9_]*$') class CompiledRouter(object): @@ -31,26 +50,67 @@ processing quite fast. """ + __slots__ = ( + '_ast', + '_converter_map', + '_converters', + '_find', + '_finder_src', + '_options', + '_patterns', + '_return_values', + '_roots', + ) + def __init__(self): + self._ast = None + self._converters = None + self._finder_src = None + + self._options = CompiledRouterOptions() + + # PERF(kgriffs): This is usually an anti-pattern, but we do it + # here to reduce lookup time. + self._converter_map = self._options.converters.data + + self._patterns = None + self._return_values = None self._roots = [] + + # NOTE(kgriffs): Call _compile() last since it depends on + # the variables above. self._find = self._compile() - self._code_lines = None - self._src = None - self._expressions = None - self._return_values = None + + @property + def options(self): + return self._options + + @property + def finder_src(self): + return self._finder_src def add_route(self, uri_template, method_map, resource): - """Adds a route between URI path template and resource.""" - # Can't start with a number, since these eventually get passed as - # args to on_* responders - if re.search('{\d', uri_template): - raise ValueError('Field names may not start with a digit.') + """Adds a route between a URI path template and a resource. - if re.search('\s', uri_template): + Args: + uri_template (str): A URI template to use for the route + method_map (dict): A mapping of HTTP methods (e.g., 'GET', + 'POST') to methods of a resource object. + resource (object): The resource instance to associate with + the URI template. + """ + + # NOTE(kgriffs): Fields may have whitespace in them, so sub + # those before checking the rest of the URI template. + if re.search(r'\s', _FIELD_PATTERN.sub('{FIELD}', uri_template)): raise ValueError('URI templates may not include whitespace.') path = uri_template.strip('/').split('/') + used_names = set() + for segment in path: + self._validate_template_segment(segment, used_names) + def insert(nodes, path_index=0): for node in nodes: segment = path[path_index] @@ -60,15 +120,21 @@ # NOTE(kgriffs): Override previous node node.method_map = method_map node.resource = resource + node.uri_template = uri_template else: insert(node.children, path_index) return if node.conflicts_with(segment): - raise ValueError('The URI template for this route ' - "conflicts with another route's " - 'template.') + msg = textwrap.dedent(""" + The URI template for this route is inconsistent or conflicts with another + route's template. This is usually caused by configuring a field converter + differently for the same field in two different routes, or by using + different field names at the same level in the path (e.g., + '/parents/{id}' and '/parents/{parent_id}/children') + """).strip().replace('\n', ' ') + raise ValueError(msg) # NOTE(richardolsson): If we got this far, the node doesn't already # exist and needs to be created. This builds a new branch of the @@ -78,38 +144,98 @@ if path_index == len(path) - 1: new_node.method_map = method_map new_node.resource = resource + new_node.uri_template = uri_template else: insert(new_node.children, path_index + 1) insert(self._roots) self._find = self._compile() - def find(self, uri): - """Finds resource and method map for a URI, or returns None.""" + def find(self, uri, req=None): + """Search for a route that matches the given partial URI. + + Args: + uri(str): The requested path to route. + + Keyword Args: + req(Request): The Request object that will be passed to + the routed responder. Currently the value of this + argument is ignored by :class:`~.CompiledRouter`. + Routing is based solely on the path. + + Returns: + tuple: A 4-member tuple composed of (resource, method_map, + params, uri_template), or ``None`` if no route matches + the requested path. + """ + path = uri.lstrip('/').split('/') params = {} - node = self._find(path, self._return_values, self._expressions, params) + node = self._find(path, self._return_values, self._patterns, + self._converters, params) if node is not None: - return node.resource, node.method_map, params + return node.resource, node.method_map, params, node.uri_template else: return None - def _compile_tree(self, nodes, indent=1, level=0, fast_return=True): - """Generates Python code for a routing tree or subtree.""" + # ----------------------------------------------------------------- + # Private + # ----------------------------------------------------------------- + + def _validate_template_segment(self, segment, used_names): + """Validates a single path segment of a URI template. + + 1. Ensure field names are valid Python identifiers, since they + will be passed as kwargs to responders. + 2. Check that there are no duplicate names, since that causes + (at least) the following problems: + + a. For simple nodes, values from deeper nodes overwrite + values from more shallow nodes. + b. For complex nodes, re.compile() raises a nasty error + 3. Check that when the converter syntax is used, the named + converter exists. + """ + + for field in _FIELD_PATTERN.finditer(segment): + name = field.group('fname') + + is_identifier = _IDENTIFIER_PATTERN.match(name) + if not is_identifier or name in keyword.kwlist: + msg_template = ('Field names must be valid identifiers ' + '("{0}" is not valid)') + msg = msg_template.format(name) + raise ValueError(msg) + + if name in used_names: + msg_template = ('Field names may not be duplicated ' + '("{0}" was used more than once)') + msg = msg_template.format(name) + raise ValueError(msg) + + used_names.add(name) + + if field.group('cname_sep') == ':': + msg = 'Missing converter for field "{0}"'.format(name) + raise ValueError(msg) + + name = field.group('cname') + if name and name not in self._converter_map: + msg = 'Unknown converter: "{0}"'.format(name) + raise ValueError(msg) - def line(text, indent_offset=0): - pad = TAB_STR * (indent + indent_offset) - self._code_lines.append(pad + text) + def _generate_ast(self, nodes, parent, return_values, patterns, level=0, fast_return=True): + """Generates a coarse AST for the router.""" # NOTE(kgriffs): Base case if not nodes: return - line('if path_len > %d:' % level) - indent += 1 + outer_parent = _CxIfPathLength('>', level) + parent.append_child(outer_parent) + parent = outer_parent - level_indent = indent found_simple = False # NOTE(kgriffs & philiptzou): Sort nodes in this sequence: @@ -138,20 +264,50 @@ # contain anything more than a single literal or variable, # and they need to be checked using a pre-compiled regular # expression. - expression_idx = len(self._expressions) - self._expressions.append(node.var_regex) + pattern_idx = len(patterns) + patterns.append(node.var_pattern) - line('match = expressions[%d].match(path[%d]) # %s' % ( - expression_idx, level, node.var_regex.pattern)) + construct = _CxIfPathSegmentPattern(level, pattern_idx, + node.var_pattern.pattern) + parent.append_child(construct) + parent = construct + + if node.var_converter_map: + parent.append_child(_CxPrefetchGroupsFromPatternMatch()) + parent = self._generate_conversion_ast(parent, node) - line('if match is not None:') - indent += 1 - line('params.update(match.groupdict())') + else: + parent.append_child(_CxSetParamsFromPatternMatch()) else: # NOTE(kgriffs): Simple nodes just capture the entire path # segment as the value for the param. - line('params["%s"] = path[%d]' % (node.var_name, level)) + + if node.var_converter_map: + assert len(node.var_converter_map) == 1 + + parent.append_child(_CxSetFragmentFromPath(level)) + + field_name = node.var_name + __, converter_name, converter_argstr = node.var_converter_map[0] + converter_class = self._converter_map[converter_name] + + converter_obj = self._instantiate_converter( + converter_class, + converter_argstr + ) + converter_idx = len(self._converters) + self._converters.append(converter_obj) + + construct = _CxIfConverterField( + field_name, + converter_idx, + ) + + parent.append_child(construct) + parent = construct + else: + parent.append_child(_CxSetParam(node.var_name, level)) # NOTE(kgriffs): We don't allow multiple simple var nodes # to exist at the same level, e.g.: @@ -165,112 +321,210 @@ else: # NOTE(kgriffs): Not a param, so must match exactly - line('if path[%d] == "%s":' % (level, node.raw_segment)) - indent += 1 + construct = _CxIfPathSegmentLiteral(level, node.raw_segment) + parent.append_child(construct) + parent = construct if node.resource is not None: # NOTE(kgriffs): This is a valid route, so we will want to # return the relevant information. - resource_idx = len(self._return_values) - self._return_values.append(node) + resource_idx = len(return_values) + return_values.append(node) - self._compile_tree(node.children, indent, level + 1, fast_return) + self._generate_ast( + node.children, + parent, + return_values, + patterns, + level + 1, + fast_return + ) if node.resource is None: if fast_return: - line('return None') + parent.append_child(_CxReturnNone()) else: # NOTE(kgriffs): Make sure that we have consumed all of # the segments for the requested route; otherwise we could # mistakenly match "/foo/23/bar" against "/foo/{id}". - line('if path_len == %d:' % (level + 1)) - line('return return_values[%d]' % resource_idx, 1) + construct = _CxIfPathLength('==', level + 1) + construct.append_child(_CxReturnValue(resource_idx)) + parent.append_child(construct) if fast_return: - line('return None') + parent.append_child(_CxReturnNone()) - indent = level_indent + parent = outer_parent if not found_simple and fast_return: - line('return None') + parent.append_child(_CxReturnNone()) + + def _generate_conversion_ast(self, parent, node): + # NOTE(kgriffs): Unroll the converter loop into + # a series of nested "if" constructs. + for field_name, converter_name, converter_argstr in node.var_converter_map: + converter_class = self._converter_map[converter_name] + + converter_obj = self._instantiate_converter( + converter_class, + converter_argstr + ) + converter_idx = len(self._converters) + self._converters.append(converter_obj) + + parent.append_child(_CxSetFragmentFromField(field_name)) + + construct = _CxIfConverterField( + field_name, + converter_idx, + ) + + parent.append_child(construct) + parent = construct + + # NOTE(kgriffs): Add remaining fields that were not + # converted, if any. + if node.num_fields > len(node.var_converter_map): + parent.append_child(_CxSetParamsFromPatternMatchPrefetched()) + + return parent def _compile(self): - """Generates Python code for entire routing tree. + """Generates Python code for the entire routing tree. - The generated code is compiled and the resulting Python method is - returned. + The generated code is compiled and the resulting Python method + is returned. """ - self._return_values = [] - self._expressions = [] - self._code_lines = [ - 'def find(path, return_values, expressions, params):', - TAB_STR + 'path_len = len(path)', + + src_lines = [ + 'def find(path, return_values, patterns, converters, params):', + _TAB_STR + 'path_len = len(path)', ] - self._compile_tree(self._roots) + self._return_values = [] + self._patterns = [] + self._converters = [] + + self._ast = _CxParent() + self._generate_ast( + self._roots, + self._ast, + self._return_values, + self._patterns + ) - self._code_lines.append( + src_lines.append(self._ast.src(0)) + + src_lines.append( # PERF(kgriffs): Explicit return of None is faster than implicit - TAB_STR + 'return None' + _TAB_STR + 'return None' ) - self._src = '\n'.join(self._code_lines) + self._finder_src = '\n'.join(src_lines) scope = {} - exec(compile(self._src, '', 'exec'), scope) + exec(compile(self._finder_src, '', 'exec'), scope) return scope['find'] + def _instantiate_converter(self, klass, argstr=None): + if argstr is None: + return klass() + + # NOTE(kgriffs): Don't try this at home. ;) + src = '{0}({1})'.format(klass.__name__, argstr) + return eval(src, {klass.__name__: klass}) + class CompiledRouterNode(object): """Represents a single URI segment in a URI.""" - _regex_vars = re.compile('{([-_a-zA-Z0-9]+)}') - - def __init__(self, raw_segment, method_map=None, resource=None): + def __init__(self, raw_segment, + method_map=None, resource=None, uri_template=None): self.children = [] self.raw_segment = raw_segment self.method_map = method_map self.resource = resource + self.uri_template = uri_template self.is_var = False self.is_complex = False + self.num_fields = 0 + + # TODO(kgriffs): Rename these since the docs talk about "fields" + # or "field expressions", not "vars" or "variables". self.var_name = None + self.var_pattern = None + self.var_converter_map = [] - seg = raw_segment.replace('.', '\\.') + # NOTE(kgriffs): CompiledRouter.add_route validates field names, + # so here we can just assume they are OK and use the simple + # _FIELD_PATTERN to match them. + matches = list(_FIELD_PATTERN.finditer(raw_segment)) - matches = list(self._regex_vars.finditer(seg)) - if matches: + if not matches: + self.is_var = False + else: self.is_var = True - # NOTE(richardolsson): if there is a single variable and it spans - # the entire segment, the segment is uncomplex and the variable - # name is simply the string contained within curly braces. - if len(matches) == 1 and matches[0].span() == (0, len(seg)): + self.num_fields = len(matches) + + for field in matches: + # NOTE(kgriffs): We already validated the field + # expression to disallow blank converter names, or names + # that don't match a known converter, so if a name is + # given, we can just go ahead and use it. + if field.group('cname'): + self.var_converter_map.append( + ( + field.group('fname'), + field.group('cname'), + field.group('argstr'), + ) + ) + + if matches[0].span() == (0, len(raw_segment)): + # NOTE(kgriffs): Single field, spans entire segment + assert len(matches) == 1 + + # TODO(kgriffs): It is not "complex" because it only + # contains a single field. Rename this variable to make + # it more descriptive. self.is_complex = False - self.var_name = raw_segment[1:-1] + + field = matches[0] + self.var_name = field.group('fname') + else: - # NOTE(richardolsson): Complex segments need to be converted - # into regular expressions will be used to match and extract - # variable values. The regular expressions contain both - # literal spans and named group expressions for the variables. + # NOTE(richardolsson): Complex segments need to be + # converted into regular expressions in order to match + # and extract variable values. The regular expressions + # contain both literal spans and named group expressions + # for the variables. + + # NOTE(kgriffs): Don't use re.escape() since we do not + # want to escape '{' or '}', and we don't want to + # introduce any unexpected side-effects by escaping + # non-ASCII characters (it is probably safe, but let's + # not take that chance in a minor point release). + # + # NOTE(kgriffs): The substitution template parser in the + # re library does not look ahead when collapsing '\\': + # therefore in the case of r'\\g<0>' the first r'\\' + # would be consumed and collapsed to r'\', and then the + # parser would examine 'g<0>' and not realize it is a + # group-escape sequence. So we add an extra backslash to + # trick the parser into doing the right thing. + escaped_segment = re.sub(r'[\.\(\)\[\]\?\$\*\+\^\|]', r'\\\g<0>', raw_segment) + + pattern_text = _FIELD_PATTERN.sub(r'(?P<\2>.+)', escaped_segment) + pattern_text = '^' + pattern_text + '$' + self.is_complex = True - seg_fields = [] - prev_end_idx = 0 - for match in matches: - var_start_idx, var_end_idx = match.span() - seg_fields.append(seg[prev_end_idx:var_start_idx]) - - var_name = match.groups()[0].replace('-', '_') - seg_fields.append('(?P<%s>[^/]+)' % var_name) - - prev_end_idx = var_end_idx - - seg_fields.append(seg[prev_end_idx:]) - seg_pattern = ''.join(seg_fields) - self.var_regex = re.compile(seg_pattern) - else: - self.is_var = False + self.var_pattern = re.compile(pattern_text) + + if self.is_complex: + assert self.is_var def matches(self, segment): """Returns True if this node matches the supplied template segment.""" @@ -292,7 +546,7 @@ # simple, complex ==> False # simple, string ==> False # complex, simple ==> False - # complex, complex ==> (Depend) + # complex, complex ==> (Maybe) # complex, string ==> False # string, simple ==> False # string, complex ==> False @@ -321,8 +575,8 @@ # if self.is_complex: if other.is_complex: - return (self._regex_vars.sub('v', self.raw_segment) == - self._regex_vars.sub('v', segment)) + return (_FIELD_PATTERN.sub('v', self.raw_segment) == + _FIELD_PATTERN.sub('v', segment)) return False else: @@ -331,3 +585,259 @@ # NOTE(kgriffs): If self is a static string match, then all the cases # for other are False, so no need to check. return False + + +class ConverterDict(UserDict): + """A dict-like class for storing field converters.""" + + def update(self, other): + try: + # NOTE(kgriffs): If it is a mapping type, it should + # implement keys(). + names = other.keys() + except AttributeError: + # NOTE(kgriffs): Not a mapping type, so assume it is an + # iterable of 2-item iterables. But we need to make it + # re-iterable if it is a generator, for when we pass + # it on to the parent's update(). + other = list(other) + names = [n for n, __ in other] + + for n in names: + self._validate(n) + + UserDict.update(self, other) + + def __setitem__(self, name, converter): + self._validate(name) + UserDict.__setitem__(self, name, converter) + + def _validate(self, name): + if not _IDENTIFIER_PATTERN.match(name): + raise ValueError( + 'Invalid converter name. Names may not be blank, and may ' + 'only use ASCII letters, digits, and underscores. Names' + 'must begin with a letter or underscore.' + ) + + +class CompiledRouterOptions(object): + """Defines a set of configurable router options. + + An instance of this class is exposed via :any:`API.router_options` + for configuring certain :py:class:`~.CompiledRouter` behaviors. + + Attributes: + converters: Represents the collection of named + converters that may be referenced in URI template field + expressions. Adding additional converters is simply a + matter of mapping an identifier to a converter class:: + + api.router_options.converters['mc'] = MyConverter + + The identifier can then be used to employ the converter + within a URI template:: + + api.add_route('/{some_field:mc}', some_resource) + + Converter names may only contain ASCII letters, digits, + and underscores, and must start with either a letter or + an underscore. + + Warning: + + Converter instances are shared between requests. + Therefore, in threaded deployments, care must be taken + to implement custom converters in a thread-safe + manner. + + (See also: :ref:`Field Converters `) + """ + + __slots__ = ('converters',) + + def __init__(self): + self.converters = ConverterDict( + (name, converter) for name, converter in converters.BUILTIN + ) + + +# -------------------------------------------------------------------- +# AST Constructs +# +# NOTE(kgriffs): These constructs are used to create a very coarse +# AST that can then be used to generate Python source code for the +# router. Using an AST like this makes it easier to reason about +# the compilation process, and affords syntactical transformations +# that would otherwise be at best confusing and at worst extremely +# tedious and error-prone if they were to be attempted directly +# against the Python source code. +# -------------------------------------------------------------------- + + +class _CxParent(object): + def __init__(self): + self._children = [] + + def append_child(self, construct): + self._children.append(construct) + + def src(self, indentation): + return self._children_src(indentation + 1) + + def _children_src(self, indentation): + src_lines = [ + child.src(indentation) + for child in self._children + ] + + return '\n'.join(src_lines) + + +class _CxIfPathLength(_CxParent): + def __init__(self, comparison, length): + super(_CxIfPathLength, self).__init__() + self._comparison = comparison + self._length = length + + def src(self, indentation): + template = '{0}if path_len {1} {2}:\n{3}' + return template.format( + _TAB_STR * indentation, + self._comparison, + self._length, + self._children_src(indentation + 1) + ) + + +class _CxIfPathSegmentLiteral(_CxParent): + def __init__(self, segment_idx, literal): + super(_CxIfPathSegmentLiteral, self).__init__() + self._segment_idx = segment_idx + self._literal = literal + + def src(self, indentation): + template = "{0}if path[{1}] == '{2}':\n{3}" + return template.format( + _TAB_STR * indentation, + self._segment_idx, + self._literal, + self._children_src(indentation + 1) + ) + + +class _CxIfPathSegmentPattern(_CxParent): + def __init__(self, segment_idx, pattern_idx, pattern_text): + super(_CxIfPathSegmentPattern, self).__init__() + self._segment_idx = segment_idx + self._pattern_idx = pattern_idx + self._pattern_text = pattern_text + + def src(self, indentation): + lines = [ + '{0}match = patterns[{1}].match(path[{2}]) # {3}'.format( + _TAB_STR * indentation, + self._pattern_idx, + self._segment_idx, + self._pattern_text, + ), + '{0}if match is not None:'.format(_TAB_STR * indentation), + self._children_src(indentation + 1), + ] + + return '\n'.join(lines) + + +class _CxIfConverterField(_CxParent): + def __init__(self, field_name, converter_idx): + super(_CxIfConverterField, self).__init__() + self._field_name = field_name + self._converter_idx = converter_idx + + def src(self, indentation): + lines = [ + '{0}field_value = converters[{1}].convert(fragment)'.format( + _TAB_STR * indentation, + self._converter_idx, + ), + '{0}if field_value is not None:'.format(_TAB_STR * indentation), + "{0}params['{1}'] = field_value".format( + _TAB_STR * (indentation + 1), + self._field_name, + ), + self._children_src(indentation + 1), + ] + + return '\n'.join(lines) + + +class _CxSetFragmentFromField(object): + def __init__(self, field_name): + self._field_name = field_name + + def src(self, indentation): + return "{0}fragment = groups.pop('{1}')".format( + _TAB_STR * indentation, + self._field_name, + ) + + +class _CxSetFragmentFromPath(object): + def __init__(self, segment_idx): + self._segment_idx = segment_idx + + def src(self, indentation): + return '{0}fragment = path[{1}]'.format( + _TAB_STR * indentation, + self._segment_idx, + ) + + +class _CxSetParamsFromPatternMatch(object): + def src(self, indentation): + return '{0}params.update(match.groupdict())'.format( + _TAB_STR * indentation + ) + + +class _CxSetParamsFromPatternMatchPrefetched(object): + def src(self, indentation): + return '{0}params.update(groups)'.format( + _TAB_STR * indentation + ) + + +class _CxPrefetchGroupsFromPatternMatch(object): + def src(self, indentation): + return '{0}groups = match.groupdict()'.format( + _TAB_STR * indentation + ) + + +class _CxReturnNone(object): + def src(self, indentation): + return '{0}return None'.format(_TAB_STR * indentation) + + +class _CxReturnValue(object): + def __init__(self, value_idx): + self._value_idx = value_idx + + def src(self, indentation): + return '{0}return return_values[{1}]'.format( + _TAB_STR * indentation, + self._value_idx + ) + + +class _CxSetParam(object): + def __init__(self, param_name, segment_idx): + self._param_name = param_name + self._segment_idx = segment_idx + + def src(self, indentation): + return "{0}params['{1}'] = path[{2}]".format( + _TAB_STR * indentation, + self._param_name, + self._segment_idx, + ) diff -Nru python-falcon-1.0.0/falcon/routing/converters.py python-falcon-1.4.1/falcon/routing/converters.py --- python-falcon-1.0.0/falcon/routing/converters.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/routing/converters.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,143 @@ +# Copyright 2017 by Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +from datetime import datetime +import uuid + +import six + + +__all__ = ( + 'BaseConverter', + 'IntConverter', + 'DateTimeConverter', + 'UUIDConverter', +) + + +# PERF(kgriffs): Avoid an extra namespace lookup when using this function +strptime = datetime.strptime + + +@six.add_metaclass(abc.ABCMeta) +class BaseConverter(object): + """Abstract base class for URI template field converters.""" + + @abc.abstractmethod # pragma: no cover + def convert(self, value): + """Convert a URI template field value to another format or type. + + Args: + value (str): Original string to convert. + + Returns: + object: Converted field value, or ``None`` if the field + can not be converted. + """ + + +class IntConverter(BaseConverter): + """Converts a field value to an int. + + Identifier: `int` + + Keyword Args: + num_digits (int): Require the value to have the given + number of digits. + min (int): Reject the value if it is less than this number. + max (int): Reject the value if it is greater than this number. + """ + + __slots__ = ('_num_digits', '_min', '_max') + + def __init__(self, num_digits=None, min=None, max=None): + if num_digits is not None and num_digits < 1: + raise ValueError('num_digits must be at least 1') + + self._num_digits = num_digits + self._min = min + self._max = max + + def convert(self, value): + if self._num_digits is not None and len(value) != self._num_digits: + return None + + # NOTE(kgriffs): int() will accept numbers with preceding or + # trailing whitespace, so we need to do our own check. Using + # strip() is faster than either a regex or a series of or'd + # membership checks via "in", esp. as the length of contiguous + # numbers in the value grows. + if value.strip() != value: + return None + + try: + value = int(value) + except ValueError: + return None + + if self._min is not None and value < self._min: + return None + + if self._max is not None and value > self._max: + return None + + return value + + +class DateTimeConverter(BaseConverter): + """Converts a field value to a datetime. + + Identifier: `dt` + + Keyword Args: + format_string (str): String used to parse the field value + into a datetime. Any format recognized by strptime() is + supported (default ``'%Y-%m-%dT%H:%M:%SZ'``). + """ + + __slots__ = ('_format_string',) + + def __init__(self, format_string='%Y-%m-%dT%H:%M:%SZ'): + self._format_string = format_string + + def convert(self, value): + try: + return strptime(value, self._format_string) + except ValueError: + return None + + +class UUIDConverter(BaseConverter): + """Converts a field value to a uuid.UUID. + + Identifier: `uuid` + + In order to be converted, the field value must consist of a + string of 32 hexadecimal digits, as defined in RFC 4122, Section 3. + Note, however, that hyphens and the URN prefix are optional. + """ + + def convert(self, value): + try: + return uuid.UUID(value) + except ValueError: + return None + + +BUILTIN = ( + ('int', IntConverter), + ('dt', DateTimeConverter), + ('uuid', UUIDConverter), +) diff -Nru python-falcon-1.0.0/falcon/routing/__init__.py python-falcon-1.4.1/falcon/routing/__init__.py --- python-falcon-1.0.0/falcon/routing/__init__.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/routing/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,9 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -from falcon.routing.compiled import CompiledRouter +"""Default router and utility functions. + +This package implements Falcon's default routing engine, field converter +classes, and utility functions to aid in the implementation of custom +routers. +""" + +from falcon.routing.compiled import CompiledRouter, CompiledRouterOptions # NOQA +from falcon.routing.static import StaticRoute # NOQA from falcon.routing.util import create_http_method_map # NOQA +from falcon.routing.util import map_http_methods # NOQA +from falcon.routing.util import set_default_responders # NOQA from falcon.routing.util import compile_uri_template # NOQA +from falcon.routing.converters import * # NOQA DefaultRouter = CompiledRouter diff -Nru python-falcon-1.0.0/falcon/routing/static.py python-falcon-1.4.1/falcon/routing/static.py --- python-falcon-1.0.0/falcon/routing/static.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/routing/static.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,98 @@ +import io +import os +import re + +import falcon + + +class StaticRoute(object): + """Represents a static route. + + Args: + prefix (str): The path prefix to match for this route. If the + path in the requested URI starts with this string, the remainder + of the path will be appended to the source directory to + determine the file to serve. This is done in a secure manner + to prevent an attacker from requesting a file outside the + specified directory. + + Note that static routes are matched in LIFO order, and are only + attempted after checking dynamic routes and sinks. + + directory (str): The source directory from which to serve files. Must + be an absolute path. + downloadable (bool): Set to ``True`` to include a + Content-Disposition header in the response. The "filename" + directive is simply set to the name of the requested file. + """ + + # NOTE(kgriffs): Don't allow control characters and reserved chars + _DISALLOWED_CHARS_PATTERN = re.compile('[\x00-\x1f\x80-\x9f~?<>:*|\'"]') + + # NOTE(kgriffs): If somehow an executable code exploit is triggerable, this + # minimizes how much can be included in the payload. + _MAX_NON_PREFIXED_LEN = 512 + + def __init__(self, prefix, directory, downloadable=False): + if not prefix.startswith('/'): + raise ValueError("prefix must start with '/'") + + if not os.path.isabs(directory): + raise ValueError('directory must be an absolute path') + + # NOTE(kgriffs): Ensure it ends with a path separator to ensure + # we only match on the complete segment. Don't raise an error + # because most people won't expect to have to append a slash. + if not prefix.endswith('/'): + prefix += '/' + + self._prefix = prefix + self._directory = directory + self._downloadable = downloadable + + def match(self, path): + """Check whether the given path matches this route.""" + return path.startswith(self._prefix) + + def __call__(self, req, resp): + """Resource responder for this route.""" + + without_prefix = req.path[len(self._prefix):] + + # NOTE(kgriffs): Check surrounding whitespace and strip trailing + # periods, which are illegal on windows + if (not without_prefix or + without_prefix.strip().rstrip('.') != without_prefix or + self._DISALLOWED_CHARS_PATTERN.search(without_prefix) or + '\\' in without_prefix or + '//' in without_prefix or + len(without_prefix) > self._MAX_NON_PREFIXED_LEN): + + raise falcon.HTTPNotFound() + + normalized = os.path.normpath(without_prefix) + + if normalized.startswith('../') or normalized.startswith('/'): + raise falcon.HTTPNotFound() + + file_path = os.path.join(self._directory, normalized) + + # NOTE(kgriffs): Final sanity-check just to be safe. This check + # should never succeed, but this should guard against us having + # overlooked something. + if '..' in file_path or not file_path.startswith(self._directory): + raise falcon.HTTPNotFound() # pragma: nocover + + try: + resp.stream = io.open(file_path, 'rb') + except IOError: + raise falcon.HTTPNotFound() + + suffix = os.path.splitext(file_path)[1] + resp.content_type = resp.options.static_media_types.get( + suffix, + 'application/octet-stream' + ) + + if self._downloadable: + resp.downloadable_as = os.path.basename(file_path) diff -Nru python-falcon-1.0.0/falcon/routing/util.py python-falcon-1.4.1/falcon/routing/util.py --- python-falcon-1.0.0/falcon/routing/util.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/routing/util.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Routing utilities.""" + import re import six -from falcon import HTTP_METHODS, responders +from falcon import COMBINED_METHODS, responders # NOTE(kgriffs): Published method; take care to avoid breaking changes. @@ -79,24 +81,27 @@ return fields, re.compile(pattern, re.IGNORECASE) -def create_http_method_map(resource): +def create_http_method_map(resource): # pragma: nocover """Maps HTTP methods (e.g., 'GET', 'POST') to methods of a resource object. + Warning: + This method is deprecated and will be removed in a future release. + Please use :py:meth:`~falcon.routing.map_http_methods` and + :py:meth:`~falcon.routing.map_http_methods` instead. + Args: resource: An object with *responder* methods, following the naming convention *on_\**, that correspond to each method the resource supports. For example, if a resource supports GET and POST, it should define ``on_get(self, req, resp)`` and ``on_post(self, req, resp)``. - Returns: dict: A mapping of HTTP methods to responders. - """ method_map = {} - for method in HTTP_METHODS: + for method in COMBINED_METHODS: try: responder = getattr(resource, 'on_' + method.lower()) except AttributeError: @@ -118,8 +123,63 @@ na_responder = responders.create_method_not_allowed(allowed_methods) - for method in HTTP_METHODS: + for method in COMBINED_METHODS: if method not in allowed_methods: method_map[method] = na_responder return method_map + + +def map_http_methods(resource): + """Maps HTTP methods (e.g., 'GET', 'POST') to methods of a resource object. + + Args: + resource: An object with *responder* methods, following the naming + convention *on_\**, that correspond to each method the resource + supports. For example, if a resource supports GET and POST, it + should define ``on_get(self, req, resp)`` and + ``on_post(self, req, resp)``. + + Returns: + dict: A mapping of HTTP methods to explicitly defined resource responders. + + """ + + method_map = {} + + for method in COMBINED_METHODS: + try: + responder = getattr(resource, 'on_' + method.lower()) + except AttributeError: + # resource does not implement this method + pass + else: + # Usually expect a method, but any callable will do + if callable(responder): + method_map[method] = responder + + return method_map + + +def set_default_responders(method_map): + """Maps HTTP methods not explicitly defined on a resource to default responders. + + Args: + method_map: A dict with HTTP methods mapped to responders explicitly + defined in a resource. + """ + + # Attach a resource for unsupported HTTP methods + allowed_methods = sorted(list(method_map.keys())) + + if 'OPTIONS' not in method_map: + # OPTIONS itself is intentionally excluded from the Allow header + opt_responder = responders.create_default_options(allowed_methods) + method_map['OPTIONS'] = opt_responder + allowed_methods.append('OPTIONS') + + na_responder = responders.create_method_not_allowed(allowed_methods) + + for method in COMBINED_METHODS: + if method not in allowed_methods: + method_map[method] = na_responder diff -Nru python-falcon-1.0.0/falcon/status_codes.py python-falcon-1.4.1/falcon/status_codes.py --- python-falcon-1.0.0/falcon/status_codes.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/status_codes.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,11 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""HTTP status line constants.""" HTTP_100 = '100 Continue' HTTP_CONTINUE = HTTP_100 HTTP_101 = '101 Switching Protocols' HTTP_SWITCHING_PROTOCOLS = HTTP_101 +HTTP_102 = '102 Processing' +HTTP_PROCESSING = HTTP_102 HTTP_200 = '200 OK' HTTP_OK = HTTP_200 @@ -32,6 +35,10 @@ HTTP_RESET_CONTENT = HTTP_205 HTTP_206 = '206 Partial Content' HTTP_PARTIAL_CONTENT = HTTP_206 +HTTP_207 = '207 Multi-Status' +HTTP_MULTI_STATUS = HTTP_207 +HTTP_208 = '208 Already Reported' +HTTP_ALREADY_REPORTED = HTTP_208 HTTP_226 = '226 IM Used' HTTP_IM_USED = HTTP_226 @@ -92,6 +99,10 @@ HTTP_IM_A_TEAPOT = HTTP_418 HTTP_422 = '422 Unprocessable Entity' HTTP_UNPROCESSABLE_ENTITY = HTTP_422 +HTTP_423 = '423 Locked' +HTTP_LOCKED = HTTP_423 +HTTP_424 = '424 Failed Dependency' +HTTP_FAILED_DEPENDENCY = HTTP_424 HTTP_426 = '426 Upgrade Required' HTTP_UPGRADE_REQUIRED = HTTP_426 HTTP_428 = '428 Precondition Required' @@ -111,10 +122,14 @@ HTTP_BAD_GATEWAY = HTTP_502 HTTP_503 = '503 Service Unavailable' HTTP_SERVICE_UNAVAILABLE = HTTP_503 -HTTP_504 = '504 Gateway Time-out' +HTTP_504 = '504 Gateway Timeout' HTTP_GATEWAY_TIMEOUT = HTTP_504 -HTTP_505 = '505 HTTP Version not supported' +HTTP_505 = '505 HTTP Version Not Supported' HTTP_HTTP_VERSION_NOT_SUPPORTED = HTTP_505 +HTTP_507 = '507 Insufficient Storage' +HTTP_INSUFFICIENT_STORAGE = HTTP_507 +HTTP_508 = '508 Loop Detected' +HTTP_LOOP_DETECTED = HTTP_508 HTTP_511 = '511 Network Authentication Required' HTTP_NETWORK_AUTHENTICATION_REQUIRED = HTTP_511 diff -Nru python-falcon-1.0.0/falcon/testing/base.py python-falcon-1.4.1/falcon/testing/base.py --- python-falcon-1.0.0/falcon/testing/base.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/base.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Test case base class (deprecated).""" + import itertools try: @@ -21,11 +23,14 @@ import falcon import falcon.request -from falcon.testing.srmock import StartResponseMock from falcon.testing.helpers import create_environ +from falcon.testing.srmock import StartResponseMock -class TestBase(unittest.TestCase): +# NOTE(kgriffs): Since this class is deprecated and we will be using it +# less and less for Falcon's own tests, coverage may be reduced, hence +# the pragma to ignore coverage errors from now on. +class TestBase(unittest.TestCase): # pragma nocover """Extends :py:mod:`unittest` to support WSGI functional testing. Warning: @@ -93,7 +98,9 @@ Args: path (str): The path to request. - decode (str, optional): If this is set to a character encoding, + + Keyword Arguments: + decode (str): If this is set to a character encoding, such as 'utf-8', `simulate_request` will assume the response is a single byte string, and will decode it as the result of the request, rather than simply returning the diff -Nru python-falcon-1.0.0/falcon/testing/client.py python-falcon-1.4.1/falcon/testing/client.py --- python-falcon-1.0.0/falcon/testing/client.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/client.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,702 @@ +# Copyright 2016 by Rackspace Hosting, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""WSGI test client utilities. + +This package includes utilities for simulating HTTP requests against a +WSGI callable, without having to stand up a WSGI server. +""" + +import platform +import re +import wsgiref.validate + +from six.moves import http_cookies + +from falcon.constants import MEDIA_JSON +from falcon.testing import helpers +from falcon.testing.srmock import StartResponseMock +from falcon.util import CaseInsensitiveDict, http_date_to_dt, to_query_str +from falcon.util import json as util_json + +_PYVER = platform.python_version_tuple()[:2] +_PY26 = _PYVER == ('2', '6') +_PY27 = _PYVER == ('2', '7') +_JYTHON = platform.python_implementation() == 'Jython' + + +class Result(object): + """Encapsulates the result of a simulated WSGI request. + + Args: + iterable (iterable): An iterable that yields zero or more + bytestrings, per PEP-3333 + status (str): An HTTP status string, including status code and + reason string + headers (list): A list of (header_name, header_value) tuples, + per PEP-3333 + + Attributes: + status (str): HTTP status string given in the response + status_code (int): The code portion of the HTTP status string + headers (CaseInsensitiveDict): A case-insensitive dictionary + containing all the headers in the response, except for + cookies, which may be accessed via the `cookies` + attribute. + + Note: + + Multiple instances of a header in the response are + currently not supported; it is unspecified which value + will "win" and be represented in `headers`. + + cookies (dict): A dictionary of + :py:class:`falcon.testing.Cookie` values parsed from the + response, by name. + encoding (str): Text encoding of the response body, or ``None`` + if the encoding can not be determined. + content (bytes): Raw response body, or ``bytes`` if the + response body was empty. + text (str): Decoded response body of type ``unicode`` + under Python 2.6 and 2.7, and of type ``str`` otherwise. + If the content type does not specify an encoding, UTF-8 is + assumed. + json (dict): Deserialized JSON body. Raises an error if + the body is empty or not JSON. + """ + + def __init__(self, iterable, status, headers): + self._text = None + + self._content = b''.join(iterable) + if hasattr(iterable, 'close'): + iterable.close() + + self._status = status + self._status_code = int(status[:3]) + self._headers = CaseInsensitiveDict(headers) + + cookies = http_cookies.SimpleCookie() + for name, value in headers: + if name.lower() == 'set-cookie': + cookies.load(value) + + if _PY26 or (_PY27 and _JYTHON): + match = re.match(r'\s*([^=;,]+)=', value) + assert match + + cookie_name = match.group(1) + + # NOTE(kgriffs): py26/Jython has a bug that causes + # SimpleCookie to incorrectly parse the "expires" + # attribute, so we have to do it ourselves. This + # algorithm is obviously very naive, but it should + # work well enough until we stop supporting + # 2.6, at which time we can remove this code. + match = re.search('expires=([^;]+)', value) + if match: + cookies[cookie_name]['expires'] = match.group(1) + + # NOTE(kgriffs): py26/Jython's SimpleCookie won't + # parse the "httponly" and "secure" attributes, so + # we have to do it ourselves. + if 'httponly' in value: + cookies[cookie_name]['httponly'] = True + + if 'secure' in value: + cookies[cookie_name]['secure'] = True + + self._cookies = dict( + (morsel.key, Cookie(morsel)) + for morsel in cookies.values() + ) + + self._encoding = helpers.get_encoding_from_headers(self._headers) + + @property + def status(self): + return self._status + + @property + def status_code(self): + return self._status_code + + @property + def headers(self): + return self._headers + + @property + def cookies(self): + return self._cookies + + @property + def encoding(self): + return self._encoding + + @property + def content(self): + return self._content + + @property + def text(self): + if self._text is None: + if not self.content: + self._text = u'' + else: + if self.encoding is None: + encoding = 'UTF-8' + else: + encoding = self.encoding + + self._text = self.content.decode(encoding) + + return self._text + + @property + def json(self): + return util_json.loads(self.text) + + +class Cookie(object): + """Represents a cookie returned by a simulated request. + + Args: + morsel: A ``Morsel`` object from which to derive the cookie + data. + + Attributes: + name (str): The cookie's name. + value (str): The value of the cookie. + expires(datetime.datetime): Expiration timestamp for the cookie, + or ``None`` if not specified. + path (str): The path prefix to which this cookie is restricted, + or ``None`` if not specified. + domain (str): The domain to which this cookie is restricted, + or ``None`` if not specified. + max_age (int): The lifetime of the cookie in seconds, or + ``None`` if not specified. + secure (bool): Whether or not the cookie may only only be + transmitted from the client via HTTPS. + http_only (bool): Whether or not the cookie may only be + included in unscripted requests from the client. + """ + + def __init__(self, morsel): + self._name = morsel.key + self._value = morsel.value + + for name in ( + 'expires', + 'path', + 'domain', + 'max_age', + 'secure', + 'httponly', + ): + value = morsel[name.replace('_', '-')] or None + setattr(self, '_' + name, value) + + @property + def name(self): + return self._name + + @property + def value(self): + return self._value + + @property + def expires(self): + if self._expires: + return http_date_to_dt(self._expires, obs_date=True) + + return None + + @property + def path(self): + return self._path + + @property + def domain(self): + return self._domain + + @property + def max_age(self): + return int(self._max_age) if self._max_age else None + + @property + def secure(self): + return bool(self._secure) + + @property + def http_only(self): + return bool(self._httponly) + + +def simulate_request(app, method='GET', path='/', query_string=None, + headers=None, body=None, json=None, file_wrapper=None, + wsgierrors=None, params=None, params_csv=True, + protocol='http'): + """Simulates a request to a WSGI application. + + Performs a request against a WSGI application. Uses + :any:`wsgiref.validate` to ensure the response is valid + WSGI. + + Keyword Args: + app (callable): The WSGI application to call + method (str): An HTTP method to use in the request + (default: 'GET') + path (str): The URL path to request (default: '/') + protocol: The protocol to use for the URL scheme + (default: 'http') + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + query_string (str): A raw query string to include in the + request (default: ``None``). If specified, overrides + `params`. + headers (dict): Additional headers to include in the request + (default: ``None``) + body (str): A string to send as the body of the request. + Accepts both byte strings and Unicode strings + (default: ``None``). If a Unicode string is provided, + it will be encoded as UTF-8 in the request. + json(JSON serializable): A JSON document to serialize as the + body of the request (default: ``None``). If specified, + overrides `body` and the Content-Type header in + `headers`. + file_wrapper (callable): Callable that returns an iterable, + to be used as the value for *wsgi.file_wrapper* in the + environ (default: ``None``). This can be used to test + high-performance file transmission when `resp.stream` is + set to a file-like object. + wsgierrors (io): The stream to use as *wsgierrors* + (default ``sys.stderr``) + + Returns: + :py:class:`~.Result`: The result of the request + """ + + if not path.startswith('/'): + raise ValueError("path must start with '/'") + + if query_string and query_string.startswith('?'): + raise ValueError("query_string should not start with '?'") + + if '?' in path: + # NOTE(kgriffs): We could allow this, but then we'd need + # to define semantics regarding whether the path takes + # precedence over the query_string. Also, it would make + # tests less consistent, since there would be "more than + # one...way to do it." + raise ValueError( + 'path may not contain a query string. Please use the ' + 'query_string parameter instead.' + ) + + if query_string is None: + query_string = to_query_str( + params, + comma_delimited_lists=params_csv, + prefix=False, + ) + + if json is not None: + body = util_json.dumps(json, ensure_ascii=False) + headers = headers or {} + headers['Content-Type'] = MEDIA_JSON + + env = helpers.create_environ( + method=method, + scheme=protocol, + path=path, + query_string=(query_string or ''), + headers=headers, + body=body, + file_wrapper=file_wrapper, + wsgierrors=wsgierrors, + ) + + srmock = StartResponseMock() + validator = wsgiref.validate.validator(app) + iterable = validator(env, srmock) + + result = Result(iterable, srmock.status, srmock.headers) + + return result + + +def simulate_get(app, path, **kwargs): + """Simulates a GET request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'GET', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + query_string (str): A raw query string to include in the + request (default: ``None``). If specified, overrides + `params`. + headers (dict): Additional headers to include in the request + (default: ``None``) + file_wrapper (callable): Callable that returns an iterable, + to be used as the value for *wsgi.file_wrapper* in the + environ (default: ``None``). This can be used to test + high-performance file transmission when `resp.stream` is + set to a file-like object. + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'GET', path, **kwargs) + + +def simulate_head(app, path, **kwargs): + """Simulates a HEAD request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'HEAD', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + query_string (str): A raw query string to include in the + request (default: ``None``). If specified, overrides + `params`. + headers (dict): Additional headers to include in the request + (default: ``None``) + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'HEAD', path, **kwargs) + + +def simulate_post(app, path, **kwargs): + """Simulates a POST request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'POST', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + headers (dict): Additional headers to include in the request + (default: ``None``) + body (str): A string to send as the body of the request. + Accepts both byte strings and Unicode strings + (default: ``None``). If a Unicode string is provided, + it will be encoded as UTF-8 in the request. + json(JSON serializable): A JSON document to serialize as the + body of the request (default: ``None``). If specified, + overrides `body` and the Content-Type header in + `headers`. + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'POST', path, **kwargs) + + +def simulate_put(app, path, **kwargs): + """Simulates a PUT request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'PUT', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + headers (dict): Additional headers to include in the request + (default: ``None``) + body (str): A string to send as the body of the request. + Accepts both byte strings and Unicode strings + (default: ``None``). If a Unicode string is provided, + it will be encoded as UTF-8 in the request. + json(JSON serializable): A JSON document to serialize as the + body of the request (default: ``None``). If specified, + overrides `body` and the Content-Type header in + `headers`. + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'PUT', path, **kwargs) + + +def simulate_options(app, path, **kwargs): + """Simulates an OPTIONS request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'OPTIONS', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + headers (dict): Additional headers to include in the request + (default: ``None``) + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'OPTIONS', path, **kwargs) + + +def simulate_patch(app, path, **kwargs): + """Simulates a PATCH request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'PATCH', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + headers (dict): Additional headers to include in the request + (default: ``None``) + body (str): A string to send as the body of the request. + Accepts both byte strings and Unicode strings + (default: ``None``). If a Unicode string is provided, + it will be encoded as UTF-8 in the request. + json(JSON serializable): A JSON document to serialize as the + body of the request (default: ``None``). If specified, + overrides `body` and the Content-Type header in + `headers`. + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'PATCH', path, **kwargs) + + +def simulate_delete(app, path, **kwargs): + """Simulates a DELETE request to a WSGI application. + + Equivalent to:: + + simulate_request(app, 'DELETE', path, **kwargs) + + Args: + app (callable): The WSGI application to call + path (str): The URL path to request + + Keyword Args: + params (dict): A dictionary of query string parameters, + where each key is a parameter name, and each value is + either a ``str`` or something that can be converted + into a ``str``, or a list of such values. If a ``list``, + the value will be converted to a comma-delimited string + of values (e.g., 'thing=1,2,3'). + params_csv (bool): Set to ``False`` to encode list values + in query string params by specifying multiple instances + of the parameter (e.g., 'thing=1&thing=2&thing=3'). + Otherwise, parameters will be encoded as comma-separated + values (e.g., 'thing=1,2,3'). Defaults to ``True``. + headers (dict): Additional headers to include in the request + (default: ``None``) + protocol: The protocol to use for the URL scheme + (default: 'http') + """ + return simulate_request(app, 'DELETE', path, **kwargs) + + +class TestClient(object): + """Simulates requests to a WSGI application. + + This class provides a contextual wrapper for Falcon's `simulate_*` + test functions. It lets you replace this:: + + simulate_get(app, '/messages') + simulate_head(app, '/messages') + + with this:: + + client = TestClient(app) + client.simulate_get('/messages') + client.simulate_head('/messages') + + Note: + The methods all call ``self.simulate_request()`` for convenient + overriding of request preparation by child classes. + + Args: + app (callable): A WSGI application to target when simulating + requests + + + Keyword Arguments: + headers (dict): Default headers to set on every request (default + ``None``). These defaults may be overridden by passing values + for the same headers to one of the `simulate_*()` methods. + """ + + def __init__(self, app, headers=None): + self.app = app + self._default_headers = headers + + def simulate_get(self, path='/', **kwargs): + """Simulates a GET request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_get`) + """ + return self.simulate_request('GET', path, **kwargs) + + def simulate_head(self, path='/', **kwargs): + """Simulates a HEAD request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_head`) + """ + return self.simulate_request('HEAD', path, **kwargs) + + def simulate_post(self, path='/', **kwargs): + """Simulates a POST request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_post`) + """ + return self.simulate_request('POST', path, **kwargs) + + def simulate_put(self, path='/', **kwargs): + """Simulates a PUT request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_put`) + """ + return self.simulate_request('PUT', path, **kwargs) + + def simulate_options(self, path='/', **kwargs): + """Simulates an OPTIONS request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_options`) + """ + return self.simulate_request('OPTIONS', path, **kwargs) + + def simulate_patch(self, path='/', **kwargs): + """Simulates a PATCH request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_patch`) + """ + return self.simulate_request('PATCH', path, **kwargs) + + def simulate_delete(self, path='/', **kwargs): + """Simulates a DELETE request to a WSGI application. + + (See also: :py:meth:`falcon.testing.simulate_delete`) + """ + return self.simulate_request('DELETE', path, **kwargs) + + def simulate_request(self, *args, **kwargs): + """Simulates a request to a WSGI application. + + Wraps :py:meth:`falcon.testing.simulate_request` to perform a + WSGI request directly against ``self.app``. Equivalent to:: + + falcon.testing.simulate_request(self.app, *args, **kwargs) + """ + + if self._default_headers: + # NOTE(kgriffs): Handle the case in which headers is explicitly + # set to None. + additional_headers = kwargs.get('headers', {}) or {} + + merged_headers = self._default_headers.copy() + merged_headers.update(additional_headers) + + kwargs['headers'] = merged_headers + + return simulate_request(self.app, *args, **kwargs) diff -Nru python-falcon-1.0.0/falcon/testing/helpers.py python-falcon-1.4.1/falcon/testing/helpers.py --- python-falcon-1.0.0/falcon/testing/helpers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/helpers.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,14 +12,27 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Testing utilities. + +This module contains various testing utilities that can be accessed +directly from the `testing` package:: + + from falcon import testing + + wsgi_environ = testing.create_environ() + +""" + import cgi -import random +import contextlib import io +import random import sys import six -from falcon.util import uri, http_now +from falcon.util import http_now, uri + # Constants DEFAULT_HOST = 'falconframework.org' @@ -108,6 +121,9 @@ """ + if query_string and query_string.startswith('?'): + raise ValueError("query_string should not start with '?'") + body = io.BytesIO(body.encode('utf-8') if isinstance(body, six.text_type) else body) @@ -115,6 +131,23 @@ # the paths before setting PATH_INFO path = uri.decode(path) + if six.PY3: + # NOTE(kgriffs): The decoded path may contain UTF-8 characters. + # But according to the WSGI spec, no strings can contain chars + # outside ISO-8859-1. Therefore, to reconcile the URI + # encoding standard that allows UTF-8 with the WSGI spec + # that does not, WSGI servers tunnel the string via + # ISO-8859-1. falcon.testing.create_environ() mimics this + # behavior, e.g.: + # + # tunnelled_path = path.encode('utf-8').decode('iso-8859-1') + # + # falcon.Request does the following to reverse the process: + # + # path = tunnelled_path.encode('iso-8859-1').decode('utf-8', 'replace') + # + path = path.encode('utf-8').decode('iso-8859-1') + if six.PY2 and isinstance(path, six.text_type): path = path.encode('utf-8') @@ -124,6 +157,12 @@ else: port = str(port) + # NOTE(kgriffs): Judging by the algorithm given in PEP-3333 for + # reconstructing the URL, SCRIPT_NAME is expected to contain a + # preceding slash character. + if app and not app.startswith('/'): + app = '/' + app + env = { 'SERVER_PROTOCOL': protocol, 'SERVER_SOFTWARE': 'gunicorn/0.17.0', @@ -174,6 +213,29 @@ return env +@contextlib.contextmanager +def redirected(stdout=sys.stdout, stderr=sys.stderr): + """ + A context manager to temporarily redirect stdout or stderr + + e.g.: + + with redirected(stderr=os.devnull): + ... + """ + + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout, sys.stderr = stdout, stderr + try: + yield + finally: + sys.stderr, sys.stdout = old_stderr, old_stdout + +# --------------------------------------------------------------------- +# Private +# --------------------------------------------------------------------- + + def _add_headers_to_environ(env, headers): if not isinstance(headers, dict): # Try to convert diff -Nru python-falcon-1.0.0/falcon/testing/__init__.py python-falcon-1.4.1/falcon/testing/__init__.py --- python-falcon-1.0.0/falcon/testing/__init__.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,10 +12,73 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Functional testing framework for Falcon apps and Falcon itself. + +Falcon's testing module contains various test classes and utility +functions to support functional testing for both Falcon-based apps and +the Falcon framework itself. + +The testing framework supports both unittest and pytest:: + + # ----------------------------------------------------------------- + # unittest + # ----------------------------------------------------------------- + + from falcon import testing + import myapp + + + class MyTestCase(testing.TestCase): + def setUp(self): + super(MyTestCase, self).setUp() + + # Assume the hypothetical `myapp` package has a + # function called `create()` to initialize and + # return a `falcon.API` instance. + self.app = myapp.create() + + + class TestMyApp(MyTestCase): + def test_get_message(self): + doc = {u'message': u'Hello world!'} + + result = self.simulate_get('/messages/42') + self.assertEqual(result.json, doc) + + + # ----------------------------------------------------------------- + # pytest + # ----------------------------------------------------------------- + + from falcon import testing + import pytest + + import myapp + + + # Depending on your testing strategy and how your application + # manages state, you may be able to broaden the fixture scope + # beyond the default 'function' scope used in this example. + + @pytest.fixture() + def client(): + # Assume the hypothetical `myapp` package has a function called + # `create()` to initialize and return a `falcon.API` instance. + return testing.TestClient(myapp.create()) + + + def test_get_message(client): + doc = {u'message': u'Hello world!'} + + result = client.simulate_get('/messages/42') + assert result.json == doc +""" + # Hoist classes and functions into the falcon.testing namespace from falcon.testing.base import TestBase # NOQA +from falcon.testing.client import * # NOQA from falcon.testing.helpers import * # NOQA -from falcon.testing.resource import capture_responder_args # NOQA +from falcon.testing.resource import capture_responder_args, set_resp_defaults # NOQA from falcon.testing.resource import SimpleTestResource, TestResource # NOQA from falcon.testing.srmock import StartResponseMock # NOQA -from falcon.testing.test_case import Result, TestCase # NOQA +from falcon.testing.test_case import TestCase # NOQA diff -Nru python-falcon-1.0.0/falcon/testing/resource.py python-falcon-1.4.1/falcon/testing/resource.py --- python-falcon-1.0.0/falcon/testing/resource.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/resource.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,7 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from json import dumps as json_dumps +"""Mock resource classes. + +This module contains mock resource classes and associated hooks for use +in Falcon framework tests. The classes and hooks may be referenced +directly from the `testing` package:: + + from falcon import testing + + resource = testing.SimpleTestResource() + +""" + +try: + from ujson import dumps as json_dumps +except ImportError: + from json import dumps as json_dumps import falcon from .helpers import rand_string @@ -54,9 +69,10 @@ as needed to test middleware, hooks, and the Falcon framework itself. - Only the ``on_get()`` responder is implemented; when adding - additional responders in child classes, they can be decorated - with the :py:meth:`falcon.testing.capture_responder_args` hook in + Only noop ``on_get()`` and ``on_post()`` responders are implemented; + when overriding these, or adding additional responders in child + classes, they can be decorated with the + :py:meth:`falcon.testing.capture_responder_args` hook in order to capture the *req*, *resp*, and *params* arguments that are passed to the responder. Responders may also be decorated with the :py:meth:`falcon.testing.set_resp_defaults` hook in order to @@ -66,13 +82,14 @@ Keyword Arguments: status (str): Default status string to use in responses body (str): Default body string to use in responses - json (dict): Default JSON document to use in responses. Will - be serialized to a string and encoded as UTF-8. Either + json (JSON serializable): Default JSON document to use in responses. + Will be serialized to a string and encoded as UTF-8. Either *json* or *body* may be specified, but not both. headers (dict): Default set of additional headers to include in responses Attributes: + called (bool): Whether or not a req/resp was captured. captured_req (falcon.Request): The last Request object passed into any one of the responder methods. captured_resp (falcon.Response): The last Response object passed @@ -96,11 +113,24 @@ else: self._default_body = body + self.captured_req = None + self.captured_resp = None + self.captured_kwargs = None + + @property + def called(self): + return self.captured_req is not None + @falcon.before(capture_responder_args) @falcon.before(set_resp_defaults) def on_get(self, req, resp, **kwargs): pass + @falcon.before(capture_responder_args) + @falcon.before(set_resp_defaults) + def on_post(self, req, resp, **kwargs): + pass + class TestResource(object): """Mock resource for functional testing. diff -Nru python-falcon-1.0.0/falcon/testing/srmock.py python-falcon-1.4.1/falcon/testing/srmock.py --- python-falcon-1.0.0/falcon/testing/srmock.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/srmock.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""WSGI start_response mock. + +This module implements a callable StartResponseMock class that can be +used, along with a mock environ dict, to simulate a WSGI request. +""" + from falcon import util diff -Nru python-falcon-1.0.0/falcon/testing/test_case.py python-falcon-1.4.1/falcon/testing/test_case.py --- python-falcon-1.0.0/falcon/testing/test_case.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/testing/test_case.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2013 by Rackspace Hosting, Inc. +# Copyright 2016 by Rackspace Hosting, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import wsgiref.validate +"""unittest-style base class and utilities for test cases. + +This package includes a unittest-style base class and requests-like +utilities for simulating and validating HTTP requests. +""" try: import testtools as unittest @@ -22,120 +25,78 @@ import falcon import falcon.request -from falcon.util import CaseInsensitiveDict -from falcon.testing.srmock import StartResponseMock -from falcon.testing.helpers import create_environ, get_encoding_from_headers - - -class Result(object): - """Encapsulates the result of a simulated WSGI request. - - Args: - iterable (iterable): An iterable that yields zero or more - bytestrings, per PEP-3333 - status (str): An HTTP status string, including status code and - reason string - headers (list): A list of (header_name, header_value) tuples, - per PEP-3333 - - Attributes: - status (str): HTTP status string given in the response - status_code (int): The code portion of the HTTP status string - headers (CaseInsensitiveDict): A case-insensitive dictionary - containing all the headers in the response - encoding (str): Text encoding of the response body, or ``None`` - if the encoding can not be determined. - content (bytes): Raw response body, or ``bytes`` if the - response body was empty. - text (str): Decoded response body of type ``unicode`` - under Python 2.6 and 2.7, and of type ``str`` otherwise. - Raises an error if the response encoding can not be - determined. - json (dict): Deserialized JSON body. Raises an error if the - response is not JSON. - """ - - def __init__(self, iterable, status, headers): - self._text = None - - self._content = b''.join(iterable) - if hasattr(iterable, 'close'): - iterable.close() - - self._status = status - self._status_code = int(status[:3]) - self._headers = CaseInsensitiveDict(headers) +from falcon.testing.client import TestClient +from falcon.testing.client import Result # NOQA - hoist for backwards compat - self._encoding = get_encoding_from_headers(self._headers) - - @property - def status(self): - return self._status - @property - def status_code(self): - return self._status_code +class TestCase(unittest.TestCase, TestClient): + """Extends :py:mod:`unittest` to support WSGI functional testing. - @property - def headers(self): - return self._headers + Note: + If available, uses :py:mod:`testtools` in lieu of + :py:mod:`unittest`. - @property - def encoding(self): - return self._encoding + This base class provides some extra plumbing for unittest-style + test cases, to help simulate WSGI calls without having to spin up + an actual web server. Various simulation methods are derived + from :py:class:`falcon.testing.TestClient`. - @property - def content(self): - return self._content + Simply inherit from this class in your test case classes instead of + :py:class:`unittest.TestCase` or :py:class:`testtools.TestCase`. - @property - def text(self): - if self._text is None: - if not self.content: - self._text = u'' - else: - if self.encoding is None: - msg = 'Response did not specify a content encoding' - raise RuntimeError(msg) + Attributes: + app (object): A WSGI application to target when simulating + requests (default: ``falcon.API()``). When testing your + application, you will need to set this to your own instance + of ``falcon.API``. For example:: - self._text = self.content.decode(self.encoding) + from falcon import testing + import myapp - return self._text - @property - def json(self): - return json.loads(self.text) + class MyTestCase(testing.TestCase): + def setUp(self): + super(MyTestCase, self).setUp() + # Assume the hypothetical `myapp` package has a + # function called `create()` to initialize and + # return a `falcon.API` instance. + self.app = myapp.create() -class TestCase(unittest.TestCase): - """Extends :py:mod:`unittest` to support WSGI functional testing. - Note: - If available, uses :py:mod:`testtools` in lieu of - :py:mod:`unittest`. + class TestMyApp(MyTestCase): + def test_get_message(self): + doc = {u'message': u'Hello world!'} - This base class provides some extra plumbing for unittest-style - test cases, to help simulate WSGI calls without having to spin up - an actual web server. Simply inherit from this class in your test - case classes instead of :py:class:`unittest.TestCase` or - :py:class:`testtools.TestCase`. + result = self.simulate_get('/messages/42') + self.assertEqual(result.json, doc) - Attributes: - api_class (class): An API class to use when instantiating - the ``api`` instance (default: :py:class:`falcon.API`) - api (object): An API instance to target when simulating - requests (default: ``self.api_class()``) + api (object): Deprecated alias for ``app`` + api_class (callable): Deprecated class variable; will be + removed in a future release. """ api_class = None + @property + def api(self): + return self.app + + @api.setter + def api(self, value): + self.app = value + def setUp(self): super(TestCase, self).setUp() if self.api_class is None: - self.api = falcon.API() + app = falcon.API() else: - self.api = self.api_class() + app = self.api_class() + + # NOTE(kgriffs): Don't use super() to avoid triggering + # unittest.TestCase.__init__() + TestClient.__init__(self, app) # Reset to simulate "restarting" the WSGI container falcon.request._maybe_wrap_wsgi_stream = True @@ -146,187 +107,3 @@ if not hasattr(unittest.TestCase, 'assertIn'): # pragma: nocover def assertIn(self, a, b): self.assertTrue(a in b) - - def simulate_get(self, path='/', **kwargs): - """Simulates a GET request to a WSGI application. - - Equivalent to ``simulate_request('GET', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - """ - return self.simulate_request('GET', path, **kwargs) - - def simulate_head(self, path='/', **kwargs): - """Simulates a HEAD request to a WSGI application. - - Equivalent to ``simulate_request('HEAD', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - """ - return self.simulate_request('HEAD', path, **kwargs) - - def simulate_post(self, path='/', **kwargs): - """Simulates a POST request to a WSGI application. - - Equivalent to ``simulate_request('POST', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - body (str): A string to send as the body of the request. - Accepts both byte strings and Unicode strings - (default: ``None``). If a Unicode string is provided, - it will be encoded as UTF-8 in the request. - """ - return self.simulate_request('POST', path, **kwargs) - - def simulate_put(self, path='/', **kwargs): - """Simulates a PUT request to a WSGI application. - - Equivalent to ``simulate_request('PUT', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - body (str): A string to send as the body of the request. - Accepts both byte strings and Unicode strings - (default: ``None``). If a Unicode string is provided, - it will be encoded as UTF-8 in the request. - """ - return self.simulate_request('PUT', path, **kwargs) - - def simulate_options(self, path='/', **kwargs): - """Simulates an OPTIONS request to a WSGI application. - - Equivalent to ``simulate_request('OPTIONS', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - """ - return self.simulate_request('OPTIONS', path, **kwargs) - - def simulate_patch(self, path='/', **kwargs): - """Simulates a PATCH request to a WSGI application. - - Equivalent to ``simulate_request('PATCH', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - body (str): A string to send as the body of the request. - Accepts both byte strings and Unicode strings - (default: ``None``). If a Unicode string is provided, - it will be encoded as UTF-8 in the request. - """ - return self.simulate_request('PATCH', path, **kwargs) - - def simulate_delete(self, path='/', **kwargs): - """Simulates a DELETE request to a WSGI application. - - Equivalent to ``simulate_request('DELETE', ...)`` - - Args: - path (str): The URL path to request (default: '/') - - Keyword Args: - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - """ - return self.simulate_request('DELETE', path, **kwargs) - - def simulate_request(self, method='GET', path='/', query_string=None, - headers=None, body=None, file_wrapper=None): - """Simulates a request to a WSGI application. - - Performs a WSGI request directly against ``self.api``. - - Keyword Args: - method (str): The HTTP method to use in the request - (default: 'GET') - path (str): The URL path to request (default: '/') - query_string (str): A raw query string to include in the - request (default: ``None``) - headers (dict): Additional headers to include in the request - (default: ``None``) - body (str): A string to send as the body of the request. - Accepts both byte strings and Unicode strings - (default: ``None``). If a Unicode string is provided, - it will be encoded as UTF-8 in the request. - file_wrapper (callable): Callable that returns an iterable, - to be used as the value for *wsgi.file_wrapper* in the - environ (default: ``None``). - - Returns: - :py:class:`~.Result`: The result of the request - """ - - if not path.startswith('/'): - raise ValueError("path must start with '/'") - - if query_string and query_string.startswith('?'): - raise ValueError("query_string should not start with '?'") - - if '?' in path: - # NOTE(kgriffs): We could allow this, but then we'd need - # to define semantics regarding whether the path takes - # precedence over the query_string. Also, it would make - # tests less consistent, since there would be "more than - # one...way to do it." - raise ValueError( - 'path may not contain a query string. Please use the ' - 'query_string parameter instead.' - ) - - env = create_environ( - method=method, - path=path, - query_string=(query_string or ''), - headers=headers, - body=body, - file_wrapper=file_wrapper, - ) - - srmock = StartResponseMock() - validator = wsgiref.validate.validator(self.api) - iterable = validator(env, srmock) - - result = Result(iterable, srmock.status, srmock.headers) - - return result diff -Nru python-falcon-1.0.0/falcon/uri.py python-falcon-1.4.1/falcon/uri.py --- python-falcon-1.0.0/falcon/uri.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/falcon/uri.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,16 @@ +"""URI utilities. + +This module provides utility functions to parse, encode, decode, and +otherwise manipulate a URI. These functions are not available directly +in the `falcon` module, and so must be explicitly imported:: + + from falcon import uri + + name, port = uri.parse_host('example.org:8080') +""" + +# NOTE(kgriffs): This module exists to make "import falcon.uri" +# work. Eventually we will remove the util module and flatten the +# falcon namespace, but in the meantime... + +from falcon.util.uri import * # NOQA diff -Nru python-falcon-1.0.0/falcon/util/__init__.py python-falcon-1.4.1/falcon/util/__init__.py --- python-falcon-1.0.0/falcon/util/__init__.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/util/__init__.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,6 +1,33 @@ +"""General utilities. + +This package includes multiple modules that implement utility functions +and classes that are useful to both apps and the Falcon framework +itself. + +All utilities in the `structures`, `misc`, and `time` modules are +imported directly into the front-door `falcon` module for convenience:: + + import falcon + + now = falcon.http_now() + +Conversely, the `uri` module must be imported explicitly:: + + from falcon import uri + + some_uri = '...' + decoded_uri = uri.decode(some_uri) + +""" + +try: + import ujson as json # NOQA +except ImportError: + import json # NOQA + # Hoist misc. utils -from falcon.util.misc import * # NOQA -from falcon.util.time import * from falcon.util import structures +from falcon.util.misc import * # NOQA +from falcon.util.time import * # NOQA CaseInsensitiveDict = structures.CaseInsensitiveDict diff -Nru python-falcon-1.0.0/falcon/util/misc.py python-falcon-1.4.1/falcon/util/misc.py --- python-falcon-1.0.0/falcon/util/misc.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/util/misc.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Miscellaneous utilities. + +This module provides misc. utility functions for apps and the Falcon +framework itself. These functions are hoisted into the front-door +`falcon` module for convenience:: + + import falcon + + now = falcon.http_now() + +""" + import datetime import functools import inspect @@ -19,6 +31,8 @@ import six +from falcon import status_codes + __all__ = ( 'deprecated', 'http_now', @@ -26,6 +40,8 @@ 'http_date_to_dt', 'to_query_str', 'get_bound_method', + 'get_argnames', + 'get_http_status' ) @@ -80,7 +96,7 @@ Returns: str: The current UTC time as an IMF-fixdate, - e.g., 'Tue, 15 Nov 1994 12:45:26 GMT'. + e.g., 'Tue, 15 Nov 1994 12:45:26 GMT'. """ return dt_to_http(utcnow()) @@ -107,13 +123,15 @@ Args: http_date (str): An RFC 1123 date string, e.g.: "Tue, 15 Nov 1994 12:45:26 GMT". - obs_date (bool, optional): Support obs-date formats according to + + Keyword Arguments: + obs_date (bool): Support obs-date formats according to RFC 7231, e.g.: "Sunday, 06-Nov-94 08:49:37 GMT" (default ``False``). Returns: datetime: A UTC datetime instance corresponding to the given - HTTP date. + HTTP date. Raises: ValueError: http_date doesn't match any of the available time formats @@ -145,19 +163,28 @@ raise ValueError('time data %r does not match known formats' % http_date) -def to_query_str(params): - """Converts a dictionary of params to a query string. +def to_query_str(params, comma_delimited_lists=True, prefix=True): + """Converts a dictionary of parameters to a query string. Args: - params (dict): A dictionary of parameters, where each key is a - parameter name, and each value is either a ``str`` or - something that can be converted into a ``str``. If `params` - is a ``list``, it will be converted to a comma-delimited string - of values (e.g., 'thing=1,2,3') + params (dict): A dictionary of parameters, where each key is + a parameter name, and each value is either a ``str`` or + something that can be converted into a ``str``, or a + list of such values. If a ``list``, the value will be + converted to a comma-delimited string of values + (e.g., 'thing=1,2,3'). + comma_delimited_lists (bool): Set to ``False`` to encode list + values by specifying multiple instances of the parameter + (e.g., 'thing=1&thing=2&thing=3'). Otherwise, parameters + will be encoded as comma-separated values (e.g., + 'thing=1,2,3'). Defaults to ``True``. + prefix (bool): Set to ``False`` to exclude the '?' prefix + in the result string (default ``True``). Returns: - str: A URI query string including the '?' prefix, or an empty string - if no params are given (the ``dict`` is empty). + str: A URI query string, including the '?' prefix (unless + `prefix` is ``False``), or an empty string if no params are + given (the ``dict`` is empty). """ if not params: @@ -165,14 +192,27 @@ # PERF: This is faster than a list comprehension and join, mainly # because it allows us to inline the value transform. - query_str = '?' + query_str = '?' if prefix else '' for k, v in params.items(): if v is True: v = 'true' elif v is False: v = 'false' elif isinstance(v, list): - v = ','.join(map(str, v)) + if comma_delimited_lists: + v = ','.join(map(str, v)) + else: + for list_value in v: + if list_value is True: + list_value = 'true' + elif list_value is False: + list_value = 'false' + else: + list_value = str(list_value) + + query_str += k + '=' + list_value + '&' + + continue else: v = str(v) @@ -210,3 +250,104 @@ raise AttributeError(msg) return method + + +def _get_func_if_nested(callable): + """Returns the function object of a given callable.""" + + if isinstance(callable, functools.partial): + return callable.func + + if inspect.isroutine(callable): + return callable + + return callable.__call__ + + +def _get_argspec(func): + """Returns an inspect.ArgSpec instance given a function object. + + We prefer this implementation rather than the inspect module's getargspec + since the latter has a strict check that the passed function is an instance + of FunctionType. Cython functions do not pass this check, but they do implement + the `func_code` and `func_defaults` attributes that we need to produce an Argspec. + + This implementation re-uses much of inspect.getargspec but removes the strict + check allowing interface failures to be raised as AttributeError. + + (See also: https://github.com/python/cpython/blob/2.7/Lib/inspect.py) + """ + if inspect.ismethod(func): + func = func.im_func + + args, varargs, varkw = inspect.getargs(func.func_code) + return inspect.ArgSpec(args, varargs, varkw, func.func_defaults) + + +def get_argnames(func): + """Introspecs the arguments of a callable. + + Args: + func: The callable to introspect + + Returns: + A list of argument names, excluding *arg and **kwargs + arguments. + """ + + if six.PY2: + func_object = _get_func_if_nested(func) + spec = _get_argspec(func_object) + + args = spec.args + + else: + sig = inspect.signature(func) + + args = [ + param.name + for param in sig.parameters.values() + if param.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) + ] + + # NOTE(kgriffs): Depending on the version of Python, 'self' may or may not + # be present, so we normalize the results by removing 'self' as needed. + # Note that this behavior varies between 3.x versions as well as between + # 3.x and 2.7. + if args[0] == 'self': + args = args[1:] + + return args + + +def get_http_status(status_code, default_reason='Unknown'): + """Gets both the http status code and description from just a code + + Args: + status_code: integer or string that can be converted to an integer + default_reason: default text to be appended to the status_code + if the lookup does not find a result + + Returns: + str: status code e.g. "404 Not Found" + + Raises: + ValueError: the value entered could not be converted to an integer + + """ + # sanitize inputs + try: + code = float(status_code) # float can validate values like "401.1" + code = int(code) # converting to int removes the decimal places + if code < 100: + raise ValueError + except ValueError: + raise ValueError('get_http_status failed: "%s" is not a ' + 'valid status code', status_code) + + # lookup the status code + try: + return getattr(status_codes, 'HTTP_' + str(code)) + except AttributeError: + # not found + return str(code) + ' ' + default_reason diff -Nru python-falcon-1.0.0/falcon/util/structures.py python-falcon-1.4.1/falcon/util/structures.py --- python-falcon-1.0.0/falcon/util/structures.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/util/structures.py 2018-08-08 23:08:36.000000000 +0000 @@ -14,6 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. + +"""Data structures. + +This module provides additional data structures not found in the +standard library. These classes are hoisted into the `falcon` module +for convenience:: + + import falcon + + things = falcon.CaseInsensitiveDict() + +""" + import collections diff -Nru python-falcon-1.0.0/falcon/util/time.py python-falcon-1.4.1/falcon/util/time.py --- python-falcon-1.0.0/falcon/util/time.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/util/time.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,3 +1,15 @@ +"""Time and date utilities. + +This module provides utility functions and classes for dealing with +times and dates. These functions are hoisted into the `falcon` module +for convenience:: + + import falcon + + tz = falcon.TimezoneGMT() + +""" + import datetime @@ -14,7 +26,7 @@ Returns: datetime.timedelta: GMT offset, which is equivalent to UTC and - so is aways 0. + so is aways 0. """ return self.GMT_ZERO diff -Nru python-falcon-1.0.0/falcon/util/uri.py python-falcon-1.4.1/falcon/util/uri.py --- python-falcon-1.0.0/falcon/util/uri.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/util/uri.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,6 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""URI utilities. + +This module provides utility functions to parse, encode, decode, and +otherwise manipulate a URI. These functions are not available directly +in the `falcon` module, and so must be explicitly imported:: + + from falcon import uri + + name, port = uri.parse_host('example.org:8080') + +""" + import six # NOTE(kgriffs): See also RFC 3986 @@ -48,6 +60,7 @@ def _create_str_encoder(is_value): allowed_chars = _UNRESERVED if is_value else _ALL_ALLOWED + allowed_chars_plus_percent = allowed_chars + '%' encode_char = _create_char_encoder(allowed_chars) def encoder(uri): @@ -55,10 +68,32 @@ if not uri.rstrip(allowed_chars): return uri + if not uri.rstrip(allowed_chars_plus_percent): + # NOTE(kgriffs): There's a good chance the string has already + # been escaped. Do one more check to increase our certainty. + tokens = uri.split('%') + for token in tokens[1:]: + hex_octet = token[:2] + + if not len(hex_octet) == 2: + break + + if not (hex_octet[0] in _HEX_DIGITS and + hex_octet[1] in _HEX_DIGITS): + break + else: + # NOTE(kgriffs): All percent-encoded sequences were + # valid, so assume that the string has already been + # encoded. + return uri + + # NOTE(kgriffs): At this point we know there is at least + # one unallowed percent character. We are going to assume + # that everything should be encoded. If the string is + # partially encoded, the caller will need to normalize it + # before passing it in here. + # Convert to a byte array if it is not one already - # - # NOTE(kgriffs): Code coverage disabled since in Py3K the uri - # is always a text type, so we get a failure for that tox env. if isinstance(uri, six.text_type): uri = uri.encode('utf-8') @@ -93,7 +128,7 @@ Returns: str: An escaped version of `uri`, where all disallowed characters - have been percent-encoded. + have been percent-encoded. """ @@ -125,11 +160,11 @@ Returns: str: An escaped version of `uri`, where all disallowed characters - have been percent-encoded. + have been percent-encoded. """ -if six.PY2: +if six.PY2: # NOQA: C901 - Work around a bug in flake8 McCabe scoring # This map construction is based on urllib _HEX_TO_BYTE = dict((a + b, (chr(int(a + b, 16)), int(a + b, 16))) @@ -139,7 +174,7 @@ def decode(encoded_uri): """Decodes percent-encoded characters in a URI or query string. - This function models the behavior of `urllib.parse.unquote_plus`, but + This function models the behavior of `urllib.unquote_plus`, but is faster. It is also more robust, in that it will decode escaped UTF-8 mutibyte sequences. @@ -148,8 +183,8 @@ Returns: str: A decoded URL. Will be of type ``unicode`` on Python 2 IFF the - URL contained escaped non-ASCII characters, in which case - UTF-8 is assumed per RFC 3986. + URL contained escaped non-ASCII characters, in which case + UTF-8 is assumed per RFC 3986. """ @@ -178,10 +213,11 @@ decoded_uri = tokens[0] for token in tokens[1:]: token_partial = token[:2] - if token_partial in _HEX_TO_BYTE: + try: char, byte = _HEX_TO_BYTE[token_partial] - else: + except KeyError: char, byte = '%', 0 + decoded_uri += char + (token[2:] if byte else token) only_ascii = only_ascii and (byte <= 127) @@ -236,9 +272,9 @@ decoded_uri = tokens[0] for token in tokens[1:]: token_partial = token[:2] - if token_partial in _HEX_TO_BYTE: + try: decoded_uri += _HEX_TO_BYTE[token_partial] + token[2:] - else: + except KeyError: # malformed percentage like "x=%" or "y=%+" decoded_uri += b'%' + token @@ -246,11 +282,12 @@ return decoded_uri.decode('utf-8', 'replace') -def parse_query_string(query_string, keep_blank_qs_values=False): +def parse_query_string(query_string, keep_blank_qs_values=False, + parse_qs_csv=True): """Parse a query string into a dict. Query string parameters are assumed to use standard form-encoding. Only - parameters with values are parsed. for example, given 'foo=bar&flag', + parameters with values are returned. For example, given 'foo=bar&flag', this function would ignore 'flag' unless the `keep_blank_qs_values` option is set. @@ -269,13 +306,21 @@ Args: query_string (str): The query string to parse. - keep_blank_qs_values (bool): If set to ``True``, preserves boolean - fields and fields with no content as blank strings. + keep_blank_qs_values (bool): Set to ``True`` to return fields even if + they do not have a value (default ``False``). For comma-separated + values, this option also determines whether or not empty elements + in the parsed list are retained. + parse_qs_csv: Set to ``False`` in order to disable splitting query + parameters on ``,`` (default ``True``). Depending on the user agent, + encoding lists as multiple occurrences of the same parameter might + be preferable. In this case, setting `parse_qs_csv` to ``False`` + will cause the framework to treat commas as literal characters in + each occurring parameter value. Returns: dict: A dictionary of (*name*, *value*) pairs, one per query - parameter. Note that *value* may be a single ``str``, or a - ``list`` of ``str``. + parameter. Note that *value* may be a single ``str``, or a + ``list`` of ``str``. Raises: TypeError: `query_string` was not a ``str``. @@ -284,6 +329,8 @@ params = {} + is_encoded = '+' in query_string or '%' in query_string + # PERF(kgriffs): This was found to be faster than using a regex, for # both short and long query strings. Tested on both CPython 2.7 and 3.4, # and on PyPy 2.3. @@ -294,7 +341,8 @@ # Note(steffgrez): Falcon first decode name parameter for handle # utf8 character. - k = decode(k) + if is_encoded: + k = decode(k) # NOTE(steffgrez): Falcon decode value at the last moment. So query # parser won't mix up between percent-encoded comma (as value) and @@ -303,13 +351,17 @@ # The key was present more than once in the POST data. Convert to # a list, or append the next value to the list. old_value = params[k] + + if is_encoded: + v = decode(v) + if isinstance(old_value, list): - old_value.append(decode(v)) + old_value.append(v) else: - params[k] = [old_value, decode(v)] + params[k] = [old_value, v] else: - if ',' in v: + if parse_qs_csv and ',' in v: # NOTE(kgriffs): Falcon supports a more compact form of # lists, in which the elements are comma-separated and # assigned to a single param instance. If it turns out that @@ -324,8 +376,10 @@ params[k] = [decode(element) for element in v if element] else: params[k] = [decode(element) for element in v] - else: + elif is_encoded: params[k] = decode(v) + else: + params[k] = v return params @@ -341,14 +395,16 @@ Args: host (str): Host string to parse, optionally containing a port number. - default_port (int, optional): Port number to return when - the host string does not contain one (default ``None``). + + Keyword Arguments: + default_port (int): Port number to return when the host string + does not contain one (default ``None``). Returns: tuple: A parsed (*host*, *port*) tuple from the given - host string, with the port converted to an ``int``. - If the host string does not specify a port, `default_port` is - used instead. + host string, with the port converted to an ``int``. + If the host string does not specify a port, `default_port` is + used instead. """ diff -Nru python-falcon-1.0.0/falcon/version.py python-falcon-1.4.1/falcon/version.py --- python-falcon-1.0.0/falcon/version.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/falcon/version.py 2018-08-08 23:08:36.000000000 +0000 @@ -12,5 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = '1.0.0' +"""Falcon version.""" + +__version__ = '1.4.1.post-1' """Current version of Falcon.""" Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/falcon.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/falcon.png differ diff -Nru python-falcon-1.0.0/.gitignore python-falcon-1.4.1/.gitignore --- python-falcon-1.0.0/.gitignore 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/.gitignore 2018-08-08 23:08:36.000000000 +0000 @@ -1,5 +1,3 @@ -*.py[cod] - # C extensions *.c *.so @@ -7,33 +5,36 @@ # Jython *$py.class -# Packages +# Build artifacts *.egg *.egg-info -dist +*.py[cod] +.eggs +.installed.cfg build -eggs -parts -var -sdist develop-eggs -.installed.cfg +dist +eggs lib lib64 +parts +sdist +var # Installer logs pip-log.txt -# Unit test / coverage reports -.coverage -.coverage_* +# Test artifacts +*.dat +.cache +.coverage* +.ecosystem .tox -nosetests.xml htmlcov -*.dat +nosetests.xml # Docs -doc/_build +docs/_build # Translations *.mo @@ -49,3 +50,7 @@ # VIM temp files *~ + +# VSCode + +.vscode \ No newline at end of file diff -Nru python-falcon-1.0.0/LICENSE python-falcon-1.4.1/LICENSE --- python-falcon-1.0.0/LICENSE 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/LICENSE 2018-08-08 23:08:36.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright 2012-2016 by Rackspace Hosting, Inc. and other contributors, +Copyright 2012-2017 by Rackspace Hosting, Inc. and other contributors, as noted in the individual source code files. Licensed under the Apache License, Version 2.0 (the "License"); Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/android-chrome-512x512.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/android-chrome-512x512.png differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/favicon-16x16.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/favicon-16x16.png differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/favicon-32x32.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/favicon-32x32.png differ diff -Nru python-falcon-1.0.0/logo/logo-alt.svg python-falcon-1.4.1/logo/logo-alt.svg --- python-falcon-1.0.0/logo/logo-alt.svg 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/logo/logo-alt.svg 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,20 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + diff -Nru python-falcon-1.0.0/logo/logo.svg python-falcon-1.4.1/logo/logo.svg --- python-falcon-1.0.0/logo/logo.svg 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/logo/logo.svg 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,50 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + + + Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/mstile-150x150.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/mstile-150x150.png differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/mstile-310x310.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/mstile-310x310.png differ Binary files /tmp/tmp_NXOBq/Ok3dQqq97p/python-falcon-1.0.0/logo/mstile-70x70.png and /tmp/tmp_NXOBq/xQydqB0I3c/python-falcon-1.4.1/logo/mstile-70x70.png differ diff -Nru python-falcon-1.0.0/MANIFEST.in python-falcon-1.4.1/MANIFEST.in --- python-falcon-1.0.0/MANIFEST.in 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/MANIFEST.in 2018-08-08 23:08:36.000000000 +0000 @@ -1,10 +1,10 @@ recursive-include tests *.py -recursive-include doc *.rst +recursive-include docs *.rst include .coveragerc include tox.ini -include README.md -include doc/conf.py doc/Makefile -graft doc/_static -graft doc/_themes -graft doc/_templates +include README.rst +include LICENSE +include docs/conf.py docs/Makefile +graft docs/_static +graft docs/_templates graft tools diff -Nru python-falcon-1.0.0/README.rst python-falcon-1.4.1/README.rst --- python-falcon-1.0.0/README.rst 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/README.rst 2018-08-08 23:08:36.000000000 +0000 @@ -1,46 +1,79 @@ -Falcon |Docs| |Build Status| |codecov.io| -========================================= +|Docs| |Build Status| |codecov.io| + +|Logo| The Falcon Web Framework +=============================== Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away. *- Antoine de Saint-Exupéry* -Falcon is a `high-performance Python -framework `__ for building cloud -APIs. It encourages the REST architectural style, and tries to do as -little as possible while remaining `highly -effective `__. +`Falcon `__ is a reliable, +high-performance Python web framework for building +large-scale app backends and microservices. It encourages the REST +architectural style, and tries to do as little as possible while +remaining highly effective. + +Falcon apps work with any WSGI server, and run great under +CPython 2.7, CPython 3.4+, PyPy2.7, and PyPy3.5. + +(Note: Support for CPython 2.6 and Jython 2.7 is deprecated and will be +removed in Falcon 2.0.) + +Support Falcon Development +-------------------------- + +Has Falcon helped you make an awesome app? Show your support today with a one-time donation or by becoming a corporate +sponsor. Backers get cool gear, an opportunity to promote their brand to Python developers, and +prioritized support. + +`Learn how to support Falcon development `_ Quick Links ----------- -* `Read the docs `__. -* `Subscribe to the community mailing list `__. -* `Hang out in #falconframework on freenode `__. - - -Design Goals ------------- - -**Fast.** Cloud APIs need to turn around requests quickly, and make -efficient use of hardware. This is particularly important when serving -many concurrent requests. Falcon is among the fastest WSGI frameworks -available, processing requests -`several times faster `__ than -other Python web frameworks. - -**Light.** Only the essentials are included, with *six* and *mimeparse* -being the only dependencies outside the standard library. We work hard -to keep the code lean, making Falcon easier to test, secure, optimize, -and deploy. - -**Flexible.** Falcon is not opinionated when it comes to talking to -databases, rendering content, authorizing requests, etc. You are free to -mix and match your own favorite libraries. Falcon apps work with -any WSGI server, and run great under `CPython 2.6-2.7, PyPy, Jython 2.7, -and CPython 3.3-3.5 `__. - +* `Read the docs `_ +* `Falcon add-ons and complementary packages `_ +* `Falcon talks, podcasts, and blog posts `_ +* `falconry/user for Falcon users `_ @ Gitter +* `falconry/dev for Falcon contributors `_ @ Gitter + +How is Falcon different? +------------------------ + +We designed Falcon to support the demanding needs of large-scale +microservices and responsive app backends. Falcon complements more +general Python web frameworks by providing bare-metal performance, +reliability, and flexibility wherever you need it. + +**Fast.** Same hardware, more requests. Falcon turns around +requests several times faster than most other Python frameworks. For +an extra speed boost, Falcon compiles itself with Cython when +available, and also works well with `PyPy `__. +Considering a move to another programming language? Benchmark with +Falcon + PyPy first. + +**Reliable.** We go to great lengths to avoid introducing +breaking changes, and when we do they are fully documented and only +introduced (in the spirit of +`SemVer `__) with a major version +increment. The code is rigorously tested with numerous inputs and we +require 100% coverage at all times. Six and mimeparse are the only +third-party dependencies. + +**Flexible.** Falcon leaves a lot of decisions and implementation +details to you, the API developer. This gives you a lot of freedom to +customize and tune your implementation. Due to Falcon's minimalist +design, Python community members are free to independently innovate on +`Falcon add-ons and complementary packages `__. + +**Debuggable.** Falcon eschews magic. It's easy to tell which inputs +lead to which outputs. Unhandled exceptions are never encapsulated or +masked. Potentially surprising behaviors, such as automatic request body +parsing, are well-documented and disabled by default. Finally, when it +comes to the framework itself, we take care to keep logic paths simple +and understandable. All this makes it easier to reason about the code +and to debug edge cases in large-scale deployments. Features -------- @@ -54,28 +87,75 @@ - Idiomatic HTTP error responses - Straightforward exception handling - Snappy unit testing through WSGI helpers and mocks -- CPython 2.6-2.7, PyPy, Jython 2.7, and CPython 3.3-3.5 support +- CPython 2.7, CPython 3.4+, PyPy2.7, and PyPy3.5 support - ~20% speed boost when Cython is available -Install -------- +Who's Using Falcon? +------------------- + +Falcon is used around the world by a growing number of organizations, +including: + +- 7ideas +- Cronitor +- EMC +- Hurricane Electric +- Leadpages +- OpenStack +- Rackspace +- Shiftgig +- tempfil.es +- Opera Software + +If you are using the Falcon framework for a community or commercial +project, please consider adding your information to our wiki under +`Who's Using Falcon? `_ + +Community +--------- + +A number of Falcon add-ons, templates, and complementary packages are +available for use in your projects. We've listed several of these on the +`Falcon wiki `_ as a starting +point, but you may also wish to search PyPI for additional resources. + +The Falconry community on Gitter is a great place to ask questions and +share your ideas. You can find us in `falconry/user +`_. We also have a +`falconry/dev `_ room for discussing +the design and development of the framework itself. + +Per our +`Code of Conduct `_, +we expect everyone who participates in community discussions to act +professionally, and lead by example in encouraging constructive +discussions. Each individual in the community is responsible for +creating a positive, constructive, and productive culture. + +Installation +------------ PyPy ^^^^ `PyPy `__ is the fastest way to run your Falcon app. -However, note that only the PyPy 2.7 compatible release is currently -supported. +Both PyPy2.7 and PyPy3.5 are supported as of PyPy v5.10. .. code:: bash $ pip install falcon +Or, to install the latest beta or release candidate, if any: + +.. code:: bash + + $ pip install --pre falcon + CPython ^^^^^^^ Falcon also fully supports -`CPython `__ 2.6-3.5. +`CPython `__ 2.6-3.6. A universal wheel is available on PyPI for the the Falcon framework. Installing it is as simple as: @@ -84,7 +164,17 @@ $ pip install falcon -Installing the wheel is a great way to get up and running with Falcon +If `ujson `__ is available, Falcon +will use it to speed up media (de)serialization, error serialization, +and query string parsing. Note that ``ujson`` can actually be slower +on PyPy than the standard ``json`` module due to ctypes overhead, and +so we recommend only using ``ujson`` with CPython deployments: + +.. code:: bash + + $ pip install ujson + +Installing the Falcon wheel is a great way to get up and running quickly in a development environment, but for an extra speed boost when deploying your application in production, Falcon can compile itself with Cython. @@ -99,6 +189,13 @@ $ pip install cython $ pip install --no-binary :all: falcon +If you want to verify that Cython is being invoked, simply +pass `-v` to pip in order to echo the compilation commands: + +.. code:: bash + + $ pip install -v --no-binary :all: falcon + **Installing on OS X** Xcode Command Line Tools are required to compile Cython. Install them @@ -122,20 +219,70 @@ $ export CFLAGS="-Qunused-arguments -Wno-unused-function" -Test ----- +Dependencies +^^^^^^^^^^^^ + +Falcon depends on `six` and `python-mimeparse`. `python-mimeparse` is a +better-maintained fork of the similarly named `mimeparse` project. +Normally the correct package will be selected by Falcon's ``setup.py``. +However, if you are using an alternate strategy to manage dependencies, +please take care to install the correct package in order to avoid +errors. + +WSGI Server +----------- + +Falcon speaks WSGI, and so in order to serve a Falcon app, you will +need a WSGI server. Gunicorn and uWSGI are some of the more popular +ones out there, but anything that can load a WSGI app will do. + +.. code:: bash + + $ pip install [gunicorn|uwsgi] + +Source Code +----------- + +Falcon `lives on GitHub `_, making the +code easy to browse, download, fork, etc. Pull requests are always welcome! Also, +please remember to star the project if it makes you happy. :) + +Once you have cloned the repo or downloaded a tarball from GitHub, you +can install Falcon like this: + +.. code:: bash + + $ cd falcon + $ pip install . + +Or, if you want to edit the code, first fork the main repo, clone the fork +to your desktop, and then run the following to install it using symbolic +linking, so that when you change your code, the changes will be automagically +available to your app without having to reinstall the package: + +.. code:: bash + + $ cd falcon + $ pip install -e . + +You can manually test changes to the Falcon framework by switching to the +directory of the cloned repo and then running pytest: .. code:: bash - $ pip install -r tools/test-requires - $ pip install nose && nosetests + $ cd falcon + $ pip install -r requirements/tests + $ pytest tests -To run the default set of tests: +Or, to run the default set of tests: .. code:: bash $ pip install tox && tox +See also the `tox.ini `_ +file for a full list of available environments. + Read the docs ------------- @@ -143,18 +290,22 @@ recommend keeping a REPL running while learning the framework so that you can query the various modules and classes as you have questions. -Online docs are available at: http://falcon.readthedocs.org +Online docs are available at: https://falcon.readthedocs.io You can build the same docs locally as follows: .. code:: bash - $ pip install -r tools/doc-requires - $ cd doc - $ make html + $ pip install tox && tox -e docs + +Once the docs have been built, you can view them by opening the following +index page in your browser. On OS X it's as simple as:: - $ # open _build/html/index.html + $ open docs/_build/html/index.html +Or on Linux: + + $ xdg-open docs/_build/html/index.html Getting started --------------- @@ -308,6 +459,8 @@ class JSONTranslator(object): + # NOTE: Starting with Falcon 1.3, you can simply + # use req.media and resp.media for this instead. def process_request(self, req, resp): # req.stream corresponds to the WSGI wsgi.input environ variable, @@ -334,10 +487,10 @@ 'UTF-8.') def process_response(self, req, resp, resource): - if 'result' not in req.context: + if 'result' not in resp.context: return - resp.body = json.dumps(req.context['result']) + resp.body = json.dumps(resp.context['result']) def max_body(limit): @@ -382,7 +535,10 @@ # create a custom class that inherits from falcon.Request. This # class could, for example, have an additional 'doc' property # that would serialize to JSON under the covers. - req.context['result'] = result + # + # NOTE: Starting with Falcon 1.3, you can simply + # use resp.media for this instead. + resp.context['result'] = result resp.set_header('Powered-By', 'Falcon') resp.status = falcon.HTTP_200 @@ -390,6 +546,8 @@ @falcon.before(max_body(64 * 1024)) def on_post(self, req, resp, user_id): try: + # NOTE: Starting with Falcon 1.3, you can simply + # use req.media for this instead. doc = req.context['doc'] except KeyError: raise falcon.HTTPBadRequest( @@ -431,42 +589,21 @@ httpd = simple_server.make_server('127.0.0.1', 8000, app) httpd.serve_forever() - -Community ---------- - -The Falcon community maintains a mailing list that you can use to share -your ideas and ask questions about the framework. We use the appropriately -minimalistic `Librelist `_ to host the discussions. - -To join the mailing list, simply send your first email to falcon@librelist.com! -This will automatically subscribe you to the mailing list *and* sends your email -along to the rest of the subscribers. For more information about managing your -subscription, check out the -`Librelist help page `_. - -We expect everyone who participates on the mailing list to act -professionally, and lead by example in encouraging constructive -discussions. Each individual in the community is responsible for -creating a positive, constructive, and productive culture. See also -the `Falcon Code of Conduct `__ - -`Discussions are archived `__ for -posterity. - -We also hang out in `#falconframework `__ on freenode, where everyone is -always welcome to ask questions and share ideas. - Contributing ------------ -Kurt Griffiths (kgriffs) is the creator and current maintainer of the -Falcon framework, with the generous help of a number of stylish and -talented contributors. - -Pull requests are always welcome. We use the GitHub issue tracker to -organize our work, put you do not need to open a new issue before -submitting a PR. +Thanks for your interest in the project! We welcome pull requests from +developers of all skill levels. To get started, simply fork the master branch +on GitHub to your personal account and then clone the fork into your +development environment. + +If you would like to contribute but don't already have something in mind, +we invite you to take a look at the issues listed under our +`next milestone `_. +If you see one you'd like to work on, please leave a quick comment so that we don't +end up with duplicated effort. Thanks in advance! + +Please note that all contributors and maintainers of this project are subject to our `Code of Conduct `_. Before submitting a pull request, please ensure you have added/updated the appropriate tests (and that all existing tests still pass with your @@ -485,12 +622,22 @@ - PERF(riker): Travel time to the nearest starbase? - APPSEC(riker): In all trust, there is the possibility for betrayal. +Kurt Griffiths (**kgriffs** on GH, Gitter, and Twitter) is the original +creator of the Falcon framework, and currently co-maintains the project +along with John Vrbanac (**jmvrbanac** on GH and Gitter, and +**jvrbanac** on Twitter). Falcon is developed by a growing community of +stylish users and contributors just like you. + +Please don't hesitate to reach out if you have any questions, or just need a +little help getting started. You can find us in +`falconry/dev `_ on Gitter. + See also: `CONTRIBUTING.md `__ Legal ----- -Copyright 2013-2016 by Rackspace Hosting, Inc. and other contributors as +Copyright 2013-2017 by Rackspace Hosting, Inc. and other contributors as noted in the individual source files. Falcon image courtesy of `John @@ -508,8 +655,11 @@ See the License for the specific language governing permissions and limitations under the License. +.. |Logo| image:: logo/logo.svg + :width: 30 + :height: 30 .. |Docs| image:: https://readthedocs.org/projects/falcon/badge/?version=stable - :target: http://falcon.readthedocs.org/en/stable/?badge=stable + :target: https://falcon.readthedocs.io/en/stable/?badge=stable :alt: Falcon web framework docs .. |Runner| image:: https://a248.e.akamai.net/assets.github.com/images/icons/emoji/runner.png :width: 20 diff -Nru python-falcon-1.0.0/requirements/bench python-falcon-1.4.1/requirements/bench --- python-falcon-1.0.0/requirements/bench 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/requirements/bench 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +django<2.0 # django>=2.0 no longer supports py27 +flask +pecan +bottle + +pprofile +vmprof \ No newline at end of file diff -Nru python-falcon-1.0.0/requirements/docs python-falcon-1.4.1/requirements/docs --- python-falcon-1.0.0/requirements/docs 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/requirements/docs 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,7 @@ +docutils +jinja2 +markupsafe +pygments +pygments-style-github +sphinx>=1.4.4,!=1.7.3 +sphinx_rtd_theme diff -Nru python-falcon-1.0.0/requirements/tests-py26-py33 python-falcon-1.4.1/requirements/tests-py26-py33 --- python-falcon-1.0.0/requirements/tests-py26-py33 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/requirements/tests-py26-py33 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# Pin to last versions to support Python 2.6/3.3 +# ----------------------------------------------------------------------------- + +pytest==3.2.5 +pyyaml==3.11 + +# Handler Specific +msgpack-python==0.4.8 + +# ----------------------------------------------------------------------------- +# Latest versions compatible with Python 2.6/3.3 +# ----------------------------------------------------------------------------- + +coverage>=4.1 +requests +six +testtools + +# Validator Specific +jsonschema diff -Nru python-falcon-1.0.0/requirements/tests-py27-py34 python-falcon-1.4.1/requirements/tests-py27-py34 --- python-falcon-1.0.0/requirements/tests-py27-py34 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/requirements/tests-py27-py34 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,17 @@ +# ----------------------------------------------------------------------------- +# Latest versions compatible with Python 2.6/3.3 +# ----------------------------------------------------------------------------- + +coverage>=4.1 +pytest +pyyaml +requests +six +testtools + +# Validator Specific +jsonschema + +# Handler Specific +msgpack + diff -Nru python-falcon-1.0.0/setup.cfg python-falcon-1.4.1/setup.cfg --- python-falcon-1.0.0/setup.cfg 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/setup.cfg 2018-08-08 23:08:36.000000000 +0000 @@ -4,11 +4,8 @@ [wheel] universal = 1 -[nosetests] -where = tests -verbosity = 2 +[aliases] +test=pytest -cover-package = falcon -cover-erase = true -cover-inclusive = true -cover-branches = true +[tool:pytest] +addopts = tests diff -Nru python-falcon-1.0.0/setup.py python-falcon-1.4.1/setup.py --- python-falcon-1.0.0/setup.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/setup.py 2018-08-08 23:08:36.000000000 +0000 @@ -3,17 +3,17 @@ import io import os from os import path -from setuptools import setup, find_packages, Extension import sys +from setuptools import Extension, find_packages, setup + MYDIR = path.abspath(os.path.dirname(__file__)) VERSION = imp.load_source('version', path.join('.', 'falcon', 'version.py')) VERSION = VERSION.__version__ -# NOTE(kgriffs): python-mimeparse is newer than mimeparse, supports Py3 -# TODO(kgriffs): Fork and optimize/modernize python-mimeparse -REQUIRES = ['six>=1.4.0', 'python-mimeparse'] +# NOTE(kgriffs): python-mimeparse is better-maintained fork of mimeparse +REQUIRES = ['six>=1.4.0', 'python-mimeparse>=1.5.2'] JYTHON = 'java' in sys.platform @@ -52,7 +52,7 @@ return module_names - package_names = ['falcon', 'falcon.util', 'falcon.routing'] + package_names = ['falcon', 'falcon.util', 'falcon.routing', 'falcon.media'] ext_modules = [ Extension( package + '.' + module, @@ -94,6 +94,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], keywords='wsgi web api framework rest http cloud', author='Kurt Griffiths', @@ -104,13 +105,13 @@ include_package_data=True, zip_safe=False, install_requires=REQUIRES, - setup_requires=[], cmdclass=cmdclass, ext_modules=ext_modules, - test_suite='nose.collector', + tests_require=['testtools', 'requests', 'pyyaml', 'pytest', 'pytest-runner'], entry_points={ 'console_scripts': [ - 'falcon-bench = falcon.cmd.bench:main' + 'falcon-bench = falcon.cmd.bench:main', + 'falcon-print-routes = falcon.cmd.print_routes:main' ] } ) diff -Nru python-falcon-1.0.0/tests/conftest.py python-falcon-1.4.1/tests/conftest.py --- python-falcon-1.0.0/tests/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/conftest.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,33 @@ +import platform + +import pytest + +import falcon + + +# NOTE(kgriffs): Some modules actually run a wsgiref server, so +# to ensure we reset the detection for the other modules, we just +# run this fixture before each one is tested. +@pytest.fixture(autouse=True, scope='module') +def reset_request_stream_detection(): + falcon.Request._wsgi_input_type_known = False + falcon.Request._always_wrap_wsgi_input = False + + +# NOTE(kgriffs): Patch pytest to make it compatible with Jython. This +# is necessary because val.encode() raises UnicodeEncodeError instead +# of UnicodeDecodeError, and running under Jython triggers this buggy +# code path in pytest. +if platform.python_implementation() == 'Jython': + import _pytest.python + + def _escape_strings(val): + if isinstance(val, bytes): + try: + return val.encode('ascii') + except UnicodeEncodeError: + return val.encode('string-escape') + else: + return val.encode('unicode-escape') + + _pytest.python._escape_strings = _escape_strings diff -Nru python-falcon-1.0.0/tests/dump_wsgi.py python-falcon-1.4.1/tests/dump_wsgi.py --- python-falcon-1.0.0/tests/dump_wsgi.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/dump_wsgi.py 2018-08-08 23:08:36.000000000 +0000 @@ -11,16 +11,19 @@ body += '}\n\n' - return [body.encode('utf-8')] + if not isinstance(body, bytes): + body = body.encode('utf-8') + + return [body] + app = application if __name__ == '__main__': - # import eventlet.wsgi - # import eventlet - # eventlet.wsgi.server(eventlet.listen(('localhost', 8000)), application) - from wsgiref.simple_server import make_server server = make_server('localhost', 8000, application) + + print('Listening on localhost:8000...') + server.serve_forever() diff -Nru python-falcon-1.0.0/tests/test_access_route.py python-falcon-1.4.1/tests/test_access_route.py --- python-falcon-1.0.0/tests/test_access_route.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_access_route.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -from falcon.request import Request -import falcon.testing as testing - - -class TestAccessRoute(testing.TestBase): - - def test_remote_addr_only(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route', - headers={ - 'Forwarded': ('for=192.0.2.43, for="[2001:db8:cafe::17]:555",' - 'for="unknown", by=_hidden,for="\\"\\\\",' - 'for="198\\.51\\.100\\.17\\:1236";' - 'proto=https;host=example.com') - })) - self.assertEqual(req.remote_addr, '127.0.0.1') - - def test_rfc_forwarded(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route', - headers={ - 'Forwarded': ('for=192.0.2.43,for=,' - 'for="[2001:db8:cafe::17]:555",' - 'for=x,' - 'for="unknown", by=_hidden,for="\\"\\\\",' - 'for="_don\\\"t_\\try_this\\\\at_home_\\42",' - 'for="198\\.51\\.100\\.17\\:1236";' - 'proto=https;host=example.com') - })) - compares = ['192.0.2.43', '2001:db8:cafe::17', 'x', - 'unknown', '"\\', '_don"t_try_this\\at_home_42', - '198.51.100.17'] - self.assertEqual(req.access_route, compares) - # test cached - self.assertEqual(req.access_route, compares) - - def test_malformed_rfc_forwarded(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route', - headers={ - 'Forwarded': 'for' - })) - self.assertEqual(req.access_route, []) - # test cached - self.assertEqual(req.access_route, []) - - def test_x_forwarded_for(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route', - headers={ - 'X-Forwarded-For': ('192.0.2.43, 2001:db8:cafe::17,' - 'unknown, _hidden, 203.0.113.60') - })) - self.assertEqual(req.access_route, - ['192.0.2.43', '2001:db8:cafe::17', - 'unknown', '_hidden', '203.0.113.60']) - - def test_x_real_ip(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route', - headers={ - 'X-Real-IP': '2001:db8:cafe::17' - })) - self.assertEqual(req.access_route, ['2001:db8:cafe::17']) - - def test_remote_addr(self): - req = Request(testing.create_environ( - host='example.com', - path='/access_route')) - self.assertEqual(req.access_route, ['127.0.0.1']) - - def test_remote_addr_missing(self): - env = testing.create_environ(host='example.com', path='/access_route') - del env['REMOTE_ADDR'] - - req = Request(env) - self.assertEqual(req.access_route, []) diff -Nru python-falcon-1.0.0/tests/test_after_hooks.py python-falcon-1.4.1/tests/test_after_hooks.py --- python-falcon-1.0.0/tests/test_after_hooks.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_after_hooks.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,11 +1,37 @@ import functools -import json + +import pytest + +try: + import ujson as json +except ImportError: + import json import falcon from falcon import testing # -------------------------------------------------------------------- +# Fixtures +# -------------------------------------------------------------------- + + +@pytest.fixture +def wrapped_resource_aware(): + return ClassResourceWithAwareHooks() + + +@pytest.fixture +def client(): + app = falcon.API() + + resource = WrappedRespondersResource() + app.add_route('/', resource) + + return testing.TestClient(app) + + +# -------------------------------------------------------------------- # Hooks # -------------------------------------------------------------------- @@ -22,8 +48,10 @@ resp.body = 'Nothing to see here. Move along.' -def fluffiness(req, resp): +def fluffiness(req, resp, animal=''): resp.body = 'fluffy' + if animal: + resp.set_header('X-Animal', animal) def resource_aware_fluffiness(req, resp, resource): @@ -37,14 +65,14 @@ fluffiness(req, resp) -def cuteness(req, resp): - if resp.body == 'fluffy': - resp.body += ' and cute' +def cuteness(req, resp, check, postfix=' and cute'): + if resp.body == check: + resp.body += postfix def resource_aware_cuteness(req, resp, resource): assert resource - cuteness(req, resp) + cuteness(req, resp, 'fluffy') class Smartness(object): @@ -57,23 +85,20 @@ # NOTE(kgriffs): Use partial methods for these next two in order # to make sure we handle that correctly. -def things_in_the_head(header, value, req, resp, params): +def things_in_the_head(header, value, req, resp): resp.set_header(header, value) bunnies_in_the_head = functools.partial(things_in_the_head, 'X-Bunnies', 'fluffy') + cuteness_in_the_head = functools.partial(things_in_the_head, 'X-Cuteness', 'cute') -def fluffiness_in_the_head(req, resp): - resp.set_header('X-Fluffiness', 'fluffy') - - -def cuteness_in_the_head(req, resp): - resp.set_header('X-Cuteness', 'cute') +def fluffiness_in_the_head(req, resp, value='fluffy'): + resp.set_header('X-Fluffiness', value) # -------------------------------------------------------------------- @@ -100,8 +125,8 @@ pass -@falcon.after(cuteness) -@falcon.after(fluffiness) +@falcon.after(cuteness, 'fluffy', postfix=' and innocent') +@falcon.after(fluffiness, 'kitten') class WrappedClassResource(object): # Test that the decorator skips non-callables @@ -122,6 +147,31 @@ self.resp = resp +class WrappedClassResourceChild(WrappedClassResource): + def on_head(self, req, resp): + # Test passing no extra args + super(WrappedClassResourceChild, self).on_head(req, resp) + + +class ClassResourceWithURIFields(object): + + @falcon.after(fluffiness_in_the_head, 'fluffy') + def on_get(self, req, resp, field1, field2): + self.fields = (field1, field2) + + +class ClassResourceWithURIFieldsChild(ClassResourceWithURIFields): + + def on_get(self, req, resp, field1, field2): + # Test passing mixed args and kwargs + super(ClassResourceWithURIFieldsChild, self).on_get( + req, + resp, + field1, + field2=field2 + ) + + # NOTE(swistakm): we use both type of hooks (class and method) # at once for the sake of simplicity @falcon.after(resource_aware_cuteness) @@ -162,70 +212,94 @@ # -------------------------------------------------------------------- -class TestHooks(testing.TestCase): - - def setUp(self): - super(TestHooks, self).setUp() - - self.resource = WrappedRespondersResource() - self.api.add_route('/', self.resource) - - self.wrapped_resource = WrappedClassResource() - self.api.add_route('/wrapped', self.wrapped_resource) - - self.wrapped_resource_aware = ClassResourceWithAwareHooks() - self.api.add_route('/wrapped_aware', self.wrapped_resource_aware) - - def test_output_validator(self): - result = self.simulate_get() - self.assertEqual(result.status_code, 723) - self.assertEqual(result.text, '{\n "title": "Tricky"\n}') - - def test_serializer(self): - result = self.simulate_put() - self.assertEqual('{"animal": "falcon"}', result.text) - - def test_hook_as_callable_class(self): - result = self.simulate_post() - self.assertEqual('smart', result.text) - - def test_wrapped_resource(self): - result = self.simulate_get('/wrapped') - self.assertEqual(result.status_code, 200) - self.assertEqual(result.text, 'fluffy and cute', ) - - result = self.simulate_head('/wrapped') - self.assertEqual(result.status_code, 200) - self.assertEqual(result.headers['X-Fluffiness'], 'fluffy') - self.assertEqual(result.headers['X-Cuteness'], 'cute') - - result = self.simulate_post('/wrapped') - self.assertEqual(result.status_code, 405) - - result = self.simulate_patch('/wrapped') - self.assertEqual(result.status_code, 405) - - # Decorator should not affect the default on_options responder - result = self.simulate_options('/wrapped') - self.assertEqual(result.status_code, 204) - self.assertFalse(result.text) - - def test_wrapped_resource_with_hooks_aware_of_resource(self): - expected = 'fluffy and cute' - - result = self.simulate_get('/wrapped_aware') - self.assertEqual(result.status_code, 200) - self.assertEqual(expected, result.text) - - for test in (self.simulate_head, self.simulate_put, self.simulate_post): - result = test('/wrapped_aware') - self.assertEqual(result.status_code, 200) - self.assertEqual(self.wrapped_resource_aware.resp.body, expected) - - result = self.simulate_patch('/wrapped_aware') - self.assertEqual(result.status_code, 405) - - # Decorator should not affect the default on_options responder - result = self.simulate_options('/wrapped_aware') - self.assertEqual(result.status_code, 204) - self.assertFalse(result.text) +def test_output_validator(client): + result = client.simulate_get() + assert result.status_code == 723 + assert result.text == json.dumps({'title': 'Tricky'}) + + +def test_serializer(client): + result = client.simulate_put() + assert result.text == json.dumps({'animal': 'falcon'}) + + +def test_hook_as_callable_class(client): + result = client.simulate_post() + assert 'smart' == result.text + + +@pytest.mark.parametrize( + 'resource', + [ + ClassResourceWithURIFields(), + ClassResourceWithURIFieldsChild() + ] +) +def test_resource_with_uri_fields(client, resource): + client.app.add_route('/{field1}/{field2}', resource) + + result = client.simulate_get('/82074/58927') + + assert result.status_code == 200 + assert result.headers['X-Fluffiness'] == 'fluffy' + assert 'X-Cuteness' not in result.headers + assert resource.fields == ('82074', '58927') + + +@pytest.mark.parametrize( + 'resource', + [ + WrappedClassResource(), + WrappedClassResourceChild() + ] +) +def test_wrapped_resource(client, resource): + client.app.add_route('/wrapped', resource) + result = client.simulate_get('/wrapped') + assert result.status_code == 200 + assert result.text == 'fluffy and innocent' + assert result.headers['X-Animal'] == 'kitten' + + result = client.simulate_head('/wrapped') + assert result.status_code == 200 + assert result.headers['X-Fluffiness'] == 'fluffy' + assert result.headers['X-Cuteness'] == 'cute' + assert result.headers['X-Animal'] == 'kitten' + + result = client.simulate_post('/wrapped') + assert result.status_code == 405 + + result = client.simulate_patch('/wrapped') + assert result.status_code == 405 + + # Decorator should not affect the default on_options responder + result = client.simulate_options('/wrapped') + assert result.status_code == 200 + assert not result.text + assert 'X-Animal' not in result.headers + + +def test_wrapped_resource_with_hooks_aware_of_resource(client, wrapped_resource_aware): + client.app.add_route('/wrapped_aware', wrapped_resource_aware) + expected = 'fluffy and cute' + + result = client.simulate_get('/wrapped_aware') + assert result.status_code == 200 + assert expected == result.text + + for test in ( + client.simulate_head, + client.simulate_put, + client.simulate_post, + ): + result = test(path='/wrapped_aware') + assert result.status_code == 200 + assert wrapped_resource_aware.resp.body == expected + + result = client.simulate_patch('/wrapped_aware') + assert result.status_code == 405 + + # Decorator should not affect the default on_options responder + result = client.simulate_options('/wrapped_aware') + assert result.status_code == 200 + assert not result.text diff -Nru python-falcon-1.0.0/tests/test_before_hooks.py python-falcon-1.4.1/tests/test_before_hooks.py --- python-falcon-1.0.0/tests/test_before_hooks.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_before_hooks.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,7 +1,13 @@ import functools -import json import io +import pytest + +try: + import ujson as json +except ImportError: + import json + import falcon import falcon.testing as testing @@ -11,28 +17,29 @@ 'formatted correctly.') -def validate_param(req, resp, params): - limit = req.get_param_as_int('limit') - if limit and int(limit) > 100: - raise falcon.HTTPBadRequest('Out of range', 'limit must be <= 100') +def validate_param(req, resp, params, param_name, maxval=100): + limit = req.get_param_as_int(param_name) + if limit and int(limit) > maxval: + msg = '{0} must be <= {1}'.format(param_name, maxval) + raise falcon.HTTPBadRequest('Out of Range', msg) -def resource_aware_validate_param(req, resp, resource, params): +def resource_aware_validate_param(req, resp, resource, params, param_name, maxval=100): assert resource - validate_param(req, resp, params) + validate_param(req, resp, params, param_name, maxval) class ResourceAwareValidateParam(object): def __call__(self, req, resp, resource, params): assert resource - validate_param(req, resp, params) + validate_param(req, resp, params, 'limit') -def validate_field(req, resp, params): +def validate_field(req, resp, params, field_name='test'): try: - params['id'] = int(params['id']) + params[field_name] = int(params[field_name]) except ValueError: - raise falcon.HTTPBadRequest('Invalid ID', 'ID was not valid.') + raise falcon.HTTPBadRequest() def parse_body(req, resp, params): @@ -71,16 +78,22 @@ resp.set_header(header, value) -bunnies_in_the_head = functools.partial(things_in_the_head, - 'X-Bunnies', 'fluffy') - -frogs_in_the_head = functools.partial(things_in_the_head, - 'X-Frogs', 'not fluffy') +bunnies_in_the_head = functools.partial( + things_in_the_head, + 'X-Bunnies', + 'fluffy' +) + +frogs_in_the_head = functools.partial( + things_in_the_head, + 'X-Frogs', + 'not fluffy' +) class WrappedRespondersResource(object): - @falcon.before(validate_param) + @falcon.before(validate_param, 'limit', 100) @falcon.before(parse_body) def on_get(self, req, resp, doc): self.req = req @@ -93,6 +106,17 @@ self.resp = resp +class WrappedRespondersResourceChild(WrappedRespondersResource): + + @falcon.before(validate_param, 'x', maxval=1000) + def on_get(self, req, resp): + pass + + def on_put(self, req, resp): + # Test passing no extra args + super(WrappedRespondersResourceChild, self).on_put(req, resp) + + @falcon.before(bunnies) class WrappedClassResource(object): @@ -101,11 +125,11 @@ # Test non-callable should be skipped by decorator on_patch = {} - @falcon.before(validate_param) + @falcon.before(validate_param, 'limit') def on_get(self, req, resp, bunnies): self._capture(req, resp, bunnies) - @falcon.before(validate_param) + @falcon.before(validate_param, 'limit') def on_head(self, req, resp, bunnies): self._capture(req, resp, bunnies) @@ -131,11 +155,11 @@ class ClassResourceWithAwareHooks(object): hook_as_class = ResourceAwareValidateParam() - @falcon.before(resource_aware_validate_param) + @falcon.before(resource_aware_validate_param, 'limit', 10) def on_get(self, req, resp, bunnies): self._capture(req, resp, bunnies) - @falcon.before(resource_aware_validate_param) + @falcon.before(resource_aware_validate_param, 'limit') def on_head(self, req, resp, bunnies): self._capture(req, resp, bunnies) @@ -155,11 +179,25 @@ class TestFieldResource(object): - @falcon.before(validate_field) + @falcon.before(validate_field, field_name='id') def on_get(self, req, resp, id): self.id = id +class TestFieldResourceChild(TestFieldResource): + + def on_get(self, req, resp, id): + # Test passing a single extra arg + super(TestFieldResourceChild, self).on_get(req, resp, id) + + +class TestFieldResourceChildToo(TestFieldResource): + + def on_get(self, req, resp, id): + # Test passing a single kwarg, but no extra args + super(TestFieldResourceChildToo, self).on_get(req, resp, id=id) + + @falcon.before(bunnies) @falcon.before(frogs) @falcon.before(Fish()) @@ -173,93 +211,141 @@ self.fish = fish -class TestHooks(testing.TestBase): +class ZooResourceChild(ZooResource): + + def on_get(self, req, resp): + super(ZooResourceChild, self).on_get( + req, + resp, + + # Test passing a mixture of args and kwargs + 'fluffy', + 'not fluffy', + fish='slippery' + ) + + +@pytest.fixture +def wrapped_aware_resource(): + return ClassResourceWithAwareHooks() + + +@pytest.fixture +def wrapped_resource(): + return WrappedClassResource() + + +@pytest.fixture +def resource(): + return WrappedRespondersResource() + + +@pytest.fixture +def client(resource): + app = falcon.API() + app.add_route('/', resource) + return testing.TestClient(app) + + +@pytest.mark.parametrize('resource', [ZooResource(), ZooResourceChild()]) +def test_multiple_resource_hooks(client, resource): + client.app.add_route('/', resource) + + result = client.simulate_get('/') + + assert 'not fluffy' == result.headers['X-Frogs'] + assert 'fluffy' == result.headers['X-Bunnies'] + + assert 'fluffy' == resource.bunnies + assert 'not fluffy' == resource.frogs + assert 'slippery' == resource.fish + + +def test_input_validator(client): + result = client.simulate_put('/') + assert result.status_code == 400 - def before(self): - self.resource = WrappedRespondersResource() - self.api.add_route(self.test_route, self.resource) - self.field_resource = TestFieldResource() - self.api.add_route('/queue/{id}/messages', self.field_resource) +def test_input_validator_inherited(client): + client.app.add_route('/', WrappedRespondersResourceChild()) + result = client.simulate_put('/') + assert result.status_code == 400 - self.wrapped_resource = WrappedClassResource() - self.api.add_route('/wrapped', self.wrapped_resource) + result = client.simulate_get('/', query_string='x=1000') + assert result.status_code == 200 - self.wrapped_aware_resource = ClassResourceWithAwareHooks() - self.api.add_route('/wrapped_aware', self.wrapped_aware_resource) + result = client.simulate_get('/', query_string='x=1001') + assert result.status_code == 400 - def test_multiple_resource_hooks(self): - zoo_resource = ZooResource() - self.api.add_route(self.test_route, zoo_resource) - self.simulate_request(self.test_route) +def test_param_validator(client): + result = client.simulate_get('/', query_string='limit=10', body='{}') + assert result.status_code == 200 - self.assertEqual('not fluffy', self.srmock.headers_dict['X-Frogs']) - self.assertEqual('fluffy', self.srmock.headers_dict['X-Bunnies']) + result = client.simulate_get('/', query_string='limit=101') + assert result.status_code == 400 - self.assertEqual('fluffy', zoo_resource.bunnies) - self.assertEqual('not fluffy', zoo_resource.frogs) - self.assertEqual('slippery', zoo_resource.fish) - def test_input_validator(self): - self.simulate_request(self.test_route, method='PUT') - self.assertEqual(falcon.HTTP_400, self.srmock.status) +@pytest.mark.parametrize( + 'resource', + [ + TestFieldResource(), + TestFieldResourceChild(), + TestFieldResourceChildToo(), + ] +) +def test_field_validator(client, resource): + client.app.add_route('/queue/{id}/messages', resource) + result = client.simulate_get('/queue/10/messages') + assert result.status_code == 200 + assert resource.id == 10 - def test_param_validator(self): - self.simulate_request(self.test_route, query_string='limit=10', - body='{}') - self.assertEqual(falcon.HTTP_200, self.srmock.status) + result = client.simulate_get('/queue/bogus/messages') + assert result.status_code == 400 - self.simulate_request(self.test_route, query_string='limit=101') - self.assertEqual(falcon.HTTP_400, self.srmock.status) - def test_field_validator(self): - self.simulate_request('/queue/10/messages') - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual(self.field_resource.id, 10) +def test_parser(client, resource): + client.simulate_get('/', body=json.dumps({'animal': 'falcon'})) + assert resource.doc == {'animal': 'falcon'} - self.simulate_request('/queue/bogus/messages') - self.assertEqual(falcon.HTTP_400, self.srmock.status) - def test_parser(self): - self.simulate_request(self.test_route, - body=json.dumps({'animal': 'falcon'})) +def test_wrapped_resource(client, wrapped_resource): + client.app.add_route('/wrapped', wrapped_resource) + result = client.simulate_patch('/wrapped') + assert result.status_code == 405 - self.assertEqual(self.resource.doc, {'animal': 'falcon'}) + result = client.simulate_get('/wrapped', query_string='limit=10') + assert result.status_code == 200 + assert 'fuzzy' == wrapped_resource.bunnies - def test_wrapped_resource(self): - self.simulate_request('/wrapped', method='PATCH') - self.assertEqual(falcon.HTTP_405, self.srmock.status) + result = client.simulate_head('/wrapped') + assert result.status_code == 200 + assert 'fuzzy' == wrapped_resource.bunnies - self.simulate_request('/wrapped', query_string='limit=10') - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_resource.bunnies) + result = client.simulate_post('/wrapped') + assert result.status_code == 200 + assert 'slippery' == wrapped_resource.fish - self.simulate_request('/wrapped', method='HEAD') - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_resource.bunnies) + result = client.simulate_get('/wrapped', query_string='limit=101') + assert result.status_code == 400 + assert wrapped_resource.bunnies == 'fuzzy' - self.simulate_request('/wrapped', method='POST') - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual('slippery', self.wrapped_resource.fish) - self.simulate_request('/wrapped', query_string='limit=101') - self.assertEqual(falcon.HTTP_400, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_resource.bunnies) +def test_wrapped_resource_with_hooks_aware_of_resource(client, wrapped_aware_resource): + client.app.add_route('/wrapped_aware', wrapped_aware_resource) - def test_wrapped_resource_with_hooks_aware_of_resource(self): - self.simulate_request('/wrapped_aware', method='PATCH') - self.assertEqual(falcon.HTTP_405, self.srmock.status) + result = client.simulate_patch('/wrapped_aware') + assert result.status_code == 405 - self.simulate_request('/wrapped_aware', query_string='limit=10') - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_aware_resource.bunnies) + result = client.simulate_get('/wrapped_aware', query_string='limit=10') + assert result.status_code == 200 + assert wrapped_aware_resource.bunnies == 'fuzzy' - for method in ('HEAD', 'PUT', 'POST'): - self.simulate_request('/wrapped_aware', method=method) - self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_aware_resource.bunnies) + for method in ('HEAD', 'PUT', 'POST'): + result = client.simulate_request(method, '/wrapped_aware') + assert result.status_code == 200 + assert wrapped_aware_resource.bunnies == 'fuzzy' - self.simulate_request('/wrapped_aware', query_string='limit=101') - self.assertEqual(falcon.HTTP_400, self.srmock.status) - self.assertEqual('fuzzy', self.wrapped_aware_resource.bunnies) + result = client.simulate_get('/wrapped_aware', query_string='limit=11') + assert result.status_code == 400 + assert wrapped_aware_resource.bunnies == 'fuzzy' diff -Nru python-falcon-1.0.0/tests/test_boundedstream.py python-falcon-1.4.1/tests/test_boundedstream.py --- python-falcon-1.0.0/tests/test_boundedstream.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_boundedstream.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,17 @@ +import io + +import pytest + +from falcon.request_helpers import BoundedStream + + +@pytest.fixture +def bounded_stream(): + return BoundedStream(io.BytesIO(), 1024) + + +def test_not_writeable(bounded_stream): + assert not bounded_stream.writeable() + + with pytest.raises(IOError): + bounded_stream.write(b'something something') diff -Nru python-falcon-1.0.0/tests/test_cmd_print_api.py python-falcon-1.4.1/tests/test_cmd_print_api.py --- python-falcon-1.0.0/tests/test_cmd_print_api.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_cmd_print_api.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,48 @@ +import six + +from falcon import API +from falcon.cmd import print_routes +from falcon.testing import redirected + + +class DummyResource(object): + + def on_get(self, req, resp): + resp.body = 'Test\n' + resp.status = '200 OK' + + +_api = API() +_api.add_route('/test', DummyResource()) + + +def test_traverse_with_verbose(): + """Ensure traverse() finds the proper routes and outputs verbose info.""" + + output = six.moves.StringIO() + with redirected(stdout=output): + print_routes.traverse(_api._router._roots, verbose=True) + + route, get_info, options_info = output.getvalue().strip().split('\n') + assert '-> /test' == route + + # NOTE(kgriffs) We might receive these in either order, since the + # method map is not ordered, so check and swap if necessary. + if options_info.startswith('-->GET'): + get_info, options_info = options_info, get_info + + assert options_info.startswith('-->OPTIONS') + assert 'falcon/responders.py:' in options_info + + assert get_info.startswith('-->GET') + assert 'tests/test_cmd_print_api.py:' in get_info + + +def test_traverse(): + """Ensure traverse() finds the proper routes.""" + output = six.moves.StringIO() + with redirected(stdout=output): + print_routes.traverse(_api._router._roots, verbose=False) + + route = output.getvalue().strip() + assert '-> /test' == route diff -Nru python-falcon-1.0.0/tests/test_cookies.py python-falcon-1.4.1/tests/test_cookies.py --- python-falcon-1.0.0/tests/test_cookies.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_cookies.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,14 +1,12 @@ -import re -import sys from datetime import datetime, timedelta, tzinfo +import re -import ddt +import pytest from six.moves.http_cookies import Morsel -from testtools.matchers import LessThan import falcon import falcon.testing as testing -from falcon.util import TimezoneGMT, http_date_to_dt +from falcon.util import http_date_to_dt, TimezoneGMT UNICODE_TEST_STRING = u'Unicode_\xc3\xa6\xc3\xb8' @@ -25,6 +23,7 @@ def dst(self, dt): return timedelta(hours=1) + GMT_PLUS_ONE = TimezoneGMTPlus1() @@ -59,167 +58,277 @@ 'foostring', 'bar', max_age='15', secure=False, http_only=False) -@ddt.ddt -class TestCookies(testing.TestBase): +@pytest.fixture() +def client(): + app = falcon.API() + app.add_route('/', CookieResource()) + app.add_route('/test-convert', CookieResourceMaxAgeFloatString()) - # - # Response - # - - def test_response_base_case(self): - self.resource = CookieResource() - self.api.add_route(self.test_route, self.resource) - self.simulate_request(self.test_route, method='GET') - if sys.version_info >= (3, 4, 3): - value = 'foo=bar; Domain=example.com; HttpOnly; Path=/; Secure' - else: - value = 'foo=bar; Domain=example.com; httponly; Path=/; secure' - self.assertIn(('set-cookie', value), self.srmock.headers) - - def test_response_complex_case(self): - self.resource = CookieResource() - self.api.add_route(self.test_route, self.resource) - self.simulate_request(self.test_route, method='HEAD') - if sys.version_info >= (3, 4, 3): - value = 'foo=bar; HttpOnly; Max-Age=300; Secure' - else: - value = 'foo=bar; httponly; Max-Age=300; secure' - self.assertIn(('set-cookie', value), self.srmock.headers) - if sys.version_info >= (3, 4, 3): - value = 'bar=baz; Secure' - else: - value = 'bar=baz; secure' - self.assertIn(('set-cookie', value), self.srmock.headers) - self.assertNotIn(('set-cookie', 'bad=cookie'), self.srmock.headers) - - def test_cookie_expires_naive(self): - self.resource = CookieResource() - self.api.add_route(self.test_route, self.resource) - self.simulate_request(self.test_route, method='POST') - self.assertIn( - ('set-cookie', 'foo=bar; expires=Sat, 01 Jan 2050 00:00:00 GMT'), - self.srmock.headers) - - def test_cookie_expires_aware(self): - self.resource = CookieResource() - self.api.add_route(self.test_route, self.resource) - self.simulate_request(self.test_route, method='PUT') - self.assertIn( - ('set-cookie', 'foo=bar; expires=Fri, 31 Dec 2049 23:00:00 GMT'), - self.srmock.headers) + return testing.TestClient(app) - def test_cookies_setable(self): - resp = falcon.Response() - self.assertIsNone(resp._cookies) +# ===================================================================== +# Response +# ===================================================================== - resp.set_cookie('foo', 'wrong-cookie', max_age=301) - resp.set_cookie('foo', 'bar', max_age=300) - morsel = resp._cookies['foo'] - self.assertIsInstance(morsel, Morsel) - self.assertEqual(morsel.key, 'foo') - self.assertEqual(morsel.value, 'bar') - self.assertEqual(morsel['max-age'], 300) - - def test_cookie_max_age_float_and_string(self): - # Falcon implicitly converts max-age values to integers, - # for ensuring RFC 6265-compliance of the attribute value. - self.resource = CookieResourceMaxAgeFloatString() - self.api.add_route(self.test_route, self.resource) - self.simulate_request(self.test_route, method='GET') - self.assertIn( - ('set-cookie', 'foofloat=bar; Max-Age=15'), self.srmock.headers) - self.assertIn( - ('set-cookie', 'foostring=bar; Max-Age=15'), self.srmock.headers) +def test_response_base_case(client): + result = client.simulate_get('/') - def test_response_unset_cookie(self): - resp = falcon.Response() - resp.unset_cookie('bad') - resp.set_cookie('bad', 'cookie', max_age=300) - resp.unset_cookie('bad') + cookie = result.cookies['foo'] + assert cookie.name == 'foo' + assert cookie.value == 'bar' + assert cookie.domain == 'example.com' + assert cookie.http_only + + # NOTE(kgriffs): Explicitly test for None to ensure + # falcon.testing.Cookie is returning exactly what we + # expect. Apps using falcon.testing.Cookie can be a + # bit more cavalier if they wish. + assert cookie.max_age is None + assert cookie.expires is None + + assert cookie.path == '/' + assert cookie.secure + + +def test_response_disable_secure_globally(client): + client.app.resp_options.secure_cookies_by_default = False + result = client.simulate_get('/') + cookie = result.cookies['foo'] + assert not cookie.secure + + client.app.resp_options.secure_cookies_by_default = True + result = client.simulate_get('/') + cookie = result.cookies['foo'] + assert cookie.secure + + +def test_response_complex_case(client): + result = client.simulate_head('/') + + assert len(result.cookies) == 3 + + cookie = result.cookies['foo'] + assert cookie.value == 'bar' + assert cookie.domain is None + assert cookie.expires is None + assert cookie.http_only + assert cookie.max_age == 300 + assert cookie.path is None + assert cookie.secure + + cookie = result.cookies['bar'] + assert cookie.value == 'baz' + assert cookie.domain is None + assert cookie.expires is None + assert not cookie.http_only + assert cookie.max_age is None + assert cookie.path is None + assert cookie.secure + + cookie = result.cookies['bad'] + assert cookie.value == '' # An unset cookie has an empty value + assert cookie.domain is None + + assert cookie.expires < datetime.utcnow() + + # NOTE(kgriffs): I know accessing a private attr like this is + # naughty of me, but we just need to sanity-check that the + # string is GMT. + assert cookie._expires.endswith('GMT') + + assert cookie.http_only + assert cookie.max_age is None + assert cookie.path is None + assert cookie.secure + + +def test_cookie_expires_naive(client): + result = client.simulate_post('/') + + cookie = result.cookies['foo'] + assert cookie.value == 'bar' + assert cookie.domain is None + assert cookie.expires == datetime(year=2050, month=1, day=1) + assert not cookie.http_only + assert cookie.max_age is None + assert cookie.path is None + assert not cookie.secure + + +def test_cookie_expires_aware(client): + result = client.simulate_put('/') + + cookie = result.cookies['foo'] + assert cookie.value == 'bar' + assert cookie.domain is None + assert cookie.expires == datetime(year=2049, month=12, day=31, hour=23) + assert not cookie.http_only + assert cookie.max_age is None + assert cookie.path is None + assert not cookie.secure + + +def test_cookies_setable(client): + resp = falcon.Response() + + assert resp._cookies is None + + resp.set_cookie('foo', 'wrong-cookie', max_age=301) + resp.set_cookie('foo', 'bar', max_age=300) + morsel = resp._cookies['foo'] - morsels = list(resp._cookies.values()) - self.assertEqual(len(morsels), 1) + assert isinstance(morsel, Morsel) + assert morsel.key == 'foo' + assert morsel.value == 'bar' + assert morsel['max-age'] == 300 - bad_cookie = morsels[0] - self.assertEqual(bad_cookie['expires'], -1) - output = bad_cookie.OutputString() - self.assertTrue('bad=;' in output or 'bad="";' in output) +@pytest.mark.parametrize('cookie_name', ('foofloat', 'foostring')) +def test_cookie_max_age_float_and_string(client, cookie_name): + # NOTE(tbug): Falcon implicitly converts max-age values to integers, + # to ensure RFC 6265-compliance of the attribute value. - match = re.search('expires=([^;]+)', output) - self.assertIsNotNone(match) - - expiration = http_date_to_dt(match.group(1), obs_date=True) - self.assertThat(expiration, LessThan(datetime.utcnow())) - - def test_cookie_timezone(self): - tz = TimezoneGMT() - self.assertEqual('GMT', tz.tzname(timedelta(0))) - - # - # Request - # - - def test_request_cookie_parsing(self): - # testing with a github-ish set of cookies - headers = [ - ('Cookie', ''' - logged_in=no;_gh_sess=eyJzZXXzaW9uX2lkIjoiN2; - tz=Europe/Berlin; _ga=GA1.2.332347814.1422308165; - _gat=1; - _octo=GH1.1.201722077.1422308165'''), - ] - - environ = testing.create_environ(headers=headers) - req = falcon.Request(environ) - - self.assertEqual('no', req.cookies['logged_in']) - self.assertEqual('Europe/Berlin', req.cookies['tz']) - self.assertEqual('GH1.1.201722077.1422308165', req.cookies['_octo']) - - self.assertIn('logged_in', req.cookies) - self.assertIn('_gh_sess', req.cookies) - self.assertIn('tz', req.cookies) - self.assertIn('_ga', req.cookies) - self.assertIn('_gat', req.cookies) - self.assertIn('_octo', req.cookies) - - def test_unicode_inside_ascii_range(self): - resp = falcon.Response() - - # should be ok - resp.set_cookie('non_unicode_ascii_name_1', 'ascii_value') - resp.set_cookie(u'unicode_ascii_name_1', 'ascii_value') - resp.set_cookie('non_unicode_ascii_name_2', u'unicode_ascii_value') - resp.set_cookie(u'unicode_ascii_name_2', u'unicode_ascii_value') + result = client.simulate_get('/test-convert') - @ddt.data( + cookie = result.cookies[cookie_name] + assert cookie.value == 'bar' + assert cookie.domain is None + assert cookie.expires is None + assert not cookie.http_only + assert cookie.max_age == 15 + assert cookie.path is None + assert not cookie.secure + + +def test_response_unset_cookie(client): + resp = falcon.Response() + resp.unset_cookie('bad') + resp.set_cookie('bad', 'cookie', max_age=300) + resp.unset_cookie('bad') + + morsels = list(resp._cookies.values()) + len(morsels) == 1 + + bad_cookie = morsels[0] + bad_cookie['expires'] == -1 + + output = bad_cookie.OutputString() + assert 'bad=;' in output or 'bad="";' in output + + match = re.search('expires=([^;]+)', output) + assert match + + expiration = http_date_to_dt(match.group(1), obs_date=True) + assert expiration < datetime.utcnow() + + +def test_cookie_timezone(client): + tz = TimezoneGMT() + assert tz.tzname(timedelta(0)) == 'GMT' + + +# ===================================================================== +# Request +# ===================================================================== + + +def test_request_cookie_parsing(): + # testing with a github-ish set of cookies + headers = [ + ( + 'Cookie', + """ + logged_in=no;_gh_sess=eyJzZXXzaW9uX2lkIjoiN2; + tz=Europe/Berlin; _ga=GA1.2.332347814.1422308165; + _gat=1; + _octo=GH1.1.201722077.1422308165 + """ + ), + ] + + environ = testing.create_environ(headers=headers) + req = falcon.Request(environ) + + assert req.cookies['logged_in'] == 'no' + assert req.cookies['tz'] == 'Europe/Berlin' + assert req.cookies['_octo'] == 'GH1.1.201722077.1422308165' + + assert 'logged_in' in req.cookies + assert '_gh_sess' in req.cookies + assert 'tz' in req.cookies + assert '_ga' in req.cookies + assert '_gat' in req.cookies + assert '_octo' in req.cookies + + +def test_invalid_cookies_are_ignored(): + headers = [ + ( + 'Cookie', + """ + good_cookie=foo; + bad{cookie=bar + """ + ), + ] + + environ = testing.create_environ(headers=headers) + req = falcon.Request(environ) + + assert req.cookies['good_cookie'] == 'foo' + assert 'bad{cookie' not in req.cookies + + +def test_cookie_header_is_missing(): + environ = testing.create_environ(headers={}) + req = falcon.Request(environ) + assert req.cookies == {} + + +def test_unicode_inside_ascii_range(): + resp = falcon.Response() + + # should be ok + resp.set_cookie('non_unicode_ascii_name_1', 'ascii_value') + resp.set_cookie(u'unicode_ascii_name_1', 'ascii_value') + resp.set_cookie('non_unicode_ascii_name_2', u'unicode_ascii_value') + resp.set_cookie(u'unicode_ascii_name_2', u'unicode_ascii_value') + + +@pytest.mark.parametrize( + 'name', + ( UNICODE_TEST_STRING, UNICODE_TEST_STRING.encode('utf-8'), 42 ) - def test_non_ascii_name(self, name): - resp = falcon.Response() - self.assertRaises(KeyError, resp.set_cookie, - name, 'ok_value') +) +def test_non_ascii_name(name): + resp = falcon.Response() + with pytest.raises(KeyError): + resp.set_cookie(name, 'ok_value') + - @ddt.data( +@pytest.mark.parametrize( + 'value', + ( UNICODE_TEST_STRING, UNICODE_TEST_STRING.encode('utf-8'), 42 ) - def test_non_ascii_value(self, value): - resp = falcon.Response() - - # NOTE(tbug): we need to grab the exception to check - # that it is not instance of UnicodeEncodeError, so - # we cannot simply use assertRaises - try: - resp.set_cookie('ok_name', value) - except ValueError as e: - self.assertIsInstance(e, ValueError) - self.assertNotIsInstance(e, UnicodeEncodeError) - else: - self.fail('set_bad_cookie_value did not fail as expected') +) +def test_non_ascii_value(value): + resp = falcon.Response() + + # NOTE(tbug): we need to grab the exception to check + # that it is not instance of UnicodeEncodeError, so + # we cannot simply use pytest.raises + try: + resp.set_cookie('ok_name', value) + except ValueError as e: + assert isinstance(e, ValueError) + assert not isinstance(e, UnicodeEncodeError) + else: + pytest.fail('set_bad_cookie_value did not fail as expected') diff -Nru python-falcon-1.0.0/tests/test_custom_router.py python-falcon-1.4.1/tests/test_custom_router.py --- python-falcon-1.0.0/tests/test_custom_router.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_custom_router.py 2018-08-08 23:08:36.000000000 +0000 @@ -2,70 +2,121 @@ from falcon import testing -class TestCustomRouter(testing.TestBase): +def test_custom_router_add_route_should_be_used(): + check = [] - def test_custom_router_add_route_should_be_used(self): + class CustomRouter(object): + def add_route(self, uri_template, *args, **kwargs): + check.append(uri_template) - check = [] + def find(self, uri): + pass - class CustomRouter(object): - def add_route(self, uri_template, *args, **kwargs): - check.append(uri_template) + app = falcon.API(router=CustomRouter()) + app.add_route('/test', 'resource') - api = falcon.API(router=CustomRouter()) - api.add_route('/test', 'resource') + assert len(check) == 1 + assert '/test' in check - self.assertEqual(len(check), 1) - self.assertIn('/test', check) - def test_custom_router_find_should_be_used(self): +def test_custom_router_find_should_be_used(): - def resource(req, resp, **kwargs): - resp.body = '{"status": "ok"}' + def resource(req, resp, **kwargs): + resp.body = '{{"uri_template": "{0}"}}'.format(req.uri_template) - class CustomRouter(object): - def __init__(self): - self.reached_backwards_compat = False + class CustomRouter(object): + def __init__(self): + self.reached_backwards_compat = False - def find(self, uri): - if uri == '/test': - return resource, {'GET': resource}, {} + def find(self, uri): + if uri == '/test/42': + return resource, {'GET': resource}, {}, '/test/{id}' - if uri == '/404/backwards-compat': - self.reached_backwards_compat = True - return (None, None, None) + if uri == '/test/42/no-uri-template': + return resource, {'GET': resource}, {}, None - return None + if uri == '/test/42/uri-template/backwards-compat': + return resource, {'GET': resource}, {} - router = CustomRouter() - self.api = falcon.API(router=router) - body = self.simulate_request('/test') - self.assertEqual(body, [b'{"status": "ok"}']) + if uri == '/404/backwards-compat': + self.reached_backwards_compat = True + return (None, None, None) - for uri in ('/404', '/404/backwards-compat'): - body = self.simulate_request(uri) - self.assertFalse(body) - self.assertEqual(self.srmock.status, falcon.HTTP_404) + return None - self.assertTrue(router.reached_backwards_compat) + router = CustomRouter() + app = falcon.API(router=router) + client = testing.TestClient(app) - def test_can_pass_additional_params_to_add_route(self): + response = client.simulate_request(path='/test/42') + assert response.content == b'{"uri_template": "/test/{id}"}' - check = [] + response = client.simulate_request(path='/test/42/no-uri-template') + assert response.content == b'{"uri_template": "None"}' - class CustomRouter(object): - def add_route(self, uri_template, method_map, resource, name): - self._index = {name: uri_template} - check.append(name) + response = client.simulate_request(path='/test/42/uri-template/backwards-compat') + assert response.content == b'{"uri_template": "None"}' - api = falcon.API(router=CustomRouter()) - api.add_route('/test', 'resource', name='my-url-name') + for uri in ('/404', '/404/backwards-compat'): + response = client.simulate_request(path=uri) + assert not response.content + assert response.status == falcon.HTTP_404 - self.assertEqual(len(check), 1) - self.assertIn('my-url-name', check) + assert router.reached_backwards_compat - # Also as arg. - api.add_route('/test', 'resource', 'my-url-name-arg') - self.assertEqual(len(check), 2) - self.assertIn('my-url-name-arg', check) +def test_can_pass_additional_params_to_add_route(): + + check = [] + + class CustomRouter(object): + def add_route(self, uri_template, method_map, resource, name): + self._index = {name: uri_template} + check.append(name) + + def find(self, uri): + pass + + app = falcon.API(router=CustomRouter()) + app.add_route('/test', 'resource', name='my-url-name') + + assert len(check) == 1 + assert 'my-url-name' in check + + # Also as arg. + app.add_route('/test', 'resource', 'my-url-name-arg') + + assert len(check) == 2 + assert 'my-url-name-arg' in check + + +def test_custom_router_takes_req_positional_argument(): + def responder(req, resp): + resp.body = 'OK' + + class CustomRouter(object): + def find(self, uri, req): + if uri == '/test' and isinstance(req, falcon.Request): + return responder, {'GET': responder}, {}, None + + router = CustomRouter() + app = falcon.API(router=router) + client = testing.TestClient(app) + response = client.simulate_request(path='/test') + assert response.content == b'OK' + + +def test_custom_router_takes_req_keyword_argument(): + def responder(req, resp): + resp.body = 'OK' + + class CustomRouter(object): + def find(self, uri, req=None): + if uri == '/test' and isinstance(req, falcon.Request): + return responder, {'GET': responder}, {}, None + + router = CustomRouter() + app = falcon.API(router=router) + client = testing.TestClient(app) + response = client.simulate_request(path='/test') + assert response.content == b'OK' diff -Nru python-falcon-1.0.0/tests/test_default_router.py python-falcon-1.4.1/tests/test_default_router.py --- python-falcon-1.0.0/tests/test_default_router.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_default_router.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,7 +1,117 @@ -import ddt +import textwrap +import pytest + +import falcon +from falcon import testing from falcon.routing import DefaultRouter -import falcon.testing as testing + + +@pytest.fixture +def client(): + return testing.TestClient(falcon.API()) + + +@pytest.fixture +def router(): + router = DefaultRouter() + + router.add_route( + '/repos', {}, ResourceWithId(1)) + router.add_route( + '/repos/{org}', {}, ResourceWithId(2)) + router.add_route( + '/repos/{org}/{repo}', {}, ResourceWithId(3)) + router.add_route( + '/repos/{org}/{repo}/commits', {}, ResourceWithId(4)) + router.add_route( + u'/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}', + {}, ResourceWithId(5)) + + router.add_route( + '/teams/{id}', {}, ResourceWithId(6)) + router.add_route( + '/teams/{id}/members', {}, ResourceWithId(7)) + + router.add_route( + '/teams/default', {}, ResourceWithId(19)) + router.add_route( + '/teams/default/members/thing', {}, ResourceWithId(19)) + + router.add_route( + '/user/memberships', {}, ResourceWithId(8)) + router.add_route( + '/emojis', {}, ResourceWithId(9)) + router.add_route( + '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/full', + {}, ResourceWithId(10)) + router.add_route( + '/repos/{org}/{repo}/compare/all', {}, ResourceWithId(11)) + + # NOTE(kgriffs): The ordering of these calls is significant; we + # need to test that the {id} field does not match the other routes, + # regardless of the order they are added. + router.add_route( + '/emojis/signs/0', {}, ResourceWithId(12)) + router.add_route( + '/emojis/signs/{id}', {}, ResourceWithId(13)) + router.add_route( + '/emojis/signs/42', {}, ResourceWithId(14)) + router.add_route( + '/emojis/signs/42/small.jpg', {}, ResourceWithId(23)) + router.add_route( + '/emojis/signs/78/small.png', {}, ResourceWithId(24)) + + # Test some more special chars + router.add_route( + '/emojis/signs/78/small(png)', {}, ResourceWithId(25)) + router.add_route( + '/emojis/signs/78/small_png', {}, ResourceWithId(26)) + router.add_route('/images/{id}.gif', {}, ResourceWithId(27)) + + router.add_route( + '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/part', + {}, ResourceWithId(15)) + router.add_route( + '/repos/{org}/{repo}/compare/{usr0}:{branch0}', + {}, ResourceWithId(16)) + router.add_route( + '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full', + {}, ResourceWithId(17)) + + router.add_route( + '/gists/{id}/{representation}', {}, ResourceWithId(21)) + router.add_route( + '/gists/{id}/raw', {}, ResourceWithId(18)) + router.add_route( + '/gists/first', {}, ResourceWithId(20)) + + router.add_route('/item/{q}', {}, ResourceWithId(28)) + + # ---------------------------------------------------------------- + # Routes with field converters + # ---------------------------------------------------------------- + + router.add_route( + '/cvt/teams/{id:int(min=7)}', {}, ResourceWithId(29)) + router.add_route( + '/cvt/teams/{id:int(min=7)}/members', {}, ResourceWithId(30)) + router.add_route( + '/cvt/teams/default', {}, ResourceWithId(31)) + router.add_route( + '/cvt/teams/default/members/{id:int}-{tenure:int}', {}, ResourceWithId(32)) + + router.add_route( + '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}...{usr1}:{branch1:int}/part', + {}, ResourceWithId(33)) + router.add_route( + '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}', + {}, ResourceWithId(34)) + router.add_route( + '/cvt/repos/{org}/{repo}/compare/{usr0}:{branch0:int}/full', + {}, ResourceWithId(35)) + + return router class ResourceWithId(object): @@ -15,283 +125,534 @@ resp.body = self.resource_id -class TestRegressionCases(testing.TestBase): - """Test specific repros reported by users of the framework.""" +class SpamConverter(object): + def __init__(self, times, eggs=False): + self._times = times + self._eggs = eggs - def before(self): - self.router = DefaultRouter() + def convert(self, fragment): + item = fragment + if self._eggs: + item += '&eggs' - def test_versioned_url(self): - self.router.add_route('/{version}/messages', {}, ResourceWithId(2)) + return ', '.join(item for i in range(self._times)) - resource, method_map, params = self.router.find('/v2/messages') - self.assertEqual(resource.resource_id, 2) - - self.router.add_route('/v2', {}, ResourceWithId(1)) - - resource, method_map, params = self.router.find('/v2') - self.assertEqual(resource.resource_id, 1) - - resource, method_map, params = self.router.find('/v2/messages') - self.assertEqual(resource.resource_id, 2) - - resource, method_map, params = self.router.find('/v1/messages') - self.assertEqual(resource.resource_id, 2) - - route = self.router.find('/v1') - self.assertIs(route, None) - - def test_recipes(self): - self.router.add_route( - '/recipes/{activity}/{type_id}', {}, ResourceWithId(1)) - self.router.add_route( - '/recipes/baking', {}, ResourceWithId(2)) - - resource, method_map, params = self.router.find('/recipes/baking/4242') - self.assertEqual(resource.resource_id, 1) - - resource, method_map, params = self.router.find('/recipes/baking') - self.assertEqual(resource.resource_id, 2) - - route = self.router.find('/recipes/grilling') - self.assertIs(route, None) - - -@ddt.ddt -class TestComplexRouting(testing.TestBase): - def before(self): - self.router = DefaultRouter() - - self.router.add_route( - '/repos', {}, ResourceWithId(1)) - self.router.add_route( - '/repos/{org}', {}, ResourceWithId(2)) - self.router.add_route( - '/repos/{org}/{repo}', {}, ResourceWithId(3)) - self.router.add_route( - '/repos/{org}/{repo}/commits', {}, ResourceWithId(4)) - self.router.add_route( - '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}', - {}, ResourceWithId(5)) - - self.router.add_route( - '/teams/{id}', {}, ResourceWithId(6)) - self.router.add_route( - '/teams/{id}/members', {}, ResourceWithId(7)) - - self.router.add_route( - '/teams/default', {}, ResourceWithId(19)) - self.router.add_route( - '/teams/default/members/thing', {}, ResourceWithId(19)) - - self.router.add_route( - '/user/memberships', {}, ResourceWithId(8)) - self.router.add_route( - '/emojis', {}, ResourceWithId(9)) - self.router.add_route( - '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/full', - {}, ResourceWithId(10)) - self.router.add_route( - '/repos/{org}/{repo}/compare/all', {}, ResourceWithId(11)) - - # NOTE(kgriffs): The ordering of these calls is significant; we - # need to test that the {id} field does not match the other routes, - # regardless of the order they are added. - self.router.add_route( - '/emojis/signs/0', {}, ResourceWithId(12)) - self.router.add_route( - '/emojis/signs/{id}', {}, ResourceWithId(13)) - self.router.add_route( - '/emojis/signs/42', {}, ResourceWithId(14)) - self.router.add_route( - '/emojis/signs/42/small', {}, ResourceWithId(14.1)) - self.router.add_route( - '/emojis/signs/78/small', {}, ResourceWithId(22)) - - self.router.add_route( - '/repos/{org}/{repo}/compare/{usr0}:{branch0}...{usr1}:{branch1}/part', - {}, ResourceWithId(15)) - self.router.add_route( - '/repos/{org}/{repo}/compare/{usr0}:{branch0}', - {}, ResourceWithId(16)) - self.router.add_route( - '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full', - {}, ResourceWithId(17)) - - self.router.add_route( - '/gists/{id}/{representation}', {}, ResourceWithId(21)) - self.router.add_route( - '/gists/{id}/raw', {}, ResourceWithId(18)) - self.router.add_route( - '/gists/first', {}, ResourceWithId(20)) - - @ddt.data( - '/teams/{collision}', # simple vs simple - '/emojis/signs/{id_too}', # another simple vs simple - '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}:{collision}', - ) - def test_collision(self, template): - self.assertRaises( - ValueError, - self.router.add_route, template, {}, ResourceWithId(-1) - ) - - @ddt.data( - '/repos/{org}/{repo}/compare/{simple-vs-complex}', - '/repos/{complex}.{vs}.{simple}', - '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}/full', - ) - def test_non_collision(self, template): - self.router.add_route(template, {}, ResourceWithId(-1)) - def test_dump(self): - print(self.router._src) +# ===================================================================== +# Regression tests for use cases reported by users +# ===================================================================== - def test_override(self): - self.router.add_route('/emojis/signs/0', {}, ResourceWithId(-1)) - resource, method_map, params = self.router.find('/emojis/signs/0') - self.assertEqual(resource.resource_id, -1) +def test_user_regression_versioned_url(): + router = DefaultRouter() + router.add_route('/{version}/messages', {}, ResourceWithId(2)) - def test_literal_segment(self): - resource, method_map, params = self.router.find('/emojis/signs/0') - self.assertEqual(resource.resource_id, 12) + resource, __, __, __ = router.find('/v2/messages') + assert resource.resource_id == 2 - resource, method_map, params = self.router.find('/emojis/signs/1') - self.assertEqual(resource.resource_id, 13) + router.add_route('/v2', {}, ResourceWithId(1)) - resource, method_map, params = self.router.find('/emojis/signs/42') - self.assertEqual(resource.resource_id, 14) + resource, __, __, __ = router.find('/v2') + assert resource.resource_id == 1 - resource, method_map, params = self.router.find('/emojis/signs/42/small') - self.assertEqual(resource.resource_id, 14.1) + resource, __, __, __ = router.find('/v2/messages') + assert resource.resource_id == 2 - route = self.router.find('/emojis/signs/1/small') - self.assertEqual(route, None) + resource, __, __, __ = router.find('/v1/messages') + assert resource.resource_id == 2 - @ddt.data( - '/teams', - '/emojis/signs', - '/gists', - '/gists/42', - ) - def test_dead_segment(self, template): - route = self.router.find(template) - self.assertIs(route, None) - - def test_malformed_pattern(self): - route = self.router.find( - '/repos/racker/falcon/compare/foo') - self.assertIs(route, None) - - route = self.router.find( - '/repos/racker/falcon/compare/foo/full') - self.assertIs(route, None) - - def test_literal(self): - resource, method_map, params = self.router.find('/user/memberships') - self.assertEqual(resource.resource_id, 8) - - def test_variable(self): - resource, method_map, params = self.router.find('/teams/42') - self.assertEqual(resource.resource_id, 6) - self.assertEqual(params, {'id': '42'}) - - resource, method_map, params = self.router.find('/emojis/signs/stop') - self.assertEqual(params, {'id': 'stop'}) - - resource, method_map, params = self.router.find('/gists/42/raw') - self.assertEqual(params, {'id': '42'}) - - @ddt.data( - ('/teams/default', 19), - ('/teams/default/members', 7), - ('/teams/foo', 6), - ('/teams/foo/members', 7), - ('/gists/first', 20), - ('/gists/first/raw', 18), - ('/gists/first/pdf', 21), - ('/gists/1776/pdf', 21), - ('/emojis/signs/78', 13), - ('/emojis/signs/78/small', 22), + route = router.find('/v1') + assert route is None + + +def test_user_regression_recipes(): + router = DefaultRouter() + router.add_route( + '/recipes/{activity}/{type_id}', + {}, + ResourceWithId(1) ) - @ddt.unpack - def test_literal_vs_variable(self, path, expected_id): - resource, method_map, params = self.router.find(path) - self.assertEqual(resource.resource_id, expected_id) - - @ddt.data( - # Misc. - '/this/does/not/exist', - '/user/bogus', - '/repos/racker/falcon/compare/johndoe:master...janedoe:dev/bogus', - - # Literal vs variable (teams) - '/teams', - '/teams/42/members/undefined', - '/teams/42/undefined', - '/teams/42/undefined/segments', - '/teams/default/members/undefined', - '/teams/default/members/thing/undefined', - '/teams/default/members/thing/undefined/segments', - '/teams/default/undefined', - '/teams/default/undefined/segments', - - # Literal vs variable (emojis) - '/emojis/signs', - '/emojis/signs/0/small', - '/emojis/signs/0/undefined', - '/emojis/signs/0/undefined/segments', - '/emojis/signs/20/small', - '/emojis/signs/20/undefined', - '/emojis/signs/42/undefined', - '/emojis/signs/78/undefined', + router.add_route( + '/recipes/baking', + {}, + ResourceWithId(2) ) - def test_not_found(self, path): - route = self.router.find(path) - self.assertIs(route, None) - - def test_subsegment_not_found(self): - route = self.router.find('/emojis/signs/0/x') - self.assertIs(route, None) - - def test_multivar(self): - resource, method_map, params = self.router.find( - '/repos/racker/falcon/commits') - self.assertEqual(resource.resource_id, 4) - self.assertEqual(params, {'org': 'racker', 'repo': 'falcon'}) - - resource, method_map, params = self.router.find( - '/repos/racker/falcon/compare/all') - self.assertEqual(resource.resource_id, 11) - self.assertEqual(params, {'org': 'racker', 'repo': 'falcon'}) - - @ddt.data(('', 5), ('/full', 10), ('/part', 15)) - @ddt.unpack - def test_complex(self, url_postfix, resource_id): - uri = '/repos/racker/falcon/compare/johndoe:master...janedoe:dev' - resource, method_map, params = self.router.find(uri + url_postfix) - - self.assertEqual(resource.resource_id, resource_id) - self.assertEqual(params, { - 'org': 'racker', - 'repo': 'falcon', - 'usr0': 'johndoe', - 'branch0': 'master', - 'usr1': 'janedoe', - 'branch1': 'dev', - }) - @ddt.data(('', 16), ('/full', 17)) - @ddt.unpack - def test_complex_alt(self, url_postfix, resource_id): - uri = '/repos/falconry/falcon/compare/johndoe:master' - resource, method_map, params = self.router.find(uri + url_postfix) - - self.assertEqual(resource.resource_id, resource_id) - self.assertEqual(params, { - 'org': 'falconry', - 'repo': 'falcon', - 'usr0': 'johndoe', - 'branch0': 'master', + resource, __, __, __ = router.find('/recipes/baking/4242') + assert resource.resource_id == 1 + + resource, __, __, __ = router.find('/recipes/baking') + assert resource.resource_id == 2 + + route = router.find('/recipes/grilling') + assert route is None + + +@pytest.mark.parametrize('uri_template,path,expected_params', [ + ('/serviceRoot/People|{field}', '/serviceRoot/People|susie', {'field': 'susie'}), + ('/serviceRoot/People[{field}]', "/serviceRoot/People['calvin']", {'field': "'calvin'"}), + ('/serviceRoot/People({field})', "/serviceRoot/People('hobbes')", {'field': "'hobbes'"}), + ('/serviceRoot/People({field})', "/serviceRoot/People('hob)bes')", {'field': "'hob)bes'"}), + ('/serviceRoot/People({field})(z)', '/serviceRoot/People(hobbes)(z)', {'field': 'hobbes'}), + ("/serviceRoot/People('{field}')", "/serviceRoot/People('rosalyn')", {'field': 'rosalyn'}), + ('/^{field}', '/^42', {'field': '42'}), + ('/+{field}', '/+42', {'field': '42'}), + ( + '/foo/{first}_{second}/bar', + '/foo/abc_def_ghijk/bar', + + # NOTE(kgriffs): The regex pattern is greedy, so this is + # expected. We can not change this behavior in a minor + # release, since it would be a breaking change. If there + # is enough demand for it, we could introduce an option + # to toggle this behavior. + {'first': 'abc_def', 'second': 'ghijk'}, + ), + + # NOTE(kgriffs): Why someone would use a question mark like this + # I have no idea (esp. since it would have to be encoded to avoid + # being mistaken for the query string separator). Including it only + # for completeness. + ('/items/{x}?{y}', '/items/1080?768', {'x': '1080', 'y': '768'}), + + ('/items/{x}|{y}', '/items/1080|768', {'x': '1080', 'y': '768'}), + ('/items/{x},{y}', '/items/1080,768', {'x': '1080', 'y': '768'}), + ('/items/{x}^^{y}', '/items/1080^^768', {'x': '1080', 'y': '768'}), + ('/items/{x}*{y}*', '/items/1080*768*', {'x': '1080', 'y': '768'}), + ('/thing-2/something+{field}+', '/thing-2/something+42+', {'field': '42'}), + ('/thing-2/something*{field}/notes', '/thing-2/something*42/notes', {'field': '42'}), + ( + '/thing-2/something+{field}|{q}/notes', + '/thing-2/something+else|z/notes', + {'field': 'else', 'q': 'z'}, + ), + ( + "serviceRoot/$metadata#Airports('{field}')/Name", + "serviceRoot/$metadata#Airports('KSFO')/Name", + {'field': 'KSFO'}, + ), +]) +def test_user_regression_special_chars(uri_template, path, expected_params): + router = DefaultRouter() + + router.add_route(uri_template, {}, ResourceWithId(1)) + + route = router.find(path) + assert route is not None + + resource, __, params, __ = route + assert resource.resource_id == 1 + assert params == expected_params + + +# ===================================================================== +# Other tests +# ===================================================================== + + +@pytest.mark.parametrize('uri_template', [ + {}, + set(), + object() +]) +def test_not_str(uri_template): + app = falcon.API() + with pytest.raises(TypeError): + app.add_route(uri_template, ResourceWithId(-1)) + + +def test_root_path(): + router = DefaultRouter() + router.add_route('/', {}, ResourceWithId(42)) + + resource, __, __, __ = router.find('/') + assert resource.resource_id == 42 + + expected_src = textwrap.dedent(""" + def find(path, return_values, patterns, converters, params): + path_len = len(path) + if path_len > 0: + if path[0] == '': + if path_len == 1: + return return_values[0] + return None + return None + return None + """).strip() + + assert router.finder_src == expected_src + + +@pytest.mark.parametrize('uri_template', [ + '/{field}{field}', + '/{field}...{field}', + '/{field}/{another}/{field}', + '/{field}/something/something/{field}/something', +]) +def test_duplicate_field_names(uri_template): + router = DefaultRouter() + with pytest.raises(ValueError): + router.add_route(uri_template, {}, ResourceWithId(1)) + + +@pytest.mark.parametrize('uri_template,path', [ + ('/items/thing', '/items/t'), + ('/items/{x}|{y}|', '/items/1080|768'), + ('/items/{x}*{y}foo', '/items/1080*768foobar'), + ('/items/{x}*768*', '/items/1080*768***'), +]) +def test_match_entire_path(uri_template, path): + router = DefaultRouter() + + router.add_route(uri_template, {}, ResourceWithId(1)) + + route = router.find(path) + assert route is None + + +@pytest.mark.parametrize('uri_template', [ + '/teams/{conflict}', # simple vs simple + '/emojis/signs/{id_too}', # another simple vs simple + '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}:{conflict}', + '/teams/{id:int}/settings', # converted vs. non-converted +]) +def test_conflict(router, uri_template): + with pytest.raises(ValueError): + router.add_route(uri_template, {}, ResourceWithId(-1)) + + +@pytest.mark.parametrize('uri_template', [ + '/repos/{org}/{repo}/compare/{simple_vs_complex}', + '/repos/{complex}.{vs}.{simple}', + '/repos/{org}/{repo}/compare/{complex}:{vs}...{complex2}/full', +]) +def test_non_conflict(router, uri_template): + router.add_route(uri_template, {}, ResourceWithId(-1)) + + +@pytest.mark.parametrize('uri_template', [ + # Missing field name + '/{}', + '/repos/{org}/{repo}/compare/{}', + '/repos/{complex}.{}.{thing}', + + # Field names must be valid Python identifiers + '/{9v}', + '/{524hello}/world', + '/hello/{1world}', + '/repos/{complex}.{9v}.{thing}/etc', + '/{*kgriffs}', + '/{@kgriffs}', + '/repos/{complex}.{v}.{@thing}/etc', + '/{-kgriffs}', + '/repos/{complex}.{-v}.{thing}/etc', + '/repos/{simple-thing}/etc', + + # Neither fields nor literal segments may not contain whitespace + '/this and that', + '/this\tand\tthat' + '/this\nand\nthat' + '/{thing }/world', + '/{thing\t}/world', + '/{\nthing}/world', + '/{th\ving}/world', + '/{ thing}/world', + '/{ thing }/world', + '/{thing}/wo rld', + '/{thing} /world', + '/repos/{or g}/{repo}/compare/{thing}', + '/repos/{org}/{repo}/compare/{th\ting}', +]) +def test_invalid_field_name(router, uri_template): + with pytest.raises(ValueError): + router.add_route(uri_template, {}, ResourceWithId(-1)) + + +def test_print_src(router): + """Diagnostic test that simply prints the router's find() source code. + + Example: + + $ tox -e py27_debug -- -k test_print_src -s + """ + print('\n\n' + router.finder_src + '\n') + + +def test_override(router): + router.add_route('/emojis/signs/0', {}, ResourceWithId(-1)) + + resource, __, __, __ = router.find('/emojis/signs/0') + assert resource.resource_id == -1 + + +def test_literal_segment(router): + resource, __, __, __ = router.find('/emojis/signs/0') + assert resource.resource_id == 12 + + resource, __, __, __ = router.find('/emojis/signs/1') + assert resource.resource_id == 13 + + resource, __, __, __ = router.find('/emojis/signs/42') + assert resource.resource_id == 14 + + resource, __, __, __ = router.find('/emojis/signs/42/small.jpg') + assert resource.resource_id == 23 + + route = router.find('/emojis/signs/1/small') + assert route is None + + +@pytest.mark.parametrize('path', [ + '/teams', + '/emojis/signs', + '/gists', + '/gists/42', +]) +def test_dead_segment(router, path): + route = router.find(path) + assert route is None + + +@pytest.mark.parametrize('path', [ + '/repos/racker/falcon/compare/foo', + '/repos/racker/falcon/compare/foo/full', +]) +def test_malformed_pattern(router, path): + route = router.find(path) + assert route is None + + +def test_literal(router): + resource, __, __, __ = router.find('/user/memberships') + assert resource.resource_id == 8 + + +@pytest.mark.parametrize('path,expected_params', [ + ('/cvt/teams/007', {'id': 7}), + ('/cvt/teams/1234/members', {'id': 1234}), + ('/cvt/teams/default/members/700-5', {'id': 700, 'tenure': 5}), + ( + '/cvt/repos/org/repo/compare/xkcd:353', + {'org': 'org', 'repo': 'repo', 'usr0': 'xkcd', 'branch0': 353}, + ), + ( + '/cvt/repos/org/repo/compare/gunmachan:1234...kumamon:5678/part', + { + 'org': 'org', + 'repo': 'repo', + 'usr0': 'gunmachan', + 'branch0': 1234, + 'usr1': 'kumamon', + 'branch1': 5678, + } + ), + ( + '/cvt/repos/xkcd/353/compare/susan:0001/full', + {'org': 'xkcd', 'repo': '353', 'usr0': 'susan', 'branch0': 1}, + ) +]) +def test_converters(router, path, expected_params): + __, __, params, __ = router.find(path) + assert params == expected_params + + +@pytest.mark.parametrize('uri_template', [ + '/foo/{bar:int(0)}', + '/foo/{bar:int(num_digits=0)}', + '/foo/{bar:int(-1)}/baz', + '/foo/{bar:int(num_digits=-1)}/baz', +]) +def test_converters_with_invalid_options(router, uri_template): + # NOTE(kgriffs): Sanity-check that errors are properly bubbled up + # when calling add_route(). Additional checks can be found + # in test_uri_converters.py + with pytest.raises(ValueError): + router.add_route(uri_template, {}, ResourceWithId(1)) + + +@pytest.mark.parametrize('uri_template', [ + '/foo/{bar:}', + '/foo/{bar:unknown}/baz', +]) +def test_converters_malformed_specification(router, uri_template): + with pytest.raises(ValueError): + router.add_route(uri_template, {}, ResourceWithId(1)) + + +def test_variable(router): + resource, __, params, __ = router.find('/teams/42') + assert resource.resource_id == 6 + assert params == {'id': '42'} + + __, __, params, __ = router.find('/emojis/signs/stop') + assert params == {'id': 'stop'} + + __, __, params, __ = router.find('/gists/42/raw') + assert params == {'id': '42'} + + __, __, params, __ = router.find('/images/42.gif') + assert params == {'id': '42'} + + +def test_single_character_field_name(router): + __, __, params, __ = router.find('/item/1234') + assert params == {'q': '1234'} + + +@pytest.mark.parametrize('path,expected_id', [ + ('/teams/default', 19), + ('/teams/default/members', 7), + ('/cvt/teams/default', 31), + ('/cvt/teams/default/members/1234-10', 32), + ('/teams/1234', 6), + ('/teams/1234/members', 7), + ('/gists/first', 20), + ('/gists/first/raw', 18), + ('/gists/first/pdf', 21), + ('/gists/1776/pdf', 21), + ('/emojis/signs/78', 13), + ('/emojis/signs/78/small.png', 24), + ('/emojis/signs/78/small(png)', 25), + ('/emojis/signs/78/small_png', 26), +]) +def test_literal_vs_variable(router, path, expected_id): + resource, __, __, __ = router.find(path) + assert resource.resource_id == expected_id + + +@pytest.mark.parametrize('path', [ + # Misc. + '/this/does/not/exist', + '/user/bogus', + '/repos/racker/falcon/compare/johndoe:master...janedoe:dev/bogus', + + # Literal vs variable (teams) + '/teams', + '/teams/42/members/undefined', + '/teams/42/undefined', + '/teams/42/undefined/segments', + '/teams/default/members/undefined', + '/teams/default/members/thing/undefined', + '/teams/default/members/thing/undefined/segments', + '/teams/default/undefined', + '/teams/default/undefined/segments', + + # Literal vs. variable (converters) + '/cvt/teams/default/members', # 'default' can't be converted to an int + '/cvt/teams/NaN', + '/cvt/teams/default/members/NaN', + + # Literal vs variable (emojis) + '/emojis/signs', + '/emojis/signs/0/small', + '/emojis/signs/0/undefined', + '/emojis/signs/0/undefined/segments', + '/emojis/signs/20/small', + '/emojis/signs/20/undefined', + '/emojis/signs/42/undefined', + '/emojis/signs/78/undefined', +]) +def test_not_found(router, path): + route = router.find(path) + assert route is None + + +def test_subsegment_not_found(router): + route = router.find('/emojis/signs/0/x') + assert route is None + + +def test_multivar(router): + resource, __, params, __ = router.find('/repos/racker/falcon/commits') + assert resource.resource_id == 4 + assert params == {'org': 'racker', 'repo': 'falcon'} + + resource, __, params, __ = router.find('/repos/racker/falcon/compare/all') + assert resource.resource_id == 11 + assert params == {'org': 'racker', 'repo': 'falcon'} + + +@pytest.mark.parametrize('url_postfix,resource_id', [ + ('', 5), + ('/full', 10), + ('/part', 15), +]) +def test_complex(router, url_postfix, resource_id): + uri = '/repos/racker/falcon/compare/johndoe:master...janedoe:dev' + resource, __, params, __ = router.find(uri + url_postfix) + + assert resource.resource_id == resource_id + assert (params == { + 'org': 'racker', + 'repo': 'falcon', + 'usr0': 'johndoe', + 'branch0': 'master', + 'usr1': 'janedoe', + 'branch1': 'dev', + }) + + +@pytest.mark.parametrize('url_postfix,resource_id,expected_template', [ + ('', 16, '/repos/{org}/{repo}/compare/{usr0}:{branch0}'), + ('/full', 17, '/repos/{org}/{repo}/compare/{usr0}:{branch0}/full') +]) +def test_complex_alt(router, url_postfix, resource_id, expected_template): + uri = '/repos/falconry/falcon/compare/johndoe:master' + url_postfix + resource, __, params, uri_template = router.find(uri) + + assert resource.resource_id == resource_id + assert (params == { + 'org': 'falconry', + 'repo': 'falcon', + 'usr0': 'johndoe', + 'branch0': 'master', + }) + assert uri_template == expected_template + + +def test_options_converters_set(router): + router.options.converters['spam'] = SpamConverter + + router.add_route('/{food:spam(3, eggs=True)}', {}, ResourceWithId(1)) + resource, __, params, __ = router.find('/spam') + + assert params == {'food': 'spam&eggs, spam&eggs, spam&eggs'} + + +@pytest.mark.parametrize('converter_name', [ + 'spam', + 'spam_2' +]) +def test_options_converters_update(router, converter_name): + router.options.converters.update({ + 'spam': SpamConverter, + 'spam_2': SpamConverter, + }) + + template = '/{food:' + converter_name + '(3, eggs=True)}' + router.add_route(template, {}, ResourceWithId(1)) + resource, __, params, __ = router.find('/spam') + + assert params == {'food': 'spam&eggs, spam&eggs, spam&eggs'} + + +@pytest.mark.parametrize('name', [ + 'has whitespace', + 'whitespace ', + ' whitespace ', + ' whitespace', + 'funky$character', + '42istheanswer', + 'with-hyphen', +]) +def test_options_converters_invalid_name(router, name): + with pytest.raises(ValueError): + router.options.converters[name] = object + + +def test_options_converters_invalid_name_on_update(router): + with pytest.raises(ValueError): + router.options.converters.update({ + 'valid_name': SpamConverter, + '7eleven': SpamConverter, }) diff -Nru python-falcon-1.0.0/tests/test_deps.py python-falcon-1.4.1/tests/test_deps.py --- python-falcon-1.0.0/tests/test_deps.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_deps.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,17 @@ +import mimeparse + + +def test_deps_mimeparse_correct_package(): + """Ensure we are dealing with python-mimeparse, not mimeparse.""" + + tokens = mimeparse.__version__.split('.') + msg = ( + 'Incorrect dependency detected. Please install the ' + '"python-mimeparse" package instead of the "mimeparse" ' + 'package.' + ) + + # NOTE(kgriffs): python-mimeparse starts at version 1.5.2, + # whereas the mimeparse package is at version 0.1.4 at the time + # of this writing. + assert int(tokens[0]) > 0, msg diff -Nru python-falcon-1.0.0/tests/test_error_handlers.py python-falcon-1.4.1/tests/test_error_handlers.py --- python-falcon-1.0.0/tests/test_error_handlers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_error_handlers.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,3 +1,5 @@ +import pytest + import falcon from falcon import testing @@ -40,50 +42,75 @@ raise CustomException('CustomException') -class TestErrorHandler(testing.TestCase): +@pytest.fixture +def client(): + app = falcon.API() + app.add_route('/', ErroredClassResource()) + return testing.TestClient(app) + + +class TestErrorHandler(object): + + def test_caught_error(self, client): + client.app.add_error_handler(Exception, capture_error) + + result = client.simulate_get() + assert result.text == 'error: Plain Exception' + + result = client.simulate_head() + assert result.status_code == 723 + assert not result.content + + def test_uncaught_error(self, client): + client.app.add_error_handler(CustomException, capture_error) + with pytest.raises(Exception): + client.simulate_get() - def setUp(self): - super(TestErrorHandler, self).setUp() - self.api.add_route('/', ErroredClassResource()) + def test_uncaught_error_else(self, client): + with pytest.raises(Exception): + client.simulate_get() - def test_caught_error(self): - self.api.add_error_handler(Exception, capture_error) + def test_converted_error(self, client): + client.app.add_error_handler(CustomException) - result = self.simulate_get() - self.assertEqual(result.text, 'error: Plain Exception') + result = client.simulate_delete() + assert result.status_code == 792 + assert result.json[u'title'] == u'Internet crashed!' - result = self.simulate_head() - self.assertEqual(result.status_code, 723) - self.assertFalse(result.content) + def test_handle_not_defined(self, client): + with pytest.raises(AttributeError): + client.app.add_error_handler(CustomBaseException) - def test_uncaught_error(self): - self.api.add_error_handler(CustomException, capture_error) - self.assertRaises(Exception, self.simulate_get) + def test_subclass_error(self, client): + client.app.add_error_handler(CustomBaseException, capture_error) - def test_uncaught_error_else(self): - self.assertRaises(Exception, self.simulate_get) + result = client.simulate_delete() + assert result.status_code == 723 + assert result.text == 'error: CustomException' - def test_converted_error(self): - self.api.add_error_handler(CustomException) + def test_error_order_duplicate(self, client): + client.app.add_error_handler(Exception, capture_error) + client.app.add_error_handler(Exception, handle_error_first) - result = self.simulate_delete() - self.assertEqual(result.status_code, 792) - self.assertEqual(result.json[u'title'], u'Internet crashed!') + result = client.simulate_get() + assert result.text == 'first error handler' - def test_handle_not_defined(self): - self.assertRaises(AttributeError, - self.api.add_error_handler, CustomBaseException) + def test_error_order_subclass(self, client): + client.app.add_error_handler(Exception, capture_error) + client.app.add_error_handler(CustomException, handle_error_first) - def test_subclass_error(self): - self.api.add_error_handler(CustomBaseException, capture_error) + result = client.simulate_delete() + assert result.status_code == 200 + assert result.text == 'first error handler' - result = self.simulate_delete() - self.assertEqual(result.status_code, 723) - self.assertEqual(result.text, 'error: CustomException') + result = client.simulate_get() + assert result.status_code == 723 + assert result.text == 'error: Plain Exception' - def test_error_order(self): - self.api.add_error_handler(Exception, capture_error) - self.api.add_error_handler(Exception, handle_error_first) + def test_error_order_subclass_masked(self, client): + client.app.add_error_handler(CustomException, handle_error_first) + client.app.add_error_handler(Exception, capture_error) - result = self.simulate_get() - self.assertEqual(result.text, 'first error handler') + result = client.simulate_delete() + assert result.status_code == 723 + assert result.text == 'error: CustomException' diff -Nru python-falcon-1.0.0/tests/test_error.py python-falcon-1.4.1/tests/test_error.py --- python-falcon-1.0.0/tests/test_error.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_error.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,142 @@ +import pytest + +import falcon +import falcon.status_codes as status + + +@pytest.mark.parametrize('err, title', [ + (falcon.HTTPBadRequest, status.HTTP_400), + (falcon.HTTPForbidden, status.HTTP_403), + (falcon.HTTPConflict, status.HTTP_409), + (falcon.HTTPLengthRequired, status.HTTP_411), + (falcon.HTTPPreconditionFailed, status.HTTP_412), + (falcon.HTTPRequestEntityTooLarge, status.HTTP_413), + (falcon.HTTPUriTooLong, status.HTTP_414), + (falcon.HTTPUnprocessableEntity, status.HTTP_422), + (falcon.HTTPLocked, status.HTTP_423), + (falcon.HTTPFailedDependency, status.HTTP_424), + (falcon.HTTPPreconditionRequired, status.HTTP_428), + (falcon.HTTPTooManyRequests, status.HTTP_429), + (falcon.HTTPRequestHeaderFieldsTooLarge, status.HTTP_431), + (falcon.HTTPUnavailableForLegalReasons, status.HTTP_451), + (falcon.HTTPInternalServerError, status.HTTP_500), + (falcon.HTTPNotImplemented, status.HTTP_501), + (falcon.HTTPBadGateway, status.HTTP_502), + (falcon.HTTPServiceUnavailable, status.HTTP_503), + (falcon.HTTPGatewayTimeout, status.HTTP_504), + (falcon.HTTPVersionNotSupported, status.HTTP_505), + (falcon.HTTPInsufficientStorage, status.HTTP_507), + (falcon.HTTPLoopDetected, status.HTTP_508), + (falcon.HTTPNetworkAuthenticationRequired, status.HTTP_511), +]) +def test_with_default_title_and_desc(err, title): + with pytest.raises(err) as e: + raise err() + + assert e.value.title == title + assert e.value.description is None + + if e.value.headers: + assert 'Retry-After' not in e.value.headers + + +@pytest.mark.parametrize('err', [ + falcon.HTTPBadRequest, + falcon.HTTPForbidden, + falcon.HTTPConflict, + falcon.HTTPLengthRequired, + falcon.HTTPPreconditionFailed, + falcon.HTTPPreconditionRequired, + falcon.HTTPUriTooLong, + falcon.HTTPUnprocessableEntity, + falcon.HTTPLocked, + falcon.HTTPFailedDependency, + falcon.HTTPRequestHeaderFieldsTooLarge, + falcon.HTTPUnavailableForLegalReasons, + falcon.HTTPInternalServerError, + falcon.HTTPNotImplemented, + falcon.HTTPBadGateway, + falcon.HTTPServiceUnavailable, + falcon.HTTPGatewayTimeout, + falcon.HTTPVersionNotSupported, + falcon.HTTPInsufficientStorage, + falcon.HTTPLoopDetected, + falcon.HTTPNetworkAuthenticationRequired, +]) +def test_with_title_and_desc(err): + title = 'trace' + desc = 'boom' + + with pytest.raises(err) as e: + raise err(title=title, description=desc) + + assert e.value.title == title + assert e.value.description == desc + + +@pytest.mark.parametrize('err', [ + falcon.HTTPServiceUnavailable, + falcon.HTTPTooManyRequests, + falcon.HTTPRequestEntityTooLarge, +]) +def test_with_retry_after(err): + with pytest.raises(err) as e: + raise err(retry_after='123') + + assert e.value.headers['Retry-After'] == '123' + + +def test_http_unauthorized_no_title_and_desc_and_challenges(): + try: + raise falcon.HTTPUnauthorized() + except falcon.HTTPUnauthorized as e: + assert status.HTTP_401 == e.title + assert e.description is None + assert 'WWW-Authenticate' not in e.headers + + +def test_http_unauthorized_with_title_and_desc_and_challenges(): + try: + raise falcon.HTTPUnauthorized( + title='Test', + description='Testdescription', + challenges=['Testch'] + ) + except falcon.HTTPUnauthorized as e: + assert 'Test' == e.title + assert 'Testdescription' == e.description + assert 'Testch' == e.headers['WWW-Authenticate'] + + +def test_http_not_acceptable_no_title_and_desc_and_challenges(): + try: + raise falcon.HTTPNotAcceptable() + except falcon.HTTPNotAcceptable as e: + assert e.description is None + + +def test_http_not_acceptable_with_title_and_desc_and_challenges(): + try: + raise falcon.HTTPNotAcceptable(description='Testdescription') + except falcon.HTTPNotAcceptable as e: + assert 'Testdescription' == e.description + + +def test_http_unsupported_media_type_no_title_and_desc_and_challenges(): + try: + raise falcon.HTTPUnsupportedMediaType() + except falcon.HTTPUnsupportedMediaType as e: + assert e.description is None + + +def test_http_unsupported_media_type_with_title_and_desc_and_challenges(): + try: + raise falcon.HTTPUnsupportedMediaType(description='boom') + except falcon.HTTPUnsupportedMediaType as e: + assert e.description == 'boom' + + +def test_http_error_repr(): + error = falcon.HTTPBadRequest() + _repr = '<%s: %s>' % (error.__class__.__name__, error.status) + assert error.__repr__() == _repr diff -Nru python-falcon-1.0.0/tests/test_example.py python-falcon-1.4.1/tests/test_example.py --- python-falcon-1.0.0/tests/test_example.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_example.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,11 +1,15 @@ -import json +try: + import ujson as json +except ImportError: + import json import logging import uuid from wsgiref import simple_server -import falcon import requests +import falcon + class StorageEngine(object): diff -Nru python-falcon-1.0.0/tests/test_headers.py python-falcon-1.4.1/tests/test_headers.py --- python-falcon-1.0.0/tests/test_headers.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_headers.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,13 +1,22 @@ from collections import defaultdict from datetime import datetime -import ddt +import pytest import six import falcon from falcon import testing +SAMPLE_BODY = testing.rand_string(0, 128 * 1024) + + +@pytest.fixture +def client(): + app = falcon.API() + return testing.TestClient(app) + + class XmlResource(object): def __init__(self, content_type): self.content_type = content_type @@ -36,6 +45,7 @@ 'proxy-revalidate', 'max-age=3600', 's-maxage=60', 'no-transform' ] + resp.etag = None # Header not set yet, so should be a noop resp.etag = 'fa0d1a60ef6616bb28038515c8ea4cb2' resp.last_modified = self.last_modified resp.retry_after = 3601 @@ -44,12 +54,22 @@ resp.location = '/things/87' resp.content_location = '/things/78' + resp.downloadable_as = None # Header not set yet, so should be a noop + resp.downloadable_as = 'Some File.zip' + if req.range_unit is None or req.range_unit == 'bytes': # bytes 0-499/10240 resp.content_range = (0, 499, 10 * 1024) else: resp.content_range = (0, 25, 100, req.range_unit) + resp.accept_ranges = 'bytes' + + # Test the removal of custom headers + resp.set_header('X-Client-Should-Never-See-This', 'abc') + assert resp.get_header('x-client-should-never-see-this') == 'abc' + resp.delete_header('x-client-should-never-see-this') + self.resp = resp def on_head(self, req, resp): @@ -107,14 +127,16 @@ (u'X-auTH-toKEN', 'toomanysecrets'), ('Content-TYpE', u'application/json'), (u'X-symBOl', u'@'), + ]) - # TODO(kgriffs): This will cause the wsgiref validator - # to raise an error. Falcon itself does not currently - # check for non-ASCII chars to save some CPU cycles. The - # app is responsible for doing the right thing, and - # validating its own output as needed. - # - # (u'X-symb\u00F6l', u'\u00FF'), + def on_post(self, req, resp): + resp.set_headers([ + (u'X-symb\u00F6l', 'thing'), + ]) + + def on_put(self, req, resp): + resp.set_headers([ + ('X-Thing', u'\u00FF'), ]) @@ -159,75 +181,96 @@ resp.append_header('X-Things', 'thing-1') -@ddt.ddt -class TestHeaders(testing.TestCase): +class RemoveHeaderResource(object): + def on_get(self, req, resp): + resp.etag = 'fa0d1a60ef6616bb28038515c8ea4cb2' + assert resp.etag == 'fa0d1a60ef6616bb28038515c8ea4cb2' + resp.etag = None + + resp.downloadable_as = 'foo.zip' + assert resp.downloadable_as == 'attachment; filename="foo.zip"' + resp.downloadable_as = None - def setUp(self): - super(TestHeaders, self).setUp() - self.sample_body = testing.rand_string(0, 128 * 1024) - self.resource = testing.SimpleTestResource(body=self.sample_body) - self.api.add_route('/', self.resource) +class TestHeaders(object): - def test_content_length(self): - result = self.simulate_get() + def test_content_length(self, client): + resource = testing.SimpleTestResource(body=SAMPLE_BODY) + client.app.add_route('/', resource) + result = client.simulate_get() - content_length = str(len(self.sample_body)) - self.assertEqual(result.headers['Content-Length'], content_length) + content_length = str(len(SAMPLE_BODY)) + assert result.headers['Content-Length'] == content_length - def test_default_value(self): - self.simulate_get() + def test_default_value(self, client): + resource = testing.SimpleTestResource(body=SAMPLE_BODY) + client.app.add_route('/', resource) + client.simulate_get() - req = self.resource.captured_req + req = resource.captured_req value = req.get_header('X-Not-Found') or '876' - self.assertEqual(value, '876') + assert value == '876' + + value = req.get_header('X-Not-Found', default='some-value') + assert value == 'some-value' - def test_required_header(self): - self.simulate_get() + def test_unset_header(self, client): + client.app.add_route('/', RemoveHeaderResource()) + result = client.simulate_get() + + assert 'Etag' not in result.headers + assert 'Content-Disposition' not in result.headers + + def test_required_header(self, client): + resource = testing.SimpleTestResource(body=SAMPLE_BODY) + client.app.add_route('/', resource) + client.simulate_get() try: - req = self.resource.captured_req + req = resource.captured_req req.get_header('X-Not-Found', required=True) - self.fail('falcon.HTTPMissingHeader not raised') + pytest.fail('falcon.HTTPMissingHeader not raised') except falcon.HTTPMissingHeader as ex: - self.assertIsInstance(ex, falcon.HTTPBadRequest) - self.assertEqual(ex.title, 'Missing header value') + assert isinstance(ex, falcon.HTTPBadRequest) + assert ex.title == 'Missing header value' expected_desc = 'The X-Not-Found header is required.' - self.assertEqual(ex.description, expected_desc) + assert ex.description == expected_desc - @ddt.data(falcon.HTTP_204, falcon.HTTP_304) - def test_no_content_length(self, status): - self.api.add_route('/xxx', testing.SimpleTestResource(status=status)) + @pytest.mark.parametrize('status', (falcon.HTTP_204, falcon.HTTP_304)) + def test_no_content_length(self, client, status): + client.app.add_route('/xxx', testing.SimpleTestResource(status=status)) - result = self.simulate_get('/xxx') - self.assertNotIn('Content-Length', result.headers) - self.assertFalse(result.content) + result = client.simulate_get('/xxx') + assert 'Content-Length' not in result.headers + assert not result.content - def test_content_header_missing(self): + def test_content_header_missing(self, client): environ = testing.create_environ() req = falcon.Request(environ) for header in ('Content-Type', 'Content-Length'): - self.assertIs(req.get_header(header), None) + assert req.get_header(header) is None - def test_passthrough_request_headers(self): + def test_passthrough_request_headers(self, client): + resource = testing.SimpleTestResource(body=SAMPLE_BODY) + client.app.add_route('/', resource) request_headers = { 'X-Auth-Token': 'Setec Astronomy', 'Content-Type': 'text/plain; charset=utf-8' } - self.simulate_get(headers=request_headers) + client.simulate_get(headers=request_headers) for name, expected_value in request_headers.items(): - actual_value = self.resource.captured_req.get_header(name) - self.assertEqual(actual_value, expected_value) + actual_value = resource.captured_req.get_header(name) + assert actual_value == expected_value - self.simulate_get(headers=self.resource.captured_req.headers) + client.simulate_get(headers=resource.captured_req.headers) # Compare the request HTTP headers with the original headers for name, expected_value in request_headers.items(): - actual_value = self.resource.captured_req.get_header(name) - self.assertEqual(actual_value, expected_value) + actual_value = resource.captured_req.get_header(name) + assert actual_value == expected_value - def test_headers_as_list(self): + def test_headers_as_list(self, client): headers = [ ('Client-ID', '692ba466-74bb-11e3-bf3f-7567c531c7ca'), ('Accept', 'audio/*; q=0.2, audio/basic') @@ -238,201 +281,214 @@ req = falcon.Request(environ) for name, value in headers: - self.assertIn((name.upper(), value), req.headers.items()) + assert (name.upper(), value) in req.headers.items() # Functional test - self.api.add_route('/', testing.SimpleTestResource(headers=headers)) - result = self.simulate_get() + client.app.add_route('/', testing.SimpleTestResource(headers=headers)) + result = client.simulate_get() for name, value in headers: - self.assertEqual(result.headers[name], value) + assert result.headers[name] == value - def test_default_media_type(self): + def test_default_media_type(self, client): resource = testing.SimpleTestResource(body='Hello world!') - self._check_header(resource, 'Content-Type', falcon.DEFAULT_MEDIA_TYPE) + self._check_header(client, resource, 'Content-Type', falcon.DEFAULT_MEDIA_TYPE) - @ddt.data( + @pytest.mark.parametrize('content_type,body', [ ('text/plain; charset=UTF-8', u'Hello Unicode! \U0001F638'), - # NOTE(kgriffs): This only works because the client defaults to # ISO-8859-1 IFF the media type is 'text'. ('text/plain', 'Hello ISO-8859-1!'), - ) - @ddt.unpack - def test_override_default_media_type(self, content_type, body): - self.api = falcon.API(media_type=content_type) - self.api.add_route('/', testing.SimpleTestResource(body=body)) - result = self.simulate_get() - - self.assertEqual(result.text, body) - self.assertEqual(result.headers['Content-Type'], content_type) - - def test_override_default_media_type_missing_encoding(self): - body = b'{}' - - self.api = falcon.API(media_type='application/json') - self.api.add_route('/', testing.SimpleTestResource(body=body)) - result = self.simulate_get() - - self.assertEqual(result.content, body) - self.assertRaises(RuntimeError, lambda: result.text) - self.assertRaises(RuntimeError, lambda: result.json) + ]) + def test_override_default_media_type(self, client, content_type, body): + client.app = falcon.API(media_type=content_type) + client.app.add_route('/', testing.SimpleTestResource(body=body)) + result = client.simulate_get() + + assert result.text == body + assert result.headers['Content-Type'] == content_type + + def test_override_default_media_type_missing_encoding(self, client): + body = u'{"msg": "Hello Unicode! \U0001F638"}' + + client.app = falcon.API(media_type='application/json') + client.app.add_route('/', testing.SimpleTestResource(body=body)) + result = client.simulate_get() + + assert result.content == body.encode('utf-8') + assert isinstance(result.text, six.text_type) + assert result.text == body + assert result.json == {u'msg': u'Hello Unicode! \U0001F638'} - def test_response_header_helpers_on_get(self): + def test_response_header_helpers_on_get(self, client): last_modified = datetime(2013, 1, 1, 10, 30, 30) resource = HeaderHelpersResource(last_modified) - self.api.add_route('/', resource) - result = self.simulate_get() + client.app.add_route('/', resource) + result = client.simulate_get() resp = resource.resp content_type = 'x-falcon/peregrine' - self.assertEqual(resp.content_type, content_type) - self.assertEqual(result.headers['Content-Type'], content_type) + assert resp.content_type == content_type + assert result.headers['Content-Type'] == content_type + assert result.headers['Content-Disposition'] == 'attachment; filename="Some File.zip"' cache_control = ('public, private, no-cache, no-store, ' 'must-revalidate, proxy-revalidate, max-age=3600, ' 's-maxage=60, no-transform') - self.assertEqual(resp.cache_control, cache_control) - self.assertEqual(result.headers['Cache-Control'], cache_control) + assert resp.cache_control == cache_control + assert result.headers['Cache-Control'] == cache_control etag = 'fa0d1a60ef6616bb28038515c8ea4cb2' - self.assertEqual(resp.etag, etag) - self.assertEqual(result.headers['Etag'], etag) + assert resp.etag == etag + assert result.headers['Etag'] == etag lm_date = 'Tue, 01 Jan 2013 10:30:30 GMT' - self.assertEqual(resp.last_modified, lm_date) - self.assertEqual(result.headers['Last-Modified'], lm_date) + assert resp.last_modified == lm_date + assert result.headers['Last-Modified'] == lm_date - self.assertEqual(resp.retry_after, '3601') - self.assertEqual(result.headers['Retry-After'], '3601') + assert resp.retry_after == '3601' + assert result.headers['Retry-After'] == '3601' - self.assertEqual(resp.location, '/things/87') - self.assertEqual(result.headers['Location'], '/things/87') + assert resp.location == '/things/87' + assert result.headers['Location'] == '/things/87' - self.assertEqual(resp.content_location, '/things/78') - self.assertEqual(result.headers['Content-Location'], '/things/78') + assert resp.content_location == '/things/78' + assert result.headers['Content-Location'] == '/things/78' content_range = 'bytes 0-499/10240' - self.assertEqual(resp.content_range, content_range) - self.assertEqual(result.headers['Content-Range'], content_range) + assert resp.content_range == content_range + assert result.headers['Content-Range'] == content_range - resp.content_range = (1, 499, 10 * 1024, 'bytes') - self.assertEqual(resp.content_range, 'bytes 1-499/10240') + resp.content_range = (1, 499, 10 * 1024, u'bytes') + assert isinstance(resp.content_range, str) + assert resp.content_range == 'bytes 1-499/10240' + + assert resp.accept_ranges == 'bytes' + assert result.headers['Accept-Ranges'] == 'bytes' req_headers = {'Range': 'items=0-25'} - result = self.simulate_get(headers=req_headers) - self.assertEqual(result.headers['Content-Range'], 'items 0-25/100') + result = client.simulate_get(headers=req_headers) + assert result.headers['Content-Range'] == 'items 0-25/100' # Check for duplicate headers hist = defaultdict(lambda: 0) for name, value in result.headers.items(): hist[name] += 1 - self.assertEqual(1, hist[name]) + assert 1 == hist[name] - def test_unicode_location_headers(self): - self.api.add_route('/', LocationHeaderUnicodeResource()) + def test_unicode_location_headers(self, client): + client.app.add_route('/', LocationHeaderUnicodeResource()) - result = self.simulate_get() - self.assertEqual(result.headers['Location'], '/%C3%A7runchy/bacon') - self.assertEqual(result.headers['Content-Location'], 'ab%C3%A7') + result = client.simulate_get() + assert result.headers['Location'] == '/%C3%A7runchy/bacon' + assert result.headers['Content-Location'] == 'ab%C3%A7' # Test with the values swapped - result = self.simulate_head() - self.assertEqual(result.headers['Content-Location'], - '/%C3%A7runchy/bacon') - self.assertEqual(result.headers['Location'], 'ab%C3%A7') + result = client.simulate_head() + assert result.headers['Content-Location'] == '/%C3%A7runchy/bacon' + assert result.headers['Location'] == 'ab%C3%A7' + + def test_unicode_headers_convertable(self, client): + client.app.add_route('/', UnicodeHeaderResource()) + + result = client.simulate_get('/') - def test_unicode_headers(self): - self.api.add_route('/', UnicodeHeaderResource()) + assert result.headers['Content-Type'] == 'application/json' + assert result.headers['X-Auth-Token'] == 'toomanysecrets' + assert result.headers['X-Symbol'] == '@' - result = self.simulate_get('/') + @pytest.mark.skipif(six.PY3, reason='Test only applies to Python 2') + def test_unicode_headers_not_convertable(self, client): + client.app.add_route('/', UnicodeHeaderResource()) + with pytest.raises(UnicodeEncodeError): + client.simulate_post('/') - self.assertEqual(result.headers['Content-Type'], 'application/json') - self.assertEqual(result.headers['X-Auth-Token'], 'toomanysecrets') - self.assertEqual(result.headers['X-Symbol'], '@') + with pytest.raises(UnicodeEncodeError): + client.simulate_put('/') - def test_response_set_and_get_header(self): + def test_response_set_and_get_header(self, client): resource = HeaderHelpersResource() - self.api.add_route('/', resource) + client.app.add_route('/', resource) for method in ('HEAD', 'POST', 'PUT'): - result = self.simulate_request(method=method) + result = client.simulate_request(method=method) content_type = 'x-falcon/peregrine' - self.assertEqual(result.headers['Content-Type'], content_type) - self.assertEqual(resource.resp.get_header('content-TyPe'), - content_type) + assert result.headers['Content-Type'] == content_type + assert resource.resp.get_header('content-TyPe') == content_type - self.assertEqual(result.headers['Cache-Control'], 'no-store') - self.assertEqual(result.headers['X-Auth-Token'], 'toomanysecrets') + assert result.headers['Cache-Control'] == 'no-store' + assert result.headers['X-Auth-Token'] == 'toomanysecrets' - self.assertEqual(resource.resp.location, None) - self.assertEqual(resource.resp.get_header('not-real'), None) + assert resource.resp.location is None + assert resource.resp.get_header('not-real') is None # Check for duplicate headers hist = defaultdict(int) for name, value in result.headers.items(): hist[name] += 1 - self.assertEqual(hist[name], 1) + assert hist[name] == 1 - def test_response_append_header(self): - self.api.add_route('/', AppendHeaderResource()) + # Ensure that deleted headers were not sent + assert resource.resp.get_header('x-client-should-never-see-this') is None + + def test_response_append_header(self, client): + client.app.add_route('/', AppendHeaderResource()) for method in ('HEAD', 'GET'): - result = self.simulate_request(method=method) + result = client.simulate_request(method=method) value = result.headers['x-things'] - self.assertEqual(value, 'thing-1,thing-2,thing-3') + assert value == 'thing-1,thing-2,thing-3' - result = self.simulate_request(method='POST') - self.assertEqual(result.headers['x-things'], 'thing-1') + result = client.simulate_request(method='POST') + assert result.headers['x-things'] == 'thing-1' - def test_vary_star(self): - self.api.add_route('/', VaryHeaderResource(['*'])) - result = self.simulate_get() - self.assertEqual(result.headers['vary'], '*') + def test_vary_star(self, client): + client.app.add_route('/', VaryHeaderResource(['*'])) + result = client.simulate_get() + assert result.headers['vary'] == '*' - @ddt.data( + @pytest.mark.parametrize('vary,expected_value', [ (['accept-encoding'], 'accept-encoding'), - (['accept-encoding', 'x-auth-token'], 'accept-encoding, x-auth-token'), - (('accept-encoding', 'x-auth-token'), 'accept-encoding, x-auth-token'), - ) - @ddt.unpack - def test_vary_header(self, vary, expected_value): + ([u'accept-encoding', 'x-auth-token'], 'accept-encoding, x-auth-token'), + (('accept-encoding', u'x-auth-token'), 'accept-encoding, x-auth-token'), + ]) + def test_vary_header(self, client, vary, expected_value): resource = VaryHeaderResource(vary) - self._check_header(resource, 'Vary', expected_value) + self._check_header(client, resource, 'Vary', expected_value) - def test_content_type_no_body(self): - self.api.add_route('/', testing.SimpleTestResource()) - result = self.simulate_get() + def test_content_type_no_body(self, client): + client.app.add_route('/', testing.SimpleTestResource()) + result = client.simulate_get() # NOTE(kgriffs): Even when there is no body, Content-Type # should still be included per wsgiref.validate - self.assertIn('Content-Type', result.headers) - self.assertEqual(result.headers['Content-Length'], '0') + assert 'Content-Type' in result.headers + assert result.headers['Content-Length'] == '0' - @ddt.data(falcon.HTTP_204, falcon.HTTP_304) - def test_no_content_type(self, status): - self.api.add_route('/', testing.SimpleTestResource(status=status)) + @pytest.mark.parametrize('status', (falcon.HTTP_204, falcon.HTTP_304)) + def test_no_content_type(self, client, status): + client.app.add_route('/', testing.SimpleTestResource(status=status)) - result = self.simulate_get() - self.assertNotIn('Content-Type', result.headers) + result = client.simulate_get() + assert 'Content-Type' not in result.headers - def test_custom_content_type(self): + def test_custom_content_type(self, client): content_type = 'application/xml; charset=utf-8' resource = XmlResource(content_type) - self._check_header(resource, 'Content-Type', content_type) + self._check_header(client, resource, 'Content-Type', content_type) - def test_add_link_single(self): + def test_add_link_single(self, client): expected_value = '; rel=next' resource = LinkHeaderResource() resource.add_link('/things/2842', 'next') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_multiple(self): + def test_add_link_multiple(self, client): expected_value = ( '; rel=next, ' + '; rel=contents, ' + @@ -452,9 +508,9 @@ resource.add_link('/alt-thing', u'alternate http://example.com/\u00e7runchy') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_title(self): + def test_add_link_with_title(self, client): expected_value = ('; rel=item; ' 'title="A related thing"') @@ -462,9 +518,9 @@ resource.add_link('/related/thing', 'item', title='A related thing') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_title_star(self): + def test_add_link_with_title_star(self, client): expected_value = ('; rel=item; ' "title*=UTF-8''A%20related%20thing, " '; rel=item; ' @@ -477,9 +533,9 @@ resource.add_link(u'/\u00e7runchy/thing', 'item', title_star=('en', u'A \u00e7runchy thing')) - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_anchor(self): + def test_add_link_with_anchor(self, client): expected_value = ('; rel=item; ' 'anchor="/some%20thing/or-other"') @@ -487,18 +543,18 @@ resource.add_link('/related/thing', 'item', anchor='/some thing/or-other') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_hreflang(self): + def test_add_link_with_hreflang(self, client): expected_value = ('; rel=about; ' 'hreflang=en') resource = LinkHeaderResource() resource.add_link('/related/thing', 'about', hreflang='en') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_hreflang_multi(self): + def test_add_link_with_hreflang_multi(self, client): expected_value = ('; rel=about; ' 'hreflang=en-GB; hreflang=de') @@ -506,9 +562,9 @@ resource.add_link('/related/thing', 'about', hreflang=('en-GB', 'de')) - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_with_type_hint(self): + def test_add_link_with_type_hint(self, client): expected_value = ('; rel=alternate; ' 'type="video/mp4; codecs=avc1.640028"') @@ -516,9 +572,9 @@ resource.add_link('/related/thing', 'alternate', type_hint='video/mp4; codecs=avc1.640028') - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) - def test_add_link_complex(self): + def test_add_link_complex(self, client): expected_value = ('; rel=alternate; ' 'title="A related thing"; ' "title*=UTF-8'en'A%20%C3%A7runchy%20thing; " @@ -532,17 +588,23 @@ type_hint='application/json', title_star=('en', u'A \u00e7runchy thing')) - self._check_link_header(resource, expected_value) + self._check_link_header(client, resource, expected_value) + + def test_content_length_options(self, client): + result = client.simulate_options() + + content_length = '0' + assert result.headers['Content-Length'] == content_length # ---------------------------------------------------------------------- # Helpers # ---------------------------------------------------------------------- - def _check_link_header(self, resource, expected_value): - self._check_header(resource, 'Link', expected_value) + def _check_link_header(self, client, resource, expected_value): + self._check_header(client, resource, 'Link', expected_value) - def _check_header(self, resource, header, expected_value): - self.api.add_route('/', resource) + def _check_header(self, client, resource, header, expected_value): + client.app.add_route('/', resource) - result = self.simulate_get() - self.assertEqual(result.headers[header], expected_value) + result = client.simulate_get() + assert result.headers[header] == expected_value diff -Nru python-falcon-1.0.0/tests/test_hello.py python-falcon-1.4.1/tests/test_hello.py --- python-falcon-1.0.0/tests/test_hello.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_hello.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,10 +1,15 @@ -import ddt +import io + +import pytest +import six import falcon -import io import falcon.testing as testing -import six + +@pytest.fixture +def client(): + return testing.TestClient(falcon.API()) # NOTE(kgriffs): Concept from Gunicorn's source (wsgi.py) @@ -69,111 +74,168 @@ self.on_get(req, resp) +class ClosingBytesIO(io.BytesIO): + + close_called = False + + def close(self): + super(ClosingBytesIO, self).close() + self.close_called = True + + +class NonClosingBytesIO(io.BytesIO): + + # Not callable; test that CloseableStreamIterator ignores it + close = False + + +class ClosingFilelikeHelloResource(object): + sample_status = '200 OK' + sample_unicode = (u'Hello World! \x80' + + six.text_type(testing.rand_string(0, 0))) + + sample_utf8 = sample_unicode.encode('utf-8') + + def __init__(self, stream_factory): + self.called = False + self.stream = stream_factory(self.sample_utf8) + self.stream_len = len(self.sample_utf8) + + def on_get(self, req, resp): + self.called = True + self.req, self.resp = req, resp + resp.status = falcon.HTTP_200 + resp.set_stream(self.stream, self.stream_len) + + class NoStatusResource(object): def on_get(self, req, resp): pass -@ddt.ddt -class TestHelloWorld(testing.TestCase): - - def setUp(self): - super(TestHelloWorld, self).setUp() +class TestHelloWorld(object): def test_env_headers_list_of_tuples(self): env = testing.create_environ(headers=[('User-Agent', 'Falcon-Test')]) - self.assertEqual(env['HTTP_USER_AGENT'], 'Falcon-Test') + assert env['HTTP_USER_AGENT'] == 'Falcon-Test' - def test_root_route(self): - doc = {u"message": u"Hello world!"} + def test_root_route(self, client): + doc = {u'message': u'Hello world!'} resource = testing.SimpleTestResource(json=doc) - self.api.add_route('/', resource) + client.app.add_route('/', resource) - result = self.simulate_get() - self.assertEqual(result.json, doc) + result = client.simulate_get() + assert result.json == doc - def test_no_route(self): - result = self.simulate_get('/seenoevil') - self.assertEqual(result.status_code, 404) + def test_no_route(self, client): + result = client.simulate_get('/seenoevil') + assert result.status_code == 404 - @ddt.data( + @pytest.mark.parametrize('path,resource,get_body', [ ('/body', HelloResource('body'), lambda r: r.body.encode('utf-8')), ('/bytes', HelloResource('body, bytes'), lambda r: r.body), ('/data', HelloResource('data'), lambda r: r.data), - ) - @ddt.unpack - def test_body(self, path, resource, get_body): - self.api.add_route(path, resource) + ]) + def test_body(self, client, path, resource, get_body): + client.app.add_route(path, resource) - result = self.simulate_get(path) + result = client.simulate_get(path) resp = resource.resp content_length = int(result.headers['content-length']) - self.assertEqual(content_length, len(resource.sample_utf8)) + assert content_length == len(resource.sample_utf8) - self.assertEqual(result.status, resource.sample_status) - self.assertEqual(resp.status, resource.sample_status) - self.assertEqual(get_body(resp), resource.sample_utf8) - self.assertEqual(result.content, resource.sample_utf8) + assert result.status == resource.sample_status + assert resp.status == resource.sample_status + assert get_body(resp) == resource.sample_utf8 + assert result.content == resource.sample_utf8 - def test_no_body_on_head(self): - self.api.add_route('/body', HelloResource('body')) - result = self.simulate_head('/body') + def test_no_body_on_head(self, client): + client.app.add_route('/body', HelloResource('body')) + result = client.simulate_head('/body') - self.assertFalse(result.content) - self.assertEqual(result.status_code, 200) + assert not result.content + assert result.status_code == 200 - def test_stream_chunked(self): + def test_stream_chunked(self, client): resource = HelloResource('stream') - self.api.add_route('/chunked-stream', resource) + client.app.add_route('/chunked-stream', resource) - result = self.simulate_get('/chunked-stream') + result = client.simulate_get('/chunked-stream') - self.assertEqual(result.content, resource.sample_utf8) - self.assertNotIn('content-length', result.headers) + assert result.content == resource.sample_utf8 + assert 'content-length' not in result.headers - def test_stream_known_len(self): + def test_stream_known_len(self, client): resource = HelloResource('stream, stream_len') - self.api.add_route('/stream', resource) + client.app.add_route('/stream', resource) - result = self.simulate_get('/stream') - self.assertTrue(resource.called) + result = client.simulate_get('/stream') + assert resource.called expected_len = resource.resp.stream_len actual_len = int(result.headers['content-length']) - self.assertEqual(actual_len, expected_len) - self.assertEqual(len(result.content), expected_len) - self.assertEqual(result.content, resource.sample_utf8) + assert actual_len == expected_len + assert len(result.content) == expected_len + assert result.content == resource.sample_utf8 - def test_filelike(self): + def test_filelike(self, client): resource = HelloResource('stream, stream_len, filelike') - self.api.add_route('/filelike', resource) + client.app.add_route('/filelike', resource) for file_wrapper in (None, FileWrapper): - result = self.simulate_get('/filelike', file_wrapper=file_wrapper) - self.assertTrue(resource.called) + result = client.simulate_get('/filelike', file_wrapper=file_wrapper) + assert resource.called expected_len = resource.resp.stream_len actual_len = int(result.headers['content-length']) - self.assertEqual(actual_len, expected_len) - self.assertEqual(len(result.content), expected_len) + assert actual_len == expected_len + assert len(result.content) == expected_len + + for file_wrapper in (None, FileWrapper): + result = client.simulate_get('/filelike', file_wrapper=file_wrapper) + assert resource.called + + expected_len = resource.resp.stream_len + actual_len = int(result.headers['content-length']) + assert actual_len == expected_len + assert len(result.content) == expected_len + + @pytest.mark.parametrize('stream_factory,assert_closed', [ + (ClosingBytesIO, True), # Implements close() + (NonClosingBytesIO, False), # Has a non-callable "close" attr + ]) + def test_filelike_closing(self, client, stream_factory, assert_closed): + resource = ClosingFilelikeHelloResource(stream_factory) + client.app.add_route('/filelike-closing', resource) + + result = client.simulate_get('/filelike-closing', file_wrapper=None) + assert resource.called + + expected_len = resource.resp.stream_len + actual_len = int(result.headers['content-length']) + assert actual_len == expected_len + assert len(result.content) == expected_len + + if assert_closed: + assert resource.stream.close_called - def test_filelike_using_helper(self): + def test_filelike_using_helper(self, client): resource = HelloResource('stream, stream_len, filelike, use_helper') - self.api.add_route('/filelike-helper', resource) + client.app.add_route('/filelike-helper', resource) - result = self.simulate_get('/filelike-helper') - self.assertTrue(resource.called) + result = client.simulate_get('/filelike-helper') + assert resource.called expected_len = resource.resp.stream_len actual_len = int(result.headers['content-length']) - self.assertEqual(actual_len, expected_len) - self.assertEqual(len(result.content), expected_len) + assert actual_len == expected_len + assert len(result.content) == expected_len - def test_status_not_set(self): - self.api.add_route('/nostatus', NoStatusResource()) + def test_status_not_set(self, client): + client.app.add_route('/nostatus', NoStatusResource()) - result = self.simulate_get('/nostatus') + result = client.simulate_get('/nostatus') - self.assertFalse(result.content) - self.assertEqual(result.status_code, 200) + assert not result.content + assert result.status_code == 200 diff -Nru python-falcon-1.0.0/tests/test_httperror.py python-falcon-1.4.1/tests/test_httperror.py --- python-falcon-1.0.0/tests/test_httperror.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_httperror.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,15 +1,24 @@ # -*- coding: utf-8 import datetime -import json -import xml.etree.ElementTree as et +import xml.etree.ElementTree as et # noqa: I202 -import ddt -from testtools.matchers import raises, Not +import pytest import yaml -import falcon.testing as testing import falcon +import falcon.testing as testing +from falcon.util import json + + +@pytest.fixture +def client(): + app = falcon.API() + + resource = FaultyResource() + app.add_route('/fail', resource) + + return testing.TestClient(app) class FaultyResource: @@ -99,6 +108,18 @@ raise falcon.HTTPNotFound(description='Not Found') +class GoneResource: + + def on_get(self, req, resp): + raise falcon.HTTPGone() + + +class GoneResourceWithBody: + + def on_get(self, req, resp): + raise falcon.HTTPGone(description='Gone with the wind') + + class MethodNotAllowedResource: def on_get(self, req, resp): @@ -153,6 +174,19 @@ retry_after=self.retry_after) +class UriTooLongResource: + + def __init__(self, title=None, description=None, code=None): + self.title = title + self.description = description + self.code = code + + def on_get(self, req, resp): + raise falcon.HTTPUriTooLong(self.title, + self.description, + code=self.code) + + class RangeNotSatisfiableResource: def on_get(self, req, resp): @@ -209,20 +243,15 @@ raise falcon.HTTPMissingParam('id', code='P1003') -@ddt.ddt -class TestHTTPError(testing.TestBase): +class TestHTTPError(object): - def before(self): - self.resource = FaultyResource() - self.api.add_route('/fail', self.resource) + def _misc_test(self, client, exception, status, needs_title=True): + client.app.add_route('/misc', MiscErrorsResource(exception, needs_title)) - def _misc_test(self, exception, status, needs_title=True): - self.api.add_route('/misc', MiscErrorsResource(exception, needs_title)) + response = client.simulate_request(path='/misc') + assert response.status == status - self.simulate_request('/misc') - self.assertEqual(self.srmock.status, status) - - def test_base_class(self): + def test_base_class(self, client): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' @@ -241,37 +270,36 @@ # Try it with Accept: */* headers['Accept'] = '*/*' - body = self.simulate_request('/fail', headers=headers, decode='utf-8') + response = client.simulate_request(path='/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) - self.assertIn(('vary', 'Accept'), self.srmock.headers) - self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) - self.assertEqual(expected_body, json.loads(body)) + assert response.status == headers['X-Error-Status'] + assert response.headers['vary'] == 'Accept' + assert expected_body == response.json # Now try it with application/json headers['Accept'] = 'application/json' - body = self.simulate_request('/fail', headers=headers, decode='utf-8') + response = client.simulate_request(path='/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) - self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) - self.assertEqual(json.loads(body), expected_body) + assert response.status == headers['X-Error-Status'] + assert response.json == expected_body - def test_no_description_json(self): - body = self.simulate_request('/fail', method='PATCH') - self.assertEqual(self.srmock.status, falcon.HTTP_400) - self.assertEqual(body, [b'{}']) + def test_no_description_json(self, client): + response = client.simulate_patch('/fail') + assert response.status == falcon.HTTP_400 + assert response.json == {'title': '400 Bad Request'} - def test_no_description_xml(self): - body = self.simulate_request('/fail', method='PATCH', - headers={'Accept': 'application/xml'}) - self.assertEqual(self.srmock.status, falcon.HTTP_400) + def test_no_description_xml(self, client): + response = client.simulate_patch( + path='/fail', headers={'Accept': 'application/xml'} + ) + assert response.status == falcon.HTTP_400 - expected_xml = (b'' - b'') + expected_xml = (b'' + b'400 Bad Request') - self.assertEqual(body, [expected_xml]) + assert response.content == expected_xml - def test_client_does_not_accept_json_or_xml(self): + def test_client_does_not_accept_json_or_xml(self, client): headers = { 'Accept': 'application/x-yaml', 'X-Error-Title': 'Storage service down', @@ -281,11 +309,12 @@ 'X-Error-Status': falcon.HTTP_503 } - body = self.simulate_request('/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) - self.assertEqual(body, []) + response = client.simulate_request(path='/fail', headers=headers) + assert response.status == headers['X-Error-Status'] + assert response.headers['Vary'] == 'Accept' + assert not response.content - def test_custom_old_error_serializer(self): + def test_custom_old_error_serializer(self, client): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' @@ -319,24 +348,28 @@ def _check(media_type, deserializer): headers['Accept'] = media_type - self.api.set_error_serializer(_my_serializer) - body = self.simulate_request('/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) + client.app.set_error_serializer(_my_serializer) + response = client.simulate_request(path='/fail', headers=headers) + assert response.status == headers['X-Error-Status'] - actual_doc = deserializer(body[0].decode('utf-8')) - self.assertEqual(expected_doc, actual_doc) + actual_doc = deserializer(response.content.decode('utf-8')) + assert expected_doc == actual_doc _check('application/x-yaml', yaml.load) _check('application/json', json.loads) - def test_custom_old_error_serializer_no_body(self): + def test_custom_old_error_serializer_no_body(self, client): + headers = { + 'X-Error-Status': falcon.HTTP_503 + } + def _my_serializer(req, exception): - return (None, None) + return None, None - self.api.set_error_serializer(_my_serializer) - self.simulate_request('/fail') + client.app.set_error_serializer(_my_serializer) + client.simulate_request(path='/fail', headers=headers) - def test_custom_new_error_serializer(self): + def test_custom_new_error_serializer(self, client): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' @@ -371,17 +404,17 @@ def _check(media_type, deserializer): headers['Accept'] = media_type - self.api.set_error_serializer(_my_serializer) - body = self.simulate_request('/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) + client.app.set_error_serializer(_my_serializer) + response = client.simulate_request(path='/fail', headers=headers) + assert response.status == headers['X-Error-Status'] - actual_doc = deserializer(body[0].decode('utf-8')) - self.assertEqual(expected_doc, actual_doc) + actual_doc = deserializer(response.content.decode('utf-8')) + assert expected_doc == actual_doc _check('application/x-yaml', yaml.load) _check('application/json', json.loads) - def test_client_does_not_accept_anything(self): + def test_client_does_not_accept_anything(self, client): headers = { 'Accept': '45087gigo;;;;', 'X-Error-Title': 'Storage service down', @@ -391,16 +424,16 @@ 'X-Error-Status': falcon.HTTP_503 } - body = self.simulate_request('/fail', headers=headers) - self.assertEqual(self.srmock.status, headers['X-Error-Status']) - self.assertEqual(body, []) + response = client.simulate_request(path='/fail', headers=headers) + assert response.status == headers['X-Error-Status'] + assert not response.content - @ddt.data( + @pytest.mark.parametrize('media_type', [ 'application/json', 'application/vnd.company.system.project.resource+json;v=1.1', 'application/json-patch+json', - ) - def test_forbidden(self, media_type): + ]) + def test_forbidden(self, client, media_type): headers = {'Accept': media_type} expected_body = { @@ -414,14 +447,12 @@ }, } - body = self.simulate_request('/fail', headers=headers, method='POST', - decode='utf-8') + response = client.simulate_post(path='/fail', headers=headers) - self.assertEqual(self.srmock.status, falcon.HTTP_403) - self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_403 + assert response.json == expected_body - def test_epic_fail_json(self): + def test_epic_fail_json(self, client): headers = {'Accept': 'application/json'} expected_body = { @@ -435,20 +466,18 @@ }, } - body = self.simulate_request('/fail', headers=headers, method='PUT', - decode='utf-8') + response = client.simulate_put('/fail', headers=headers) - self.assertEqual(self.srmock.status, falcon.HTTP_792) - self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_792 + assert response.json == expected_body - @ddt.data( + @pytest.mark.parametrize('media_type', [ 'text/xml', 'application/xml', 'application/vnd.company.system.project.resource+xml;v=1.1', 'application/atom+xml', - ) - def test_epic_fail_xml(self, media_type): + ]) + def test_epic_fail_xml(self, client, media_type): headers = {'Accept': media_type} expected_body = ('' + @@ -465,14 +494,16 @@ '' + '') - body = self.simulate_request('/fail', headers=headers, method='PUT', - decode='utf-8') + response = client.simulate_put(path='/fail', headers=headers) - self.assertEqual(self.srmock.status, falcon.HTTP_792) - self.assertThat(lambda: et.fromstring(body), Not(raises(ValueError))) - self.assertEqual(body, expected_body) + assert response.status == falcon.HTTP_792 + try: + et.fromstring(response.content.decode('utf-8')) + except ValueError: + pytest.fail() + assert response.text == expected_body - def test_unicode_json(self): + def test_unicode_json(self, client): unicode_resource = UnicodeFaultyResource() expected_body = { @@ -485,14 +516,14 @@ }, } - self.api.add_route('/unicode', unicode_resource) - body = self.simulate_request('/unicode', decode='utf-8') + client.app.add_route('/unicode', unicode_resource) + response = client.simulate_request(path='/unicode') - self.assertTrue(unicode_resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_792) - self.assertEqual(expected_body, json.loads(body)) + assert unicode_resource.called + assert response.status == falcon.HTTP_792 + assert expected_body == response.json - def test_unicode_xml(self): + def test_unicode_xml(self, client): unicode_resource = UnicodeFaultyResource() expected_body = (u'' + @@ -508,209 +539,255 @@ u'' + u'') - self.api.add_route('/unicode', unicode_resource) - body = self.simulate_request('/unicode', decode='utf-8', - headers={'accept': 'application/xml'}) + client.app.add_route('/unicode', unicode_resource) + response = client.simulate_request( + path='/unicode', + headers={'accept': 'application/xml'} + ) - self.assertTrue(unicode_resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_792) - self.assertEqual(expected_body, body) + assert unicode_resource.called + assert response.status == falcon.HTTP_792 + assert expected_body == response.text - def test_401(self): - self.api.add_route('/401', UnauthorizedResource()) - self.simulate_request('/401') + def test_401(self, client): + client.app.add_route('/401', UnauthorizedResource()) + response = client.simulate_request(path='/401') - self.assertEqual(self.srmock.status, falcon.HTTP_401) - self.assertIn(('www-authenticate', 'Basic realm="simple"'), - self.srmock.headers) + assert response.status == falcon.HTTP_401 + assert response.headers['www-authenticate'] == 'Basic realm="simple"' - self.simulate_request('/401', method='POST') + response = client.simulate_post('/401') - self.assertEqual(self.srmock.status, falcon.HTTP_401) - self.assertIn(('www-authenticate', 'Newauth realm="apps", ' - 'Basic realm="simple"'), - self.srmock.headers) + assert response.status == falcon.HTTP_401 + assert response.headers['www-authenticate'] == 'Newauth realm="apps", Basic realm="simple"' - self.simulate_request('/401', method='PUT') + response = client.simulate_put('/401') - self.assertEqual(self.srmock.status, falcon.HTTP_401) - self.assertNotIn(('www-authenticate', []), self.srmock.headers) + assert response.status == falcon.HTTP_401 + assert 'www-authenticate' not in response.headers - def test_404_without_body(self): - self.api.add_route('/404', NotFoundResource()) - body = self.simulate_request('/404') + def test_404_without_body(self, client): + client.app.add_route('/404', NotFoundResource()) + response = client.simulate_request(path='/404') - self.assertEqual(self.srmock.status, falcon.HTTP_404) - self.assertEqual(body, []) + assert response.status == falcon.HTTP_404 + assert not response.content - def test_404_with_body(self): - self.api.add_route('/404', NotFoundResourceWithBody()) + def test_404_with_body(self, client): + client.app.add_route('/404', NotFoundResourceWithBody()) - response = self.simulate_request('/404', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_404) - self.assertNotEqual(response, []) + response = client.simulate_request(path='/404') + assert response.status == falcon.HTTP_404 + assert response.content expected_body = { + u'title': u'404 Not Found', u'description': u'Not Found' } - self.assertEqual(json.loads(response), expected_body) + assert response.json == expected_body - def test_405_without_body(self): - self.api.add_route('/405', MethodNotAllowedResource()) + def test_405_without_body(self, client): + client.app.add_route('/405', MethodNotAllowedResource()) - response = self.simulate_request('/405') - self.assertEqual(self.srmock.status, falcon.HTTP_405) - self.assertEqual(response, []) - self.assertIn(('allow', 'PUT'), self.srmock.headers) - - def test_405_without_body_with_extra_headers(self): - self.api.add_route('/405', MethodNotAllowedResourceWithHeaders()) - - response = self.simulate_request('/405') - self.assertEqual(self.srmock.status, falcon.HTTP_405) - self.assertEqual(response, []) - self.assertIn(('allow', 'PUT'), self.srmock.headers) - self.assertIn(('x-ping', 'pong'), self.srmock.headers) - - def test_405_without_body_with_extra_headers_double_check(self): - self.api.add_route('/405', - MethodNotAllowedResourceWithHeadersWithAccept()) - - response = self.simulate_request('/405') - self.assertEqual(self.srmock.status, falcon.HTTP_405) - self.assertEqual(response, []) - self.assertIn(('allow', 'PUT'), self.srmock.headers) - self.assertNotIn(('allow', 'GET,PUT'), self.srmock.headers) - self.assertNotIn(('allow', 'GET'), self.srmock.headers) - self.assertIn(('x-ping', 'pong'), self.srmock.headers) - - def test_405_with_body(self): - self.api.add_route('/405', MethodNotAllowedResourceWithBody()) - - response = self.simulate_request('/405', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_405) - self.assertNotEqual(response, []) + response = client.simulate_request(path='/405') + assert response.status == falcon.HTTP_405 + assert not response.content + assert response.headers['allow'] == 'PUT' + + def test_405_without_body_with_extra_headers(self, client): + client.app.add_route('/405', MethodNotAllowedResourceWithHeaders()) + + response = client.simulate_request(path='/405') + assert response.status == falcon.HTTP_405 + assert not response.content + assert response.headers['allow'] == 'PUT' + assert response.headers['x-ping'] == 'pong' + + def test_405_without_body_with_extra_headers_double_check(self, client): + client.app.add_route( + '/405/', MethodNotAllowedResourceWithHeadersWithAccept() + ) + + response = client.simulate_request(path='/405') + assert response.status == falcon.HTTP_405 + assert not response.content + assert response.headers['allow'] == 'PUT' + assert response.headers['allow'] != 'GET,PUT' + assert response.headers['allow'] != 'GET' + assert response.headers['x-ping'] == 'pong' + + def test_405_with_body(self, client): + client.app.add_route('/405', MethodNotAllowedResourceWithBody()) + + response = client.simulate_request(path='/405') + assert response.status == falcon.HTTP_405 + assert response.content expected_body = { + u'title': u'405 Method Not Allowed', u'description': u'Not Allowed' } - self.assertEqual(json.loads(response), expected_body) - self.assertIn(('allow', 'PUT'), self.srmock.headers) + assert response.json == expected_body + assert response.headers['allow'] == 'PUT' - def test_411(self): - self.api.add_route('/411', LengthRequiredResource()) - body = self.simulate_request('/411') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_411) - self.assertEqual(parsed_body['title'], 'title') - self.assertEqual(parsed_body['description'], 'description') - - def test_413(self): - self.api.add_route('/413', RequestEntityTooLongResource()) - body = self.simulate_request('/413') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_413) - self.assertEqual(parsed_body['title'], 'Request Rejected') - self.assertEqual(parsed_body['description'], 'Request Body Too Large') - self.assertNotIn('retry-after', self.srmock.headers) - - def test_temporary_413_integer_retry_after(self): - self.api.add_route('/413', TemporaryRequestEntityTooLongResource('6')) - body = self.simulate_request('/413') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_413) - self.assertEqual(parsed_body['title'], 'Request Rejected') - self.assertEqual(parsed_body['description'], 'Request Body Too Large') - self.assertIn(('retry-after', '6'), self.srmock.headers) + def test_410_without_body(self, client): + client.app.add_route('/410', GoneResource()) + response = client.simulate_request(path='/410') + + assert response.status == falcon.HTTP_410 + assert not response.content + + def test_410_with_body(self, client): + client.app.add_route('/410', GoneResourceWithBody()) + + response = client.simulate_request(path='/410') + assert response.status == falcon.HTTP_410 + assert response.content + expected_body = { + u'title': u'410 Gone', + u'description': u'Gone with the wind' + } + assert response.json == expected_body + + def test_411(self, client): + client.app.add_route('/411', LengthRequiredResource()) + response = client.simulate_request(path='/411') + assert response.status == falcon.HTTP_411 + + parsed_body = response.json + assert parsed_body['title'] == 'title' + assert parsed_body['description'] == 'description' + + def test_413(self, client): + client.app.add_route('/413', RequestEntityTooLongResource()) + response = client.simulate_request(path='/413') + assert response.status == falcon.HTTP_413 + + parsed_body = response.json + assert parsed_body['title'] == 'Request Rejected' + assert parsed_body['description'] == 'Request Body Too Large' + assert 'retry-after' not in response.headers + + def test_temporary_413_integer_retry_after(self, client): + client.app.add_route('/413', TemporaryRequestEntityTooLongResource('6')) + response = client.simulate_request(path='/413') + assert response.status == falcon.HTTP_413 + + parsed_body = response.json + assert parsed_body['title'] == 'Request Rejected' + assert parsed_body['description'] == 'Request Body Too Large' + assert response.headers['retry-after'] == '6' - def test_temporary_413_datetime_retry_after(self): + def test_temporary_413_datetime_retry_after(self, client): date = datetime.datetime.now() + datetime.timedelta(minutes=5) - self.api.add_route('/413', - TemporaryRequestEntityTooLongResource(date)) - body = self.simulate_request('/413') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_413) - self.assertEqual(parsed_body['title'], 'Request Rejected') - self.assertEqual(parsed_body['description'], 'Request Body Too Large') - self.assertIn(('retry-after', falcon.util.dt_to_http(date)), - self.srmock.headers) - - def test_416(self): - self.api = falcon.API() - self.api.add_route('/416', RangeNotSatisfiableResource()) - body = self.simulate_request('/416', headers={'accept': 'text/xml'}) - - self.assertEqual(self.srmock.status, falcon.HTTP_416) - self.assertEqual(body, []) - self.assertIn(('content-range', 'bytes */123456'), self.srmock.headers) - self.assertIn(('content-length', '0'), self.srmock.headers) - - def test_429_no_retry_after(self): - self.api.add_route('/429', TooManyRequestsResource()) - body = self.simulate_request('/429') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_429) - self.assertEqual(parsed_body['title'], 'Too many requests') - self.assertEqual(parsed_body['description'], '1 per minute') - self.assertNotIn('retry-after', self.srmock.headers) - - def test_429(self): - self.api.add_route('/429', TooManyRequestsResource(60)) - body = self.simulate_request('/429') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_429) - self.assertEqual(parsed_body['title'], 'Too many requests') - self.assertEqual(parsed_body['description'], '1 per minute') - self.assertIn(('retry-after', '60'), self.srmock.headers) + client.app.add_route( + '/413', + TemporaryRequestEntityTooLongResource(date) + ) + response = client.simulate_request(path='/413') + + assert response.status == falcon.HTTP_413 + + parsed_body = response.json + assert parsed_body['title'] == 'Request Rejected' + assert parsed_body['description'] == 'Request Body Too Large' + assert response.headers['retry-after'] == falcon.util.dt_to_http(date) + + def test_414(self, client): + client.app.add_route('/414', UriTooLongResource()) + response = client.simulate_request(path='/414') + assert response.status == falcon.HTTP_414 + + def test_414_with_title(self, client): + title = 'Argh! Error!' + client.app.add_route('/414', UriTooLongResource(title=title)) + response = client.simulate_request(path='/414', headers={}) + parsed_body = json.loads(response.content.decode()) + assert parsed_body['title'] == title + + def test_414_with_description(self, client): + description = 'Be short please.' + client.app.add_route('/414', UriTooLongResource(description=description)) + response = client.simulate_request(path='/414', headers={}) + parsed_body = json.loads(response.content.decode()) + assert parsed_body['description'] == description + + def test_414_with_custom_kwargs(self, client): + code = 'someid' + client.app.add_route('/414', UriTooLongResource(code=code)) + response = client.simulate_request(path='/414', headers={}) + parsed_body = json.loads(response.content.decode()) + assert parsed_body['code'] == code + + def test_416(self, client): + client.app = falcon.API() + client.app.add_route('/416', RangeNotSatisfiableResource()) + response = client.simulate_request(path='/416', headers={'accept': 'text/xml'}) + + assert response.status == falcon.HTTP_416 + assert not response.content + assert response.headers['content-range'] == 'bytes */123456' + assert response.headers['content-length'] == '0' + + def test_429_no_retry_after(self, client): + client.app.add_route('/429', TooManyRequestsResource()) + response = client.simulate_request(path='/429') + parsed_body = response.json + + assert response.status == falcon.HTTP_429 + assert parsed_body['title'] == 'Too many requests' + assert parsed_body['description'] == '1 per minute' + assert 'retry-after' not in response.headers + + def test_429(self, client): + client.app.add_route('/429', TooManyRequestsResource(60)) + response = client.simulate_request(path='/429') + parsed_body = response.json + + assert response.status == falcon.HTTP_429 + assert parsed_body['title'] == 'Too many requests' + assert parsed_body['description'] == '1 per minute' + assert response.headers['retry-after'] == '60' - def test_429_datetime(self): + def test_429_datetime(self, client): date = datetime.datetime.now() + datetime.timedelta(minutes=1) - self.api.add_route('/429', TooManyRequestsResource(date)) - body = self.simulate_request('/429') - parsed_body = json.loads(body[0].decode()) - - self.assertEqual(self.srmock.status, falcon.HTTP_429) - self.assertEqual(parsed_body['title'], 'Too many requests') - self.assertEqual(parsed_body['description'], '1 per minute') - self.assertIn(('retry-after', falcon.util.dt_to_http(date)), - self.srmock.headers) - - def test_503_integer_retry_after(self): - self.api.add_route('/503', ServiceUnavailableResource(60)) - body = self.simulate_request('/503', decode='utf-8') + client.app.add_route('/429', TooManyRequestsResource(date)) + response = client.simulate_request(path='/429') + parsed_body = response.json + + assert response.status == falcon.HTTP_429 + assert parsed_body['title'] == 'Too many requests' + assert parsed_body['description'] == '1 per minute' + assert response.headers['retry-after'] == falcon.util.dt_to_http(date) + + def test_503_integer_retry_after(self, client): + client.app.add_route('/503', ServiceUnavailableResource(60)) + response = client.simulate_request(path='/503') expected_body = { u'title': u'Oops', u'description': u'Stand by...', } - self.assertEqual(self.srmock.status, falcon.HTTP_503) - self.assertEqual(json.loads(body), expected_body) - self.assertIn(('retry-after', '60'), self.srmock.headers) + assert response.status == falcon.HTTP_503 + assert response.json == expected_body + assert response.headers['retry-after'] == '60' - def test_503_datetime_retry_after(self): + def test_503_datetime_retry_after(self, client): date = datetime.datetime.now() + datetime.timedelta(minutes=5) - self.api.add_route('/503', - ServiceUnavailableResource(date)) - body = self.simulate_request('/503', decode='utf-8') + client.app.add_route('/503', ServiceUnavailableResource(date)) + response = client.simulate_request(path='/503') expected_body = { u'title': u'Oops', u'description': u'Stand by...', } - self.assertEqual(self.srmock.status, falcon.HTTP_503) - self.assertEqual(json.loads(body), expected_body) - self.assertIn(('retry-after', falcon.util.dt_to_http(date)), - self.srmock.headers) - - def test_invalid_header(self): - self.api.add_route('/400', InvalidHeaderResource()) - body = self.simulate_request('/400', decode='utf-8') + assert response.status == falcon.HTTP_503 + assert response.json == expected_body + assert response.headers['retry-after'] == falcon.util.dt_to_http(date) + + def test_invalid_header(self, client): + client.app.add_route('/400', InvalidHeaderResource()) + response = client.simulate_request(path='/400') expected_desc = (u'The value provided for the X-Auth-Token ' u'header is invalid. Please provide a valid token.') @@ -721,24 +798,24 @@ u'code': u'A1001', } - self.assertEqual(self.srmock.status, falcon.HTTP_400) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_400 + assert response.json == expected_body - def test_missing_header(self): - self.api.add_route('/400', MissingHeaderResource()) - body = self.simulate_request('/400', decode='utf-8') + def test_missing_header(self, client): + client.app.add_route('/400', MissingHeaderResource()) + response = client.simulate_request(path='/400') expected_body = { u'title': u'Missing header value', u'description': u'The X-Auth-Token header is required.', } - self.assertEqual(self.srmock.status, falcon.HTTP_400) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_400 + assert response.json == expected_body - def test_invalid_param(self): - self.api.add_route('/400', InvalidParamResource()) - body = self.simulate_request('/400', decode='utf-8') + def test_invalid_param(self, client): + client.app.add_route('/400', InvalidParamResource()) + response = client.simulate_request(path='/400') expected_desc = (u'The "id" parameter is invalid. The ' u'value must be a hex-encoded UUID.') @@ -748,12 +825,12 @@ u'code': u'P1002', } - self.assertEqual(self.srmock.status, falcon.HTTP_400) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_400 + assert response.json == expected_body - def test_missing_param(self): - self.api.add_route('/400', MissingParamResource()) - body = self.simulate_request('/400', decode='utf-8') + def test_missing_param(self, client): + client.app.add_route('/400', MissingParamResource()) + response = client.simulate_request(path='/400') expected_body = { u'title': u'Missing parameter', @@ -761,19 +838,29 @@ u'code': u'P1003', } - self.assertEqual(self.srmock.status, falcon.HTTP_400) - self.assertEqual(json.loads(body), expected_body) + assert response.status == falcon.HTTP_400 + assert response.json == expected_body - def test_misc(self): - self._misc_test(falcon.HTTPBadRequest, falcon.HTTP_400) - self._misc_test(falcon.HTTPNotAcceptable, falcon.HTTP_406, + def test_misc(self, client): + self._misc_test(client, falcon.HTTPBadRequest, falcon.HTTP_400) + self._misc_test(client, falcon.HTTPNotAcceptable, falcon.HTTP_406, needs_title=False) - self._misc_test(falcon.HTTPConflict, falcon.HTTP_409) - self._misc_test(falcon.HTTPPreconditionFailed, falcon.HTTP_412) - self._misc_test(falcon.HTTPUnsupportedMediaType, falcon.HTTP_415, + self._misc_test(client, falcon.HTTPConflict, falcon.HTTP_409) + self._misc_test(client, falcon.HTTPPreconditionFailed, falcon.HTTP_412) + self._misc_test(client, falcon.HTTPUnsupportedMediaType, falcon.HTTP_415, needs_title=False) - self._misc_test(falcon.HTTPUnprocessableEntity, falcon.HTTP_422) - self._misc_test(falcon.HTTPUnavailableForLegalReasons, falcon.HTTP_451, + self._misc_test(client, falcon.HTTPUnprocessableEntity, falcon.HTTP_422) + self._misc_test(client, falcon.HTTPUnavailableForLegalReasons, falcon.HTTP_451, needs_title=False) - self._misc_test(falcon.HTTPInternalServerError, falcon.HTTP_500) - self._misc_test(falcon.HTTPBadGateway, falcon.HTTP_502) + self._misc_test(client, falcon.HTTPInternalServerError, falcon.HTTP_500) + self._misc_test(client, falcon.HTTPBadGateway, falcon.HTTP_502) + + def test_title_default_message_if_none(self, client): + headers = { + 'X-Error-Status': falcon.HTTP_503 + } + + response = client.simulate_request(path='/fail', headers=headers) + + assert response.status == headers['X-Error-Status'] + assert response.json['title'] == headers['X-Error-Status'] diff -Nru python-falcon-1.0.0/tests/test_http_method_routing.py python-falcon-1.4.1/tests/test_http_method_routing.py --- python-falcon-1.0.0/tests/test_http_method_routing.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_http_method_routing.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,6 +1,6 @@ from functools import wraps -from testtools.matchers import Contains +import pytest import falcon import falcon.testing as testing @@ -18,6 +18,54 @@ ) +WEBDAV_METHODS = ( + 'CHECKIN', + 'CHECKOUT', + 'REPORT', + 'UNCHECKIN', + 'UPDATE', + 'VERSION-CONTROL', +) + + +@pytest.fixture +def stonewall(): + return Stonewall() + + +@pytest.fixture +def resource_things(): + return ThingsResource() + + +@pytest.fixture +def resource_misc(): + return MiscResource() + + +@pytest.fixture +def resource_get_with_faulty_put(): + return GetWithFaultyPutResource() + + +@pytest.fixture +def client(): + app = falcon.API() + + app.add_route('/stonewall', Stonewall()) + + resource_things = ThingsResource() + app.add_route('/things', resource_things) + app.add_route('/things/{id}/stuff/{sid}', resource_things) + + resource_misc = MiscResource() + app.add_route('/misc', resource_misc) + + resource_get_with_faulty_put = GetWithFaultyPutResource() + app.add_route('/get_with_param/{param}', resource_get_with_faulty_put) + return testing.TestClient(app) + + class ThingsResource(object): def __init__(self): self.called = False @@ -45,6 +93,12 @@ self.req, self.resp = req, resp resp.status = falcon.HTTP_201 + def on_report(self, req, resp, id, sid): + self.called = True + + self.req, self.resp = req, resp + resp.status = falcon.HTTP_204 + class Stonewall(object): pass @@ -89,6 +143,7 @@ pass def on_options(self, req, resp): + # NOTE(kgriffs): The default responder returns 200 resp.status = falcon.HTTP_204 # NOTE(kgriffs): This is incorrect, but only return GET so @@ -116,101 +171,103 @@ pass -class TestHttpMethodRouting(testing.TestBase): +class TestHttpMethodRouting(object): - def before(self): - self.api.add_route('/stonewall', Stonewall()) + def test_get(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things/42/stuff/57') + assert response.status == falcon.HTTP_204 + assert resource_things.called + + def test_put(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things/42/stuff/1337', method='PUT') + assert response.status == falcon.HTTP_201 + assert resource_things.called + + def test_post_not_allowed(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things/42/stuff/1337', method='POST') + assert response.status == falcon.HTTP_405 + assert not resource_things.called + + def test_report(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things/42/stuff/1337', method='REPORT') + assert response.status == falcon.HTTP_204 + assert resource_things.called - self.resource_things = ThingsResource() - self.api.add_route('/things', self.resource_things) - self.api.add_route('/things/{id}/stuff/{sid}', self.resource_things) - - self.resource_misc = MiscResource() - self.api.add_route('/misc', self.resource_misc) - - self.resource_get_with_faulty_put = GetWithFaultyPutResource() - self.api.add_route('/get_with_param/{param}', - self.resource_get_with_faulty_put) - - def test_get(self): - self.simulate_request('/things/42/stuff/57') - self.assertEqual(self.srmock.status, falcon.HTTP_204) - self.assertTrue(self.resource_things.called) - - def test_put(self): - self.simulate_request('/things/42/stuff/1337', method='PUT') - self.assertEqual(self.srmock.status, falcon.HTTP_201) - self.assertTrue(self.resource_things.called) - - def test_post_not_allowed(self): - self.simulate_request('/things/42/stuff/1337', method='POST') - self.assertEqual(self.srmock.status, falcon.HTTP_405) - self.assertFalse(self.resource_things.called) - - def test_misc(self): + def test_misc(self, client, resource_misc): + client.app.add_route('/misc', resource_misc) for method in ['GET', 'HEAD', 'PUT', 'PATCH']: - self.resource_misc.called = False - self.simulate_request('/misc', method=method) - self.assertTrue(self.resource_misc.called) - self.assertEqual(self.resource_misc.req.method, method) + resource_misc.called = False + client.simulate_request(path='/misc', method=method) + assert resource_misc.called + assert resource_misc.req.method == method - def test_methods_not_allowed_simple(self): + def test_methods_not_allowed_simple(self, client, stonewall): + client.app.add_route('/stonewall', stonewall) for method in ['GET', 'HEAD', 'PUT', 'PATCH']: - self.simulate_request('/stonewall', method=method) - self.assertEqual(self.srmock.status, falcon.HTTP_405) + response = client.simulate_request(path='/stonewall', method=method) + assert response.status == falcon.HTTP_405 - def test_methods_not_allowed_complex(self): - for method in HTTP_METHODS: - if method in ('GET', 'PUT', 'HEAD', 'OPTIONS'): + def test_methods_not_allowed_complex(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + for method in HTTP_METHODS + WEBDAV_METHODS: + if method in ('GET', 'PUT', 'HEAD', 'OPTIONS', 'REPORT'): continue - self.resource_things.called = False - self.simulate_request('/things/84/stuff/65', method=method) - - self.assertFalse(self.resource_things.called) - self.assertEqual(self.srmock.status, falcon.HTTP_405) + resource_things.called = False + response = client.simulate_request(path='/things/84/stuff/65', method=method) - headers = self.srmock.headers - allow_header = ('allow', 'GET, HEAD, PUT, OPTIONS') + assert not resource_things.called + assert response.status == falcon.HTTP_405 - self.assertThat(headers, Contains(allow_header)) + headers = response.headers + assert headers['allow'] == 'GET, HEAD, PUT, REPORT, OPTIONS' - def test_method_not_allowed_with_param(self): - for method in HTTP_METHODS: + def test_method_not_allowed_with_param(self, client, resource_get_with_faulty_put): + client.app.add_route('/get_with_param/{param}', resource_get_with_faulty_put) + for method in HTTP_METHODS + WEBDAV_METHODS: if method in ('GET', 'PUT', 'OPTIONS'): continue - self.resource_get_with_faulty_put.called = False - self.simulate_request( - '/get_with_param/bogus_param', method=method) - - self.assertFalse(self.resource_get_with_faulty_put.called) - self.assertEqual(self.srmock.status, falcon.HTTP_405) - - headers = self.srmock.headers - allow_header = ('allow', 'GET, PUT, OPTIONS') - - self.assertThat(headers, Contains(allow_header)) - - def test_on_options(self): - self.simulate_request('/things/84/stuff/65', method='OPTIONS') - self.assertEqual(self.srmock.status, falcon.HTTP_204) - - headers = self.srmock.headers - allow_header = ('allow', 'GET, HEAD, PUT') - - self.assertThat(headers, Contains(allow_header)) - - def test_default_on_options(self): - self.simulate_request('/misc', method='OPTIONS') - self.assertEqual(self.srmock.status, falcon.HTTP_204) - - headers = self.srmock.headers - allow_header = ('allow', 'GET') - - self.assertThat(headers, Contains(allow_header)) - - def test_bogus_method(self): - self.simulate_request('/things', method=self.getUniqueString()) - self.assertFalse(self.resource_things.called) - self.assertEqual(self.srmock.status, falcon.HTTP_400) + resource_get_with_faulty_put.called = False + response = client.simulate_request( + method=method, + path='/get_with_param/bogus_param', + ) + + assert not resource_get_with_faulty_put.called + assert response.status == falcon.HTTP_405 + + headers = response.headers + assert headers['allow'] == 'GET, PUT, OPTIONS' + + def test_default_on_options(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things/84/stuff/65', method='OPTIONS') + assert response.status == falcon.HTTP_200 + + headers = response.headers + assert headers['allow'] == 'GET, HEAD, PUT, REPORT' + + def test_on_options(self, client): + response = client.simulate_request(path='/misc', method='OPTIONS') + assert response.status == falcon.HTTP_204 + + headers = response.headers + assert headers['allow'] == 'GET' + + def test_bogus_method(self, client, resource_things): + client.app.add_route('/things', resource_things) + client.app.add_route('/things/{id}/stuff/{sid}', resource_things) + response = client.simulate_request(path='/things', method=testing.rand_string(3, 4)) + assert not resource_things.called + assert response.status == falcon.HTTP_400 diff -Nru python-falcon-1.0.0/tests/test_httpstatus.py python-falcon-1.4.1/tests/test_httpstatus.py --- python-falcon-1.0.0/tests/test_httpstatus.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_httpstatus.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,8 +1,8 @@ # -*- coding: utf-8 -import falcon.testing as testing import falcon from falcon.http_status import HTTPStatus +import falcon.testing as testing def before_hook(req, resp, params): @@ -40,13 +40,14 @@ @falcon.after(after_hook) def on_put(self, req, resp): - resp.status = falcon.HTTP_500 + # NOTE(kgriffs): Test that passing a unicode status string + # works just fine. + resp.status = u'500 Internal Server Error' resp.set_header('X-Failed', 'True') resp.body = 'Fail' def on_patch(self, req, resp): - raise HTTPStatus(falcon.HTTP_200, - body=None) + raise HTTPStatus(falcon.HTTP_200, body=None) @falcon.after(noop_after_hook) def on_delete(self, req, resp): @@ -72,50 +73,62 @@ body='Pass') -class TestHTTPStatus(testing.TestBase): - def before(self): - self.resource = TestStatusResource() - self.api.add_route('/status', self.resource) - +class TestHTTPStatus(object): def test_raise_status_in_before_hook(self): """ Make sure we get the 200 raised by before hook """ - body = self.simulate_request('/status', method='GET', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API() + app.add_route('/status', TestStatusResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='GET') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_in_responder(self): """ Make sure we get the 200 raised by responder """ - body = self.simulate_request('/status', method='POST', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API() + app.add_route('/status', TestStatusResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='POST') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_runs_after_hooks(self): """ Make sure after hooks still run """ - body = self.simulate_request('/status', method='PUT', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API() + app.add_route('/status', TestStatusResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='PUT') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_survives_after_hooks(self): """ Make sure after hook doesn't overwrite our status """ - body = self.simulate_request('/status', method='DELETE', - decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API() + app.add_route('/status', TestStatusResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='DELETE') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_empty_body(self): """ Make sure passing None to body results in empty body """ - body = self.simulate_request('/status', method='PATCH', decode='utf-8') - self.assertEqual(body, '') + app = falcon.API() + app.add_route('/status', TestStatusResource()) + client = testing.TestClient(app) + response = client.simulate_request(path='/status', method='PATCH') + assert response.text == '' -class TestHTTPStatusWithMiddleware(testing.TestBase): - def before(self): - self.resource = TestHookResource() +class TestHTTPStatusWithMiddleware(object): def test_raise_status_in_process_request(self): """ Make sure we can raise status from middleware process request """ class TestMiddleware: @@ -124,13 +137,14 @@ headers={'X-Failed': 'False'}, body='Pass') - self.api = falcon.API(middleware=TestMiddleware()) - self.api.add_route('/status', self.resource) - - body = self.simulate_request('/status', method='GET', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API(middleware=TestMiddleware()) + app.add_route('/status', TestHookResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='GET') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_in_process_resource(self): """ Make sure we can raise status from middleware process resource """ @@ -140,13 +154,14 @@ headers={'X-Failed': 'False'}, body='Pass') - self.api = falcon.API(middleware=TestMiddleware()) - self.api.add_route('/status', self.resource) - - body = self.simulate_request('/status', method='GET', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API(middleware=TestMiddleware()) + app.add_route('/status', TestHookResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='GET') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' def test_raise_status_runs_process_response(self): """ Make sure process_response still runs """ @@ -156,10 +171,11 @@ resp.set_header('X-Failed', 'False') resp.body = 'Pass' - self.api = falcon.API(middleware=TestMiddleware()) - self.api.add_route('/status', self.resource) - - body = self.simulate_request('/status', method='GET', decode='utf-8') - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn(('x-failed', 'False'), self.srmock.headers) - self.assertEqual(body, 'Pass') + app = falcon.API(middleware=TestMiddleware()) + app.add_route('/status', TestHookResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/status', method='GET') + assert response.status == falcon.HTTP_200 + assert response.headers['x-failed'] == 'False' + assert response.text == 'Pass' diff -Nru python-falcon-1.0.0/tests/test_media_handlers.py python-falcon-1.4.1/tests/test_media_handlers.py --- python-falcon-1.0.0/tests/test_media_handlers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_media_handlers.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,13 @@ +import pytest + +from falcon import media + + +def test_base_handler_contract(): + class TestHandler(media.BaseHandler): + pass + + with pytest.raises(TypeError) as err: + TestHandler() + + assert 'abstract methods deserialize, serialize' in str(err.value) diff -Nru python-falcon-1.0.0/tests/test_middleware.py python-falcon-1.4.1/tests/test_middleware.py --- python-falcon-1.0.0/tests/test_middleware.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_middleware.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,12 +1,28 @@ -import json +from datetime import datetime + +import pytest + +try: + import ujson as json +except ImportError: + import json import falcon import falcon.testing as testing -from datetime import datetime _EXPECTED_BODY = {u'status': u'ok'} context = {'executed_methods': []} +TEST_ROUTE = '/test_path' + + +class CaptureResponseMiddleware(object): + + def process_response(self, req, resp, resource, req_succeeded): + self.req = req + self.resp = resp + self.resource = resource + self.req_succeeded = req_succeeded class RequestTimeMiddleware(object): @@ -19,9 +35,10 @@ global context context['mid_time'] = datetime.utcnow() - def process_response(self, req, resp, resource): + def process_response(self, req, resp, resource, req_succeeded): global context context['end_time'] = datetime.utcnow() + context['req_succeeded'] = req_succeeded class TransactionIdMiddleware(object): @@ -30,6 +47,9 @@ global context context['transaction_id'] = 'unique-req-id' + def process_response(self, req, resp, resource): + pass + class ExecutedFirstMiddleware(object): @@ -43,11 +63,18 @@ context['executed_methods'].append( '{0}.{1}'.format(self.__class__.__name__, 'process_resource')) + # NOTE(kgriffs): This also tests that the framework can continue to + # call process_response() methods that do not have a 'req_succeeded' + # arg. def process_response(self, req, resp, resource): global context context['executed_methods'].append( '{0}.{1}'.format(self.__class__.__name__, 'process_response')) + context['req'] = req + context['resp'] = resp + context['resource'] = resource + class ExecutedLastMiddleware(ExecutedFirstMiddleware): pass @@ -73,34 +100,32 @@ resp.status = falcon.HTTP_200 resp.body = json.dumps(_EXPECTED_BODY) + def on_post(self, req, resp): + raise falcon.HTTPForbidden(falcon.HTTP_403, 'Setec Astronomy') -class TestMiddleware(testing.TestBase): - def setUp(self): +class TestMiddleware(object): + def setup_method(self, method): # Clear context global context context = {'executed_methods': []} - testing.TestBase.setUp(self) - - # TODO(kgriffs): Consider adding this to TestBase - def simulate_json_request(self, *args, **kwargs): - result = self.simulate_request(*args, decode='utf-8', **kwargs) - return json.loads(result) class TestRequestTimeMiddleware(TestMiddleware): def test_skip_process_resource(self): global context - self.api = falcon.API(middleware=[RequestTimeMiddleware()]) + app = falcon.API(middleware=[RequestTimeMiddleware()]) - self.api.add_route('/', MiddlewareClassResource()) + app.add_route('/', MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request('/404') - self.assertEqual(self.srmock.status, falcon.HTTP_404) - self.assertIn('start_time', context) - self.assertNotIn('mid_time', context) - self.assertIn('end_time', context) + response = client.simulate_request(path='/404') + assert response.status == falcon.HTTP_404 + assert 'start_time' in context + assert 'mid_time' not in context + assert 'end_time' in context + assert not context['req_succeeded'] def test_add_invalid_middleware(self): """Test than an invalid class can not be added as middleware""" @@ -109,11 +134,14 @@ pass mw_list = [RequestTimeMiddleware(), InvalidMiddleware] - self.assertRaises(AttributeError, falcon.API, middleware=mw_list) + with pytest.raises(AttributeError): + falcon.API(middleware=mw_list) mw_list = [RequestTimeMiddleware(), 'InvalidMiddleware'] - self.assertRaises(TypeError, falcon.API, middleware=mw_list) + with pytest.raises(TypeError): + falcon.API(middleware=mw_list) mw_list = [{'process_request': 90}] - self.assertRaises(TypeError, falcon.API, middleware=mw_list) + with pytest.raises(TypeError): + falcon.API(middleware=mw_list) def test_response_middleware_raises_exception(self): """Test that error in response middleware is propagated up""" @@ -122,79 +150,121 @@ def process_response(self, req, resp, resource): raise Exception('Always fail') - self.api = falcon.API(middleware=[RaiseErrorMiddleware()]) + app = falcon.API(middleware=[RaiseErrorMiddleware()]) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.assertRaises(Exception, self.simulate_request, self.test_route) + with pytest.raises(Exception): + client.simulate_request(path=TEST_ROUTE) def test_log_get_request(self): """Test that Log middleware is executed""" global context - self.api = falcon.API(middleware=[RequestTimeMiddleware()]) + app = falcon.API(middleware=[RequestTimeMiddleware()]) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - body = self.simulate_json_request(self.test_route) - self.assertEqual(_EXPECTED_BODY, body) - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn('start_time', context) - self.assertIn('mid_time', context) - self.assertIn('end_time', context) - self.assertTrue(context['mid_time'] >= context['start_time'], - 'process_resource not executed after request') - self.assertTrue(context['end_time'] >= context['start_time'], - 'process_response not executed after request') + response = client.simulate_request(path=TEST_ROUTE) + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 + + assert 'start_time' in context + assert 'mid_time' in context + assert 'end_time' in context + assert context['mid_time'] >= context['start_time'], \ + 'process_resource not executed after request' + assert context['end_time'] >= context['start_time'], \ + 'process_response not executed after request' + assert context['req_succeeded'] -class TestTransactionIdMiddleware(TestMiddleware): +class TestTransactionIdMiddleware(TestMiddleware): def test_generate_trans_id_with_request(self): """Test that TransactionIdmiddleware is executed""" global context - self.api = falcon.API(middleware=TransactionIdMiddleware()) + app = falcon.API(middleware=TransactionIdMiddleware()) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - body = self.simulate_json_request(self.test_route) - self.assertEqual(_EXPECTED_BODY, body) - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn('transaction_id', context) - self.assertEqual('unique-req-id', context['transaction_id']) + response = client.simulate_request(path=TEST_ROUTE) + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 + assert 'transaction_id' in context + assert 'unique-req-id' == context['transaction_id'] class TestSeveralMiddlewares(TestMiddleware): - def test_generate_trans_id_and_time_with_request(self): global context - self.api = falcon.API(middleware=[TransactionIdMiddleware(), - RequestTimeMiddleware()]) + app = falcon.API(middleware=[TransactionIdMiddleware(), + RequestTimeMiddleware()]) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - body = self.simulate_json_request(self.test_route) - self.assertEqual(_EXPECTED_BODY, body) - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.assertIn('transaction_id', context) - self.assertEqual('unique-req-id', context['transaction_id']) - self.assertIn('start_time', context) - self.assertIn('mid_time', context) - self.assertIn('end_time', context) - self.assertTrue(context['mid_time'] >= context['start_time'], - 'process_resource not executed after request') - self.assertTrue(context['end_time'] >= context['start_time'], - 'process_response not executed after request') + response = client.simulate_request(path=TEST_ROUTE) + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 + assert 'transaction_id' in context + assert 'unique-req-id' == context['transaction_id'] + assert 'start_time' in context + assert 'mid_time' in context + assert 'end_time' in context + assert context['mid_time'] >= context['start_time'], \ + 'process_resource not executed after request' + assert context['end_time'] >= context['start_time'], \ + 'process_response not executed after request' + + def test_legacy_middleware_called_with_correct_args(self): + global context + app = falcon.API(middleware=[ExecutedFirstMiddleware()]) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + client.simulate_request(path=TEST_ROUTE) + assert isinstance(context['req'], falcon.Request) + assert isinstance(context['resp'], falcon.Response) + assert isinstance(context['resource'], MiddlewareClassResource) def test_middleware_execution_order(self): global context - self.api = falcon.API(middleware=[ExecutedFirstMiddleware(), - ExecutedLastMiddleware()]) + app = falcon.API(middleware=[ExecutedFirstMiddleware(), + ExecutedLastMiddleware()]) + + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path=TEST_ROUTE) + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 + # as the method registration is in a list, the order also is + # tested + expectedExecutedMethods = [ + 'ExecutedFirstMiddleware.process_request', + 'ExecutedLastMiddleware.process_request', + 'ExecutedFirstMiddleware.process_resource', + 'ExecutedLastMiddleware.process_resource', + 'ExecutedLastMiddleware.process_response', + 'ExecutedFirstMiddleware.process_response' + ] + assert expectedExecutedMethods == context['executed_methods'] + + def test_independent_middleware_execution_order(self): + global context + app = falcon.API(independent_middleware=True, + middleware=[ExecutedFirstMiddleware(), + ExecutedLastMiddleware()]) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - body = self.simulate_json_request(self.test_route) - self.assertEqual(_EXPECTED_BODY, body) - self.assertEqual(self.srmock.status, falcon.HTTP_200) + response = client.simulate_request(path=TEST_ROUTE) + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 # as the method registration is in a list, the order also is # tested expectedExecutedMethods = [ @@ -205,7 +275,43 @@ 'ExecutedLastMiddleware.process_response', 'ExecutedFirstMiddleware.process_response' ] - self.assertEqual(expectedExecutedMethods, context['executed_methods']) + assert expectedExecutedMethods == context['executed_methods'] + + def test_multiple_reponse_mw_throw_exception(self): + """Test that error in inner middleware leaves""" + global context + + context['req_succeeded'] = [] + + class RaiseStatusMiddleware(object): + def process_response(self, req, resp, resource): + raise falcon.HTTPStatus(falcon.HTTP_201) + + class RaiseErrorMiddleware(object): + def process_response(self, req, resp, resource): + raise falcon.HTTPError(falcon.HTTP_748) + + class ProcessResponseMiddleware(object): + def process_response(self, req, resp, resource, req_succeeded): + context['executed_methods'].append('process_response') + context['req_succeeded'].append(req_succeeded) + + app = falcon.API(middleware=[ProcessResponseMiddleware(), + RaiseErrorMiddleware(), + ProcessResponseMiddleware(), + RaiseStatusMiddleware(), + ProcessResponseMiddleware()]) + + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path=TEST_ROUTE) + + assert response.status == falcon.HTTP_748 + + expected_methods = ['process_response'] * 3 + assert context['executed_methods'] == expected_methods + assert context['req_succeeded'] == [True, False, False] def test_inner_mw_throw_exception(self): """Test that error in inner middleware leaves""" @@ -216,19 +322,21 @@ def process_request(self, req, resp): raise Exception('Always fail') - self.api = falcon.API(middleware=[TransactionIdMiddleware(), - RequestTimeMiddleware(), - RaiseErrorMiddleware()]) + app = falcon.API(middleware=[TransactionIdMiddleware(), + RequestTimeMiddleware(), + RaiseErrorMiddleware()]) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.assertRaises(Exception, self.simulate_request, self.test_route) + with pytest.raises(Exception): + client.simulate_request(path=TEST_ROUTE) # RequestTimeMiddleware process_response should be executed - self.assertIn('transaction_id', context) - self.assertIn('start_time', context) - self.assertNotIn('mid_time', context) - self.assertIn('end_time', context) + assert 'transaction_id' in context + assert 'start_time' in context + assert 'mid_time' not in context + assert 'end_time' in context def test_inner_mw_with_ex_handler_throw_exception(self): """Test that error in inner middleware leaves""" @@ -239,25 +347,26 @@ def process_request(self, req, resp, resource): raise Exception('Always fail') - self.api = falcon.API(middleware=[TransactionIdMiddleware(), - RequestTimeMiddleware(), - RaiseErrorMiddleware()]) + app = falcon.API(middleware=[TransactionIdMiddleware(), + RequestTimeMiddleware(), + RaiseErrorMiddleware()]) def handler(ex, req, resp, params): context['error_handler'] = True - self.api.add_error_handler(Exception, handler) + app.add_error_handler(Exception, handler) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request(self.test_route) + client.simulate_request(path=TEST_ROUTE) # RequestTimeMiddleware process_response should be executed - self.assertIn('transaction_id', context) - self.assertIn('start_time', context) - self.assertNotIn('mid_time', context) - self.assertIn('end_time', context) - self.assertIn('error_handler', context) + assert 'transaction_id' in context + assert 'start_time' in context + assert 'mid_time' not in context + assert 'end_time' in context + assert 'error_handler' in context def test_outer_mw_with_ex_handler_throw_exception(self): """Test that error in inner middleware leaves""" @@ -268,25 +377,26 @@ def process_request(self, req, resp): raise Exception('Always fail') - self.api = falcon.API(middleware=[TransactionIdMiddleware(), - RaiseErrorMiddleware(), - RequestTimeMiddleware()]) + app = falcon.API(middleware=[TransactionIdMiddleware(), + RaiseErrorMiddleware(), + RequestTimeMiddleware()]) def handler(ex, req, resp, params): context['error_handler'] = True - self.api.add_error_handler(Exception, handler) + app.add_error_handler(Exception, handler) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request(self.test_route) + client.simulate_request(path=TEST_ROUTE) # Any mw is executed now... - self.assertIn('transaction_id', context) - self.assertNotIn('start_time', context) - self.assertNotIn('mid_time', context) - self.assertNotIn('end_time', context) - self.assertIn('error_handler', context) + assert 'transaction_id' in context + assert 'start_time' not in context + assert 'mid_time' not in context + assert 'end_time' not in context + assert 'error_handler' in context def test_order_mw_executed_when_exception_in_resp(self): """Test that error in inner middleware leaves""" @@ -297,18 +407,54 @@ def process_response(self, req, resp, resource): raise Exception('Always fail') - self.api = falcon.API(middleware=[ExecutedFirstMiddleware(), - RaiseErrorMiddleware(), - ExecutedLastMiddleware()]) + app = falcon.API(middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) + + def handler(ex, req, resp, params): + pass + + app.add_error_handler(Exception, handler) + + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + client.simulate_request(path=TEST_ROUTE) + + # Any mw is executed now... + expectedExecutedMethods = [ + 'ExecutedFirstMiddleware.process_request', + 'ExecutedLastMiddleware.process_request', + 'ExecutedFirstMiddleware.process_resource', + 'ExecutedLastMiddleware.process_resource', + 'ExecutedLastMiddleware.process_response', + 'ExecutedFirstMiddleware.process_response' + ] + assert expectedExecutedMethods == context['executed_methods'] + + def test_order_independent_mw_executed_when_exception_in_resp(self): + """Test that error in inner middleware leaves""" + global context + + class RaiseErrorMiddleware(object): + + def process_response(self, req, resp, resource): + raise Exception('Always fail') + + app = falcon.API(independent_middleware=True, + middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) def handler(ex, req, resp, params): pass - self.api.add_error_handler(Exception, handler) + app.add_error_handler(Exception, handler) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request(self.test_route) + client.simulate_request(path=TEST_ROUTE) # Any mw is executed now... expectedExecutedMethods = [ @@ -319,7 +465,7 @@ 'ExecutedLastMiddleware.process_response', 'ExecutedFirstMiddleware.process_response' ] - self.assertEqual(expectedExecutedMethods, context['executed_methods']) + assert expectedExecutedMethods == context['executed_methods'] def test_order_mw_executed_when_exception_in_req(self): """Test that error in inner middleware leaves""" @@ -330,25 +476,58 @@ def process_request(self, req, resp): raise Exception('Always fail') - self.api = falcon.API(middleware=[ExecutedFirstMiddleware(), - RaiseErrorMiddleware(), - ExecutedLastMiddleware()]) + app = falcon.API(middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) def handler(ex, req, resp, params): pass - self.api.add_error_handler(Exception, handler) + app.add_error_handler(Exception, handler) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request(self.test_route) + client.simulate_request(path=TEST_ROUTE) # Any mw is executed now... expectedExecutedMethods = [ 'ExecutedFirstMiddleware.process_request', 'ExecutedFirstMiddleware.process_response' ] - self.assertEqual(expectedExecutedMethods, context['executed_methods']) + assert expectedExecutedMethods == context['executed_methods'] + + def test_order_independent_mw_executed_when_exception_in_req(self): + """Test that error in inner middleware leaves""" + global context + + class RaiseErrorMiddleware(object): + + def process_request(self, req, resp): + raise Exception('Always fail') + + app = falcon.API(independent_middleware=True, + middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) + + def handler(ex, req, resp, params): + pass + + app.add_error_handler(Exception, handler) + + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + client.simulate_request(path=TEST_ROUTE) + + # All response middleware still executed... + expectedExecutedMethods = [ + 'ExecutedFirstMiddleware.process_request', + 'ExecutedLastMiddleware.process_response', + 'ExecutedFirstMiddleware.process_response' + ] + assert expectedExecutedMethods == context['executed_methods'] def test_order_mw_executed_when_exception_in_rsrc(self): """Test that error in inner middleware leaves""" @@ -359,18 +538,19 @@ def process_resource(self, req, resp, resource): raise Exception('Always fail') - self.api = falcon.API(middleware=[ExecutedFirstMiddleware(), - RaiseErrorMiddleware(), - ExecutedLastMiddleware()]) + app = falcon.API(middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) def handler(ex, req, resp, params): pass - self.api.add_error_handler(Exception, handler) + app.add_error_handler(Exception, handler) - self.api.add_route(self.test_route, MiddlewareClassResource()) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) - self.simulate_request(self.test_route) + client.simulate_request(path=TEST_ROUTE) # Any mw is executed now... expectedExecutedMethods = [ @@ -380,27 +560,60 @@ 'ExecutedLastMiddleware.process_response', 'ExecutedFirstMiddleware.process_response' ] - self.assertEqual(expectedExecutedMethods, context['executed_methods']) + assert expectedExecutedMethods == context['executed_methods'] + + def test_order_independent_mw_executed_when_exception_in_rsrc(self): + """Test that error in inner middleware leaves""" + global context + class RaiseErrorMiddleware(object): -class TestRemoveBasePathMiddleware(TestMiddleware): + def process_resource(self, req, resp, resource): + raise Exception('Always fail') + + app = falcon.API(independent_middleware=True, + middleware=[ExecutedFirstMiddleware(), + RaiseErrorMiddleware(), + ExecutedLastMiddleware()]) + + def handler(ex, req, resp, params): + pass + + app.add_error_handler(Exception, handler) + app.add_route(TEST_ROUTE, MiddlewareClassResource()) + client = testing.TestClient(app) + + client.simulate_request(path=TEST_ROUTE) + + # Any mw is executed now... + expectedExecutedMethods = [ + 'ExecutedFirstMiddleware.process_request', + 'ExecutedLastMiddleware.process_request', + 'ExecutedFirstMiddleware.process_resource', + 'ExecutedLastMiddleware.process_response', + 'ExecutedFirstMiddleware.process_response' + ] + assert expectedExecutedMethods == context['executed_methods'] + + +class TestRemoveBasePathMiddleware(TestMiddleware): def test_base_path_is_removed_before_routing(self): """Test that RemoveBasePathMiddleware is executed before routing""" - self.api = falcon.API(middleware=RemoveBasePathMiddleware()) + app = falcon.API(middleware=RemoveBasePathMiddleware()) # We dont include /base_path as it will be removed in middleware - self.api.add_route('/sub_path', MiddlewareClassResource()) + app.add_route('/sub_path', MiddlewareClassResource()) + client = testing.TestClient(app) - body = self.simulate_json_request('/base_path/sub_path') - self.assertEqual(_EXPECTED_BODY, body) - self.assertEqual(self.srmock.status, falcon.HTTP_200) - self.simulate_request('/base_pathIncorrect/sub_path') - self.assertEqual(self.srmock.status, falcon.HTTP_404) + response = client.simulate_request(path='/base_path/sub_path') + assert _EXPECTED_BODY == response.json + assert response.status == falcon.HTTP_200 + response = client.simulate_request(path='/base_pathIncorrect/sub_path') + assert response.status == falcon.HTTP_404 class TestResourceMiddleware(TestMiddleware): - def test_can_access_resource_params(self): """Test that params can be accessed from within process_resource""" global context @@ -409,11 +622,51 @@ def on_get(self, req, resp, **params): resp.body = json.dumps(params) - self.api = falcon.API(middleware=AccessParamsMiddleware()) - self.api.add_route('/path/{id}', Resource()) - body = self.simulate_json_request('/path/22') - - self.assertIn('params', context) - self.assertTrue(context['params']) - self.assertEqual(context['params']['id'], '22') - self.assertEqual(body, {'added': True, 'id': '22'}) + app = falcon.API(middleware=AccessParamsMiddleware()) + app.add_route('/path/{id}', Resource()) + client = testing.TestClient(app) + response = client.simulate_request(path='/path/22') + + assert 'params' in context + assert context['params'] + assert context['params']['id'] == '22' + assert response.json == {'added': True, 'id': '22'} + + +class TestErrorHandling(TestMiddleware): + def test_error_composed_before_resp_middleware_called(self): + mw = CaptureResponseMiddleware() + app = falcon.API(middleware=mw) + app.add_route('/', MiddlewareClassResource()) + client = testing.TestClient(app) + + response = client.simulate_request(path='/', method='POST') + assert response.status == falcon.HTTP_403 + assert mw.resp.status == response.status + + composed_body = json.loads(mw.resp.body) + assert composed_body['title'] == response.status + + assert not mw.req_succeeded + + # NOTE(kgriffs): Sanity-check the other params passed to + # process_response() + assert isinstance(mw.req, falcon.Request) + assert isinstance(mw.resource, MiddlewareClassResource) + + def test_http_status_raised_from_error_handler(self): + mw = CaptureResponseMiddleware() + app = falcon.API(middleware=mw) + app.add_route('/', MiddlewareClassResource()) + client = testing.TestClient(app) + + def _http_error_handler(error, req, resp, params): + raise falcon.HTTPStatus(falcon.HTTP_201) + + # NOTE(kgriffs): This will take precedence over the default + # handler for facon.HTTPError. + app.add_error_handler(falcon.HTTPError, _http_error_handler) + + response = client.simulate_request(path='/', method='POST') + assert response.status == falcon.HTTP_201 + assert mw.resp.status == response.status diff -Nru python-falcon-1.0.0/tests/test_options.py python-falcon-1.4.1/tests/test_options.py --- python-falcon-1.0.0/tests/test_options.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_options.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,16 +1,32 @@ +import pytest + from falcon.request import RequestOptions -import falcon.testing as testing -class TestRequestOptions(testing.TestBase): +class TestRequestOptions(object): + + def test_option_defaults(self): + options = RequestOptions() - def test_correct_options(self): + assert not options.keep_blank_qs_values + assert not options.auto_parse_form_urlencoded + assert options.auto_parse_qs_csv + assert options.strip_url_path_trailing_slash + + @pytest.mark.parametrize('option_name', [ + 'keep_blank_qs_values', + 'auto_parse_form_urlencoded', + 'auto_parse_qs_csv', + 'strip_url_path_trailing_slash', + ]) + def test_options_toggle(self, option_name): options = RequestOptions() - self.assertFalse(options.keep_blank_qs_values) - options.keep_blank_qs_values = True - self.assertTrue(options.keep_blank_qs_values) - options.keep_blank_qs_values = False - self.assertFalse(options.keep_blank_qs_values) + + setattr(options, option_name, True) + assert getattr(options, option_name) + + setattr(options, option_name, False) + assert not getattr(options, option_name) def test_incorrect_options(self): options = RequestOptions() @@ -18,4 +34,5 @@ def _assign_invalid(): options.invalid_option_and_attribute = True - self.assertRaises(AttributeError, _assign_invalid) + with pytest.raises(AttributeError): + _assign_invalid() diff -Nru python-falcon-1.0.0/tests/test_query_params.py python-falcon-1.4.1/tests/test_query_params.py --- python-falcon-1.0.0/tests/test_query_params.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_query_params.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,303 +1,440 @@ -from datetime import date +from datetime import date, datetime +from uuid import UUID -import ddt +try: + import ujson as json +except ImportError: + import json + +import pytest import falcon -import falcon.testing as testing from falcon.errors import HTTPInvalidParam +import falcon.testing as testing + + +class Resource(testing.SimpleTestResource): + + @falcon.before(testing.capture_responder_args) + @falcon.before(testing.set_resp_defaults) + def on_put(self, req, resp, **kwargs): + pass + + @falcon.before(testing.capture_responder_args) + @falcon.before(testing.set_resp_defaults) + def on_patch(self, req, resp, **kwargs): + pass + + @falcon.before(testing.capture_responder_args) + @falcon.before(testing.set_resp_defaults) + def on_delete(self, req, resp, **kwargs): + pass + + @falcon.before(testing.capture_responder_args) + @falcon.before(testing.set_resp_defaults) + def on_head(self, req, resp, **kwargs): + pass + + @falcon.before(testing.capture_responder_args) + @falcon.before(testing.set_resp_defaults) + def on_options(self, req, resp, **kwargs): + pass + + +@pytest.fixture +def resource(): + return Resource() + + +@pytest.fixture +def client(): + app = falcon.API() + app.req_options.auto_parse_form_urlencoded = True + return testing.TestClient(app) + + +def simulate_request_get_query_params(client, path, query_string, **kwargs): + return client.simulate_request(path=path, query_string=query_string, **kwargs) + +def simulate_request_post_query_params(client, path, query_string, **kwargs): + headers = kwargs.setdefault('headers', {}) + headers['Content-Type'] = 'application/x-www-form-urlencoded' + if 'method' not in kwargs: + kwargs['method'] = 'POST' + return client.simulate_request(path=path, body=query_string, **kwargs) -@ddt.ddt -class _TestQueryParams(testing.TestBase): - def before(self): - self.resource = testing.TestResource() - self.api.add_route('/', self.resource) +@pytest.mark.parametrize('simulate_request', [ + simulate_request_get_query_params, + simulate_request_post_query_params, +]) +class TestQueryParams(object): - def test_none(self): + def test_none(self, simulate_request, client, resource): query_string = '' - self.simulate_request('/', query_string=query_string) + client.app.add_route('/', resource) # TODO: DRY up this setup logic + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req store = {} - self.assertIs(req.get_param('marker'), None) - self.assertIs(req.get_param('limit', store), None) - self.assertNotIn('limit', store) - self.assertIs(req.get_param_as_int('limit'), None) - self.assertIs(req.get_param_as_bool('limit'), None) - self.assertIs(req.get_param_as_list('limit'), None) + assert req.get_param('marker') is None + assert req.get_param('limit', store) is None + assert 'limit' not in store + assert req.get_param_as_int('limit') is None + assert req.get_param_as_bool('limit') is None + assert req.get_param_as_list('limit') is None - def test_blank(self): + def test_blank(self, simulate_request, client, resource): query_string = 'marker=' - self.simulate_request('/', query_string=query_string) + client.app.add_route('/', resource) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req - self.assertIs(req.get_param('marker'), None) + req = resource.captured_req + assert req.get_param('marker') is None store = {} - self.assertIs(req.get_param('marker', store=store), None) - self.assertNotIn('marker', store) + assert req.get_param('marker', store=store) is None + assert 'marker' not in store - def test_simple(self): + def test_simple(self, simulate_request, client, resource): query_string = 'marker=deadbeef&limit=25' - self.simulate_request('/', query_string=query_string) + client.app.add_route('/', resource) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req store = {} - self.assertEqual(req.get_param('marker', store=store) or 'nada', - 'deadbeef') - self.assertEqual(req.get_param('limit', store=store) or '0', '25') + assert req.get_param('marker', store=store) or 'nada' == 'deadbeef' + assert req.get_param('limit', store=store) or '0' == '25' - self.assertEqual(store['marker'], 'deadbeef') - self.assertEqual(store['limit'], '25') + assert store['marker'] == 'deadbeef' + assert store['limit'] == '25' - def test_percent_encoded(self): + def test_percent_encoded(self, simulate_request, client, resource): query_string = 'id=23,42&q=%e8%b1%86+%e7%93%a3' - self.simulate_request('/', query_string=query_string) + client.app.add_route('/', resource) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # NOTE(kgriffs): For lists, get_param will return one of the # elements, but which one it will choose is undefined. - self.assertIn(req.get_param('id'), [u'23', u'42']) + assert req.get_param('id') in [u'23', u'42'] + + assert req.get_param_as_list('id', int) == [23, 42] + assert req.get_param('q') == u'\u8c46 \u74e3' + + def test_option_auto_parse_qs_csv_simple_false(self, simulate_request, client, resource): + client.app.add_route('/', resource) + client.app.req_options.auto_parse_qs_csv = False + + query_string = 'id=23,42,,&id=2' + simulate_request(client=client, path='/', query_string=query_string) + + req = resource.captured_req + + assert req.params['id'] == [u'23,42,,', u'2'] + assert req.get_param('id') in [u'23,42,,', u'2'] + assert req.get_param_as_list('id') == [u'23,42,,', u'2'] + + def test_option_auto_parse_qs_csv_simple_true(self, simulate_request, client, resource): + client.app.add_route('/', resource) + client.app.req_options.auto_parse_qs_csv = True + + query_string = 'id=23,42,,&id=2' + simulate_request(client=client, path='/', query_string=query_string) + + req = resource.captured_req + + assert req.params['id'] == [u'23', u'42', u'2'] + assert req.get_param('id') in [u'23', u'42', u'2'] + assert req.get_param_as_list('id', int) == [23, 42, 2] + + def test_option_auto_parse_qs_csv_complex_false(self, simulate_request, client, resource): + client.app.add_route('/', resource) + client.app.req_options.auto_parse_qs_csv = False + + encoded_json = '%7B%22msg%22:%22Testing%201,2,3...%22,%22code%22:857%7D' + decoded_json = '{"msg":"Testing 1,2,3...","code":857}' + + query_string = ('colors=red,green,blue&limit=1' + '&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b' + '&empty1=&empty2=,&empty3=,,' + '&thing=' + encoded_json) + + simulate_request(client=client, path='/', query_string=query_string) + + req = resource.captured_req - self.assertEqual(req.get_param_as_list('id', int), [23, 42]) - self.assertEqual(req.get_param('q'), u'\u8c46 \u74e3') + assert req.get_param('colors') in 'red,green,blue' + assert req.get_param_as_list('colors') == [u'red,green,blue'] - def test_bad_percentage(self): + assert req.get_param_as_list('limit') == ['1'] + + assert req.get_param_as_list('empty1') is None + assert req.get_param_as_list('empty2') == [u','] + assert req.get_param_as_list('empty3') == [u',,'] + + assert req.get_param_as_list('list-ish1') == [u'f,,x'] + assert req.get_param_as_list('list-ish2') == [u',0'] + assert req.get_param_as_list('list-ish3') == [u'a,,,b'] + + assert req.get_param('thing') == decoded_json + + def test_bad_percentage(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'x=%%20%+%&y=peregrine&z=%a%z%zz%1%20e' - self.simulate_request('/', query_string=query_string) - self.assertEqual(self.srmock.status, falcon.HTTP_200) + response = simulate_request(client=client, path='/', query_string=query_string) + assert response.status == falcon.HTTP_200 - req = self.resource.req - self.assertEqual(req.get_param('x'), '% % %') - self.assertEqual(req.get_param('y'), 'peregrine') - self.assertEqual(req.get_param('z'), '%a%z%zz%1 e') + req = resource.captured_req + assert req.get_param('x') == '% % %' + assert req.get_param('y') == 'peregrine' + assert req.get_param('z') == '%a%z%zz%1 e' - def test_allowed_names(self): + def test_allowed_names(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = ('p=0&p1=23&2p=foo&some-thing=that&blank=&' 'some_thing=x&-bogus=foo&more.things=blah&' '_thing=42&_charset_=utf-8') - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param('p'), '0') - self.assertEqual(req.get_param('p1'), '23') - self.assertEqual(req.get_param('2p'), 'foo') - self.assertEqual(req.get_param('some-thing'), 'that') - self.assertIs(req.get_param('blank'), None) - self.assertEqual(req.get_param('some_thing'), 'x') - self.assertEqual(req.get_param('-bogus'), 'foo') - self.assertEqual(req.get_param('more.things'), 'blah') - self.assertEqual(req.get_param('_thing'), '42') - self.assertEqual(req.get_param('_charset_'), 'utf-8') - - @ddt.data('get_param', 'get_param_as_int', 'get_param_as_bool', - 'get_param_as_list') - def test_required(self, method_name): + req = resource.captured_req + assert req.get_param('p') == '0' + assert req.get_param('p1') == '23' + assert req.get_param('2p') == 'foo' + assert req.get_param('some-thing') == 'that' + assert req.get_param('blank') is None + assert req.get_param('some_thing') == 'x' + assert req.get_param('-bogus') == 'foo' + assert req.get_param('more.things') == 'blah' + assert req.get_param('_thing') == '42' + assert req.get_param('_charset_') == 'utf-8' + + @pytest.mark.parametrize('method_name', [ + 'get_param', + 'get_param_as_int', + 'get_param_as_uuid', + 'get_param_as_bool', + 'get_param_as_list', + ]) + def test_required(self, simulate_request, client, resource, method_name): + client.app.add_route('/', resource) query_string = '' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req try: getattr(req, method_name)('marker', required=True) - self.fail('falcon.HTTPMissingParam not raised') + pytest.fail('falcon.HTTPMissingParam not raised') except falcon.HTTPMissingParam as ex: - self.assertIsInstance(ex, falcon.HTTPBadRequest) - self.assertEqual(ex.title, 'Missing parameter') + assert isinstance(ex, falcon.HTTPBadRequest) + assert ex.title == 'Missing parameter' expected_desc = 'The "marker" parameter is required.' - self.assertEqual(ex.description, expected_desc) + assert ex.description == expected_desc - def test_int(self): + def test_int(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'marker=deadbeef&limit=25' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req try: req.get_param_as_int('marker') except Exception as ex: - self.assertIsInstance(ex, falcon.HTTPBadRequest) - self.assertIsInstance(ex, falcon.HTTPInvalidParam) - self.assertEqual(ex.title, 'Invalid parameter') + assert isinstance(ex, falcon.HTTPBadRequest) + assert isinstance(ex, falcon.HTTPInvalidParam) + assert ex.title == 'Invalid parameter' expected_desc = ('The "marker" parameter is invalid. ' 'The value must be an integer.') - self.assertEqual(ex.description, expected_desc) + assert ex.description == expected_desc - self.assertEqual(req.get_param_as_int('limit'), 25) + assert req.get_param_as_int('limit') == 25 store = {} - self.assertEqual(req.get_param_as_int('limit', store=store), 25) - self.assertEqual(store['limit'], 25) + assert req.get_param_as_int('limit', store=store) == 25 + assert store['limit'] == 25 - self.assertEqual( - req.get_param_as_int('limit', min=1, max=50), 25) + assert req.get_param_as_int('limit', min=1, max=50) == 25 - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'limit', min=0, max=10) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('limit', min=0, max=10) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'limit', min=0, max=24) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('limit', min=0, max=24) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'limit', min=30, max=24) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('limit', min=30, max=24) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'limit', min=30, max=50) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('limit', min=30, max=50) - self.assertEqual( - req.get_param_as_int('limit', min=1), 25) + assert req.get_param_as_int('limit', min=1) == 25 - self.assertEqual( - req.get_param_as_int('limit', max=50), 25) + assert req.get_param_as_int('limit', max=50) == 25 - self.assertEqual( - req.get_param_as_int('limit', max=25), 25) + assert req.get_param_as_int('limit', max=25) == 25 - self.assertEqual( - req.get_param_as_int('limit', max=26), 25) + assert req.get_param_as_int('limit', max=26) == 25 - self.assertEqual( - req.get_param_as_int('limit', min=25), 25) + assert req.get_param_as_int('limit', min=25) == 25 - self.assertEqual( - req.get_param_as_int('limit', min=24), 25) + assert req.get_param_as_int('limit', min=24) == 25 - self.assertEqual( - req.get_param_as_int('limit', min=-24), 25) + assert req.get_param_as_int('limit', min=-24) == 25 - def test_int_neg(self): + def test_int_neg(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'marker=deadbeef&pos=-7' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param_as_int('pos'), -7) + req = resource.captured_req + assert req.get_param_as_int('pos') == -7 - self.assertEqual( - req.get_param_as_int('pos', min=-10, max=10), -7) + assert req.get_param_as_int('pos', min=-10, max=10) == -7 - self.assertEqual( - req.get_param_as_int('pos', max=10), -7) + assert req.get_param_as_int('pos', max=10) == -7 - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'pos', min=-6, max=0) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('pos', min=-6, max=0) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'pos', min=-6) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('pos', min=-6) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'pos', min=0, max=10) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('pos', min=0, max=10) - self.assertRaises( - falcon.HTTPBadRequest, - req.get_param_as_int, 'pos', min=0, max=10) + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_int('pos', min=0, max=10) - def test_boolean(self): - query_string = ('echo=true&doit=false&bogus=bar&bogus2=foo&' - 't1=True&f1=False&t2=yes&f2=no&blank&one=1&zero=0') - self.simulate_request('/', query_string=query_string) + def test_uuid(self, simulate_request, client, resource): + client.app.add_route('/', resource) + query_string = ('marker1=8d76b7b3-d0dd-46ca-ad6e-3989dcd66959&' + 'marker2=64be949b-3433-4d36-a4a8-9f19d352fee8&' + 'marker2=8D76B7B3-d0dd-46ca-ad6e-3989DCD66959&' + 'short=4be949b-3433-4d36-a4a8-9f19d352fee8') + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req - self.assertRaises(falcon.HTTPBadRequest, req.get_param_as_bool, - 'bogus') + req = resource.captured_req + + expected_uuid = UUID('8d76b7b3-d0dd-46ca-ad6e-3989dcd66959') + assert req.get_param_as_uuid('marker1') == expected_uuid + assert req.get_param_as_uuid('marker2') == expected_uuid + assert req.get_param_as_uuid('marker3') is None + assert req.get_param_as_uuid('marker3', required=False) is None + + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_uuid('short') + + store = {} + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_uuid('marker3', required=True, store=store) + + assert not store + assert req.get_param_as_uuid('marker1', store=store) + assert store['marker1'] == expected_uuid + + def test_boolean(self, simulate_request, client, resource): + client.app.add_route('/', resource) + query_string = ('echo=true&doit=false&bogus=bar&bogus2=foo&' + 't1=True&f1=False&t2=yes&f2=no&blank&one=1&zero=0&' + 'checkbox1=on&checkbox2=off') + simulate_request(client=client, path='/', query_string=query_string) + + req = resource.captured_req + with pytest.raises(falcon.HTTPBadRequest): + req.get_param_as_bool('bogus') try: req.get_param_as_bool('bogus2') except Exception as ex: - self.assertIsInstance(ex, falcon.HTTPInvalidParam) - self.assertEqual(ex.title, 'Invalid parameter') + assert isinstance(ex, falcon.HTTPInvalidParam) + assert ex.title == 'Invalid parameter' expected_desc = ('The "bogus2" parameter is invalid. ' 'The value of the parameter must be "true" ' 'or "false".') - self.assertEqual(ex.description, expected_desc) + assert ex.description == expected_desc - self.assertEqual(req.get_param_as_bool('echo'), True) - self.assertEqual(req.get_param_as_bool('doit'), False) + assert req.get_param_as_bool('echo') is True + assert req.get_param_as_bool('doit') is False - self.assertEqual(req.get_param_as_bool('t1'), True) - self.assertEqual(req.get_param_as_bool('t2'), True) - self.assertEqual(req.get_param_as_bool('f1'), False) - self.assertEqual(req.get_param_as_bool('f2'), False) - self.assertEqual(req.get_param_as_bool('one'), True) - self.assertEqual(req.get_param_as_bool('zero'), False) - self.assertEqual(req.get_param('blank'), None) - - store = {} - self.assertEqual(req.get_param_as_bool('echo', store=store), True) - self.assertEqual(store['echo'], True) - - def test_boolean_blank(self): - self.api.req_options.keep_blank_qs_values = True - self.simulate_request( - '/', - query_string='blank&blank2=', - ) + assert req.get_param_as_bool('t1') is True + assert req.get_param_as_bool('t2') is True + assert req.get_param_as_bool('f1') is False + assert req.get_param_as_bool('f2') is False + assert req.get_param_as_bool('one') is True + assert req.get_param_as_bool('zero') is False + assert req.get_param('blank') is None - req = self.resource.req - self.assertEqual(req.get_param('blank'), '') - self.assertEqual(req.get_param('blank2'), '') - self.assertRaises(falcon.HTTPInvalidParam, req.get_param_as_bool, - 'blank') - self.assertRaises(falcon.HTTPInvalidParam, req.get_param_as_bool, - 'blank2') - self.assertEqual(req.get_param_as_bool('blank', blank_as_true=True), - True) - self.assertEqual(req.get_param_as_bool('blank3', blank_as_true=True), - None) + assert req.get_param_as_bool('checkbox1') is True + assert req.get_param_as_bool('checkbox2') is False - def test_list_type(self): + store = {} + assert req.get_param_as_bool('echo', store=store) is True + assert store['echo'] is True + + def test_boolean_blank(self, simulate_request, client, resource): + client.app.add_route('/', resource) + client.app.req_options.keep_blank_qs_values = True + simulate_request(client=client, path='/', query_string='blank&blank2=') + + req = resource.captured_req + assert req.get_param('blank') == '' + assert req.get_param('blank2') == '' + with pytest.raises(falcon.HTTPInvalidParam): + req.get_param_as_bool('blank') + + with pytest.raises(falcon.HTTPInvalidParam): + req.get_param_as_bool('blank2') + assert req.get_param_as_bool('blank', blank_as_true=True) + assert req.get_param_as_bool('blank3', blank_as_true=True) is None + + def test_list_type(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = ('colors=red,green,blue&limit=1' '&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b' '&empty1=&empty2=,&empty3=,,' '&thing_one=1,,3' '&thing_two=1&thing_two=&thing_two=3') - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # NOTE(kgriffs): For lists, get_param will return one of the # elements, but which one it will choose is undefined. - self.assertIn(req.get_param('colors'), ('red', 'green', 'blue')) + assert req.get_param('colors') in ('red', 'green', 'blue') + + assert req.get_param_as_list('colors') == ['red', 'green', 'blue'] + assert req.get_param_as_list('limit') == ['1'] + assert req.get_param_as_list('marker') is None - self.assertEqual(req.get_param_as_list('colors'), - ['red', 'green', 'blue']) - self.assertEqual(req.get_param_as_list('limit'), ['1']) - self.assertIs(req.get_param_as_list('marker'), None) - - self.assertEqual(req.get_param_as_list('empty1'), None) - self.assertEqual(req.get_param_as_list('empty2'), []) - self.assertEqual(req.get_param_as_list('empty3'), []) + assert req.get_param_as_list('empty1') is None + assert req.get_param_as_list('empty2') == [] + assert req.get_param_as_list('empty3') == [] - self.assertEqual(req.get_param_as_list('list-ish1'), - ['f', 'x']) + assert req.get_param_as_list('list-ish1') == ['f', 'x'] # Ensure that '0' doesn't get translated to None - self.assertEqual(req.get_param_as_list('list-ish2'), - ['0']) + assert req.get_param_as_list('list-ish2') == ['0'] # Ensure that '0' doesn't get translated to None - self.assertEqual(req.get_param_as_list('list-ish3'), - ['a', 'b']) + assert req.get_param_as_list('list-ish3') == ['a', 'b'] # Ensure consistency between list conventions - self.assertEqual(req.get_param_as_list('thing_one'), - ['1', '3']) - self.assertEqual(req.get_param_as_list('thing_one'), - req.get_param_as_list('thing_two')) + assert req.get_param_as_list('thing_one') == ['1', '3'] + assert ( + req.get_param_as_list('thing_one') == + req.get_param_as_list('thing_two') + ) store = {} - self.assertEqual(req.get_param_as_list('limit', store=store), ['1']) - self.assertEqual(store['limit'], ['1']) + assert req.get_param_as_list('limit', store=store) == ['1'] + assert store['limit'] == ['1'] - def test_list_type_blank(self): + def test_list_type_blank(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = ('colors=red,green,blue&limit=1' '&list-ish1=f,,x&list-ish2=,0&list-ish3=a,,,b' '&empty1=&empty2=,&empty3=,,' @@ -305,214 +442,319 @@ '&thing_two=1&thing_two=&thing_two=3' '&empty4=&empty4&empty4=' '&empty5&empty5&empty5') - self.api.req_options.keep_blank_qs_values = True - self.simulate_request( - '/', - query_string=query_string - ) + client.app.req_options.keep_blank_qs_values = True + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # NOTE(kgriffs): For lists, get_param will return one of the # elements, but which one it will choose is undefined. - self.assertIn(req.get_param('colors'), ('red', 'green', 'blue')) + assert req.get_param('colors') in ('red', 'green', 'blue') + + assert req.get_param_as_list('colors') == ['red', 'green', 'blue'] + assert req.get_param_as_list('limit') == ['1'] + assert req.get_param_as_list('marker') is None - self.assertEqual(req.get_param_as_list('colors'), - ['red', 'green', 'blue']) - self.assertEqual(req.get_param_as_list('limit'), ['1']) - self.assertIs(req.get_param_as_list('marker'), None) - - self.assertEqual(req.get_param_as_list('empty1'), ['']) - self.assertEqual(req.get_param_as_list('empty2'), ['', '']) - self.assertEqual(req.get_param_as_list('empty3'), ['', '', '']) + assert req.get_param_as_list('empty1') == [''] + assert req.get_param_as_list('empty2') == ['', ''] + assert req.get_param_as_list('empty3') == ['', '', ''] - self.assertEqual(req.get_param_as_list('list-ish1'), - ['f', '', 'x']) + assert req.get_param_as_list('list-ish1') == ['f', '', 'x'] # Ensure that '0' doesn't get translated to None - self.assertEqual(req.get_param_as_list('list-ish2'), - ['', '0']) + assert req.get_param_as_list('list-ish2') == ['', '0'] # Ensure that '0' doesn't get translated to None - self.assertEqual(req.get_param_as_list('list-ish3'), - ['a', '', '', 'b']) + assert req.get_param_as_list('list-ish3') == ['a', '', '', 'b'] # Ensure consistency between list conventions - self.assertEqual(req.get_param_as_list('thing_one'), - ['1', '', '3']) - self.assertEqual(req.get_param_as_list('thing_one'), - req.get_param_as_list('thing_two')) + assert req.get_param_as_list('thing_one') == ['1', '', '3'] + assert req.get_param_as_list('thing_one') == req.get_param_as_list('thing_two') store = {} - self.assertEqual(req.get_param_as_list('limit', store=store), ['1']) - self.assertEqual(store['limit'], ['1']) + assert req.get_param_as_list('limit', store=store) == ['1'] + assert store['limit'] == ['1'] # Test empty elements - self.assertEqual(req.get_param_as_list('empty4'), ['', '', '']) - self.assertEqual(req.get_param_as_list('empty5'), ['', '', '']) - self.assertEqual(req.get_param_as_list('empty4'), - req.get_param_as_list('empty5')) + assert req.get_param_as_list('empty4') == ['', '', ''] + assert req.get_param_as_list('empty5') == ['', '', ''] + assert req.get_param_as_list('empty4') == req.get_param_as_list('empty5') - def test_list_transformer(self): + def test_list_transformer(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'coord=1.4,13,15.1&limit=100&things=4,,1' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # NOTE(kgriffs): For lists, get_param will return one of the # elements, but which one it will choose is undefined. - self.assertIn(req.get_param('coord'), ('1.4', '13', '15.1')) + assert req.get_param('coord') in ('1.4', '13', '15.1') expected = [1.4, 13.0, 15.1] actual = req.get_param_as_list('coord', transform=float) - self.assertEqual(actual, expected) + assert actual == expected expected = ['4', '1'] actual = req.get_param_as_list('things', transform=str) - self.assertEqual(actual, expected) + assert actual == expected expected = [4, 1] actual = req.get_param_as_list('things', transform=int) - self.assertEqual(actual, expected) + assert actual == expected try: req.get_param_as_list('coord', transform=int) except Exception as ex: - self.assertIsInstance(ex, falcon.HTTPInvalidParam) - self.assertEqual(ex.title, 'Invalid parameter') + assert isinstance(ex, falcon.HTTPInvalidParam) + assert ex.title == 'Invalid parameter' expected_desc = ('The "coord" parameter is invalid. ' 'The value is not formatted correctly.') - self.assertEqual(ex.description, expected_desc) + assert ex.description == expected_desc - def test_param_property(self): + def test_param_property(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'ant=4&bee=3&cat=2&dog=1' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req - self.assertEqual( - sorted(req.params.items()), - [('ant', '4'), ('bee', '3'), ('cat', '2'), ('dog', '1')]) + req = resource.captured_req + assert ( + sorted(req.params.items()) == + [('ant', '4'), ('bee', '3'), ('cat', '2'), ('dog', '1')] + ) - def test_multiple_form_keys(self): + def test_multiple_form_keys(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'ant=1&ant=2&bee=3&cat=6&cat=5&cat=4' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # By definition, we cannot guarantee which of the multiple keys will # be returned by .get_param(). - self.assertIn(req.get_param('ant'), ('1', '2')) + assert req.get_param('ant') in ('1', '2') # There is only one 'bee' key so it remains a scalar. - self.assertEqual(req.get_param('bee'), '3') + assert req.get_param('bee') == '3' # There are three 'cat' keys; order is preserved. - self.assertIn(req.get_param('cat'), ('6', '5', '4')) + assert req.get_param('cat') in ('6', '5', '4') - def test_multiple_keys_as_bool(self): + def test_multiple_keys_as_bool(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'ant=true&ant=yes&ant=True' - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param_as_bool('ant'), True) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_bool('ant') is True - def test_multiple_keys_as_int(self): + def test_multiple_keys_as_int(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'ant=1&ant=2&ant=3' - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertIn(req.get_param_as_int('ant'), (1, 2, 3)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_int('ant') in (1, 2, 3) - def test_multiple_form_keys_as_list(self): + def test_multiple_form_keys_as_list(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'ant=1&ant=2&bee=3&cat=6&cat=5&cat=4' - self.simulate_request('/', query_string=query_string) + simulate_request(client=client, path='/', query_string=query_string) - req = self.resource.req + req = resource.captured_req # There are two 'ant' keys. - self.assertEqual(req.get_param_as_list('ant'), ['1', '2']) + assert req.get_param_as_list('ant') == ['1', '2'] # There is only one 'bee' key.. - self.assertEqual(req.get_param_as_list('bee'), ['3']) + assert req.get_param_as_list('bee') == ['3'] # There are three 'cat' keys; order is preserved. - self.assertEqual(req.get_param_as_list('cat'), ['6', '5', '4']) + assert req.get_param_as_list('cat') == ['6', '5', '4'] - def test_get_date_valid(self): + def test_get_date_valid(self, simulate_request, client, resource): + client.app.add_route('/', resource) date_value = '2015-04-20' query_string = 'thedate={0}'.format(date_value) - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param_as_date('thedate'), - date(2015, 4, 20)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_date('thedate') == date(2015, 4, 20) - def test_get_date_missing_param(self): + def test_get_date_missing_param(self, simulate_request, client, resource): + client.app.add_route('/', resource) query_string = 'notthedate=2015-04-20' - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param_as_date('thedate'), - None) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_date('thedate') is None - def test_get_date_valid_with_format(self): + def test_get_date_valid_with_format(self, simulate_request, client, resource): + client.app.add_route('/', resource) date_value = '20150420' query_string = 'thedate={0}'.format(date_value) format_string = '%Y%m%d' - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertEqual(req.get_param_as_date('thedate', - format_string=format_string), - date(2015, 4, 20)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_date('thedate', format_string=format_string) == date(2015, 4, 20) - def test_get_date_store(self): + def test_get_date_store(self, simulate_request, client, resource): + client.app.add_route('/', resource) date_value = '2015-04-20' query_string = 'thedate={0}'.format(date_value) - self.simulate_request('/', query_string=query_string) - req = self.resource.req + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req store = {} req.get_param_as_date('thedate', store=store) - self.assertNotEqual(len(store), 0) + assert len(store) != 0 - def test_get_date_invalid(self): + def test_get_date_invalid(self, simulate_request, client, resource): + client.app.add_route('/', resource) date_value = 'notarealvalue' query_string = 'thedate={0}'.format(date_value) format_string = '%Y%m%d' - self.simulate_request('/', query_string=query_string) - req = self.resource.req - self.assertRaises(HTTPInvalidParam, req.get_param_as_date, - 'thedate', format_string=format_string) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + with pytest.raises(HTTPInvalidParam): + req.get_param_as_date('thedate', format_string=format_string) + + def test_get_datetime_valid(self, simulate_request, client, resource): + client.app.add_route('/', resource) + date_value = '2015-04-20T10:10:10Z' + query_string = 'thedate={0}'.format(date_value) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_datetime('thedate') == datetime(2015, 4, 20, 10, 10, 10) + + def test_get_datetime_missing_param(self, simulate_request, client, resource): + client.app.add_route('/', resource) + query_string = 'notthedate=2015-04-20T10:10:10Z' + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_datetime('thedate') is None + + def test_get_datetime_valid_with_format(self, simulate_request, client, resource): + client.app.add_route('/', resource) + date_value = '20150420 10:10:10' + query_string = 'thedate={0}'.format(date_value) + format_string = '%Y%m%d %H:%M:%S' + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_datetime( + 'thedate', format_string=format_string) == datetime(2015, 4, 20, 10, 10, 10) + + def test_get_datetime_store(self, simulate_request, client, resource): + client.app.add_route('/', resource) + datetime_value = '2015-04-20T10:10:10Z' + query_string = 'thedate={0}'.format(datetime_value) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + store = {} + req.get_param_as_datetime('thedate', store=store) + assert len(store) != 0 + assert store.get('thedate') == datetime(2015, 4, 20, 10, 10, 10) + def test_get_datetime_invalid(self, simulate_request, client, resource): + client.app.add_route('/', resource) + date_value = 'notarealvalue' + query_string = 'thedate={0}'.format(date_value) + format_string = '%Y%m%dT%H:%M:%S' + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + with pytest.raises(HTTPInvalidParam): + req.get_param_as_datetime('thedate', format_string=format_string) + + def test_get_dict_valid(self, simulate_request, client, resource): + client.app.add_route('/', resource) + payload_dict = {'foo': 'bar'} + query_string = 'payload={0}'.format(json.dumps(payload_dict)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_dict('payload') == payload_dict + + def test_get_dict_missing_param(self, simulate_request, client, resource): + client.app.add_route('/', resource) + payload_dict = {'foo': 'bar'} + query_string = 'notthepayload={0}'.format(json.dumps(payload_dict)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + assert req.get_param_as_dict('payload') is None + + def test_get_dict_store(self, simulate_request, client, resource): + client.app.add_route('/', resource) + payload_dict = {'foo': 'bar'} + query_string = 'payload={0}'.format(json.dumps(payload_dict)) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + store = {} + req.get_param_as_dict('payload', store=store) + assert len(store) != 0 -class PostQueryParams(_TestQueryParams): - def before(self): - super(PostQueryParams, self).before() - self.api.req_options.auto_parse_form_urlencoded = True + def test_get_dict_invalid(self, simulate_request, client, resource): + client.app.add_route('/', resource) + payload_dict = 'foobar' + query_string = 'payload={0}'.format(payload_dict) + simulate_request(client=client, path='/', query_string=query_string) + req = resource.captured_req + with pytest.raises(HTTPInvalidParam): + req.get_param_as_dict('payload') + + +class TestPostQueryParams(object): + @pytest.mark.parametrize('http_method', ('POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS')) + def test_http_methods_body_expected(self, client, resource, http_method): + client.app.add_route('/', resource) + query_string = 'marker=deadbeef&limit=25' + simulate_request_post_query_params(client=client, path='/', query_string=query_string, + method=http_method) - def simulate_request(self, path, query_string, **kwargs): - headers = {'Content-Type': 'application/x-www-form-urlencoded'} - super(PostQueryParams, self).simulate_request( - path, body=query_string, headers=headers, **kwargs) + req = resource.captured_req + assert req.get_param('marker') == 'deadbeef' + assert req.get_param('limit') == '25' + + @pytest.mark.parametrize('http_method', ('GET', 'HEAD')) + def test_http_methods_body_not_expected(self, client, resource, http_method): + client.app.add_route('/', resource) + query_string = 'marker=deadbeef&limit=25' + simulate_request_post_query_params(client=client, path='/', query_string=query_string, + method=http_method) - def test_non_ascii(self): + req = resource.captured_req + assert req.get_param('marker') is None + assert req.get_param('limit') is None + + def test_non_ascii(self, client, resource): + client.app.add_route('/', resource) value = u'\u8c46\u74e3' query_string = b'q=' + value.encode('utf-8') - self.simulate_request('/', query_string=query_string) + simulate_request_post_query_params(client=client, path='/', query_string=query_string) + + req = resource.captured_req + assert req.get_param('q') is None - req = self.resource.req - self.assertIs(req.get_param('q'), None) + def test_empty_body(self, client, resource): + client.app.add_route('/', resource) + simulate_request_post_query_params(client=client, path='/', query_string=None) - def test_explicitly_disable_auto_parse(self): - self.api.req_options.auto_parse_form_urlencoded = False - self.simulate_request('/', query_string='q=42') + req = resource.captured_req + assert req.get_param('q') is None - req = self.resource.req - self.assertIs(req.get_param('q'), None) + def test_empty_body_no_content_length(self, client, resource): + client.app.add_route('/', resource) + simulate_request_post_query_params(client=client, path='/', query_string=None) + req = resource.captured_req + assert req.get_param('q') is None -class GetQueryParams(_TestQueryParams): - def simulate_request(self, path, query_string, **kwargs): - super(GetQueryParams, self).simulate_request( - path, query_string=query_string, **kwargs) + def test_explicitly_disable_auto_parse(self, client, resource): + client.app.add_route('/', resource) + client.app.req_options.auto_parse_form_urlencoded = False + simulate_request_post_query_params(client=client, path='/', query_string='q=42') + req = resource.captured_req + assert req.get_param('q') is None -class PostQueryParamsDefaultBehavior(testing.TestBase): + +class TestPostQueryParamsDefaultBehavior(object): def test_dont_auto_parse_by_default(self): - self.resource = testing.TestResource() - self.api.add_route('/', self.resource) + app = falcon.API() + resource = testing.SimpleTestResource() + app.add_route('/', resource) + + client = testing.TestClient(app) headers = {'Content-Type': 'application/x-www-form-urlencoded'} - self.simulate_request('/', body='q=42', headers=headers) + client.simulate_request(path='/', body='q=42', headers=headers) - req = self.resource.req - self.assertIs(req.get_param('q'), None) + req = resource.captured_req + assert req.get_param('q') is None diff -Nru python-falcon-1.0.0/tests/test_redirects.py python-falcon-1.4.1/tests/test_redirects.py --- python-falcon-1.0.0/tests/test_redirects.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_redirects.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,7 +1,17 @@ -import ddt +import pytest -import falcon.testing as testing import falcon +import falcon.testing as testing + + +@pytest.fixture +def client(): + app = falcon.API() + + resource = RedirectingResource() + app.add_route('/', resource) + + return testing.TestClient(app) class RedirectingResource(object): @@ -26,24 +36,18 @@ raise falcon.HTTPPermanentRedirect('/perm/redirect') -@ddt.ddt -class TestRedirects(testing.TestBase): - - def before(self): - self.api.add_route('/', RedirectingResource()) +class TestRedirects(object): - @ddt.data( + @pytest.mark.parametrize('method,expected_status,expected_location', [ ('GET', falcon.HTTP_301, '/moved/perm'), ('POST', falcon.HTTP_302, '/found'), ('PUT', falcon.HTTP_303, '/see/other'), ('DELETE', falcon.HTTP_307, '/tmp/redirect'), ('HEAD', falcon.HTTP_308, '/perm/redirect'), - ) - @ddt.unpack - def test_redirect(self, method, expected_status, expected_location): - result = self.simulate_request('/', method=method) - - self.assertEqual(result, []) - self.assertEqual(self.srmock.status, expected_status) - self.assertEqual(self.srmock.headers_dict['location'], - expected_location) + ]) + def test_redirect(self, client, method, expected_status, expected_location): + result = client.simulate_request(path='/', method=method) + + assert not result.content + assert result.status == expected_status + assert result.headers['location'] == expected_location diff -Nru python-falcon-1.0.0/tests/test_request_access_route.py python-falcon-1.4.1/tests/test_request_access_route.py --- python-falcon-1.0.0/tests/test_request_access_route.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_access_route.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,99 @@ +from falcon.request import Request +import falcon.testing as testing + + +def test_remote_addr_only(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route', + headers={ + 'Forwarded': ('for=192.0.2.43, for="[2001:db8:cafe::17]:555",' + 'for="unknown", by=_hidden,for="\\"\\\\",' + 'for="198\\.51\\.100\\.17\\:1236";' + 'proto=https;host=example.com') + })) + + assert req.remote_addr == '127.0.0.1' + + +def test_rfc_forwarded(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route', + headers={ + 'Forwarded': ('for=192.0.2.43,for=,' + 'for="[2001:db8:cafe::17]:555",' + 'for=x,' + 'for="unknown", by=_hidden,for="\\"\\\\",' + 'for="_don\\\"t_\\try_this\\\\at_home_\\42",' + 'for="198\\.51\\.100\\.17\\:1236";' + 'proto=https;host=example.com') + })) + + compares = ['192.0.2.43', '2001:db8:cafe::17', 'x', + 'unknown', '"\\', '_don"t_try_this\\at_home_42', + '198.51.100.17'] + + req.access_route == compares + + # test cached + req.access_route == compares + + +def test_malformed_rfc_forwarded(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route', + headers={ + 'Forwarded': 'for' + })) + + req.access_route == [] + + # test cached + req.access_route == [] + + +def test_x_forwarded_for(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route', + headers={ + 'X-Forwarded-For': ('192.0.2.43, 2001:db8:cafe::17,' + 'unknown, _hidden, 203.0.113.60') + })) + + assert req.access_route == [ + '192.0.2.43', + '2001:db8:cafe::17', + 'unknown', + '_hidden', + '203.0.113.60' + ] + + +def test_x_real_ip(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route', + headers={ + 'X-Real-IP': '2001:db8:cafe::17' + })) + + assert req.access_route == ['2001:db8:cafe::17'] + + +def test_remote_addr(): + req = Request(testing.create_environ( + host='example.com', + path='/access_route')) + + assert req.access_route == ['127.0.0.1'] + + +def test_remote_addr_missing(): + env = testing.create_environ(host='example.com', path='/access_route') + del env['REMOTE_ADDR'] + + req = Request(env) + assert req.access_route == [] diff -Nru python-falcon-1.0.0/tests/test_request_attrs.py python-falcon-1.4.1/tests/test_request_attrs.py --- python-falcon-1.0.0/tests/test_request_attrs.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_attrs.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,813 @@ +import datetime +import itertools + +import pytest +import six + +import falcon +from falcon.request import Request, RequestOptions +import falcon.testing as testing +import falcon.uri + +_PROTOCOLS = ['HTTP/1.0', 'HTTP/1.1'] + + +class TestRequestAttributes(object): + + def setup_method(self, method): + self.qs = 'marker=deadbeef&limit=10' + + self.headers = { + 'Content-Type': 'text/plain', + 'Content-Length': '4829', + 'Authorization': '' + } + + self.app = '/test' + self.path = '/hello' + self.relative_uri = self.path + '?' + self.qs + + self.req = Request(testing.create_environ( + app=self.app, + port=8080, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + self.req_noqs = Request(testing.create_environ( + app=self.app, + path='/hello', + headers=self.headers)) + + def test_missing_qs(self): + env = testing.create_environ() + if 'QUERY_STRING' in env: + del env['QUERY_STRING'] + + # Should not cause an exception when Request is instantiated + Request(env) + + def test_empty(self): + assert self.req.auth is None + + def test_host(self): + assert self.req.host == testing.DEFAULT_HOST + + def test_subdomain(self): + req = Request(testing.create_environ( + host='com', + path='/hello', + headers=self.headers)) + assert req.subdomain is None + + req = Request(testing.create_environ( + host='example.com', + path='/hello', + headers=self.headers)) + assert req.subdomain == 'example' + + req = Request(testing.create_environ( + host='highwire.example.com', + path='/hello', + headers=self.headers)) + assert req.subdomain == 'highwire' + + req = Request(testing.create_environ( + host='lb01.dfw01.example.com', + port=8080, + path='/hello', + headers=self.headers)) + assert req.subdomain == 'lb01' + + # NOTE(kgriffs): Behavior for IP addresses is undefined, + # so just make sure it doesn't blow up. + req = Request(testing.create_environ( + host='127.0.0.1', + path='/hello', + headers=self.headers)) + assert type(req.subdomain) == str + + # NOTE(kgriffs): Test fallback to SERVER_NAME by using + # HTTP 1.0, which will cause .create_environ to not set + # HTTP_HOST. + req = Request(testing.create_environ( + protocol='HTTP/1.0', + host='example.com', + path='/hello', + headers=self.headers)) + assert req.subdomain == 'example' + + def test_reconstruct_url(self): + req = self.req + + scheme = req.protocol + host = req.get_header('host') + app = req.app + path = req.path + query_string = req.query_string + + expected_prefix = ''.join([scheme, '://', host, app]) + expected_uri = ''.join([expected_prefix, path, '?', query_string]) + + assert req.uri == expected_uri + assert req.prefix == expected_prefix + assert req.prefix == expected_prefix # Check cached value + + @pytest.mark.skipif(not six.PY3, reason='Test only applies to Python 3') + @pytest.mark.parametrize('test_path', [ + u'/hello_\u043f\u0440\u0438\u0432\u0435\u0442', + u'/test/%E5%BB%B6%E5%AE%89', + u'/test/%C3%A4%C3%B6%C3%BC%C3%9F%E2%82%AC', + ]) + def test_nonlatin_path(self, test_path): + # NOTE(kgriffs): When a request comes in, web servers decode + # the path. The decoded path may contain UTF-8 characters, + # but according to the WSGI spec, no strings can contain chars + # outside ISO-8859-1. Therefore, to reconcile the URI + # encoding standard that allows UTF-8 with the WSGI spec + # that does not, WSGI servers tunnel the string via + # ISO-8859-1. falcon.testing.create_environ() mimics this + # behavior, e.g.: + # + # tunnelled_path = path.encode('utf-8').decode('iso-8859-1') + # + # falcon.Request does the following to reverse the process: + # + # path = tunnelled_path.encode('iso-8859-1').decode('utf-8', 'replace') + # + + req = Request(testing.create_environ( + host='com', + path=test_path, + headers=self.headers)) + + assert req.path == falcon.uri.decode(test_path) + + def test_uri(self): + prefix = 'http://' + testing.DEFAULT_HOST + ':8080' + self.app + uri = prefix + self.relative_uri + + assert self.req.url == uri + assert self.req.prefix == prefix + + # NOTE(kgriffs): Call twice to check caching works + assert self.req.uri == uri + assert self.req.uri == uri + + uri_noqs = ('http://' + testing.DEFAULT_HOST + self.app + self.path) + assert self.req_noqs.uri == uri_noqs + + def test_uri_https(self): + # ======================================================= + # Default port, implicit + # ======================================================= + req = Request(testing.create_environ( + path='/hello', scheme='https')) + uri = ('https://' + testing.DEFAULT_HOST + '/hello') + + assert req.uri == uri + + # ======================================================= + # Default port, explicit + # ======================================================= + req = Request(testing.create_environ( + path='/hello', scheme='https', port=443)) + uri = ('https://' + testing.DEFAULT_HOST + '/hello') + + assert req.uri == uri + + # ======================================================= + # Non-default port + # ======================================================= + req = Request(testing.create_environ( + path='/hello', scheme='https', port=22)) + uri = ('https://' + testing.DEFAULT_HOST + ':22/hello') + + assert req.uri == uri + + def test_uri_http_1_0(self): + # ======================================================= + # HTTP, 80 + # ======================================================= + req = Request(testing.create_environ( + protocol='HTTP/1.0', + app=self.app, + port=80, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + uri = ('http://' + testing.DEFAULT_HOST + + self.app + self.relative_uri) + + assert req.uri == uri + + # ======================================================= + # HTTP, 80 + # ======================================================= + req = Request(testing.create_environ( + protocol='HTTP/1.0', + app=self.app, + port=8080, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + uri = ('http://' + testing.DEFAULT_HOST + ':8080' + + self.app + self.relative_uri) + + assert req.uri == uri + + # ======================================================= + # HTTP, 80 + # ======================================================= + req = Request(testing.create_environ( + protocol='HTTP/1.0', + scheme='https', + app=self.app, + port=443, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + uri = ('https://' + testing.DEFAULT_HOST + + self.app + self.relative_uri) + + assert req.uri == uri + + # ======================================================= + # HTTP, 80 + # ======================================================= + req = Request(testing.create_environ( + protocol='HTTP/1.0', + scheme='https', + app=self.app, + port=22, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + uri = ('https://' + testing.DEFAULT_HOST + ':22' + + self.app + self.relative_uri) + + assert req.uri == uri + + def test_relative_uri(self): + assert self.req.relative_uri == self.app + self.relative_uri + assert self.req_noqs.relative_uri == self.app + self.path + + req_noapp = Request(testing.create_environ( + path='/hello', + query_string=self.qs, + headers=self.headers)) + + assert req_noapp.relative_uri == self.relative_uri + + req_noapp = Request(testing.create_environ( + path='/hello/', + query_string=self.qs, + headers=self.headers)) + + # NOTE(kgriffs): Call twice to check caching works + assert req_noapp.relative_uri == self.relative_uri + assert req_noapp.relative_uri == self.relative_uri + + options = RequestOptions() + options.strip_url_path_trailing_slash = False + req_noapp = Request(testing.create_environ( + path='/hello/', + query_string=self.qs, + headers=self.headers), + options=options) + + assert req_noapp.relative_uri == '/hello/' + '?' + self.qs + + def test_client_accepts(self): + headers = {'Accept': 'application/xml'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('application/xml') + + headers = {'Accept': '*/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('application/xml') + assert req.client_accepts('application/json') + assert req.client_accepts('application/x-msgpack') + + headers = {'Accept': 'application/x-msgpack'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts('application/xml') + assert not req.client_accepts('application/json') + assert req.client_accepts('application/x-msgpack') + + headers = {} # NOTE(kgriffs): Equivalent to '*/*' per RFC + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('application/xml') + + headers = {'Accept': 'application/json'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts('application/xml') + + headers = {'Accept': 'application/x-msgpack'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('application/x-msgpack') + + headers = {'Accept': 'application/xm'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts('application/xml') + + headers = {'Accept': 'application/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('application/json') + assert req.client_accepts('application/xml') + assert req.client_accepts('application/x-msgpack') + + headers = {'Accept': 'text/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('text/plain') + assert req.client_accepts('text/csv') + assert not req.client_accepts('application/xhtml+xml') + + headers = {'Accept': 'text/*, application/xhtml+xml; q=0.0'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('text/plain') + assert req.client_accepts('text/csv') + assert not req.client_accepts('application/xhtml+xml') + + headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('text/plain') + assert req.client_accepts('application/xhtml+xml') + + headers = {'Accept': 'text/*, application/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('text/plain') + assert req.client_accepts('application/xml') + assert req.client_accepts('application/json') + assert req.client_accepts('application/x-msgpack') + + headers = {'Accept': 'text/*,application/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts('text/plain') + assert req.client_accepts('application/xml') + assert req.client_accepts('application/json') + assert req.client_accepts('application/x-msgpack') + + def test_client_accepts_bogus(self): + headers = {'Accept': '~'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts('text/plain') + assert not req.client_accepts('application/json') + + def test_client_accepts_props(self): + headers = {'Accept': 'application/xml'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts_xml + assert not req.client_accepts_json + assert not req.client_accepts_msgpack + + headers = {'Accept': 'application/*'} + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts_xml + assert req.client_accepts_json + assert req.client_accepts_msgpack + + headers = {'Accept': 'application/json'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts_xml + assert req.client_accepts_json + assert not req.client_accepts_msgpack + + headers = {'Accept': 'application/x-msgpack'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts_xml + assert not req.client_accepts_json + assert req.client_accepts_msgpack + + headers = {'Accept': 'application/msgpack'} + req = Request(testing.create_environ(headers=headers)) + assert not req.client_accepts_xml + assert not req.client_accepts_json + assert req.client_accepts_msgpack + + headers = { + 'Accept': 'application/json,application/xml,application/x-msgpack' + } + req = Request(testing.create_environ(headers=headers)) + assert req.client_accepts_xml + assert req.client_accepts_json + assert req.client_accepts_msgpack + + def test_client_prefers(self): + headers = {'Accept': 'application/xml'} + req = Request(testing.create_environ(headers=headers)) + preferred_type = req.client_prefers(['application/xml']) + assert preferred_type == 'application/xml' + + headers = {'Accept': '*/*'} + preferred_type = req.client_prefers(('application/xml', + 'application/json')) + + # NOTE(kgriffs): If client doesn't care, "prefer" the first one + assert preferred_type == 'application/xml' + + headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'} + req = Request(testing.create_environ(headers=headers)) + preferred_type = req.client_prefers(['application/xhtml+xml']) + assert preferred_type == 'application/xhtml+xml' + + headers = {'Accept': '3p12845j;;;asfd;'} + req = Request(testing.create_environ(headers=headers)) + preferred_type = req.client_prefers(['application/xhtml+xml']) + assert preferred_type is None + + def test_range(self): + headers = {'Range': 'bytes=10-'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (10, -1) + + headers = {'Range': 'bytes=10-20'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (10, 20) + + headers = {'Range': 'bytes=-10240'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (-10240, -1) + + headers = {'Range': 'bytes=0-2'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (0, 2) + + headers = {'Range': ''} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPInvalidHeader): + req.range + + req = Request(testing.create_environ()) + assert req.range is None + + def test_range_unit(self): + headers = {'Range': 'bytes=10-'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (10, -1) + assert req.range_unit == 'bytes' + + headers = {'Range': 'items=10-'} + req = Request(testing.create_environ(headers=headers)) + assert req.range == (10, -1) + assert req.range_unit == 'items' + + headers = {'Range': ''} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPInvalidHeader): + req.range_unit + + req = Request(testing.create_environ()) + assert req.range_unit is None + + def test_range_invalid(self): + headers = {'Range': 'bytes=10240'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=-'} + expected_desc = ('The value provided for the Range header is ' + 'invalid. The range offsets are missing.') + self._test_error_details(headers, 'range', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + headers = {'Range': 'bytes=--'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=-3-'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=-3-4'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=3-3-4'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=3-3-'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=3-3- '} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=fizbit'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=a-'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=a-3'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=-b'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=3-b'} + req = Request(testing.create_environ(headers=headers)) + with pytest.raises(falcon.HTTPBadRequest): + req.range + + headers = {'Range': 'bytes=x-y'} + expected_desc = ('The value provided for the Range header is ' + 'invalid. It must be a range formatted ' + 'according to RFC 7233.') + self._test_error_details(headers, 'range', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + headers = {'Range': 'bytes=0-0,-1'} + expected_desc = ('The value provided for the Range ' + 'header is invalid. The value must be a ' + 'continuous range.') + self._test_error_details(headers, 'range', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + headers = {'Range': '10-'} + expected_desc = ('The value provided for the Range ' + 'header is invalid. The value must be ' + "prefixed with a range unit, e.g. 'bytes='") + self._test_error_details(headers, 'range', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + def test_missing_attribute_header(self): + req = Request(testing.create_environ()) + assert req.range is None + + req = Request(testing.create_environ()) + assert req.content_length is None + + def test_content_length(self): + headers = {'content-length': '5656'} + req = Request(testing.create_environ(headers=headers)) + assert req.content_length == 5656 + + headers = {'content-length': ''} + req = Request(testing.create_environ(headers=headers)) + assert req.content_length is None + + def test_bogus_content_length_nan(self): + headers = {'content-length': 'fuzzy-bunnies'} + expected_desc = ('The value provided for the ' + 'Content-Length header is invalid. The value ' + 'of the header must be a number.') + self._test_error_details(headers, 'content_length', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + def test_bogus_content_length_neg(self): + headers = {'content-length': '-1'} + expected_desc = ('The value provided for the Content-Length ' + 'header is invalid. The value of the header ' + 'must be a positive number.') + self._test_error_details(headers, 'content_length', + falcon.HTTPInvalidHeader, + 'Invalid header value', expected_desc) + + @pytest.mark.parametrize('header,attr', [ + ('Date', 'date'), + ('If-Modified-Since', 'if_modified_since'), + ('If-Unmodified-Since', 'if_unmodified_since'), + ]) + def test_date(self, header, attr): + date = datetime.datetime(2013, 4, 4, 5, 19, 18) + date_str = 'Thu, 04 Apr 2013 05:19:18 GMT' + + self._test_header_expected_value(header, date_str, attr, date) + + @pytest.mark.parametrize('header,attr', [ + ('Date', 'date'), + ('If-Modified-Since', 'if_modified_since'), + ('If-Unmodified-Since', 'if_unmodified_since'), + ]) + def test_date_invalid(self, header, attr): + + # Date formats don't conform to RFC 1123 + headers = {header: 'Thu, 04 Apr 2013'} + expected_desc = ('The value provided for the {0} ' + 'header is invalid. It must be formatted ' + 'according to RFC 7231, Section 7.1.1.1') + + self._test_error_details(headers, attr, + falcon.HTTPInvalidHeader, + 'Invalid header value', + expected_desc.format(header)) + + headers = {header: ''} + self._test_error_details(headers, attr, + falcon.HTTPInvalidHeader, + 'Invalid header value', + expected_desc.format(header)) + + @pytest.mark.parametrize('attr', ('date', 'if_modified_since', 'if_unmodified_since')) + def test_date_missing(self, attr): + req = Request(testing.create_environ()) + assert getattr(req, attr) is None + + def test_attribute_headers(self): + hash = 'fa0d1a60ef6616bb28038515c8ea4cb2' + auth = 'HMAC_SHA1 c590afa9bb59191ffab30f223791e82d3fd3e3af' + agent = 'testing/1.0.1' + default_agent = 'curl/7.24.0 (x86_64-apple-darwin12.0)' + referer = 'https://www.google.com/' + + self._test_attribute_header('Accept', 'x-falcon', 'accept', + default='*/*') + + self._test_attribute_header('Authorization', auth, 'auth') + + self._test_attribute_header('Content-Type', 'text/plain', + 'content_type') + self._test_attribute_header('Expect', '100-continue', 'expect') + + self._test_attribute_header('If-Match', hash, 'if_match') + self._test_attribute_header('If-None-Match', hash, 'if_none_match') + self._test_attribute_header('If-Range', hash, 'if_range') + + self._test_attribute_header('User-Agent', agent, 'user_agent', + default=default_agent) + self._test_attribute_header('Referer', referer, 'referer') + + def test_method(self): + assert self.req.method == 'GET' + + self.req = Request(testing.create_environ(path='', method='HEAD')) + assert self.req.method == 'HEAD' + + def test_empty_path(self): + self.req = Request(testing.create_environ(path='')) + assert self.req.path == '/' + + def test_content_type_method(self): + assert self.req.get_header('content-type') == 'text/plain' + + def test_content_length_method(self): + assert self.req.get_header('content-length') == '4829' + + # TODO(kgriffs): Migrate to pytest and parametrized fixtures + # to DRY things up a bit. + @pytest.mark.parametrize('protocol', _PROTOCOLS) + def test_port_explicit(self, protocol): + port = 9000 + req = Request(testing.create_environ( + protocol=protocol, + port=port, + app=self.app, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + assert req.port == port + + @pytest.mark.parametrize('protocol', _PROTOCOLS) + def test_scheme_https(self, protocol): + scheme = 'https' + req = Request(testing.create_environ( + protocol=protocol, + scheme=scheme, + app=self.app, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + assert req.scheme == scheme + assert req.port == 443 + + @pytest.mark.parametrize( + 'protocol, set_forwarded_proto', + list(itertools.product(_PROTOCOLS, [True, False])) + ) + def test_scheme_http(self, protocol, set_forwarded_proto): + scheme = 'http' + forwarded_scheme = 'HttPs' + + headers = dict(self.headers) + + if set_forwarded_proto: + headers['X-Forwarded-Proto'] = forwarded_scheme + + req = Request(testing.create_environ( + protocol=protocol, + scheme=scheme, + app=self.app, + path='/hello', + query_string=self.qs, + headers=headers)) + + assert req.scheme == scheme + assert req.port == 80 + + if set_forwarded_proto: + assert req.forwarded_scheme == forwarded_scheme.lower() + else: + assert req.forwarded_scheme == scheme + + @pytest.mark.parametrize('protocol', _PROTOCOLS) + def test_netloc_default_port(self, protocol): + req = Request(testing.create_environ( + protocol=protocol, + app=self.app, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + assert req.netloc == 'falconframework.org' + + @pytest.mark.parametrize('protocol', _PROTOCOLS) + def test_netloc_nondefault_port(self, protocol): + req = Request(testing.create_environ( + protocol=protocol, + port='8080', + app=self.app, + path='/hello', + query_string=self.qs, + headers=self.headers)) + + assert req.netloc == 'falconframework.org:8080' + + @pytest.mark.parametrize('protocol', _PROTOCOLS) + def test_netloc_from_env(self, protocol): + port = 9000 + host = 'example.org' + env = testing.create_environ( + protocol=protocol, + host=host, + port=port, + app=self.app, + path='/hello', + query_string=self.qs, + headers=self.headers) + + req = Request(env) + + assert req.port == port + assert req.netloc == '{0}:{1}'.format(host, port) + + def test_app_present(self): + req = Request(testing.create_environ(app='/moving-pictures')) + assert req.app == '/moving-pictures' + + def test_app_blank(self): + req = Request(testing.create_environ(app='')) + assert req.app == '' + + def test_app_missing(self): + env = testing.create_environ() + del env['SCRIPT_NAME'] + req = Request(env) + + assert req.app == '' + + # ------------------------------------------------------------------------- + # Helpers + # ------------------------------------------------------------------------- + + def _test_attribute_header(self, name, value, attr, default=None): + headers = {name: value} + req = Request(testing.create_environ(headers=headers)) + assert getattr(req, attr) == value + + req = Request(testing.create_environ()) + assert getattr(req, attr) == default + + def _test_header_expected_value(self, name, value, attr, expected_value): + headers = {name: value} + req = Request(testing.create_environ(headers=headers)) + assert getattr(req, attr) == expected_value + + def _test_error_details(self, headers, attr_name, + error_type, title, description): + req = Request(testing.create_environ(headers=headers)) + + try: + getattr(req, attr_name) + pytest.fail('{0} not raised'.format(error_type.__name__)) + except error_type as ex: + assert ex.title == title + assert ex.description == description diff -Nru python-falcon-1.0.0/tests/test_request_body.py python-falcon-1.4.1/tests/test_request_body.py --- python-falcon-1.0.0/tests/test_request_body.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_body.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,105 +1,99 @@ import io -import threading -from wsgiref import simple_server +from wsgiref.validate import InputWrapper -import requests +import pytest import falcon from falcon import request_helpers +import falcon.request import falcon.testing as testing SIZE_1_KB = 1024 -class TestRequestBody(testing.TestBase): +@pytest.fixture +def resource(): + return testing.TestResource() + + +@pytest.fixture +def client(): + app = falcon.API() + return testing.TestClient(app) + + +class TestRequestBody(object): + def _get_wrapped_stream(self, req): + # Getting wrapped wsgi.input: + stream = req.stream + if isinstance(stream, request_helpers.BoundedStream): + stream = stream.stream + if isinstance(stream, InputWrapper): + stream = stream.input + if isinstance(stream, io.BytesIO): + return stream + + def test_empty_body(self, client, resource): + client.app.add_route('/', resource) + client.simulate_request(path='/', body='') + stream = self._get_wrapped_stream(resource.req) + assert stream.tell() == 0 - def before(self): - self.resource = testing.TestResource() - self.api.add_route('/', self.resource) - - def test_empty_body(self): - self.simulate_request('/', body='') - stream = self.resource.req.stream - - stream.seek(0, 2) - self.assertEqual(stream.tell(), 0) - - def test_tiny_body(self): + def test_tiny_body(self, client, resource): + client.app.add_route('/', resource) expected_body = '.' - self.simulate_request('', body=expected_body) - stream = self.resource.req.stream + client.simulate_request(path='/', body=expected_body) + stream = self._get_wrapped_stream(resource.req) actual_body = stream.read(1) - self.assertEqual(actual_body, expected_body.encode('utf-8')) + assert actual_body == expected_body.encode('utf-8') - stream.seek(0, 2) - self.assertEqual(stream.tell(), 1) + assert stream.tell() == 1 - def test_tiny_body_overflow(self): + def test_tiny_body_overflow(self, client, resource): + client.app.add_route('/', resource) expected_body = '.' - self.simulate_request('', body=expected_body) - stream = self.resource.req.stream + client.simulate_request(path='/', body=expected_body) + stream = self._get_wrapped_stream(resource.req) # Read too many bytes; shouldn't block actual_body = stream.read(len(expected_body) + 1) - self.assertEqual(actual_body, expected_body.encode('utf-8')) + assert actual_body == expected_body.encode('utf-8') - def test_read_body(self): + def test_read_body(self, client, resource): + client.app.add_route('/', resource) expected_body = testing.rand_string(SIZE_1_KB / 2, SIZE_1_KB) expected_len = len(expected_body) headers = {'Content-Length': str(expected_len)} - self.simulate_request('', body=expected_body, headers=headers) + client.simulate_request(path='/', body=expected_body, headers=headers) - content_len = self.resource.req.get_header('content-length') - self.assertEqual(content_len, str(expected_len)) + content_len = resource.req.get_header('content-length') + assert content_len == str(expected_len) - stream = self.resource.req.stream + stream = self._get_wrapped_stream(resource.req) actual_body = stream.read() - self.assertEqual(actual_body, expected_body.encode('utf-8')) + assert actual_body == expected_body.encode('utf-8') stream.seek(0, 2) - self.assertEqual(stream.tell(), expected_len) + assert stream.tell() == expected_len - self.assertEqual(stream.tell(), expected_len) + assert stream.tell() == expected_len - def test_read_socket_body(self): - expected_body = testing.rand_string(SIZE_1_KB / 2, SIZE_1_KB) - - def server(): - class Echo(object): - def on_post(self, req, resp): - # wsgiref socket._fileobject blocks when len not given, - # but Falcon is smarter than that. :D - body = req.stream.read() - resp.body = body - - def on_put(self, req, resp): - # wsgiref socket._fileobject blocks when len too long, - # but Falcon should work around that for me. - body = req.stream.read(req.content_length + 1) - resp.body = body - - api = falcon.API() - api.add_route('/echo', Echo()) - - httpd = simple_server.make_server('127.0.0.1', 8989, api) - httpd.serve_forever() - - thread = threading.Thread(target=server) - thread.daemon = True - thread.start() - - # Let it boot - thread.join(1) - - url = 'http://127.0.0.1:8989/echo' - resp = requests.post(url, data=expected_body) - self.assertEqual(resp.text, expected_body) - - resp = requests.put(url, data=expected_body) - self.assertEqual(resp.text, expected_body) + def test_bounded_stream_property_empty_body(self): + """Test that we can get a bounded stream outside of wsgiref.""" + environ = testing.create_environ() + req = falcon.Request(environ) + + bounded_stream = req.bounded_stream + + # NOTE(kgriffs): Verify that we aren't creating a new object + # each time the property is called. Also ensures branch + # coverage of the property implementation. + assert bounded_stream is req.bounded_stream + data = bounded_stream.read() + assert len(data) == 0 def test_body_stream_wrapper(self): data = testing.rand_string(SIZE_1_KB / 2, SIZE_1_KB) @@ -117,15 +111,15 @@ stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.read(), expected_body) + assert body.read() == expected_body stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.read(2), expected_body[0:2]) + assert body.read(2) == expected_body[0:2] stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.read(expected_len + 1), expected_body) + assert body.read(expected_len + 1) == expected_body # NOTE(kgriffs): Test that reading past the end does not # hang, but returns the empty string. @@ -133,37 +127,43 @@ body = request_helpers.Body(stream, expected_len) for i in range(expected_len + 1): expected_value = expected_body[i:i + 1] if i < expected_len else b'' - self.assertEqual(body.read(1), expected_value) + assert body.read(1) == expected_value stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readline(), expected_lines[0]) + assert body.readline() == expected_lines[0] stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readline(-1), expected_lines[0]) + assert body.readline(-1) == expected_lines[0] stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readline(expected_len + 1), expected_lines[0]) + assert body.readline(expected_len + 1) == expected_lines[0] stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readlines(), expected_lines) + assert body.readlines() == expected_lines stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readlines(-1), expected_lines) + assert body.readlines(-1) == expected_lines stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(body.readlines(expected_len + 1), expected_lines) + assert body.readlines(expected_len + 1) == expected_lines stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) - self.assertEqual(next(body), expected_lines[0]) + assert next(body) == expected_lines[0] stream = io.BytesIO(expected_body) body = request_helpers.Body(stream, expected_len) for i, line in enumerate(body): - self.assertEqual(line, expected_lines[i]) + assert line == expected_lines[i] + + def test_request_repr(self): + environ = testing.create_environ() + req = falcon.Request(environ) + _repr = '<%s: %s %r>' % (req.__class__.__name__, req.method, req.url) + assert req.__repr__() == _repr diff -Nru python-falcon-1.0.0/tests/test_request_context.py python-falcon-1.4.1/tests/test_request_context.py --- python-falcon-1.0.0/tests/test_request_context.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_context.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,14 +1,15 @@ -import falcon.testing as testing +import pytest from falcon.request import Request +import falcon.testing as testing -class TestRequestContext(testing.TestBase): +class TestRequestContext(object): def test_default_request_context(self): env = testing.create_environ() req = Request(env) - self.assertIsInstance(req.context, dict) + assert isinstance(req.context, dict) def test_custom_request_context(self): @@ -21,7 +22,7 @@ env = testing.create_environ() req = MyCustomRequest(env) - self.assertIsInstance(req.context, MyCustomContextType) + assert isinstance(req.context, MyCustomContextType) def test_custom_request_context_failure(self): @@ -30,7 +31,8 @@ context_type = False env = testing.create_environ() - self.assertRaises(TypeError, MyCustomRequest, env) + with pytest.raises(TypeError): + MyCustomRequest(env) def test_custom_request_context_request_access(self): @@ -43,5 +45,5 @@ env = testing.create_environ() req = MyCustomRequest(env) - self.assertIsInstance(req.context, dict) - self.assertEqual(req.context['uri'], req.uri) + assert isinstance(req.context, dict) + assert req.context['uri'] == req.uri diff -Nru python-falcon-1.0.0/tests/test_request_forwarded.py python-falcon-1.4.1/tests/test_request_forwarded.py --- python-falcon-1.0.0/tests/test_request_forwarded.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_forwarded.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,161 @@ +import pytest + +from falcon.request import Request +import falcon.testing as testing + + +def test_no_forwarded_headers(): + req = Request(testing.create_environ( + host='example.com', + path='/languages', + app='backoffice' + )) + + assert req.forwarded is None + assert req.forwarded_uri == req.uri + assert req.forwarded_uri == 'http://example.com/backoffice/languages' + assert req.forwarded_prefix == 'http://example.com/backoffice' + + +def test_x_forwarded_host(): + req = Request(testing.create_environ( + host='suchproxy.suchtesting.com', + path='/languages', + headers={'X-Forwarded-Host': 'something.org'} + )) + + assert req.forwarded is None + assert req.forwarded_host == 'something.org' + assert req.forwarded_uri != req.uri + assert req.forwarded_uri == 'http://something.org/languages' + assert req.forwarded_prefix == 'http://something.org' + assert req.forwarded_prefix == 'http://something.org' # Check cached value + + +def test_x_forwarded_proto(): + req = Request(testing.create_environ( + host='example.org', + path='/languages', + headers={'X-Forwarded-Proto': 'HTTPS'} + )) + + assert req.forwarded is None + assert req.forwarded_scheme == 'https' + assert req.forwarded_uri != req.uri + assert req.forwarded_uri == 'https://example.org/languages' + assert req.forwarded_prefix == 'https://example.org' + + +def test_forwarded_host(): + req = Request(testing.create_environ( + host='suchproxy02.suchtesting.com', + path='/languages', + headers={ + 'Forwarded': 'host=something.org , host=suchproxy01.suchtesting.com' + } + )) + + assert req.forwarded is not None + for f in req.forwarded: + assert f.src is None + assert f.dest is None + assert f.scheme is None + + assert req.forwarded[0].host == 'something.org' + assert req.forwarded[1].host == 'suchproxy01.suchtesting.com' + + assert req.forwarded_host == 'something.org' + assert req.forwarded_uri != req.uri + assert req.forwarded_uri == 'http://something.org/languages' + assert req.forwarded_prefix == 'http://something.org' + + +def test_forwarded_multiple_params(): + req = Request(testing.create_environ( + host='suchproxy02.suchtesting.com', + path='/languages', + headers={ + 'Forwarded': ( + 'host=something.org;proto=hTTps;ignore=me;for=108.166.30.185, ' + 'by=203.0.113.43;host=suchproxy01.suchtesting.com;proto=httP' + ) + } + )) + + assert req.forwarded is not None + + assert req.forwarded[0].host == 'something.org' + assert req.forwarded[0].scheme == 'https' + assert req.forwarded[0].src == '108.166.30.185' + assert req.forwarded[0].dest is None + + assert req.forwarded[1].host == 'suchproxy01.suchtesting.com' + assert req.forwarded[1].scheme == 'http' + assert req.forwarded[1].src is None + assert req.forwarded[1].dest == '203.0.113.43' + + assert req.forwarded_scheme == 'https' + assert req.forwarded_host == 'something.org' + assert req.forwarded_uri != req.uri + assert req.forwarded_uri == 'https://something.org/languages' + assert req.forwarded_prefix == 'https://something.org' + + +def test_forwarded_missing_first_hop_host(): + req = Request(testing.create_environ( + host='suchproxy02.suchtesting.com', + path='/languages', + app='doge', + headers={ + 'Forwarded': 'for=108.166.30.185,host=suchproxy01.suchtesting.com' + } + )) + + assert req.forwarded[0].host is None + assert req.forwarded[0].src == '108.166.30.185' + + assert req.forwarded[1].host == 'suchproxy01.suchtesting.com' + assert req.forwarded[1].src is None + + assert req.forwarded_scheme == 'http' + assert req.forwarded_host == 'suchproxy02.suchtesting.com' + assert req.forwarded_uri == req.uri + assert req.forwarded_uri == 'http://suchproxy02.suchtesting.com/doge/languages' + assert req.forwarded_prefix == 'http://suchproxy02.suchtesting.com/doge' + + +def test_forwarded_quote_escaping(): + req = Request(testing.create_environ( + host='suchproxy02.suchtesting.com', + path='/languages', + app='doge', + headers={ + 'Forwarded': 'for="1\\.2\\.3\\.4";some="extra,\\"info\\""' + } + )) + + assert req.forwarded[0].host is None + assert req.forwarded[0].src == '1.2.3.4' + + +@pytest.mark.parametrize('forwarded, expected_dest', [ + ('for=1.2.3.4;by="', None), + ('for=1.2.3.4;by=4\\.3.2.1thing=blah', '4'), + ('for=1.2.3.4;by="\\4.3.2.1"thing=blah', '4.3.2.1'), + ('for=1.2.3.4;by="4.3.2.\\1"thing="blah"', '4.3.2.1'), + ('for=1.2.3.4;by="4.3.\\2\\.1" thing="blah"', '4.3.2.1'), +]) +def test_escape_malformed_requests(forwarded, expected_dest): + + req = Request(testing.create_environ( + host='suchproxy02.suchtesting.com', + path='/languages', + app='doge', + headers={ + 'Forwarded': forwarded + } + )) + + assert len(req.forwarded) == 1 + assert req.forwarded[0].src == '1.2.3.4' + assert req.forwarded[0].dest == expected_dest diff -Nru python-falcon-1.0.0/tests/test_request_media.py python-falcon-1.4.1/tests/test_request_media.py --- python-falcon-1.0.0/tests/test_request_media.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_request_media.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,127 @@ +import pytest + +import falcon +from falcon import errors, media, testing + + +def create_client(handlers=None): + res = testing.SimpleTestResource() + + app = falcon.API() + app.add_route('/', res) + + if handlers: + app.req_options.media_handlers.update(handlers) + + client = testing.TestClient(app) + client.resource = res + + return client + + +@pytest.mark.parametrize('media_type', [ + (None), + ('*/*'), + ('application/json'), + ('application/json; charset=utf-8'), +]) +def test_json(media_type): + client = create_client() + expected_body = b'{"something": true}' + headers = {'Content-Type': media_type} + client.simulate_post('/', body=expected_body, headers=headers) + + media = client.resource.captured_req.media + assert media is not None + assert media.get('something') is True + + +@pytest.mark.parametrize('media_type', [ + ('application/msgpack'), + ('application/msgpack; charset=utf-8'), + ('application/x-msgpack'), +]) +def test_msgpack(media_type): + client = create_client({ + 'application/msgpack': media.MessagePackHandler(), + 'application/x-msgpack': media.MessagePackHandler(), + }) + headers = {'Content-Type': media_type} + + # Bytes + expected_body = b'\x81\xc4\tsomething\xc3' + client.simulate_post('/', body=expected_body, headers=headers) + + req_media = client.resource.captured_req.media + assert req_media.get(b'something') is True + + # Unicode + expected_body = b'\x81\xa9something\xc3' + client.simulate_post('/', body=expected_body, headers=headers) + + req_media = client.resource.captured_req.media + assert req_media.get(u'something') is True + + +@pytest.mark.parametrize('media_type', [ + ('nope/json'), +]) +def test_unknown_media_type(media_type): + client = create_client() + headers = {'Content-Type': media_type} + client.simulate_post('/', body='', headers=headers) + + with pytest.raises(errors.HTTPUnsupportedMediaType) as err: + client.resource.captured_req.media + + msg = '{0} is an unsupported media type.'.format(media_type) + assert err.value.description == msg + + +def test_invalid_json(): + client = create_client() + expected_body = b'{' + headers = {'Content-Type': 'application/json'} + client.simulate_post('/', body=expected_body, headers=headers) + + with pytest.raises(errors.HTTPBadRequest) as err: + client.resource.captured_req.media + + assert 'Could not parse JSON body' in err.value.description + + +def test_invalid_msgpack(): + client = create_client({'application/msgpack': media.MessagePackHandler()}) + expected_body = '/////////////////////' + headers = {'Content-Type': 'application/msgpack'} + client.simulate_post('/', body=expected_body, headers=headers) + + with pytest.raises(errors.HTTPBadRequest) as err: + client.resource.captured_req.media + + desc = 'Could not parse MessagePack body - unpack(b) received extra data.' + assert err.value.description == desc + + +def test_invalid_stream_fails_gracefully(): + client = create_client() + client.simulate_post('/') + + req = client.resource.captured_req + req.headers['Content-Type'] = 'application/json' + req._bounded_stream = None + + with pytest.raises(errors.HTTPBadRequest) as err: + req.media + + assert 'Could not parse JSON body' in err.value.description + + +def test_use_cached_media(): + client = create_client() + client.simulate_post('/') + + req = client.resource.captured_req + req._media = {'something': True} + + assert req.media == {'something': True} diff -Nru python-falcon-1.0.0/tests/test_req_vars.py python-falcon-1.4.1/tests/test_req_vars.py --- python-falcon-1.0.0/tests/test_req_vars.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_req_vars.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,649 +0,0 @@ -import datetime -import six -import testtools - -import ddt - -import falcon -from falcon.request import Request -import falcon.testing as testing - - -@ddt.ddt -class TestReqVars(testing.TestBase): - - def before(self): - self.qs = 'marker=deadbeef&limit=10' - - self.headers = { - 'Content-Type': 'text/plain', - 'Content-Length': '4829', - 'Authorization': '' - } - - self.app = '/test' - self.path = '/hello' - self.relative_uri = self.path + '?' + self.qs - - self.req = Request(testing.create_environ( - app=self.app, - port=8080, - path='/hello', - query_string=self.qs, - headers=self.headers)) - - self.req_noqs = Request(testing.create_environ( - app=self.app, - path='/hello', - headers=self.headers)) - - def test_missing_qs(self): - env = testing.create_environ() - if 'QUERY_STRING' in env: - del env['QUERY_STRING'] - - # Should not cause an exception when Request is instantiated - Request(env) - - def test_empty(self): - self.assertIs(self.req.auth, None) - - def test_host(self): - self.assertEqual(self.req.host, testing.DEFAULT_HOST) - - def test_subdomain(self): - req = Request(testing.create_environ( - host='com', - path='/hello', - headers=self.headers)) - self.assertIs(req.subdomain, None) - - req = Request(testing.create_environ( - host='example.com', - path='/hello', - headers=self.headers)) - self.assertEqual(req.subdomain, 'example') - - req = Request(testing.create_environ( - host='highwire.example.com', - path='/hello', - headers=self.headers)) - self.assertEqual(req.subdomain, 'highwire') - - req = Request(testing.create_environ( - host='lb01.dfw01.example.com', - port=8080, - path='/hello', - headers=self.headers)) - self.assertEqual(req.subdomain, 'lb01') - - # NOTE(kgriffs): Behavior for IP addresses is undefined, - # so just make sure it doesn't blow up. - req = Request(testing.create_environ( - host='127.0.0.1', - path='/hello', - headers=self.headers)) - self.assertEqual(type(req.subdomain), str) - - # NOTE(kgriffs): Test fallback to SERVER_NAME by using - # HTTP 1.0, which will cause .create_environ to not set - # HTTP_HOST. - req = Request(testing.create_environ( - protocol='HTTP/1.0', - host='example.com', - path='/hello', - headers=self.headers)) - self.assertEqual(req.subdomain, 'example') - - def test_reconstruct_url(self): - req = self.req - - scheme = req.protocol - host = req.get_header('host') - app = req.app - path = req.path - query_string = req.query_string - - expected_uri = ''.join([scheme, '://', host, app, path, - '?', query_string]) - - self.assertEqual(expected_uri, req.uri) - - @testtools.skipUnless(six.PY3, 'Test only applies to Python 3') - def test_nonlatin_path(self): - cyrillic_path = u'/hello_\u043f\u0440\u0438\u0432\u0435\u0442' - cyrillic_path_decoded = cyrillic_path.encode('utf-8').decode('latin1') - req = Request(testing.create_environ( - host='com', - path=cyrillic_path_decoded, - headers=self.headers)) - self.assertEqual(req.path, cyrillic_path) - - def test_uri(self): - uri = ('http://' + testing.DEFAULT_HOST + ':8080' + - self.app + self.relative_uri) - - self.assertEqual(self.req.url, uri) - - # NOTE(kgriffs): Call twice to check caching works - self.assertEqual(self.req.uri, uri) - self.assertEqual(self.req.uri, uri) - - uri_noqs = ('http://' + testing.DEFAULT_HOST + self.app + self.path) - self.assertEqual(self.req_noqs.uri, uri_noqs) - - def test_uri_https(self): - # ======================================================= - # Default port, implicit - # ======================================================= - req = Request(testing.create_environ( - path='/hello', scheme='https')) - uri = ('https://' + testing.DEFAULT_HOST + '/hello') - - self.assertEqual(req.uri, uri) - - # ======================================================= - # Default port, explicit - # ======================================================= - req = Request(testing.create_environ( - path='/hello', scheme='https', port=443)) - uri = ('https://' + testing.DEFAULT_HOST + '/hello') - - self.assertEqual(req.uri, uri) - - # ======================================================= - # Non-default port - # ======================================================= - req = Request(testing.create_environ( - path='/hello', scheme='https', port=22)) - uri = ('https://' + testing.DEFAULT_HOST + ':22/hello') - - self.assertEqual(req.uri, uri) - - def test_uri_http_1_0(self): - # ======================================================= - # HTTP, 80 - # ======================================================= - req = Request(testing.create_environ( - protocol='HTTP/1.0', - app=self.app, - port=80, - path='/hello', - query_string=self.qs, - headers=self.headers)) - - uri = ('http://' + testing.DEFAULT_HOST + - self.app + self.relative_uri) - - self.assertEqual(req.uri, uri) - - # ======================================================= - # HTTP, 80 - # ======================================================= - req = Request(testing.create_environ( - protocol='HTTP/1.0', - app=self.app, - port=8080, - path='/hello', - query_string=self.qs, - headers=self.headers)) - - uri = ('http://' + testing.DEFAULT_HOST + ':8080' + - self.app + self.relative_uri) - - self.assertEqual(req.uri, uri) - - # ======================================================= - # HTTP, 80 - # ======================================================= - req = Request(testing.create_environ( - protocol='HTTP/1.0', - scheme='https', - app=self.app, - port=443, - path='/hello', - query_string=self.qs, - headers=self.headers)) - - uri = ('https://' + testing.DEFAULT_HOST + - self.app + self.relative_uri) - - self.assertEqual(req.uri, uri) - - # ======================================================= - # HTTP, 80 - # ======================================================= - req = Request(testing.create_environ( - protocol='HTTP/1.0', - scheme='https', - app=self.app, - port=22, - path='/hello', - query_string=self.qs, - headers=self.headers)) - - uri = ('https://' + testing.DEFAULT_HOST + ':22' + - self.app + self.relative_uri) - - self.assertEqual(req.uri, uri) - - def test_relative_uri(self): - self.assertEqual(self.req.relative_uri, self.app + self.relative_uri) - self.assertEqual( - self.req_noqs.relative_uri, self.app + self.path) - - req_noapp = Request(testing.create_environ( - path='/hello', - query_string=self.qs, - headers=self.headers)) - - self.assertEqual(req_noapp.relative_uri, self.relative_uri) - - req_noapp = Request(testing.create_environ( - path='/hello/', - query_string=self.qs, - headers=self.headers)) - - # NOTE(kgriffs): Call twice to check caching works - self.assertEqual(req_noapp.relative_uri, self.relative_uri) - self.assertEqual(req_noapp.relative_uri, self.relative_uri) - - def test_client_accepts(self): - headers = {'Accept': 'application/xml'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('application/xml')) - - headers = {'Accept': '*/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('application/xml')) - self.assertTrue(req.client_accepts('application/json')) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - headers = {'Accept': 'application/x-msgpack'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts('application/xml')) - self.assertFalse(req.client_accepts('application/json')) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - headers = {} # NOTE(kgriffs): Equivalent to '*/*' per RFC - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('application/xml')) - - headers = {'Accept': 'application/json'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts('application/xml')) - - headers = {'Accept': 'application/x-msgpack'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - headers = {'Accept': 'application/xm'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts('application/xml')) - - headers = {'Accept': 'application/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('application/json')) - self.assertTrue(req.client_accepts('application/xml')) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - headers = {'Accept': 'text/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('text/plain')) - self.assertTrue(req.client_accepts('text/csv')) - self.assertFalse(req.client_accepts('application/xhtml+xml')) - - headers = {'Accept': 'text/*, application/xhtml+xml; q=0.0'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('text/plain')) - self.assertTrue(req.client_accepts('text/csv')) - self.assertFalse(req.client_accepts('application/xhtml+xml')) - - headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('text/plain')) - self.assertTrue(req.client_accepts('application/xhtml+xml')) - - headers = {'Accept': 'text/*, application/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('text/plain')) - self.assertTrue(req.client_accepts('application/xml')) - self.assertTrue(req.client_accepts('application/json')) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - headers = {'Accept': 'text/*,application/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts('text/plain')) - self.assertTrue(req.client_accepts('application/xml')) - self.assertTrue(req.client_accepts('application/json')) - self.assertTrue(req.client_accepts('application/x-msgpack')) - - def test_client_accepts_bogus(self): - headers = {'Accept': '~'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts('text/plain')) - self.assertFalse(req.client_accepts('application/json')) - - def test_client_accepts_props(self): - headers = {'Accept': 'application/xml'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts_xml) - self.assertFalse(req.client_accepts_json) - self.assertFalse(req.client_accepts_msgpack) - - headers = {'Accept': 'application/*'} - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts_xml) - self.assertTrue(req.client_accepts_json) - self.assertTrue(req.client_accepts_msgpack) - - headers = {'Accept': 'application/json'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts_xml) - self.assertTrue(req.client_accepts_json) - self.assertFalse(req.client_accepts_msgpack) - - headers = {'Accept': 'application/x-msgpack'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts_xml) - self.assertFalse(req.client_accepts_json) - self.assertTrue(req.client_accepts_msgpack) - - headers = {'Accept': 'application/msgpack'} - req = Request(testing.create_environ(headers=headers)) - self.assertFalse(req.client_accepts_xml) - self.assertFalse(req.client_accepts_json) - self.assertTrue(req.client_accepts_msgpack) - - headers = { - 'Accept': 'application/json,application/xml,application/x-msgpack' - } - req = Request(testing.create_environ(headers=headers)) - self.assertTrue(req.client_accepts_xml) - self.assertTrue(req.client_accepts_json) - self.assertTrue(req.client_accepts_msgpack) - - def test_client_prefers(self): - headers = {'Accept': 'application/xml'} - req = Request(testing.create_environ(headers=headers)) - preferred_type = req.client_prefers(['application/xml']) - self.assertEqual(preferred_type, 'application/xml') - - headers = {'Accept': '*/*'} - preferred_type = req.client_prefers(('application/xml', - 'application/json')) - - # NOTE(kgriffs): If client doesn't care, "prefer" the first one - self.assertEqual(preferred_type, 'application/xml') - - headers = {'Accept': 'text/*; q=0.1, application/xhtml+xml; q=0.5'} - req = Request(testing.create_environ(headers=headers)) - preferred_type = req.client_prefers(['application/xhtml+xml']) - self.assertEqual(preferred_type, 'application/xhtml+xml') - - headers = {'Accept': '3p12845j;;;asfd;'} - req = Request(testing.create_environ(headers=headers)) - preferred_type = req.client_prefers(['application/xhtml+xml']) - self.assertEqual(preferred_type, None) - - def test_range(self): - headers = {'Range': 'bytes=10-'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (10, -1)) - - headers = {'Range': 'bytes=10-20'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (10, 20)) - - headers = {'Range': 'bytes=-10240'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (-10240, -1)) - - headers = {'Range': 'bytes=0-2'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (0, 2)) - - headers = {'Range': ''} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPInvalidHeader, lambda: req.range) - - req = Request(testing.create_environ()) - self.assertIs(req.range, None) - - def test_range_unit(self): - headers = {'Range': 'bytes=10-'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (10, -1)) - self.assertEqual(req.range_unit, 'bytes') - - headers = {'Range': 'items=10-'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.range, (10, -1)) - self.assertEqual(req.range_unit, 'items') - - headers = {'Range': ''} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPInvalidHeader, lambda: req.range_unit) - - req = Request(testing.create_environ()) - self.assertIs(req.range_unit, None) - - def test_range_invalid(self): - headers = {'Range': 'bytes=10240'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=-'} - expected_desc = ('The value provided for the Range header is ' - 'invalid. The range offsets are missing.') - self._test_error_details(headers, 'range', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - headers = {'Range': 'bytes=--'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=-3-'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=-3-4'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=3-3-4'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=3-3-'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=3-3- '} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=fizbit'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=a-'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=a-3'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=-b'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=3-b'} - req = Request(testing.create_environ(headers=headers)) - self.assertRaises(falcon.HTTPBadRequest, lambda: req.range) - - headers = {'Range': 'bytes=x-y'} - expected_desc = ('The value provided for the Range header is ' - 'invalid. It must be a range formatted ' - 'according to RFC 7233.') - self._test_error_details(headers, 'range', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - headers = {'Range': 'bytes=0-0,-1'} - expected_desc = ('The value provided for the Range ' - 'header is invalid. The value must be a ' - 'continuous range.') - self._test_error_details(headers, 'range', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - headers = {'Range': '10-'} - expected_desc = ('The value provided for the Range ' - 'header is invalid. The value must be ' - "prefixed with a range unit, e.g. 'bytes='") - self._test_error_details(headers, 'range', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - def test_missing_attribute_header(self): - req = Request(testing.create_environ()) - self.assertEqual(req.range, None) - - req = Request(testing.create_environ()) - self.assertEqual(req.content_length, None) - - def test_content_length(self): - headers = {'content-length': '5656'} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.content_length, 5656) - - headers = {'content-length': ''} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(req.content_length, None) - - def test_bogus_content_length_nan(self): - headers = {'content-length': 'fuzzy-bunnies'} - expected_desc = ('The value provided for the ' - 'Content-Length header is invalid. The value ' - 'of the header must be a number.') - self._test_error_details(headers, 'content_length', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - def test_bogus_content_length_neg(self): - headers = {'content-length': '-1'} - expected_desc = ('The value provided for the Content-Length ' - 'header is invalid. The value of the header ' - 'must be a positive number.') - self._test_error_details(headers, 'content_length', - falcon.HTTPInvalidHeader, - 'Invalid header value', expected_desc) - - @ddt.data(('Date', 'date'), - ('If-Modified-since', 'if_modified_since'), - ('If-Unmodified-since', 'if_unmodified_since'), - ) - @ddt.unpack - def test_date(self, header, attr): - date = datetime.datetime(2013, 4, 4, 5, 19, 18) - date_str = 'Thu, 04 Apr 2013 05:19:18 GMT' - - self._test_header_expected_value(header, date_str, attr, date) - - @ddt.data(('Date', 'date'), - ('If-Modified-Since', 'if_modified_since'), - ('If-Unmodified-Since', 'if_unmodified_since'), - ) - @ddt.unpack - def test_date_invalid(self, header, attr): - - # Date formats don't conform to RFC 1123 - headers = {header: 'Thu, 04 Apr 2013'} - expected_desc = ('The value provided for the {0} ' - 'header is invalid. It must be formatted ' - 'according to RFC 7231, Section 7.1.1.1') - - self._test_error_details(headers, attr, - falcon.HTTPInvalidHeader, - 'Invalid header value', - expected_desc.format(header)) - - headers = {header: ''} - self._test_error_details(headers, attr, - falcon.HTTPInvalidHeader, - 'Invalid header value', - expected_desc.format(header)) - - @ddt.data('date', 'if_modified_since', 'if_unmodified_since') - def test_date_missing(self, attr): - req = Request(testing.create_environ()) - self.assertIs(getattr(req, attr), None) - - def test_attribute_headers(self): - hash = 'fa0d1a60ef6616bb28038515c8ea4cb2' - auth = 'HMAC_SHA1 c590afa9bb59191ffab30f223791e82d3fd3e3af' - agent = 'testing/1.0.1' - default_agent = 'curl/7.24.0 (x86_64-apple-darwin12.0)' - - self._test_attribute_header('Accept', 'x-falcon', 'accept', - default='*/*') - - self._test_attribute_header('Authorization', auth, 'auth') - - self._test_attribute_header('Content-Type', 'text/plain', - 'content_type') - self._test_attribute_header('Expect', '100-continue', 'expect') - - self._test_attribute_header('If-Match', hash, 'if_match') - self._test_attribute_header('If-None-Match', hash, 'if_none_match') - self._test_attribute_header('If-Range', hash, 'if_range') - - self._test_attribute_header('User-Agent', agent, 'user_agent', - default=default_agent) - - def test_method(self): - self.assertEqual(self.req.method, 'GET') - - self.req = Request(testing.create_environ(path='', method='HEAD')) - self.assertEqual(self.req.method, 'HEAD') - - def test_empty_path(self): - self.req = Request(testing.create_environ(path='')) - self.assertEqual(self.req.path, '/') - - def test_content_type_method(self): - self.assertEqual(self.req.get_header('content-type'), 'text/plain') - - def test_content_length_method(self): - self.assertEqual(self.req.get_header('content-length'), '4829') - - # ------------------------------------------------------------------------- - # Helpers - # ------------------------------------------------------------------------- - - def _test_attribute_header(self, name, value, attr, default=None): - headers = {name: value} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(getattr(req, attr), value) - - req = Request(testing.create_environ()) - self.assertEqual(getattr(req, attr), default) - - def _test_header_expected_value(self, name, value, attr, expected_value): - headers = {name: value} - req = Request(testing.create_environ(headers=headers)) - self.assertEqual(getattr(req, attr), expected_value) - - def _test_error_details(self, headers, attr_name, - error_type, title, description): - req = Request(testing.create_environ(headers=headers)) - - try: - getattr(req, attr_name) - self.fail('{0} not raised'.format(error_type.__name__)) - except error_type as ex: - self.assertEqual(ex.title, title) - self.assertEqual(ex.description, description) diff -Nru python-falcon-1.0.0/tests/test_response_body.py python-falcon-1.4.1/tests/test_response_body.py --- python-falcon-1.0.0/tests/test_response_body.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_response_body.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,9 +1,8 @@ import falcon -import falcon.testing as testing -class TestResponseBody(testing.TestBase): +class TestResponseBody(object): def test_append_body(self): text = 'Hello beautiful world! ' @@ -14,4 +13,9 @@ resp.body += token resp.body += ' ' - self.assertEqual(resp.body, text) + assert resp.body == text + + def test_response_repr(self): + resp = falcon.Response() + _repr = '<%s: %s>' % (resp.__class__.__name__, resp.status) + assert resp.__repr__() == _repr diff -Nru python-falcon-1.0.0/tests/test_response_context.py python-falcon-1.4.1/tests/test_response_context.py --- python-falcon-1.0.0/tests/test_response_context.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_response_context.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,41 @@ +import pytest + +from falcon import Response + + +class TestRequestContext(object): + + def test_default_response_context(self): + resp = Response() + assert isinstance(resp.context, dict) + + def test_custom_response_context(self): + + class MyCustomContextType(object): + pass + + class MyCustomResponse(Response): + context_type = MyCustomContextType + + resp = MyCustomResponse() + assert isinstance(resp.context, MyCustomContextType) + + def test_custom_response_context_failure(self): + + class MyCustomResponse(Response): + context_type = False + + with pytest.raises(TypeError): + MyCustomResponse() + + def test_custom_response_context_factory(self): + + def create_context(resp): + return {'resp': resp} + + class MyCustomResponse(Response): + context_type = create_context + + resp = MyCustomResponse() + assert isinstance(resp.context, dict) + assert resp.context['resp'] is resp diff -Nru python-falcon-1.0.0/tests/test_response_media.py python-falcon-1.4.1/tests/test_response_media.py --- python-falcon-1.0.0/tests/test_response_media.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_response_media.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,154 @@ +import json + +import pytest + +import falcon +from falcon import errors, media, testing + + +def create_client(handlers=None): + res = testing.SimpleTestResource() + + app = falcon.API() + app.add_route('/', res) + + if handlers: + app.resp_options.media_handlers.update(handlers) + + client = testing.TestClient(app) + client.resource = res + + return client + + +class SimpleMediaResource(object): + + def __init__(self, document, media_type=falcon.MEDIA_JSON): + self._document = document + self._media_type = media_type + + def on_get(self, req, resp): + resp.content_type = self._media_type + resp.media = self._document + resp.status = falcon.HTTP_OK + + +@pytest.mark.parametrize('media_type', [ + ('*/*'), + (falcon.MEDIA_JSON), + ('application/json; charset=utf-8'), +]) +def test_json(media_type): + client = create_client() + client.simulate_get('/') + + resp = client.resource.captured_resp + resp.content_type = media_type + resp.media = {'something': True} + + assert json.loads(resp.data.decode('utf-8')) == {u'something': True} + + +@pytest.mark.parametrize('document', [ + '', + u'I am a \u1d0a\ua731\u1d0f\u0274 string.', + [u'\u2665', u'\u2660', u'\u2666', u'\u2663'], + {u'message': u'\xa1Hello Unicode! \U0001F638'}, + { + 'description': 'A collection of primitive Python 2 type examples.', + 'bool': False is not True and True is not False, + 'dict': {'example': 'mapping'}, + 'float': 1.0, + 'int': 1337, + 'list': ['a', 'sequence', 'of', 'items'], + 'none': None, + 'str': 'ASCII string', + 'unicode': u'Hello Unicode! \U0001F638', + }, +]) +def test_non_ascii_json_serialization(document): + app = falcon.API() + app.add_route('/', SimpleMediaResource(document)) + client = testing.TestClient(app) + + resp = client.simulate_get('/') + assert resp.json == document + + +@pytest.mark.parametrize('media_type', [ + (falcon.MEDIA_MSGPACK), + ('application/msgpack; charset=utf-8'), + ('application/x-msgpack'), +]) +def test_msgpack(media_type): + client = create_client({ + 'application/msgpack': media.MessagePackHandler(), + 'application/x-msgpack': media.MessagePackHandler(), + }) + client.simulate_get('/') + + resp = client.resource.captured_resp + resp.content_type = media_type + + # Bytes + resp.media = {b'something': True} + assert resp.data == b'\x81\xc4\tsomething\xc3' + + # Unicode + resp.media = {u'something': True} + assert resp.data == b'\x81\xa9something\xc3' + + +def test_unknown_media_type(): + client = create_client() + client.simulate_get('/') + + resp = client.resource.captured_resp + with pytest.raises(errors.HTTPUnsupportedMediaType) as err: + resp.content_type = 'nope/json' + resp.media = {'something': True} + + assert err.value.description == 'nope/json is an unsupported media type.' + + +def test_use_cached_media(): + expected = {'something': True} + + client = create_client() + client.simulate_get('/') + + resp = client.resource.captured_resp + resp._media = expected + + assert resp.media == expected + + +def test_default_media_type(): + client = create_client() + client.simulate_get('/') + + resp = client.resource.captured_resp + resp.content_type = '' + resp.media = {'something': True} + + assert json.loads(resp.data.decode('utf-8')) == {u'something': True} + assert resp.content_type == 'application/json; charset=UTF-8' + + +def test_mimeparse_edgecases(): + client = create_client() + client.simulate_get('/') + + resp = client.resource.captured_resp + + resp.content_type = 'application/vnd.something' + with pytest.raises(errors.HTTPUnsupportedMediaType): + resp.media = {'something': True} + + resp.content_type = 'invalid' + with pytest.raises(errors.HTTPUnsupportedMediaType): + resp.media = {'something': True} + + # Clear the content type, shouldn't raise this time + resp.content_type = None + resp.media = {'something': True} diff -Nru python-falcon-1.0.0/tests/test_response.py python-falcon-1.4.1/tests/test_response.py --- python-falcon-1.0.0/tests/test_response.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_response.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,13 @@ +import falcon +from falcon import MEDIA_TEXT + + +def test_response_set_content_type_set(): + resp = falcon.Response() + resp._set_media_type(MEDIA_TEXT) + assert resp._headers['content-type'] == MEDIA_TEXT + + +def test_response_set_content_type_not_set(): + resp = falcon.Response() + assert 'content-type' not in resp._headers diff -Nru python-falcon-1.0.0/tests/test_sinks.py python-falcon-1.4.1/tests/test_sinks.py --- python-falcon-1.0.0/tests/test_sinks.py 2016-05-11 19:09:29.000000000 +0000 +++ python-falcon-1.4.1/tests/test_sinks.py 2018-08-08 23:08:36.000000000 +0000 @@ -1,5 +1,7 @@ import re +import pytest + import falcon import falcon.testing as testing @@ -27,90 +29,102 @@ pass -class TestDefaultRouting(testing.TestBase): +@pytest.fixture +def resource(): + return BookCollection() + + +@pytest.fixture +def sink(): + return Sink() + + +@pytest.fixture +def client(): + app = falcon.API() + return testing.TestClient(app) + - def before(self): - self.sink = Sink() - self.resource = BookCollection() +class TestDefaultRouting(object): - def test_single_default_pattern(self): - self.api.add_sink(self.sink) + def test_single_default_pattern(self, client, sink, resource): + client.app.add_sink(sink) - self.simulate_request('/') - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/') + assert response.status == falcon.HTTP_503 - def test_single_simple_pattern(self): - self.api.add_sink(self.sink, r'/foo') + def test_single_simple_pattern(self, client, sink, resource): + client.app.add_sink(sink, r'/foo') - self.simulate_request('/foo/bar') - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/foo/bar') + assert response.status == falcon.HTTP_503 - def test_single_compiled_pattern(self): - self.api.add_sink(self.sink, re.compile(r'/foo')) + def test_single_compiled_pattern(self, client, sink, resource): + client.app.add_sink(sink, re.compile(r'/foo')) - self.simulate_request('/foo/bar') - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/foo/bar') + assert response.status == falcon.HTTP_503 - self.simulate_request('/auth') - self.assertEqual(self.srmock.status, falcon.HTTP_404) + response = client.simulate_request(path='/auth') + assert response.status == falcon.HTTP_404 - def test_named_groups(self): - self.api.add_sink(self.sink, r'/user/(?P\d+)') + def test_named_groups(self, client, sink, resource): + client.app.add_sink(sink, r'/user/(?P\d+)') - self.simulate_request('/user/309') - self.assertEqual(self.srmock.status, falcon.HTTP_503) - self.assertEqual(self.sink.kwargs['id'], '309') + response = client.simulate_request(path='/user/309') + assert response.status == falcon.HTTP_503 + assert sink.kwargs['id'] == '309' - self.simulate_request('/user/sally') - self.assertEqual(self.srmock.status, falcon.HTTP_404) + response = client.simulate_request(path='/user/sally') + assert response.status == falcon.HTTP_404 - def test_multiple_patterns(self): - self.api.add_sink(self.sink, r'/foo') - self.api.add_sink(sink_too, r'/foo') # Last duplicate wins + def test_multiple_patterns(self, client, sink, resource): + client.app.add_sink(sink, r'/foo') + client.app.add_sink(sink_too, r'/foo') # Last duplicate wins - self.api.add_sink(self.sink, r'/katza') + client.app.add_sink(sink, r'/katza') - self.simulate_request('/foo/bar') - self.assertEqual(self.srmock.status, falcon.HTTP_781) + response = client.simulate_request(path='/foo/bar') + assert response.status == falcon.HTTP_781 - self.simulate_request('/katza') - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/katza') + assert response.status == falcon.HTTP_503 - def test_with_route(self): - self.api.add_route('/books', self.resource) - self.api.add_sink(self.sink, '/proxy') + def test_with_route(self, client, sink, resource): + client.app.add_route('/books', resource) + client.app.add_sink(sink, '/proxy') - self.simulate_request('/proxy/books') - self.assertFalse(self.resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/proxy/books') + assert not resource.called + assert response.status == falcon.HTTP_503 - self.simulate_request('/books') - self.assertTrue(self.resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_200) + response = client.simulate_request(path='/books') + assert resource.called + assert response.status == falcon.HTTP_200 - def test_route_precedence(self): + def test_route_precedence(self, client, sink, resource): # NOTE(kgriffs): In case of collision, the route takes precedence. - self.api.add_route('/books', self.resource) - self.api.add_sink(self.sink, '/books') + client.app.add_route('/books', resource) + client.app.add_sink(sink, '/books') - self.simulate_request('/books') - self.assertTrue(self.resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_200) + response = client.simulate_request(path='/books') + assert resource.called + assert response.status == falcon.HTTP_200 - def test_route_precedence_with_id(self): + def test_route_precedence_with_id(self, client, sink, resource): # NOTE(kgriffs): In case of collision, the route takes precedence. - self.api.add_route('/books/{id}', self.resource) - self.api.add_sink(self.sink, '/books') + client.app.add_route('/books/{id}', resource) + client.app.add_sink(sink, '/books') - self.simulate_request('/books') - self.assertFalse(self.resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_503) + response = client.simulate_request(path='/books') + assert not resource.called + assert response.status == falcon.HTTP_503 - def test_route_precedence_with_both_id(self): + def test_route_precedence_with_both_id(self, client, sink, resource): # NOTE(kgriffs): In case of collision, the route takes precedence. - self.api.add_route('/books/{id}', self.resource) - self.api.add_sink(self.sink, '/books/\d+') + client.app.add_route('/books/{id}', resource) + client.app.add_sink(sink, '/books/\d+') - self.simulate_request('/books/123') - self.assertTrue(self.resource.called) - self.assertEqual(self.srmock.status, falcon.HTTP_200) + response = client.simulate_request(path='/books/123') + assert resource.called + assert response.status == falcon.HTTP_200 diff -Nru python-falcon-1.0.0/tests/test_slots.py python-falcon-1.4.1/tests/test_slots.py --- python-falcon-1.0.0/tests/test_slots.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_slots.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,24 @@ +import pytest + +from falcon import Request, Response +import falcon.testing as testing + + +class TestSlots(object): + + def test_slots_request(self): + env = testing.create_environ() + req = Request(env) + + try: + req.doesnt = 'exist' + except AttributeError: + pytest.fail('Unable to add additional variables dynamically') + + def test_slots_response(self): + resp = Response() + + try: + resp.doesnt = 'exist' + except AttributeError: + pytest.fail('Unable to add additional variables dynamically') diff -Nru python-falcon-1.0.0/tests/test_static.py python-falcon-1.4.1/tests/test_static.py --- python-falcon-1.0.0/tests/test_static.py 1970-01-01 00:00:00.000000000 +0000 +++ python-falcon-1.4.1/tests/test_static.py 2018-08-08 23:08:36.000000000 +0000 @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +import io + +import pytest + +import falcon +from falcon.request import Request +from falcon.response import Response +from falcon.routing import StaticRoute +import falcon.testing as testing + + +@pytest.fixture +def client(): + app = falcon.API() + return testing.TestClient(app) + + +@pytest.mark.parametrize('uri', [ + # Root + '/static', + '/static/', + '/static/.', + + # Attempt to jump out of the directory + '/static/..', + '/static/../.', + '/static/.././etc/passwd', + '/static/../etc/passwd', + '/static/css/../../secret', + '/static/css/../../etc/passwd', + '/static/./../etc/passwd', + + # The file system probably won't process escapes, but better safe than sorry + '/static/css/../.\\056/etc/passwd', + '/static/./\\056./etc/passwd', + '/static/\\056\\056/etc/passwd', + + # Double slash + '/static//test.css', + '/static//COM10', + '/static/path//test.css', + '/static/path///test.css', + '/static/path////test.css', + '/static/path/foo//test.css', + + # Control characters (0x00–0x1f and 0x80–0x9f) + '/static/.\x00ssh/authorized_keys', + '/static/.\x1fssh/authorized_keys', + '/static/.\x80ssh/authorized_keys', + '/static/.\x9fssh/authorized_keys', + + # Reserved characters (~, ?, <, >, :, *, |, ', and ") + '/static/~/.ssh/authorized_keys', + '/static/.ssh/authorized_key?', + '/static/.ssh/authorized_key>foo', + '/static/.ssh/authorized_key|foo', + '/static/.ssh/authorized_key