Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 8 additions & 8 deletions src/flask/sansio/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,19 +627,19 @@ def add_url_rule(
# 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
Expand Down
75 changes: 37 additions & 38 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from werkzeug.routing import RequestRedirect

import flask
from flask.testing import FlaskClient

require_cpython_gc = pytest.mark.skipif(
python_implementation() != "CPython",
Expand Down Expand Up @@ -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 index():
return flask.request.method

def more():
return flask.request.method
def test_provide_automatic_options_arg_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the route argument."""

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"
@app.get("/", provide_automatic_options=False)
def index():
return "Hello World!"

rv = client.post("/")
rv = client.options()
assert rv.status_code == 405
assert sorted(rv.allow) == ["GET", "HEAD"]

rv = client.open("/", method="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"
def test_provide_automatic_options_method_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options is ignored if the route handles options."""

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

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


def test_request_dispatching(app, client):
Expand Down
23 changes: 7 additions & 16 deletions tests/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 8 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,12 +483,18 @@ 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
54 changes: 33 additions & 21 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from werkzeug.http import parse_set_header

import flask.views
from flask.testing import FlaskClient


def common_test(app):
Expand Down Expand Up @@ -98,44 +99,55 @@ def dispatch_request(self):
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):
Expand Down Expand Up @@ -180,7 +192,7 @@ def dispatch_request(self):
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)
Expand Down
Loading