sanic-org / sanic

Accelerate your web app development | Build fast. Run fast.

Home Page:https://sanic.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unexpected behavior with bp.middleware() using classes

nicolaipre opened this issue · comments

commented

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I was playing around with middleware registration directly on a blueprint, since I want to register it directly without decorators. I therefore used bp.middleware(middleware) and noticed something quite odd in the process where middleware does not seem to get added if you are using a callable class with __call__()

Code snippet

#!/usr/bin/env python3

#sanic==23.12.1
#sanic-ext==23.12.0
#sanic-routing==23.12.0

from sanic import Sanic
from sanic.response import text
from sanic.request import Request
from sanic.response import json
from sanic import Blueprint
from inspect import isfunction

bp = Blueprint("my_blueprint")

@bp.route("/")
async def bp_root(request: Request):
    print(request.method)
    print(request.body)
    print(request.head)
    return json({"my": "blueprint"})


# Middlewares
def middleware_as_def(request: Request):
    print("middleware_as_def")


class MiddlewareAsClass:
    something = "lol"
    def __call__(self, request: Request):
        print("MiddlewareAsClass called")
        return text("failed")


# Check if callable
print(f"middleware_as_def: IsCallable: { callable(middleware_as_def) }")
print(f"MiddlewareAsClass: IsCallable: { callable(MiddlewareAsClass) }")

# middleware function
bp.middleware(middleware_as_def)

# middleware class (callable __call__() )
mw = MiddlewareAsClass()
bp.middleware(mw) # works, but only if another middleware function is registered first
print("Is function:", isfunction(mw))

# in blueprints.py (line 62 & 63), as_decorator never turns False
# in blueprints.py (line 47): set as_decorator=False as default, it will then work with classes.

# Main
app = Sanic("MiddlewareTestApp")
app.blueprint(bp)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000, dev=True)

Expected Behavior

I noticed that middleware did not work if I used a callable class, unless I registered a middleware that was a function first, i.e:

bp.middleware(middleware_as_def)    # must be added here for the one below to work
bp.middleware(MiddlewareAsClass())  # if this is the only Middleware, nothing is registered

My friends and I did some debugging on this as we couldn't figure out why, and it turns out that it is related to the lazy() function from this commit. The interesting code region is between lines 47 and 63 in sanic/blueprints.py, specifically the call to isfunction(args[0]) which will return False if you use a callable class such as MiddlewareAsClass in the code example above (unless you already have registered a middleware function before that). If you call MiddlewareAsClass.__call__ directly it works works because isfunction(args[0]) then returns True.

Attempted fixes:

  1. By defaulting as_decorator=False the class middleware gets registered and seems to work fine (from some manual testing) for both decorator and non-decorator registration. After running the test-suite with this change the following test to fail: tests/test_blueprints.py:1084: AssertionError.

  2. Replacing isfunction(args[0]) with callable(args[0]). Using this "fix" the test test_blueprints.py passes fine, but causes a few other tests to fail. Alternatively this:

if args and (isfunction(args[0]) or (inspect.isclass(type(args[0]))) and callable(args[0])):
    as_decorator = False

I am not sure what the best possible solution would be here, so I created the issue as requested in Discord #support.

How do you run Sanic?

As a script (app.run or Sanic.serve)

Operating System

Linux

Sanic Version

23.12.1

Additional context

No response

commented

FWIW; I just ran a new test after rebooting and all the initial tests seems to pass now with the following change:

$ git diff
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index 6617d09c..5b9f0f2e 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -59,7 +59,7 @@ def lazy(func, as_decorator=True):
         kwargs["apply"] = False
         pass_handler = None
 
-        if args and isfunction(args[0]):
+        if args and callable(args[0]):
             as_decorator = False
 
         def wrapper(handler):

Test output:

$ tox
py38: skipped because could not find python interpreter with spec(s): py38
py38: SKIP ⚠ in 0 seconds
py39: skipped because could not find python interpreter with spec(s): py39
py39: SKIP ⚠ in 0 seconds
.pkg: _optional_hooks> python /home/user/test/sanic/venv/lib/python3.10/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: get_requires_for_build_editable> python /home/user/test/sanic/venv/lib/python3.10/site-packages/pyproject_api/_backend.py True setuptools.build_meta
.pkg: build_editable> python /home/user/test/sanic/venv/lib/python3.10/site-packages/pyproject_api/_backend.py True setuptools.build_meta
py310: install_package> python -I -m pip install --force-reinstall --no-deps /home/user/test/sanic/.tox/.tmp/package/5/sanic-23.12.0-0.editable-py3-none-any.whl
py310: commands[0]> coverage run --source ./sanic -m pytest tests
========================================================================================================== test session starts ==========================================================================================================
platform linux -- Python 3.10.12, pytest-7.1.3, pluggy-1.4.0
cachedir: .tox/py310/.pytest_cache
benchmark: 4.0.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/user/test/sanic, configfile: tox.ini
plugins: sanic-1.9.1, anyio-4.2.0, benchmark-4.0.0
collected 1685 items                                                                                                                                                                                                                    

tests/test_app.py ........................................................                                                                                                                                                        [  3%]
tests/test_asgi.py ........................                                                                                                                                                                                       [  4%]
tests/test_bad_request.py .                                                                                                                                                                                                       [  4%]
tests/test_base.py .....................                                                                                                                                                                                          [  6%]
tests/test_blueprint_copy.py ...                                                                                                                                                                                                  [  6%]
tests/test_blueprint_group.py ...........                                                                                                                                                                                         [  6%]
tests/test_blueprints.py ..............................................                                                                                                                                                           [  9%]
tests/test_cancellederror.py .                                                                                                                                                                                                    [  9%]
tests/test_cli.py ......................................................                                                                                                                                                          [ 12%]
tests/test_coffee.py ....                                                                                                                                                                                                         [ 13%]
tests/test_config.py .........................................                                                                                                                                                                    [ 15%]
tests/test_constants.py ......                                                                                                                                                                                                    [ 15%]
tests/test_cookies.py ...................................                                                                                                                                                                         [ 17%]
tests/test_create_task.py .....                                                                                                                                                                                                   [ 18%]
tests/test_custom_request.py .                                                                                                                                                                                                    [ 18%]
tests/test_deprecation.py ....                                                                                                                                                                                                    [ 18%]
tests/test_dynamic_routes.py ....                                                                                                                                                                                                 [ 18%]
tests/test_errorpages.py ......................................................                                                                                                                                                   [ 22%]
tests/test_exceptions.py .....................                                                                                                                                                                                    [ 23%]
tests/test_exceptions_handler.py .............                                                                                                                                                                                    [ 24%]
tests/test_ext_integration.py .......                                                                                                                                                                                             [ 24%]
tests/test_graceful_shutdown.py X.                                                                                                                                                                                                [ 24%]
tests/test_handler.py .                                                                                                                                                                                                           [ 24%]
tests/test_handler_annotations.py ....                                                                                                                                                                                            [ 24%]
tests/test_headers.py ........................................................................                                                                                                                                    [ 29%]
tests/test_helpers.py .......                                                                                                                                                                                                     [ 29%]
tests/test_http.py ...                                                                                                                                                                                                            [ 29%]
tests/test_http_alt_svc.py .                                                                                                                                                                                                      [ 29%]
tests/test_init.py .............                                                                                                                                                                                                  [ 30%]
tests/test_json_decoding.py ...                                                                                                                                                                                                   [ 30%]
tests/test_json_encoding.py ...s                                                                                                                                                                                                  [ 30%]
tests/test_keep_alive_timeout.py ....                                                                                                                                                                                             [ 31%]
tests/test_late_adds.py ...                                                                                                                                                                                                       [ 31%]
tests/test_logging.py ...................                                                                                                                                                                                         [ 32%]
tests/test_logo.py .....                                                                                                                                                                                                          [ 32%]
tests/test_middleware.py ...............                                                                                                                                                                                          [ 33%]
tests/test_middleware_priority.py ....................................................                                                                                                                                            [ 36%]
tests/test_motd.py .....                                                                                                                                                                                                          [ 37%]
tests/test_multiprocessing.py .........                                                                                                                                                                                           [ 37%]
tests/test_named_routes.py ..........................                                                                                                                                                                             [ 39%]
tests/test_payload_too_large.py ...                                                                                                                                                                                               [ 39%]
tests/test_pipelining.py ....                                                                                                                                                                                                     [ 39%]
tests/test_prepare.py .....                                                                                                                                                                                                       [ 39%]
tests/test_redirect.py .........                                                                                                                                                                                                  [ 40%]
tests/test_reloader.py XxXxSXX.                                                                                                                                                                                                    [ 40%]
tests/test_request.py ..........................................                                                                                                                                                                  [ 43%]
tests/test_request_cancel.py ..                                                                                                                                                                                                   [ 43%]
tests/test_request_data.py ..                                                                                                                                                                                                     [ 43%]
tests/test_request_stream.py .........                                                                                                                                                                                            [ 44%]
tests/test_requests.py ....................................................................................................................................                                                                       [ 51%]
tests/test_response.py ...........................................................                                                                                                                                                [ 55%]
tests/test_response_file.py ......                                                                                                                                                                                                [ 55%]
tests/test_response_json.py ................                                                                                                                                                                                      [ 56%]
tests/test_response_timeout.py ....                                                                                                                                                                                               [ 56%]
tests/test_routes.py .................................................................................................................................................................                                            [ 66%]
tests/test_server_events.py ....................                                                                                                                                                                                  [ 67%]
tests/test_server_loop.py ss..                                                                                                                                                                                                    [ 67%]
tests/test_signal_handlers.py ......                                                                                                                                                                                              [ 68%]
tests/test_signals.py .................................................                                                                                                                                                           [ 71%]
tests/test_static.py ..........sssss...........................................................................s                                                                                                                  [ 76%]
tests/test_static_directory.py .......                                                                                                                                                                                            [ 77%]
tests/test_tasks.py ......                                                                                                                                                                                                        [ 77%]
tests/test_test_client_port.py ..                                                                                                                                                                                                 [ 77%]
tests/test_timeout_logic.py .....                                                                                                                                                                                                 [ 77%]
tests/test_tls.py ........xxx...................................                                                                                                                                                                  [ 80%]
tests/test_touchup.py .........                                                                                                                                                                                                   [ 81%]
tests/test_unix_socket.py X........                                                                                                                                                                                               [ 81%]
tests/test_url_building.py ...........................                                                                                                                                                                            [ 83%]
tests/test_url_for.py .........                                                                                                                                                                                                   [ 83%]
tests/test_url_for_static.py .....................                                                                                                                                                                                [ 84%]
tests/test_utf8.py ...                                                                                                                                                                                                            [ 85%]
tests/test_utils.py ......                                                                                                                                                                                                        [ 85%]
tests/test_versioning.py ..............                                                                                                                                                                                           [ 86%]
tests/test_vhosts.py ...                                                                                                                                                                                                          [ 86%]
tests/test_views.py ...................                                                                                                                                                                                           [ 87%]
tests/test_websockets.py ...............................                                                                                                                                                                          [ 89%]
tests/test_ws_handlers.py ........                                                                                                                                                                                                [ 89%]
tests/benchmark/test_route_resolution_benchmark.py ..                                                                                                                                                                             [ 90%]
tests/http3/test_http_receiver.py ............                                                                                                                                                                                    [ 90%]
tests/http3/test_server.py ....                                                                                                                                                                                                   [ 91%]
tests/http3/test_session_ticket_store.py .                                                                                                                                                                                        [ 91%]
tests/typing/test_typing.py .......                                                                                                                                                                                               [ 91%]
tests/worker/test_inspector.py .........                                                                                                                                                                                          [ 92%]
tests/worker/test_loader.py ............                                                                                                                                                                                          [ 92%]
tests/worker/test_manager.py ........................                                                                                                                                                                             [ 94%]
tests/worker/test_multiplexer.py ..................                                                                                                                                                                               [ 95%]
tests/worker/test_reloader.py ..........                                                                                                                                                                                          [ 95%]
tests/worker/test_restarter.py ...........                                                                                                                                                                                        [ 96%]
tests/worker/test_runner.py ....                                                                                                                                                                                                  [ 96%]
tests/worker/test_shared_ctx.py ...............                                                                                                                                                                                   [ 97%]
tests/worker/test_socket.py ..                                                                                                                                                                                                    [ 97%]
tests/worker/test_startup.py .........                                                                                                                                                                                            [ 98%]
tests/worker/test_state.py ....................                                                                                                                                                                                   [ 99%]
tests/worker/test_worker_serve.py .........                                                                                                                                                                                       [100%]

=========================================================================================================== warnings summary ============================================================================================================
tests/test_asgi.py: 1 warning
tests/test_cookies.py: 18 warnings
tests/test_response.py: 1 warning
...

Cleaned-up code example to test with - middleware registration now works as expected:

#!/usr/bin/env python3
from sanic import Sanic, Blueprint
from sanic.request import Request
from sanic.response import text, json

class MiddlewareAsClass:
    name = "test"
    attach_to = "request"
    apply = True  # does this even work? Doesnt seem to reflect whether True/False
    priority = 0    
    def __call__(self, request: Request):
        print("MiddlewareAsClass called")
        return text("Middleware check failed!\n")
    

bp = Blueprint("my_blueprint")

@bp.route("/")
async def bp_root(request: Request):
    return text("Home\n")


mw = MiddlewareAsClass()

# works fine
bp.middleware(mw, attach_to=mw.attach_to, apply=mw.apply, priority=mw.priority)

# doesnt work if you set "middleware_or_request" ?????
#bp.middleware(middleware_or_request=mw, attach_to=mw.attach_to, apply=mw.apply, priority=mw.priority)

app = Sanic("MiddlewareTestApp")
app.blueprint(bp)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000, dev=True)
$ curl 127.0.0.1:8000
Middleware check failed!
commented

Closed #2923 as it was solved with a wrapper function instead.