Skip to content

Commit bb056b2

Browse files
authored
Merge branch 'master' into testing-helpers
2 parents a45807e + 6adc303 commit bb056b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+887
-668
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
- "towncrier"
3636
- "look"
3737
- "asgilook"
38-
- "wslook"
38+
- "ws_tutorial"
3939
- "check_vendored"
4040
- "twine_check"
4141
- "daphne"

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ $ gnome-open docs/_build/html/index.html
133133
$ xdg-open docs/_build/html/index.html
134134
```
135135

136+
### Recipes and code snippets
137+
138+
If you are adding new recipes (in `docs/user/recipes`), try to break out code
139+
snippets into separate files inside `examples/recipes`.
140+
This allows `ruff` to format these snippets to conform to our code style, as
141+
well as check for trivial errors.
142+
Then simply use `literalinclude` to embed these snippets into your `.rst` recipe.
143+
144+
If possible, try to implement tests for your recipe in `tests/test_recipes.py`.
145+
This helps to ensure that our recipes stay up-to-date as the framework's development progresses!
146+
136147
### VS Code Dev Container development environment
137148

138149
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).

docs/_newsfragments/2253.misc.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The functions ``create_task()`` and ``get_running_loop()`` of `falcon.util.sync` are deprecated.
2+
The counterpart functions in the builtin package `asyncio` are encouraged to be used.

docs/community/help.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ First, :ref:`take a quick look at our FAQ <faq>` to see if your question has
1515
already been addressed. If not, or if the answer is unclear, please don't
1616
hesitate to reach out via one of the channels below.
1717

18+
.. _chat:
19+
1820
Chat
1921
----
2022
The Falconry community on Gitter is a great place to ask questions and

docs/user/faq.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ Therefore, as long as you implement these classes and callables in a
9898
thread-safe manner, and ensure that any third-party libraries used by your
9999
app are also thread-safe, your WSGI app as a whole will be thread-safe.
100100

101+
Can I run Falcon on free-threaded CPython?
102+
------------------------------------------
103+
104+
At the time of this writing, Falcon has not been extensively evaluated without
105+
the GIL yet.
106+
107+
We load-tested the WSGI flavor of the framework via
108+
:class:`~wsgiref.simple_server.WSGIServer` +
109+
:class:`~socketserver.ThreadingMixIn` on
110+
`free-threaded CPython 3.13.0rc1
111+
<https://docs.python.org/3.13/whatsnew/3.13.html#free-threaded-cpython>`__
112+
(under ``PYTHON_GIL=0``), and observed no issues that would point toward
113+
Falcon's reliance on the GIL. Thus, we would like to think that Falcon is still
114+
:ref:`thread-safe <faq_thread_safety>` even in free-threaded execution,
115+
but it is too early to provide a definite answer.
116+
117+
If you experimented with free-threading of Falcon or other Python web services,
118+
please :ref:`share your experience <chat>`!
119+
101120
Does Falcon support asyncio?
102121
------------------------------
103122

docs/user/recipes/header-name-case.rst

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,12 @@ clients, it is possible to override HTTP headers using
1111
`generic WSGI middleware
1212
<https://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides>`_:
1313

14-
.. code:: python
15-
16-
class CustomHeadersMiddleware:
17-
18-
def __init__(self, app, title_case=True, custom_capitalization=None):
19-
self._app = app
20-
self._title_case = title_case
21-
self._capitalization = custom_capitalization or {}
22-
23-
def __call__(self, environ, start_response):
24-
def start_response_wrapper(status, response_headers, exc_info=None):
25-
if self._title_case:
26-
headers = [
27-
(self._capitalization.get(name, name.title()), value)
28-
for name, value in response_headers]
29-
else:
30-
headers = [
31-
(self._capitalization.get(name, name), value)
32-
for name, value in response_headers]
33-
start_response(status, headers, exc_info)
34-
35-
return self._app(environ, start_response_wrapper)
14+
.. literalinclude:: ../../../examples/recipes/header_name_case_mw.py
15+
:language: python
3616

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

39-
.. code:: python
40-
41-
import falcon
42-
43-
# Import or define CustomHeadersMiddleware from the above snippet...
44-
45-
46-
class FunkyResource:
47-
48-
def on_get(self, req, resp):
49-
resp.set_header('X-Funky-Header', 'test')
50-
resp.media = {'message': 'Hello'}
51-
52-
53-
app = falcon.App()
54-
app.add_route('/test', FunkyResource())
55-
56-
app = CustomHeadersMiddleware(
57-
app,
58-
custom_capitalization={'x-funky-header': 'X-FuNkY-HeADeR'},
59-
)
19+
.. literalinclude:: ../../../examples/recipes/header_name_case_app.py
20+
:language: python
6021

6122
As a bonus, this recipe applies to non-Falcon WSGI applications too.

docs/user/recipes/multipart-mixed.rst

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,8 @@ Let us extend the multipart form parser :attr:`media handlers
2222
<falcon.media.multipart.MultipartParseOptions.media_handlers>` to recursively
2323
parse embedded forms of the ``multipart/mixed`` content type:
2424

25-
.. code:: python
26-
27-
import falcon
28-
import falcon.media
29-
30-
parser = falcon.media.MultipartFormHandler()
31-
parser.parse_options.media_handlers['multipart/mixed'] = (
32-
falcon.media.MultipartFormHandler())
25+
.. literalinclude:: ../../../examples/recipes/multipart_mixed_intro.py
26+
:language: python
3327

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

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

43-
.. code:: python
44-
45-
import falcon
46-
import falcon.media
47-
48-
class Forms:
49-
def on_post(self, req, resp):
50-
example = {}
51-
for part in req.media:
52-
if part.content_type.startswith('multipart/mixed'):
53-
for nested in part.media:
54-
example[nested.filename] = nested.text
55-
56-
resp.media = example
57-
58-
59-
parser = falcon.media.MultipartFormHandler()
60-
parser.parse_options.media_handlers['multipart/mixed'] = (
61-
falcon.media.MultipartFormHandler())
62-
63-
app = falcon.App()
64-
app.req_options.media_handlers[falcon.MEDIA_MULTIPART] = parser
65-
app.add_route('/forms', Forms())
37+
.. literalinclude:: ../../../examples/recipes/multipart_mixed_main.py
38+
:language: python
6639

6740
We should now be able to consume a form containing a nested ``multipart/mixed``
6841
part (the example is adapted from the now-obsolete

docs/user/recipes/output-csv.rst

Lines changed: 8 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,13 @@ and then assign its value to :attr:`resp.text <falcon.Response.text>`:
1313

1414
.. group-tab:: WSGI
1515

16-
.. code:: python
17-
18-
class Report:
19-
20-
def on_get(self, req, resp):
21-
output = io.StringIO()
22-
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC)
23-
writer.writerow(('fruit', 'quantity'))
24-
writer.writerow(('apples', 13))
25-
writer.writerow(('oranges', 37))
26-
27-
resp.content_type = 'text/csv'
28-
resp.downloadable_as = 'report.csv'
29-
resp.text = output.getvalue()
16+
.. literalinclude:: ../../../examples/recipes/output_csv_text_wsgi.py
17+
:language: python
3018

3119
.. group-tab:: ASGI
3220

33-
.. code:: python
34-
35-
class Report:
36-
37-
async def on_get(self, req, resp):
38-
output = io.StringIO()
39-
writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC)
40-
writer.writerow(('fruit', 'quantity'))
41-
writer.writerow(('apples', 13))
42-
writer.writerow(('oranges', 37))
43-
44-
resp.content_type = 'text/csv'
45-
resp.downloadable_as = 'report.csv'
46-
resp.text = output.getvalue()
21+
.. literalinclude:: ../../../examples/recipes/output_csv_text_asgi.py
22+
:language: python
4723

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

6743
.. group-tab:: WSGI
6844

69-
.. code:: python
70-
71-
class Report:
72-
73-
class PseudoTextStream:
74-
def __init__(self):
75-
self.clear()
76-
77-
def clear(self):
78-
self.result = []
79-
80-
def write(self, data):
81-
self.result.append(data.encode())
82-
83-
def fibonacci_generator(self, n=1000):
84-
stream = self.PseudoTextStream()
85-
writer = csv.writer(stream, quoting=csv.QUOTE_NONNUMERIC)
86-
writer.writerow(('n', 'Fibonacci Fn'))
87-
88-
previous = 1
89-
current = 0
90-
for i in range(n+1):
91-
writer.writerow((i, current))
92-
previous, current = current, current + previous
93-
94-
yield from stream.result
95-
stream.clear()
96-
97-
def on_get(self, req, resp):
98-
resp.content_type = 'text/csv'
99-
resp.downloadable_as = 'report.csv'
100-
resp.stream = self.fibonacci_generator()
45+
.. literalinclude:: ../../../examples/recipes/output_csv_stream_wsgi.py
46+
:language: python
10147

10248
.. group-tab:: ASGI
10349

104-
.. code:: python
105-
106-
class Report:
107-
108-
class PseudoTextStream:
109-
def __init__(self):
110-
self.clear()
111-
112-
def clear(self):
113-
self.result = []
114-
115-
def write(self, data):
116-
self.result.append(data.encode())
117-
118-
async def fibonacci_generator(self, n=1000):
119-
stream = self.PseudoTextStream()
120-
writer = csv.writer(stream, quoting=csv.QUOTE_NONNUMERIC)
121-
writer.writerow(('n', 'Fibonacci Fn'))
122-
123-
previous = 1
124-
current = 0
125-
for i in range(n+1):
126-
writer.writerow((i, current))
127-
previous, current = current, current + previous
128-
129-
for chunk in stream.result:
130-
yield chunk
131-
stream.clear()
132-
133-
async def on_get(self, req, resp):
134-
resp.content_type = 'text/csv'
135-
resp.downloadable_as = 'report.csv'
136-
resp.stream = self.fibonacci_generator()
50+
.. literalinclude:: ../../../examples/recipes/output_csv_stream_wsgi.py
51+
:language: python
13752

13853
.. note::
13954
At the time of writing, Python does not support ``yield from`` here

docs/user/recipes/pretty-json.rst

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,8 @@ prettify the output. By default, Falcon's :class:`JSONHandler
99
However, you can easily customize the output by simply providing the
1010
desired ``dumps`` parameters:
1111

12-
.. code:: python
13-
14-
import functools
15-
import json
16-
17-
from falcon import media
18-
19-
json_handler = media.JSONHandler(
20-
dumps=functools.partial(json.dumps, indent=4, sort_keys=True),
21-
)
12+
.. literalinclude:: ../../../examples/recipes/pretty_json_intro.py
13+
:language: python
2214

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

53-
.. code:: python
54-
55-
import json
56-
57-
import falcon
58-
59-
60-
class CustomJSONHandler(falcon.media.BaseHandler):
61-
MAX_INDENT_LEVEL = 8
62-
63-
def deserialize(self, stream, content_type, content_length):
64-
data = stream.read()
65-
return json.loads(data.decode())
66-
67-
def serialize(self, media, content_type):
68-
_, params = falcon.parse_header(content_type)
69-
indent = params.get('indent')
70-
if indent is not None:
71-
try:
72-
indent = int(indent)
73-
# NOTE: Impose a reasonable indentation level limit.
74-
if indent < 0 or indent > self.MAX_INDENT_LEVEL:
75-
indent = None
76-
except ValueError:
77-
# TODO: Handle invalid params?
78-
indent = None
45+
.. literalinclude:: ../../../examples/recipes/pretty_json_main.py
46+
:language: python
7947

80-
result = json.dumps(media, indent=indent, sort_keys=bool(indent))
81-
return result.encode()
8248

8349
Furthermore, we'll need to implement content-type negotiation to accept the
8450
indented JSON content type for response serialization. The bare-minimum

0 commit comments

Comments
 (0)