Skip to content

Commit

Permalink
docs(user): move examples and recipes to Python files (#2261)
Browse files Browse the repository at this point in the history
* docs(recipes): move recipes to Python files

* docs(recipes): move more recipe snippets to files

* docs(recipes): move the remaining snippets to files

* test(recipes): wip add skeleton and a couple of examples

* tests: port _util to a fixture

* style: fix example snippets with `ruff`

* test(examples): clean up & fix "things" and their tests

* test(recipes): add test for URL path recipes

* fix(conftest.py): replace usage of Path.with_stem(...) since it requires 3.9+...

* chore: make the code forward-compatible with py313
  • Loading branch information
vytas7 authored Aug 11, 2024
1 parent 0c1aca8 commit 262129a
Show file tree
Hide file tree
Showing 31 changed files with 661 additions and 587 deletions.
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ $ gnome-open docs/_build/html/index.html
$ xdg-open docs/_build/html/index.html
```

### Recipes and code snippets

If you are adding new recipes (in `docs/user/recipes`), try to break out code
snippets into separate files inside `examples/recipes`.
This allows `ruff` to format these snippets to conform to our code style, as
well as check for trivial errors.
Then simply use `literalinclude` to embed these snippets into your `.rst` recipe.

If possible, try to implement tests for your recipe in `tests/test_recipes.py`.
This helps to ensure that our recipes stay up-to-date as the framework's development progresses!

### VS Code Dev Container development environment

When opening the project using the [VS Code](https://code.visualstudio.com/) IDE, if you have [Docker](https://www.docker.com/) (or some drop-in replacement such as [Podman](https://podman.io/) or [Colima](https://github.com/abiosoft/colima) or [Rancher Desktop](https://rancherdesktop.io/)) installed, you can leverage the [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) feature to start a container in the background with all the dependencies required to test and debug the Falcon code. VS Code integrates with the Dev Container seamlessly, which can be configured via [devcontainer.json](.devcontainer/devcontainer.json). Once you open the project in VS Code, you can execute the "Reopen in Container" command to start the Dev Container which will run the headless VS Code Server process that the local VS Code app will connect to via a [published port](https://docs.docker.com/config/containers/container-networking/#published-ports).
Expand Down
47 changes: 4 additions & 43 deletions docs/user/recipes/header-name-case.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,12 @@ clients, it is possible to override HTTP headers using
`generic WSGI middleware
<https://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides>`_:

.. code:: python
class CustomHeadersMiddleware:
def __init__(self, app, title_case=True, custom_capitalization=None):
self._app = app
self._title_case = title_case
self._capitalization = custom_capitalization or {}
def __call__(self, environ, start_response):
def start_response_wrapper(status, response_headers, exc_info=None):
if self._title_case:
headers = [
(self._capitalization.get(name, name.title()), value)
for name, value in response_headers]
else:
headers = [
(self._capitalization.get(name, name), value)
for name, value in response_headers]
start_response(status, headers, exc_info)
return self._app(environ, start_response_wrapper)
.. literalinclude:: ../../../examples/recipes/header_name_case_mw.py
:language: python

We can now use this middleware to wrap a Falcon app:

.. code:: python
import falcon
# Import or define CustomHeadersMiddleware from the above snippet...
class FunkyResource:
def on_get(self, req, resp):
resp.set_header('X-Funky-Header', 'test')
resp.media = {'message': 'Hello'}
app = falcon.App()
app.add_route('/test', FunkyResource())
app = CustomHeadersMiddleware(
app,
custom_capitalization={'x-funky-header': 'X-FuNkY-HeADeR'},
)
.. literalinclude:: ../../../examples/recipes/header_name_case_app.py
:language: python

As a bonus, this recipe applies to non-Falcon WSGI applications too.
35 changes: 4 additions & 31 deletions docs/user/recipes/multipart-mixed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,8 @@ Let us extend the multipart form parser :attr:`media handlers
<falcon.media.multipart.MultipartParseOptions.media_handlers>` to recursively
parse embedded forms of the ``multipart/mixed`` content type:

.. code:: python
import falcon
import falcon.media
parser = falcon.media.MultipartFormHandler()
parser.parse_options.media_handlers['multipart/mixed'] = (
falcon.media.MultipartFormHandler())
.. literalinclude:: ../../../examples/recipes/multipart_mixed_intro.py
:language: python

.. note::
Here we create a new parser (with default options) for nested parts,
Expand All @@ -40,29 +34,8 @@ parse embedded forms of the ``multipart/mixed`` content type:

Let us now use the nesting-aware parser in an app:

.. code:: python
import falcon
import falcon.media
class Forms:
def on_post(self, req, resp):
example = {}
for part in req.media:
if part.content_type.startswith('multipart/mixed'):
for nested in part.media:
example[nested.filename] = nested.text
resp.media = example
parser = falcon.media.MultipartFormHandler()
parser.parse_options.media_handlers['multipart/mixed'] = (
falcon.media.MultipartFormHandler())
app = falcon.App()
app.req_options.media_handlers[falcon.MEDIA_MULTIPART] = parser
app.add_route('/forms', Forms())
.. literalinclude:: ../../../examples/recipes/multipart_mixed_main.py
:language: python

We should now be able to consume a form containing a nested ``multipart/mixed``
part (the example is adapted from the now-obsolete
Expand Down
101 changes: 8 additions & 93 deletions docs/user/recipes/output-csv.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,13 @@ and then assign its value to :attr:`resp.text <falcon.Response.text>`:

.. group-tab:: WSGI

.. code:: python
class Report:
def on_get(self, req, resp):
output = io.StringIO()
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(('fruit', 'quantity'))
writer.writerow(('apples', 13))
writer.writerow(('oranges', 37))
resp.content_type = 'text/csv'
resp.downloadable_as = 'report.csv'
resp.text = output.getvalue()
.. literalinclude:: ../../../examples/recipes/output_csv_text_wsgi.py
:language: python

.. group-tab:: ASGI

.. code:: python
class Report:
async def on_get(self, req, resp):
output = io.StringIO()
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(('fruit', 'quantity'))
writer.writerow(('apples', 13))
writer.writerow(('oranges', 37))
resp.content_type = 'text/csv'
resp.downloadable_as = 'report.csv'
resp.text = output.getvalue()
.. literalinclude:: ../../../examples/recipes/output_csv_text_asgi.py
:language: python

Here we set the response ``Content-Type`` to ``"text/csv"`` as
recommended by `RFC 4180 <https://tools.ietf.org/html/rfc4180>`_, and assign
Expand All @@ -66,74 +42,13 @@ accumulate the CSV data in a list. We will then set :attr:`resp.stream

.. group-tab:: WSGI

.. code:: python
class Report:
class PseudoTextStream:
def __init__(self):
self.clear()
def clear(self):
self.result = []
def write(self, data):
self.result.append(data.encode())
def fibonacci_generator(self, n=1000):
stream = self.PseudoTextStream()
writer = csv.writer(stream, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(('n', 'Fibonacci Fn'))
previous = 1
current = 0
for i in range(n+1):
writer.writerow((i, current))
previous, current = current, current + previous
yield from stream.result
stream.clear()
def on_get(self, req, resp):
resp.content_type = 'text/csv'
resp.downloadable_as = 'report.csv'
resp.stream = self.fibonacci_generator()
.. literalinclude:: ../../../examples/recipes/output_csv_stream_wsgi.py
:language: python

.. group-tab:: ASGI

.. code:: python
class Report:
class PseudoTextStream:
def __init__(self):
self.clear()
def clear(self):
self.result = []
def write(self, data):
self.result.append(data.encode())
async def fibonacci_generator(self, n=1000):
stream = self.PseudoTextStream()
writer = csv.writer(stream, quoting=csv.QUOTE_NONNUMERIC)
writer.writerow(('n', 'Fibonacci Fn'))
previous = 1
current = 0
for i in range(n+1):
writer.writerow((i, current))
previous, current = current, current + previous
for chunk in stream.result:
yield chunk
stream.clear()
async def on_get(self, req, resp):
resp.content_type = 'text/csv'
resp.downloadable_as = 'report.csv'
resp.stream = self.fibonacci_generator()
.. literalinclude:: ../../../examples/recipes/output_csv_stream_wsgi.py
:language: python

.. note::
At the time of writing, Python does not support ``yield from`` here
Expand Down
42 changes: 4 additions & 38 deletions docs/user/recipes/pretty-json.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,8 @@ prettify the output. By default, Falcon's :class:`JSONHandler
However, you can easily customize the output by simply providing the
desired ``dumps`` parameters:

.. code:: python
import functools
import json
from falcon import media
json_handler = media.JSONHandler(
dumps=functools.partial(json.dumps, indent=4, sort_keys=True),
)
.. literalinclude:: ../../../examples/recipes/pretty_json_intro.py
:language: python

You can now replace the default ``application/json``
:attr:`response media handlers <falcon.ResponseOptions.media_handlers>`
Expand Down Expand Up @@ -50,35 +42,9 @@ functionality" as per `RFC 6836, Section 4.3
Assuming we want to add JSON ``indent`` support to a Falcon app, this can be
implemented with a :ref:`custom media handler <custom-media-handler-type>`:

.. code:: python
import json
import falcon
class CustomJSONHandler(falcon.media.BaseHandler):
MAX_INDENT_LEVEL = 8
def deserialize(self, stream, content_type, content_length):
data = stream.read()
return json.loads(data.decode())
def serialize(self, media, content_type):
_, params = falcon.parse_header(content_type)
indent = params.get('indent')
if indent is not None:
try:
indent = int(indent)
# NOTE: Impose a reasonable indentation level limit.
if indent < 0 or indent > self.MAX_INDENT_LEVEL:
indent = None
except ValueError:
# TODO: Handle invalid params?
indent = None
.. literalinclude:: ../../../examples/recipes/pretty_json_main.py
:language: python

result = json.dumps(media, indent=indent, sort_keys=bool(indent))
return result.encode()

Furthermore, we'll need to implement content-type negotiation to accept the
indented JSON content type for response serialization. The bare-minimum
Expand Down
69 changes: 4 additions & 65 deletions docs/user/recipes/raw-url-path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,8 @@ understands two such extensions, ``RAW_URI`` (Gunicorn, Werkzeug's dev server)
and ``REQUEST_URI`` (uWSGI, Waitress, Werkzeug's dev server), and replaces
``req.path`` with a value extracted from the raw URL:

.. code:: python
import falcon
import falcon.uri
class RawPathComponent:
def process_request(self, req, resp):
raw_uri = req.env.get('RAW_URI') or req.env.get('REQUEST_URI')
# NOTE: Reconstruct the percent-encoded path from the raw URI.
if raw_uri:
req.path, _, _ = raw_uri.partition('?')
class URLResource:
def on_get(self, req, resp, url):
# NOTE: url here is potentially percent-encoded.
url = falcon.uri.decode(url)
resp.media = {'url': url}
def on_get_status(self, req, resp, url):
# NOTE: url here is potentially percent-encoded.
url = falcon.uri.decode(url)
resp.media = {'cached': True}
app = falcon.App(middleware=[RawPathComponent()])
app.add_route('/cache/{url}', URLResource())
app.add_route('/cache/{url}/status', URLResource(), suffix='status')
.. literalinclude:: ../../../examples/recipes/raw_url_path_wsgi.py
:language: python

Running the above app with a supported server such as Gunicorn or uWSGI, the
following response is rendered to
Expand Down Expand Up @@ -98,39 +68,8 @@ Similar to the WSGI snippet from the previous chapter, let us create a
middleware component that replaces ``req.path`` with the value of ``raw_path``
(provided the latter is present in the ASGI HTTP scope):

.. code:: python
import falcon.asgi
import falcon.uri
class RawPathComponent:
async def process_request(self, req, resp):
raw_path = req.scope.get('raw_path')
# NOTE: Decode the raw path from the raw_path bytestring, disallowing
# non-ASCII characters, assuming they are correctly percent-coded.
if raw_path:
req.path = raw_path.decode('ascii')
class URLResource:
async def on_get(self, req, resp, url):
# NOTE: url here is potentially percent-encoded.
url = falcon.uri.decode(url)
resp.media = {'url': url}
async def on_get_status(self, req, resp, url):
# NOTE: url here is potentially percent-encoded.
url = falcon.uri.decode(url)
resp.media = {'cached': True}
app = falcon.asgi.App(middleware=[RawPathComponent()])
app.add_route('/cache/{url}', URLResource())
app.add_route('/cache/{url}/status', URLResource(), suffix='status')
.. literalinclude:: ../../../examples/recipes/raw_url_path_asgi.py
:language: python

Running the above snippet with ``uvicorn`` (that supports ``raw_path``), the
percent-encoded ``url`` field is now correctly handled for a
Expand Down
Loading

0 comments on commit 262129a

Please sign in to comment.