Skip to content

Commit 12e95c9

Browse files
authored
fix provide_automatic_options override (#5917)
2 parents d3b78fd + e82db2c commit 12e95c9

7 files changed

Lines changed: 96 additions & 86 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Unreleased
2323
switching ``POST`` to ``GET``. This preserves the current behavior of
2424
``GET`` and ``POST`` redirects, and is also correct for frontend libraries
2525
such as HTMX. :issue:`5895`
26+
- ``provide_automatic_options=True`` can be used to enable it for a view when
27+
it's disabled in config. Previously, only disabling worked. :issue:`5916`
2628

2729

2830
Version 3.1.2

docs/config.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ The following configuration values are used internally by Flask:
445445
.. versionchanged:: 2.3
446446
``ENV`` was removed.
447447

448-
.. versionadded:: 3.10
448+
.. versionadded:: 3.1
449449
Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default
450450
addition of autogenerated OPTIONS responses.
451451

src/flask/sansio/app.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -627,19 +627,19 @@ def add_url_rule(
627627
# Methods that should always be added
628628
required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
629629

630-
# starting with Flask 0.8 the view_func object can disable and
631-
# force-enable the automatic options handling.
632630
if provide_automatic_options is None:
633631
provide_automatic_options = getattr(
634632
view_func, "provide_automatic_options", None
635633
)
636634

637-
if provide_automatic_options is None:
638-
if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
639-
provide_automatic_options = True
640-
required_methods.add("OPTIONS")
641-
else:
642-
provide_automatic_options = False
635+
if provide_automatic_options is None:
636+
provide_automatic_options = (
637+
"OPTIONS" not in methods
638+
and self.config["PROVIDE_AUTOMATIC_OPTIONS"]
639+
)
640+
641+
if provide_automatic_options:
642+
required_methods.add("OPTIONS")
643643

644644
# Add the required methods now.
645645
methods |= required_methods

tests/test_basic.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from werkzeug.routing import RequestRedirect
2121

2222
import flask
23+
from flask.testing import FlaskClient
2324

2425
require_cpython_gc = pytest.mark.skipif(
2526
python_implementation() != "CPython",
@@ -67,63 +68,61 @@ def test_method_route_no_methods(app):
6768
app.get("/", methods=["GET", "POST"])
6869

6970

70-
def test_provide_automatic_options_attr():
71-
app = flask.Flask(__name__)
71+
def test_provide_automatic_options_attr_disable(
72+
app: flask.Flask, client: FlaskClient
73+
) -> None:
74+
"""Automatic options can be disabled by the view func attribute."""
7275

7376
def index():
7477
return "Hello World!"
7578

7679
index.provide_automatic_options = False
77-
app.route("/")(index)
78-
rv = app.test_client().open("/", method="OPTIONS")
80+
app.add_url_rule("/", view_func=index)
81+
rv = client.options()
7982
assert rv.status_code == 405
8083

81-
app = flask.Flask(__name__)
8284

83-
def index2():
85+
def test_provide_automatic_options_attr_enable(
86+
app: flask.Flask, client: FlaskClient
87+
) -> None:
88+
"""When default automatic options is disabled in config, it can still be
89+
enabled by the view function attribute.
90+
"""
91+
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
92+
93+
def index():
8494
return "Hello World!"
8595

86-
index2.provide_automatic_options = True
87-
app.route("/", methods=["OPTIONS"])(index2)
88-
rv = app.test_client().open("/", method="OPTIONS")
89-
assert sorted(rv.allow) == ["OPTIONS"]
96+
index.provide_automatic_options = True
97+
app.add_url_rule("/", view_func=index)
98+
rv = client.options()
99+
assert rv.allow == {"GET", "HEAD", "OPTIONS"}
90100

91101

92-
def test_provide_automatic_options_kwarg(app, client):
93-
def index():
94-
return flask.request.method
95-
96-
def more():
97-
return flask.request.method
102+
def test_provide_automatic_options_arg_disable(
103+
app: flask.Flask, client: FlaskClient
104+
) -> None:
105+
"""Automatic options can be disabled by the route argument."""
98106

99-
app.add_url_rule("/", view_func=index, provide_automatic_options=False)
100-
app.add_url_rule(
101-
"/more",
102-
view_func=more,
103-
methods=["GET", "POST"],
104-
provide_automatic_options=False,
105-
)
106-
assert client.get("/").data == b"GET"
107+
@app.get("/", provide_automatic_options=False)
108+
def index():
109+
return "Hello World!"
107110

108-
rv = client.post("/")
111+
rv = client.options()
109112
assert rv.status_code == 405
110-
assert sorted(rv.allow) == ["GET", "HEAD"]
111113

112-
rv = client.open("/", method="OPTIONS")
113-
assert rv.status_code == 405
114114

115-
rv = client.head("/")
116-
assert rv.status_code == 200
117-
assert not rv.data # head truncates
118-
assert client.post("/more").data == b"POST"
119-
assert client.get("/more").data == b"GET"
115+
def test_provide_automatic_options_method_disable(
116+
app: flask.Flask, client: FlaskClient
117+
) -> None:
118+
"""Automatic options is ignored if the route handles options."""
120119

121-
rv = client.delete("/more")
122-
assert rv.status_code == 405
123-
assert sorted(rv.allow) == ["GET", "HEAD", "POST"]
120+
@app.route("/", methods=["OPTIONS"])
121+
def index():
122+
return "", {"X-Test": "test"}
124123

125-
rv = client.open("/more", method="OPTIONS")
126-
assert rv.status_code == 405
124+
rv = client.options()
125+
assert rv.headers["X-Test"] == "test"
127126

128127

129128
def test_request_dispatching(app, client):

tests/test_blueprints.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -220,28 +220,19 @@ def test_templates_and_static(test_apps):
220220
assert flask.render_template("nested/nested.txt") == "I'm nested"
221221

222222

223-
def test_default_static_max_age(app):
223+
def test_default_static_max_age(app: flask.Flask) -> None:
224224
class MyBlueprint(flask.Blueprint):
225225
def get_send_file_max_age(self, filename):
226226
return 100
227227

228-
blueprint = MyBlueprint("blueprint", __name__, static_folder="static")
228+
blueprint = MyBlueprint(
229+
"blueprint", __name__, url_prefix="/bp", static_folder="static"
230+
)
229231
app.register_blueprint(blueprint)
230232

231-
# try/finally, in case other tests use this app for Blueprint tests.
232-
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
233-
try:
234-
with app.test_request_context():
235-
unexpected_max_age = 3600
236-
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
237-
unexpected_max_age = 7200
238-
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
239-
rv = blueprint.send_static_file("index.html")
240-
cc = parse_cache_control_header(rv.headers["Cache-Control"])
241-
assert cc.max_age == 100
242-
rv.close()
243-
finally:
244-
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
233+
with app.test_request_context(), blueprint.send_static_file("index.html") as rv:
234+
cc = parse_cache_control_header(rv.headers["Cache-Control"])
235+
assert cc.max_age == 100
245236

246237

247238
def test_templates_list(test_apps):

tests/test_cli.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,12 +483,18 @@ def test_sort(self, app, invoke):
483483
["yyy_get_post", "static", "aaa_post"],
484484
invoke(["routes", "-s", "rule"]).output,
485485
)
486-
match_order = [r.endpoint for r in app.url_map.iter_rules()]
486+
match_order = [
487+
r.endpoint
488+
for r in app.url_map.iter_rules()
489+
if r.endpoint != "_automatic_options"
490+
]
487491
self.expect_order(match_order, invoke(["routes", "-s", "match"]).output)
488492

489493
def test_all_methods(self, invoke):
490494
output = invoke(["routes"]).output
491-
assert "GET, HEAD, OPTIONS, POST" not in output
495+
assert "HEAD" not in output
496+
assert "OPTIONS" not in output
497+
492498
output = invoke(["routes", "--all-methods"]).output
493499
assert "GET, HEAD, OPTIONS, POST" in output
494500

tests/test_views.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from werkzeug.http import parse_set_header
33

44
import flask.views
5+
from flask.testing import FlaskClient
56

67

78
def common_test(app):
@@ -98,44 +99,55 @@ def dispatch_request(self):
9899
assert rv.data == b"Awesome"
99100

100101

101-
def test_view_provide_automatic_options_attr():
102-
app = flask.Flask(__name__)
102+
def test_view_provide_automatic_options_attr_disable(
103+
app: flask.Flask, client: FlaskClient
104+
) -> None:
105+
"""Automatic options can be disabled by the view class attribute."""
103106

104-
class Index1(flask.views.View):
107+
class Index(flask.views.View):
105108
provide_automatic_options = False
106109

107110
def dispatch_request(self):
108111
return "Hello World!"
109112

110-
app.add_url_rule("/", view_func=Index1.as_view("index"))
111-
c = app.test_client()
112-
rv = c.open("/", method="OPTIONS")
113+
app.add_url_rule("/", view_func=Index.as_view("index"))
114+
rv = client.options()
113115
assert rv.status_code == 405
114116

115-
app = flask.Flask(__name__)
116117

117-
class Index2(flask.views.View):
118-
methods = ["OPTIONS"]
118+
def test_view_provide_automatic_options_attr_enable(
119+
app: flask.Flask, client: FlaskClient
120+
) -> None:
121+
"""When default automatic options is disabled in config, it can still be
122+
enabled by the view class attribute.
123+
"""
124+
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
125+
126+
class Index(flask.views.View):
119127
provide_automatic_options = True
120128

121129
def dispatch_request(self):
122130
return "Hello World!"
123131

124-
app.add_url_rule("/", view_func=Index2.as_view("index"))
125-
c = app.test_client()
126-
rv = c.open("/", method="OPTIONS")
127-
assert sorted(rv.allow) == ["OPTIONS"]
132+
app.add_url_rule("/", view_func=Index.as_view("index"))
133+
rv = client.options("/")
134+
assert rv.allow == {"GET", "HEAD", "OPTIONS"}
128135

129-
app = flask.Flask(__name__)
130136

131-
class Index3(flask.views.View):
137+
def test_provide_automatic_options_method_disable(
138+
app: flask.Flask, client: FlaskClient
139+
) -> None:
140+
"""Automatic options is ignored if the route handles options."""
141+
142+
class Index(flask.views.View):
143+
methods = ["OPTIONS"]
144+
132145
def dispatch_request(self):
133-
return "Hello World!"
146+
return "", {"X-Test": "test"}
134147

135-
app.add_url_rule("/", view_func=Index3.as_view("index"))
136-
c = app.test_client()
137-
rv = c.open("/", method="OPTIONS")
138-
assert "OPTIONS" in rv.allow
148+
app.add_url_rule("/", view_func=Index.as_view("index"))
149+
rv = client.options()
150+
assert rv.headers["X-Test"] == "test"
139151

140152

141153
def test_implicit_head(app, client):
@@ -180,7 +192,7 @@ def dispatch_request(self):
180192
app.add_url_rule("/", view_func=Index.as_view("index"))
181193

182194
with pytest.raises(AssertionError):
183-
app.add_url_rule("/", view_func=Index.as_view("index"))
195+
app.add_url_rule("/other", view_func=Index.as_view("index"))
184196

185197
# But these tests should still pass. We just log a warning.
186198
common_test(app)

0 commit comments

Comments
 (0)