-
-
Notifications
You must be signed in to change notification settings - Fork 951
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
feat: provide a .app attribute for generic middleware #1960
Comments
Hi @adriangb ! |
Hi, thanks for the quick reply. I'm thinking of a situation like the OpenTelemetry instrumentation. Currently, it monkey patches Presumably, this could be implemented by just doing |
Aha, I see, let us think about this. FWIW, this couldn't even be implemented just by doing OTOH, if this is strictly for instrumentation, let us also discuss alternative ways to achieve the same effect. Maybe we're a bit too paranoid about microbenchmarks and tuning efficiency, but I would be reluctant to add an extra function call for users that don't need that. |
I don't think there is a way of implementing this without adding a function call, because replacing |
I thought that patching would work on the instance level, maybe with some MethodType magic. But I don't like that solution anyways, so moving on. I do think adding a single method call won't do any harm, even for microbenchmarks. Falcon is pretty usable as is, but IMO worrying about that level of optimization while using Python is pointless. I think this is valuable beyond instrumentation. There's plenty of useful ASGI and WSGI middlewares out there that this would enable, while having basically no performance impact on other users. |
Well, the canonical way of applying WSGI middleware is by having it to recursively wrap the provided callable, see also: PEP 3333 Middleware: Components that Play Both Sides; I'm still not sure why indirection is needed for that. |
If you do this (which I think is the "cannonical way"): app = App()
app = Middleware(app) Now you lost the refence to your Falcon App since Of course you could just keep 2 references, but that's confusing and completely unergonomic for a library ( |
FWIW, Starlette doesn't seem to do this either, although it does add itself to the ASGI |
Yes, but they do provide a public API for adding generic ASGI middleware, so it's largely unecessary |
I see. I'd still like to discuss various ways of implementing this, including having support for an external middleware stack like Starlette does it, also, by providing WSGI/ASGI utility functions to compose applications, as well as having an optional subclass like This is also related (but not limited to) to the following issues:
So when considering alternative patterns to improve the instrumentation case, we should also have the above interactions in mind. |
I do think it makes sense to think big picture. But this can serve as the basis for an external middleware stack: you can (later on) add a method that does the iterative wrapping for users similar to how Starlette does things. I'm not sure I understand how it relates to submounts, that seems like an orthogonal feature to me, but I may be missing something |
I was just stating a consideration.
I'm unfamiliar with starlette, you mean that you could just call Maybe we could have something similar also for falcon, but I guess the biggest issue would be how to differentiate if from falcon middleware, aka naming things is hard |
Understood, sorry if I was a bit harsh. It's a valid consideration.
Yes. Their middleware is generic ASGI middleware. They provide a thin wrapper to allow middleware writers (users or other libs) to write against a request response API: https://github.com/encode/starlette/blob/master/starlette/middleware/base.py So |
(Semi off-topic)
I meant that "mounting" can also be seen as composition of two or more WSGI (or ASGI) apps based on the URI, path, etc, not dissimilar to the WSGI recipe here: How do I split requests between my original app and the part I migrated to Falcon? |
Maybe something like this, similar to starlette but requires replacing the app instance: diff --git a/falcon/app.py b/falcon/app.py
index 193eed63..ae9aefaf 100644
--- a/falcon/app.py
+++ b/falcon/app.py
@@ -870,6 +870,11 @@ class App:
self._serialize_error = serializer
+ def wrap_middleware(self, middleware_class, *arg, **kw):
+ middleware = middleware_class(self, *arg, **kw)
+ assert callable(middleware)
+ return ProxyApp(self, middleware)
+
# ------------------------------------------------------------------------
# Helpers that require self
# ------------------------------------------------------------------------
@@ -1116,3 +1121,35 @@ class API(App):
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+
+class ProxyApp:
+ __slots__ = ('_original_app', '_middleware_stack', '_call')
+
+ def __init__(self, original_app, initial_middleware):
+ self._original_app = original_app
+ self._middleware_stack = [initial_middleware]
+ self._call = initial_middleware
+
+ def __call__(self, env, start_response):
+ return self._call(env, start_response)
+
+ def wrap_middleware(self, middleware_class, *arg, **kw):
+ middleware = middleware_class(self._call, *arg, **kw)
+ assert callable(middleware)
+ self._middleware_stack.insert(9, middleware)
+ self._call = middleware
+ return self
+
+ def __getattr__(self, key):
+ if key in self.__slots__:
+ this = self
+ else:
+ this = self._original_app
+ return getattr(this, key)
+
+ def __setattr__(self, key, value):
+ if key in self.__slots__:
+ object.__setattr__(self, key, value)
+ else:
+ setattr(self._original_app, key, value)
example: from dataclasses import dataclass
from falcon import App
class Res:
def on_get(self, req, res):
res.media = {'ok': True}
app = App()
app.add_route('/foo', Res())
@dataclass
class M:
app: App
name: str
def __call__(self, env, start_response):
print(f'{self.name} start')
v = self.app(env, start_response)
print(f'{self.name} done')
return v
app = app.wrap_middleware(M, 'm1').wrap_middleware(M, 'm2')
app.add_route('/bar', Res()) the main issue is that it fails the |
Yeah something like that. I may be missing something, but can't it be as simple as: class App:
def __init__(self) -> None:
self.app = self.handle
def handle(*args) -> None:
# current __call__
def __call__(*args) -> None
self.app(*args)
def add_middleware(self, middleware) -> None:
self.app = middleware(self.app) |
I was trying to provide a zero cost support when it's not used |
Ah I see, what you have is pretty slick then. I guess performance (theoretically) would then be worse if the feature is used, but maybe that doesn't matter. I still do feel that 1 extra method call is not going to move the needle on performance and I think it would be less surprising for users, type checkers & devs. |
The main thinking was to avoid changing the current version that we know is working, more than performance.
on this I agree, the proxing as proposed above is sub-optimal, it also requires replacing the app, but I guess that's not the works thing. |
Note that the one above is just a 5 minutes test, it may well be that the use of a subcall is the best solution in the end |
Other frameworks do this.
Basically you add a level of indirection to
__call__
:This provides support for generic WSGI/ASGI middleware since users can just
app.app = Middlware(app.app)
as many times as they want, but still keep the rest of thefalcon.App
API.The text was updated successfully, but these errors were encountered: