Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(user): move examples and recipes to Python files #2261

Merged
merged 11 commits into from
Aug 11, 2024
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
Loading