forked from orbit-oss/flask
fix provide_automatic_options override
This commit is contained in:
parent
d3b78fd18a
commit
e82db2ca3a
7 changed files with 97 additions and 87 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue