Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}/<path:filename>",
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.
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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)
13 changes: 9 additions & 4 deletions src/flask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "")
Expand Down
32 changes: 21 additions & 11 deletions src/flask/sansio/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,23 +609,24 @@ 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
# methods we can use that instead. If neither exists, we go with
# 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(
Expand All @@ -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:
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading