fix provide_automatic_options override (#5917)

This commit is contained in:
David Lord 2026-02-12 13:07:50 -08:00 committed by GitHub
commit 12e95c93b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 97 additions and 87 deletions

View file

@ -23,6 +23,8 @@ Unreleased
switching ``POST`` to ``GET``. This preserves the current behavior of
``GET`` and ``POST`` redirects, and is also correct for frontend libraries
such as HTMX. :issue:`5895`
- ``provide_automatic_options=True`` can be used to enable it for a view when
it's disabled in config. Previously, only disabling worked. :issue:`5916`
Version 3.1.2

View file

@ -445,7 +445,7 @@ The following configuration values are used internally by Flask:
.. versionchanged:: 2.3
``ENV`` was removed.
.. versionadded:: 3.10
.. versionadded:: 3.1
Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default
addition of autogenerated OPTIONS responses.

View file

@ -627,19 +627,19 @@ class App(Scaffold):
# Methods that should always be added
required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
# starting with Flask 0.8 the view_func object can disable and
# force-enable the automatic options handling.
if provide_automatic_options is None:
provide_automatic_options = getattr(
view_func, "provide_automatic_options", None
)
if provide_automatic_options is None:
if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
provide_automatic_options = True
required_methods.add("OPTIONS")
else:
provide_automatic_options = False
if provide_automatic_options is None:
provide_automatic_options = (
"OPTIONS" not in methods
and self.config["PROVIDE_AUTOMATIC_OPTIONS"]
)
if provide_automatic_options:
required_methods.add("OPTIONS")
# Add the required methods now.
methods |= required_methods

View file

@ -20,6 +20,7 @@ from werkzeug.routing import BuildError
from werkzeug.routing import RequestRedirect
import flask
from flask.testing import FlaskClient
require_cpython_gc = pytest.mark.skipif(
python_implementation() != "CPython",
@ -67,63 +68,61 @@ def test_method_route_no_methods(app):
app.get("/", methods=["GET", "POST"])
def test_provide_automatic_options_attr():
app = flask.Flask(__name__)
def test_provide_automatic_options_attr_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the view func attribute."""
def index():
return "Hello World!"
index.provide_automatic_options = False
app.route("/")(index)
rv = app.test_client().open("/", method="OPTIONS")
app.add_url_rule("/", view_func=index)
rv = client.options()
assert rv.status_code == 405
app = flask.Flask(__name__)
def index2():
def test_provide_automatic_options_attr_enable(
app: flask.Flask, client: FlaskClient
) -> None:
"""When default automatic options is disabled in config, it can still be
enabled by the view function attribute.
"""
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
def index():
return "Hello World!"
index2.provide_automatic_options = True
app.route("/", methods=["OPTIONS"])(index2)
rv = app.test_client().open("/", method="OPTIONS")
assert sorted(rv.allow) == ["OPTIONS"]
index.provide_automatic_options = True
app.add_url_rule("/", view_func=index)
rv = client.options()
assert rv.allow == {"GET", "HEAD", "OPTIONS"}
def test_provide_automatic_options_kwarg(app, client):
def test_provide_automatic_options_arg_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the route argument."""
@app.get("/", provide_automatic_options=False)
def index():
return flask.request.method
return "Hello World!"
def more():
return flask.request.method
app.add_url_rule("/", view_func=index, provide_automatic_options=False)
app.add_url_rule(
"/more",
view_func=more,
methods=["GET", "POST"],
provide_automatic_options=False,
)
assert client.get("/").data == b"GET"
rv = client.post("/")
assert rv.status_code == 405
assert sorted(rv.allow) == ["GET", "HEAD"]
rv = client.open("/", method="OPTIONS")
rv = client.options()
assert rv.status_code == 405
rv = client.head("/")
assert rv.status_code == 200
assert not rv.data # head truncates
assert client.post("/more").data == b"POST"
assert client.get("/more").data == b"GET"
rv = client.delete("/more")
assert rv.status_code == 405
assert sorted(rv.allow) == ["GET", "HEAD", "POST"]
def test_provide_automatic_options_method_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options is ignored if the route handles options."""
rv = client.open("/more", method="OPTIONS")
assert rv.status_code == 405
@app.route("/", methods=["OPTIONS"])
def index():
return "", {"X-Test": "test"}
rv = client.options()
assert rv.headers["X-Test"] == "test"
def test_request_dispatching(app, client):

View file

@ -220,28 +220,19 @@ def test_templates_and_static(test_apps):
assert flask.render_template("nested/nested.txt") == "I'm nested"
def test_default_static_max_age(app):
def test_default_static_max_age(app: flask.Flask) -> None:
class MyBlueprint(flask.Blueprint):
def get_send_file_max_age(self, filename):
return 100
blueprint = MyBlueprint("blueprint", __name__, static_folder="static")
blueprint = MyBlueprint(
"blueprint", __name__, url_prefix="/bp", static_folder="static"
)
app.register_blueprint(blueprint)
# try/finally, in case other tests use this app for Blueprint tests.
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
try:
with app.test_request_context():
unexpected_max_age = 3600
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
unexpected_max_age = 7200
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
rv = blueprint.send_static_file("index.html")
cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == 100
rv.close()
finally:
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
with app.test_request_context(), blueprint.send_static_file("index.html") as rv:
cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == 100
def test_templates_list(test_apps):

View file

@ -483,12 +483,18 @@ class TestRoutes:
["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

View file

@ -2,6 +2,7 @@ import pytest
from werkzeug.http import parse_set_header
import flask.views
from flask.testing import FlaskClient
def common_test(app):
@ -98,44 +99,55 @@ def test_view_decorators(app, client):
assert rv.data == b"Awesome"
def test_view_provide_automatic_options_attr():
app = flask.Flask(__name__)
def test_view_provide_automatic_options_attr_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the view class attribute."""
class Index1(flask.views.View):
class Index(flask.views.View):
provide_automatic_options = False
def dispatch_request(self):
return "Hello World!"
app.add_url_rule("/", view_func=Index1.as_view("index"))
c = app.test_client()
rv = c.open("/", method="OPTIONS")
app.add_url_rule("/", view_func=Index.as_view("index"))
rv = client.options()
assert rv.status_code == 405
app = flask.Flask(__name__)
class Index2(flask.views.View):
methods = ["OPTIONS"]
def test_view_provide_automatic_options_attr_enable(
app: flask.Flask, client: FlaskClient
) -> None:
"""When default automatic options is disabled in config, it can still be
enabled by the view class attribute.
"""
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
class Index(flask.views.View):
provide_automatic_options = True
def dispatch_request(self):
return "Hello World!"
app.add_url_rule("/", view_func=Index2.as_view("index"))
c = app.test_client()
rv = c.open("/", method="OPTIONS")
assert sorted(rv.allow) == ["OPTIONS"]
app.add_url_rule("/", view_func=Index.as_view("index"))
rv = client.options("/")
assert rv.allow == {"GET", "HEAD", "OPTIONS"}
app = flask.Flask(__name__)
class Index3(flask.views.View):
def test_provide_automatic_options_method_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options is ignored if the route handles options."""
class Index(flask.views.View):
methods = ["OPTIONS"]
def dispatch_request(self):
return "Hello World!"
return "", {"X-Test": "test"}
app.add_url_rule("/", view_func=Index3.as_view("index"))
c = app.test_client()
rv = c.open("/", method="OPTIONS")
assert "OPTIONS" in rv.allow
app.add_url_rule("/", view_func=Index.as_view("index"))
rv = client.options()
assert rv.headers["X-Test"] == "test"
def test_implicit_head(app, client):
@ -180,7 +192,7 @@ def test_endpoint_override(app):
app.add_url_rule("/", view_func=Index.as_view("index"))
with pytest.raises(AssertionError):
app.add_url_rule("/", view_func=Index.as_view("index"))
app.add_url_rule("/other", view_func=Index.as_view("index"))
# But these tests should still pass. We just log a warning.
common_test(app)