diff --git a/src/flask/app.py b/src/flask/app.py index c17da4b49d..12ed1fb99b 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -5,7 +5,6 @@ import os import sys import typing as t -import weakref from datetime import timedelta from functools import update_wrapper from inspect import iscoroutinefunction @@ -351,16 +350,15 @@ def __init__( assert bool(static_host) == host_matching, ( "Invalid static_host/host_matching combination" ) - # Use a weakref to avoid creating a reference cycle between the app - # and the view function (see #3761). - self_ref = weakref.ref(self) self.add_url_rule( f"{self.static_url_path}/", endpoint="static", host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore + view_func=_static_view, ) + self.view_functions["_automatic_options"] = _options_view + def get_send_file_max_age(self, filename: str | None) -> int | None: """Used by :func:`send_file` to determine the ``max_age`` cache value for a given file path if it wasn't passed. @@ -977,14 +975,6 @@ def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue: if req.routing_exception is not None: self.raise_routing_exception(req) rule: Rule = req.url_rule # type: ignore[assignment] - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if ( - getattr(rule, "provide_automatic_options", False) - and req.method == "OPTIONS" - ): - return self.make_default_options_response(ctx) - # otherwise dispatch to the handler for that endpoint view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] @@ -1604,3 +1594,12 @@ def __call__( wrapped to apply middleware. """ return self.wsgi_app(environ, start_response) + + +def _static_view(filename: str) -> Response: + return app_ctx.app.send_static_file(filename) + + +def _options_view() -> Response: + ctx = app_ctx._get_current_object() + return ctx.app.make_default_options_response(ctx) diff --git a/src/flask/cli.py b/src/flask/cli.py index 3fa65cfd01..1a410887e8 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1072,10 +1072,15 @@ def routes_command(sort: str, all_methods: bool) -> None: rows = [] for rule in rules: - row = [ - rule.endpoint, - ", ".join(sorted((rule.methods or set()) - ignored_methods)), - ] + if rule.endpoint == "_automatic_options": + continue + + methods = rule.methods or set() + + if getattr(rule, "provide_automatic_options", False): + methods.add("OPTIONS") + + row = [rule.endpoint, ", ".join(sorted(methods - ignored_methods))] if has_domain: row.append((rule.host if host_matching else rule.subdomain) or "") diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py index a734793e40..ed942d7c88 100644 --- a/src/flask/sansio/app.py +++ b/src/flask/sansio/app.py @@ -609,7 +609,7 @@ def add_url_rule( ) -> None: if endpoint is None: endpoint = _endpoint_from_view_func(view_func) # type: ignore - options["endpoint"] = endpoint + methods = options.pop("methods", None) # if the methods are not given and the view_func object knows its @@ -617,15 +617,16 @@ def add_url_rule( # a tuple of only ``GET`` as default. if methods is None: methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): raise TypeError( "Allowed methods must be a list of strings, for" ' example: @app.route(..., methods=["POST"])' ) - methods = {item.upper() for item in methods} + methods = {item.upper() for item in methods} # Methods that should always be added - required_methods: set[str] = set(getattr(view_func, "required_methods", ())) + methods |= set(getattr(view_func, "required_methods", ())) if provide_automatic_options is None: provide_automatic_options = getattr( @@ -638,16 +639,12 @@ def add_url_rule( and self.config["PROVIDE_AUTOMATIC_OPTIONS"] ) - if provide_automatic_options: - required_methods.add("OPTIONS") - - # Add the required methods now. - methods |= required_methods - - rule_obj = self.url_rule_class(rule, methods=methods, **options) + rule_obj = self.url_rule_class( + rule, methods=methods, endpoint=endpoint, **options + ) rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] - self.url_map.add(rule_obj) + if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: @@ -657,6 +654,19 @@ def add_url_rule( ) self.view_functions[endpoint] = view_func + if provide_automatic_options: + try: + self.url_map.add( + self.url_rule_class( + rule, + methods={"OPTIONS"}, + endpoint="_automatic_options", + **options, + ) + ) + except Exception: + pass + @t.overload def template_filter(self, name: T_template_filter) -> T_template_filter: ... @t.overload diff --git a/tests/test_cli.py b/tests/test_cli.py index 2a34088bd5..8f8f25cf01 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -483,12 +483,17 @@ def test_sort(self, app, invoke): ["yyy_get_post", "static", "aaa_post"], invoke(["routes", "-s", "rule"]).output, ) - match_order = [r.endpoint for r in app.url_map.iter_rules()] + match_order = [ + r.endpoint + for r in app.url_map.iter_rules() + if r.endpoint != "_automatic_options" + ] self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) def test_all_methods(self, invoke): output = invoke(["routes"]).output - assert "GET, HEAD, OPTIONS, POST" not in output + assert "HEAD" not in output + assert "OPTIONS" not in output output = invoke(["routes", "--all-methods"]).output assert "GET, HEAD, OPTIONS, POST" in output