Skip to content

Commit 5012c16

Browse files
Merge branch 'master' into static-route-head-sp
2 parents f77c12c + a33301e commit 5012c16

40 files changed

+668
-140
lines changed

.devcontainer/devcontainer.json

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
22
// README at: https://github.com/devcontainers/templates/tree/main/src/python
33
{
4-
"name": "Python 3",
5-
"image": "mcr.microsoft.com/devcontainers/python:3.11",
4+
"name": "Falcon (CPython 3.14)",
5+
"image": "mcr.microsoft.com/devcontainers/python:3.14",
66
"features": {
7-
"ghcr.io/devcontainers-contrib/features/tox:1": {}
8-
}
9-
10-
// Features to add to the dev container. More info: https://containers.dev/features.
11-
// "features": {},
12-
13-
// Use 'forwardPorts' to make a list of ports inside the container available locally.
14-
// "forwardPorts": [],
7+
"ghcr.io/devcontainers-extra/features/ruff:1": {},
8+
"ghcr.io/devcontainers-extra/features/tox:2": {}
9+
},
1510

1611
// Use 'postCreateCommand' to run commands after the container is created.
17-
// "postCreateCommand": "pip3 install --user -r requirements.txt",
12+
// NOTE(vytas): We install pytest not via pipx or features so that test collection works via the default interpreter.
13+
"postCreateCommand": "pip install --upgrade pip pytest && FALCON_DISABLE_CYTHON=y pip install -e .",
1814

1915
// Configure tool-specific properties.
20-
// "customizations": {},
16+
"customizations": {
17+
"vscode": {
18+
"extensions": [
19+
"ms-python.python",
20+
"ms-python.vscode-pylance",
21+
"charliermarsh.ruff"
22+
],
23+
"settings": {
24+
"python.defaultInterpreterPath": "/usr/local/bin/python",
25+
"python.linting.enabled": true,
26+
"python.testing.pytestEnabled": true,
27+
"python.testing.unittestEnabled": false,
28+
// NOTE(vytas): Disable automatic random port-forwarding as running tox doesn't need it.
29+
"remote.autoForwardPorts": false
30+
}
31+
}
32+
}
2133

2234
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
35+
// NOTE(vytas): You will probably want to connect as root on rootless Docker/Podman/etc.
36+
// In rootless setups, the current user is typically mapped to root's UID inside the container.
37+
// (You may run into permission problems otherwise as the vscode user gets mapped to an ephemeral UID.)
2338
// "remoteUser": "root"
2439
}

.github/workflows/cibuildwheel.yaml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ jobs:
153153
- cp312
154154
- cp313
155155
- cp314
156+
include:
157+
- platform:
158+
name: manylinux_x86_64
159+
os: ubuntu-latest
160+
python: cp314t
161+
- platform:
162+
name: musllinux_x86_64
163+
os: ubuntu-latest
164+
python: cp314t
165+
- platform:
166+
name: manylinux_aarch64
167+
os: ubuntu-24.04-arm
168+
python: cp314t
169+
- platform:
170+
name: musllinux_aarch64
171+
os: ubuntu-24.04-arm
172+
python: cp314t
156173

157174
defaults:
158175
run:
@@ -171,7 +188,7 @@ jobs:
171188
platforms: all
172189

173190
- name: Build wheels
174-
uses: pypa/cibuildwheel@v3.1.0
191+
uses: pypa/cibuildwheel@v3.2.1
175192
env:
176193
# NOTE(vytas): Uncomment to test against alpha/beta CPython
177194
# (usually May-July until rc1).

.github/workflows/test-wheels.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
emulation: true
2424
- build: "cp39-manylinux_x86_64"
2525
os: ubuntu-latest
26-
- build: "cp314-musllinux_x86_64"
26+
- build: "cp314t-musllinux_x86_64"
2727
os: ubuntu-latest
2828
- build: "cp313-macosx_arm64"
2929
os: macos-15
@@ -43,7 +43,7 @@ jobs:
4343
platforms: all
4444

4545
- name: Build wheels
46-
uses: pypa/cibuildwheel@v3.1.0
46+
uses: pypa/cibuildwheel@v3.2.1
4747
env:
4848
# NOTE(vytas): Uncomment to test against alpha/beta CPython
4949
# (usually May-July until rc1).

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ jobs:
6161
python-version: "3.14"
6262
- env: py314_cython
6363
python-version: "3.14"
64+
- env: py315
65+
python-version: "3.15.0-alpha.1 - 3.15.0"
6466
- env: py312_nocover
6567
os: macos-latest
6668
platform-label: ' (macos)'

AUTHORS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ listed below by date of first contribution:
214214
* Soubhik Kumar Mitra (x612skm)
215215
* Sviatoslav Sydorenko (webknjaz)
216216
* Soney (sonephyo)
217+
* Ayan Ahmed Khan (AyanAhmedKhan)
218+
* Parman Mohammadalizadeh (MannXo)
219+
* Tudor Gradinaru (TudorGR)
220+
* Swapnaneel Patra (thisisrick25)
221+
* 0x1618
217222

218223
(et al.)
219224

CONTRIBUTING.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,25 @@ This helps to ensure that our recipes stay up-to-date as the framework's develop
153153

154154
## VS Code Dev Container development environment
155155

156-
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]. 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).
156+
When opening the project using the [VS Code](https://code.visualstudio.com/) IDE,
157+
if you have [Docker](https://www.docker.com/) (or some drop-in replacement such as
158+
[Podman](https://podman.io/), [Colima](https://github.com/abiosoft/colima), or
159+
[Rancher Desktop](https://rancherdesktop.io/), etc) installed, you can leverage the
160+
[Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers)
161+
feature to start a container in the background with all the dependencies
162+
required to test and debug the Falcon code base.
163+
164+
Once you open the project in VS Code, you can execute the "Reopen in Container"
165+
command to start the Dev Container which will run a headless VS Code Server
166+
process. In the connected Terminal, you can run the above referenced ``ruff``
167+
and ``tox`` commands inside the container (both tools are already pre-installed).
168+
169+
VS Code test collection also works, both in the container and without it.
170+
Open the "Testing" View, and you should see all the ~300 (at the time of
171+
writing) tests discovered via ``pytest``.
172+
173+
(If needed, the Dev Container's configuration can be tweaked via
174+
[devcontainer.json][devcontainer].)
157175

158176
## Use of LLMs ("AI")
159177

README.rst

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,12 @@ Note that this example assumes that the
444444
import uuid
445445
from wsgiref import simple_server
446446
447-
import falcon
448447
import requests
449448
449+
import falcon
450+
450451
451452
class StorageEngine:
452-
453453
def get_things(self, marker, limit):
454454
return [{'id': str(uuid.uuid4()), 'color': 'green'}]
455455
@@ -459,15 +459,13 @@ Note that this example assumes that the
459459
460460
461461
class StorageError(Exception):
462-
463462
@staticmethod
464-
def handle(ex, req, resp, params):
463+
def handle(req, resp, ex, params):
465464
# TODO: Log the error, clean up, etc. before raising
466465
raise falcon.HTTPInternalServerError()
467466
468467
469468
class SinkAdapter:
470-
471469
engines = {
472470
'ddg': 'https://duckduckgo.com',
473471
'y': 'https://search.yahoo.com/search',
@@ -478,54 +476,59 @@ Note that this example assumes that the
478476
params = {'q': req.get_param('q', True)}
479477
result = requests.get(url, params=params)
480478
481-
resp.status = str(result.status_code) + ' ' + result.reason
479+
resp.status = falcon.code_to_http_status(result.status_code)
482480
resp.content_type = result.headers['content-type']
483481
resp.text = result.text
484482
485483
486484
class AuthMiddleware:
487-
488485
def process_request(self, req, resp):
489486
token = req.get_header('Authorization')
490487
account_id = req.get_header('Account-ID')
491488
492489
challenges = ['Token type="Fernet"']
493490
494491
if token is None:
495-
description = ('Please provide an auth token '
496-
'as part of the request.')
492+
description = 'Please provide an auth token as part of the request.'
497493
498-
raise falcon.HTTPUnauthorized(title='Auth token required',
499-
description=description,
500-
challenges=challenges,
501-
href='http://docs.example.com/auth')
494+
raise falcon.HTTPUnauthorized(
495+
title='Auth token required',
496+
description=description,
497+
challenges=challenges,
498+
href='http://docs.example.com/auth',
499+
)
502500
503501
if not self._token_is_valid(token, account_id):
504-
description = ('The provided auth token is not valid. '
505-
'Please request a new token and try again.')
502+
description = (
503+
'The provided auth token is not valid. '
504+
'Please request a new token and try again.'
505+
)
506506
507-
raise falcon.HTTPUnauthorized(title='Authentication required',
508-
description=description,
509-
challenges=challenges,
510-
href='http://docs.example.com/auth')
507+
raise falcon.HTTPUnauthorized(
508+
title='Authentication required',
509+
description=description,
510+
challenges=challenges,
511+
href='http://docs.example.com/auth',
512+
)
511513
512514
def _token_is_valid(self, token, account_id):
513515
return True # Suuuuuure it's valid...
514516
515517
516518
class RequireJSON:
517-
518519
def process_request(self, req, resp):
519520
if not req.client_accepts_json:
520521
raise falcon.HTTPNotAcceptable(
521522
description='This API only supports responses encoded as JSON.',
522-
href='http://docs.examples.com/api/json')
523+
href='http://docs.examples.com/api/json',
524+
)
523525
524526
if req.method in ('POST', 'PUT'):
525527
if 'application/json' not in req.content_type:
526528
raise falcon.HTTPUnsupportedMediaType(
527529
title='This API only supports requests encoded as JSON.',
528-
href='http://docs.examples.com/api/json')
530+
href='http://docs.examples.com/api/json',
531+
)
529532
530533
531534
class JSONTranslator:
@@ -542,21 +545,24 @@ Note that this example assumes that the
542545
# Nothing to do
543546
return
544547
545-
body = req.stream.read()
548+
body = req.bounded_stream.read()
546549
if not body:
547-
raise falcon.HTTPBadRequest(title='Empty request body',
548-
description='A valid JSON document is required.')
550+
raise falcon.HTTPBadRequest(
551+
title='Empty request body',
552+
description='A valid JSON document is required.',
553+
)
549554
550555
try:
551556
req.context.doc = json.loads(body.decode('utf-8'))
552557
553558
except (ValueError, UnicodeDecodeError):
554-
description = ('Could not decode the request body. The '
555-
'JSON was incorrect or not encoded as '
556-
'UTF-8.')
559+
description = (
560+
'Could not decode the request body. The '
561+
'JSON was incorrect or not encoded as '
562+
'UTF-8.'
563+
)
557564
558-
raise falcon.HTTPBadRequest(title='Malformed JSON',
559-
description=description)
565+
raise falcon.HTTPBadRequest(title='Malformed JSON', description=description)
560566
561567
def process_response(self, req, resp, resource, req_succeeded):
562568
if not hasattr(resp.context, 'result'):
@@ -566,21 +572,22 @@ Note that this example assumes that the
566572
567573
568574
def max_body(limit):
569-
570575
def hook(req, resp, resource, params):
571576
length = req.content_length
572577
if length is not None and length > limit:
573-
msg = ('The size of the request is too large. The body must not '
574-
'exceed ' + str(limit) + ' bytes in length.')
578+
msg = (
579+
'The size of the request is too large. The body must not '
580+
'exceed ' + str(limit) + ' bytes in length.'
581+
)
575582
576583
raise falcon.HTTPContentTooLarge(
577-
title='Request body is too large', description=msg)
584+
title='Request body is too large', description=msg
585+
)
578586
579587
return hook
580588
581589
582590
class ThingsResource:
583-
584591
def __init__(self, db):
585592
self.db = db
586593
self.logger = logging.getLogger('thingsapp.' + __name__)
@@ -594,14 +601,15 @@ Note that this example assumes that the
594601
except Exception as ex:
595602
self.logger.error(ex)
596603
597-
description = ('Aliens have attacked our base! We will '
598-
'be back as soon as we fight them off. '
599-
'We appreciate your patience.')
604+
description = (
605+
'Aliens have attacked our base! We will '
606+
'be back as soon as we fight them off. '
607+
'We appreciate your patience.'
608+
)
600609
601610
raise falcon.HTTPServiceUnavailable(
602-
title='Service Outage',
603-
description=description,
604-
retry_after=30)
611+
title='Service Outage', description=description, retry_after=30
612+
)
605613
606614
# NOTE: Normally you would use resp.media for this sort of thing;
607615
# this example serves only to demonstrate how the context can be
@@ -619,19 +627,23 @@ Note that this example assumes that the
619627
except AttributeError:
620628
raise falcon.HTTPBadRequest(
621629
title='Missing thing',
622-
description='A thing must be submitted in the request body.')
630+
description='A thing must be submitted in the request body.',
631+
)
623632
624633
proper_thing = self.db.add_thing(doc)
625634
626635
resp.status = falcon.HTTP_201
627-
resp.location = '/%s/things/%s' % (user_id, proper_thing['id'])
636+
resp.location = '/{}/things/{}'.format(user_id, proper_thing['id'])
637+
628638
629639
# Configure your WSGI server to load "things.app" (app is a WSGI callable)
630-
app = falcon.App(middleware=[
631-
AuthMiddleware(),
632-
RequireJSON(),
633-
JSONTranslator(),
634-
])
640+
app = falcon.App(
641+
middleware=[
642+
AuthMiddleware(),
643+
RequireJSON(),
644+
JSONTranslator(),
645+
]
646+
)
635647
636648
db = StorageEngine()
637649
things = ThingsResource(db)

docs/_newsfragments/1169.newandimproved.rst

Lines changed: 0 additions & 13 deletions
This file was deleted.

docs/_newsfragments/2372.newandimproved.rst

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The :meth:`req.get_param_as_list <falcon.Request.get_param_as_list>` method now
2+
supports a new argument, `delimiter`, for splitting of values.
3+
In line with the OpenAPI v3 parameter specification, the supported delimiters
4+
currently include the ``'pipeDelimited'`` and ``'spaceDelimited'`` symbolic
5+
constants, as well as the literal ``','``, ``'|'``, and ``' '`` characters.

0 commit comments

Comments
 (0)