Skip to content

Commit 400ea5a

Browse files
committed
feat(routing): override default responders via on_request()
Add an option to CompiledRouterOptions that allows for overriding the default responders by implementing on_request() in the resource class Closes falconry#2071
1 parent c820626 commit 400ea5a

File tree

4 files changed

+84
-7
lines changed

4 files changed

+84
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Added the :attr:`~.CompiledRouterOptions.allow_on_request` router option that
2+
allows for providing a default responder by defining `on_request()` on the
3+
resource. This option is disabled by default. If enabled, `on_request()` is
4+
set as the responder for every unimplemented method except for `on_options()`.
5+
If the option is disabled or `on_request()` is not provided in the resource,
6+
the default responder for "405 Method Not Allowed" is used.

falcon/routing/compiled.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,14 @@ class can use suffixed responders to distinguish requests
209209

210210
method_map = self.map_http_methods(resource, **kwargs)
211211

212-
set_default_responders(method_map, asgi=asgi)
212+
default_responder = None
213+
214+
if self._options.allow_on_request:
215+
default_responder = getattr(resource, 'on_request', None)
216+
217+
set_default_responders(
218+
method_map, asgi=asgi, default_responder=default_responder
219+
)
213220

214221
if asgi:
215222
self._require_coroutine_responders(method_map)
@@ -941,15 +948,35 @@ class CompiledRouterOptions:
941948
(See also: :ref:`Field Converters <routing_field_converters>`)
942949
"""
943950

944-
__slots__ = ('converters',)
951+
allow_on_request: bool
952+
"""Allows for providing a default responder by defining `on_request()` on
953+
the resource.
945954
946-
def __init__(self) -> None:
955+
This feature is disabled by default and can be enabled by::
956+
957+
app.router_options.allow_on_request = True
958+
959+
Note:
960+
This option does not override `on_options()`.
961+
962+
Note:
963+
In order for this option to take effect, it must be enabled before
964+
calling :meth:`add_route()`.
965+
966+
.. versionadded:: 4.1
967+
"""
968+
969+
__slots__ = ('converters', 'allow_on_request')
970+
971+
def __init__(self, allow_on_request: bool = False) -> None:
947972
object.__setattr__(
948973
self,
949974
'converters',
950975
ConverterDict((name, converter) for name, converter in converters.BUILTIN),
951976
)
952977

978+
self.allow_on_request = allow_on_request
979+
953980
def __setattr__(self, name: str, value: Any) -> None:
954981
if name == 'converters':
955982
raise AttributeError('Cannot set "converters", please update it in place.')

falcon/routing/util.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
from __future__ import annotations
1818

19-
from typing import Optional, TYPE_CHECKING
19+
from typing import Optional, TYPE_CHECKING, Union
2020

2121
from falcon import constants
2222
from falcon import responders
2323

2424
if TYPE_CHECKING:
25+
from falcon._typing import AsgiResponderCallable
2526
from falcon._typing import MethodDict
27+
from falcon._typing import ResponderCallable
2628

2729

2830
class SuffixedMethodNotFoundError(Exception):
@@ -78,14 +80,21 @@ def map_http_methods(resource: object, suffix: Optional[str] = None) -> MethodDi
7880
return method_map
7981

8082

81-
def set_default_responders(method_map: MethodDict, asgi: bool = False) -> None:
83+
def set_default_responders(
84+
method_map: MethodDict,
85+
asgi: bool = False,
86+
default_responder: Optional[Union[ResponderCallable, AsgiResponderCallable]] = None,
87+
) -> None:
8288
"""Map HTTP methods not explicitly defined on a resource to default responders.
8389
8490
Args:
8591
method_map: A dict with HTTP methods mapped to responders explicitly
8692
defined in a resource.
8793
asgi (bool): ``True`` if using an ASGI app, ``False`` otherwise
8894
(default ``False``).
95+
default_responder: An optional default responder for unimplmented
96+
resource methods. If not provided a responder for
97+
"405 Method Not Allowed" is used.
8998
"""
9099

91100
# Attach a resource for unsupported HTTP methods
@@ -99,8 +108,11 @@ def set_default_responders(method_map: MethodDict, asgi: bool = False) -> None:
99108
method_map['OPTIONS'] = opt_responder # type: ignore[assignment]
100109
allowed_methods.append('OPTIONS')
101110

102-
na_responder = responders.create_method_not_allowed(allowed_methods, asgi=asgi)
111+
if not default_responder:
112+
default_responder = responders.create_method_not_allowed(
113+
allowed_methods, asgi=asgi
114+
)
103115

104116
for method in constants.COMBINED_METHODS:
105117
if method not in method_map:
106-
method_map[method] = na_responder # type: ignore[assignment]
118+
method_map[method] = default_responder # type: ignore[assignment]

tests/test_default_router.py

+32
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ def on_get(self, req, resp):
103103
resp.text = self.resource_id
104104

105105

106+
class ResourceWithDefaultResponder:
107+
def on_get(self, req, resp):
108+
pass
109+
110+
def on_request(self, req, resp):
111+
pass
112+
113+
106114
class SpamConverter:
107115
def __init__(self, times, eggs=False):
108116
self._times = times
@@ -695,6 +703,30 @@ def test_options_converters_invalid_name_on_update(router):
695703
)
696704

697705

706+
def test_options_allow_on_request_disabled():
707+
router = DefaultRouter()
708+
router.add_route('/default', ResourceWithDefaultResponder())
709+
710+
resource, method_map, __, __ = router.find('/default')
711+
712+
for method, responder in method_map.items():
713+
assert responder != resource.on_request
714+
715+
716+
def test_options_allow_on_request_enabled():
717+
router = DefaultRouter()
718+
router.options.allow_on_request = True
719+
router.add_route('/default', ResourceWithDefaultResponder())
720+
721+
resource, method_map, __, __ = router.find('/default')
722+
723+
for method, responder in method_map.items():
724+
if method not in ('GET', 'OPTIONS'):
725+
assert responder == resource.on_request
726+
else:
727+
assert responder != resource.on_request
728+
729+
698730
@pytest.fixture
699731
def param_router():
700732
r = DefaultRouter()

0 commit comments

Comments
 (0)