Compare commits

..

5 commits

Author SHA1 Message Date
David Lord
36e4a824f3
Any for CertParamType type 2026-05-31 07:42:46 -07:00
David Lord
954f5684e4
update dev dependencies 2026-05-18 16:35:44 -07:00
David Lord
9fcd34c9f3
Merge branch 'stable' 2026-05-13 07:37:53 -07:00
David Lord
1d49747264
update flask-mongoengine link 2026-05-13 07:37:21 -07:00
David Lord
7374c85dde
remove leftover setuptools 2026-05-02 05:59:12 -07:00
9 changed files with 465 additions and 439 deletions

View file

@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: c60c980e561ed3e73101667fe8365c609d19a438 # frozen: v0.15.9 rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
rev: 0397b68f6f88c024f1d2b355a9818779f6336d16 # frozen: 0.11.3 rev: fa60a193803535a9e2accdb3ca4b1b584b1150cb # frozen: 0.11.15
hooks: hooks:
- id: uv-lock - id: uv-lock
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell

View file

@ -10,8 +10,7 @@ A running MongoDB server and `Flask-MongoEngine`_ are required. ::
pip install flask-mongoengine pip install flask-mongoengine
.. _MongoEngine: http://mongoengine.org .. _MongoEngine: http://mongoengine.org
.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io .. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/
Configuration Configuration
------------- -------------

View file

@ -81,8 +81,7 @@ By the end, your project layout will look like this:
│ ├── test_auth.py │ ├── test_auth.py
│ └── test_blog.py │ └── test_blog.py
├── .venv/ ├── .venv/
├── pyproject.toml └── pyproject.toml
└── MANIFEST.in
If you're using version control, the following files that are generated If you're using version control, the following files that are generated
while running your project should be ignored. There may be other files while running your project should be ignored. There may be other files
@ -103,8 +102,4 @@ write. For example, with git:
.coverage .coverage
htmlcov/ htmlcov/
dist/
build/
*.egg-info/
Continue to :doc:`factory`. Continue to :doc:`factory`.

View file

@ -5,6 +5,7 @@ import inspect
import os import os
import sys import sys
import typing as t import typing as t
import weakref
from datetime import timedelta from datetime import timedelta
from functools import update_wrapper from functools import update_wrapper
from inspect import iscoroutinefunction from inspect import iscoroutinefunction
@ -351,15 +352,16 @@ class Flask(App):
assert bool(static_host) == host_matching, ( assert bool(static_host) == host_matching, (
"Invalid static_host/host_matching combination" "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( self.add_url_rule(
f"{self.static_url_path}/<path:filename>", f"{self.static_url_path}/<path:filename>",
endpoint="static", endpoint="static",
host=static_host, host=static_host,
view_func=_static_view, view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore
) )
self.view_functions["_automatic_options"] = _options_view
def get_send_file_max_age(self, filename: str | None) -> int | None: def get_send_file_max_age(self, filename: str | None) -> int | None:
"""Used by :func:`send_file` to determine the ``max_age`` cache """Used by :func:`send_file` to determine the ``max_age`` cache
value for a given file path if it wasn't passed. value for a given file path if it wasn't passed.
@ -976,6 +978,14 @@ class Flask(App):
if req.routing_exception is not None: if req.routing_exception is not None:
self.raise_routing_exception(req) self.raise_routing_exception(req)
rule: Rule = req.url_rule # type: ignore[assignment] 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] 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] return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
@ -1613,12 +1623,3 @@ class Flask(App):
wrapped to apply middleware. wrapped to apply middleware.
""" """
return self.wsgi_app(environ, start_response) return self.wsgi_app(environ, start_response)
def _static_view(filename: str) -> Response:
return app_ctx.app.send_static_file(filename)
def _options_view(**_: t.Any) -> Response:
ctx = app_ctx._get_current_object()
return ctx.app.make_default_options_response(ctx)

View file

@ -468,7 +468,7 @@ _app_option = click.Option(
def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None:
# If the flag isn't provided, it will default to False. Don't use # If the flag isn't provided, it will default to False. Don't use
# that, let debug be set by env in that case. # that, let debug be set by env in that case.
source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] source = ctx.get_parameter_source(param.name)
if source is not None and source in ( if source is not None and source in (
ParameterSource.DEFAULT, ParameterSource.DEFAULT,
@ -777,7 +777,7 @@ def show_server_banner(debug: bool, app_import_path: str | None) -> None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}") click.echo(f" * Debug mode: {'on' if debug else 'off'}")
class CertParamType(click.ParamType): class CertParamType(click.ParamType[t.Any]):
"""Click option type for the ``--cert`` option. Allows either an """Click option type for the ``--cert`` option. Allows either an
existing file, the string ``'adhoc'``, or an import for a existing file, the string ``'adhoc'``, or an import for a
:class:`~ssl.SSLContext` object. :class:`~ssl.SSLContext` object.
@ -803,7 +803,7 @@ class CertParamType(click.ParamType):
try: try:
return self.path_type(value, param, ctx) return self.path_type(value, param, ctx)
except click.BadParameter: except click.BadParameter:
value = click.STRING(value, param, ctx).lower() value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr]
if value == "adhoc": if value == "adhoc":
try: try:
@ -1072,15 +1072,10 @@ def routes_command(sort: str, all_methods: bool) -> None:
rows = [] rows = []
for rule in rules: for rule in rules:
if rule.endpoint == "_automatic_options": row = [
continue rule.endpoint,
", ".join(sorted((rule.methods or set()) - ignored_methods)),
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: if has_domain:
row.append((rule.host if host_matching else rule.subdomain) or "") row.append((rule.host if host_matching else rule.subdomain) or "")

View file

@ -612,7 +612,7 @@ class App(Scaffold):
) -> None: ) -> None:
if endpoint is None: if endpoint is None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore endpoint = _endpoint_from_view_func(view_func) # type: ignore
options["endpoint"] = endpoint
methods = options.pop("methods", None) methods = options.pop("methods", None)
# if the methods are not given and the view_func object knows its # if the methods are not given and the view_func object knows its
@ -620,16 +620,15 @@ class App(Scaffold):
# a tuple of only ``GET`` as default. # a tuple of only ``GET`` as default.
if methods is None: if methods is None:
methods = getattr(view_func, "methods", None) or ("GET",) methods = getattr(view_func, "methods", None) or ("GET",)
if isinstance(methods, str): if isinstance(methods, str):
raise TypeError( raise TypeError(
"Allowed methods must be a list of strings, for" "Allowed methods must be a list of strings, for"
' example: @app.route(..., methods=["POST"])' ' 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 # Methods that should always be added
methods |= set(getattr(view_func, "required_methods", ())) required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
if provide_automatic_options is None: if provide_automatic_options is None:
provide_automatic_options = getattr( provide_automatic_options = getattr(
@ -642,12 +641,16 @@ class App(Scaffold):
and self.config["PROVIDE_AUTOMATIC_OPTIONS"] and self.config["PROVIDE_AUTOMATIC_OPTIONS"]
) )
rule_obj = self.url_rule_class( if provide_automatic_options:
rule, methods=methods, endpoint=endpoint, **options required_methods.add("OPTIONS")
)
rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined]
self.url_map.add(rule_obj)
# Add the required methods now.
methods |= required_methods
rule_obj = self.url_rule_class(rule, methods=methods, **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: if view_func is not None:
old_func = self.view_functions.get(endpoint) old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func: if old_func is not None and old_func != view_func:
@ -657,19 +660,6 @@ class App(Scaffold):
) )
self.view_functions[endpoint] = view_func 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 @t.overload
def template_filter(self, name: T_template_filter) -> T_template_filter: ... def template_filter(self, name: T_template_filter) -> T_template_filter: ...
@t.overload @t.overload

View file

@ -52,13 +52,6 @@ def test_options_on_multiple_rules(app, client):
assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"]
def test_options_view_args(app: flask.Flask, client: FlaskClient) -> None:
"""The automatic options view accepts any view args."""
app.add_url_rule("/<a>/<b>", endpoint="add")
rv = client.options("/1/2")
assert rv.allow == {"GET", "HEAD", "OPTIONS"}
@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) @pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"])
def test_method_route(app, client, method): def test_method_route(app, client, method):
method_route = getattr(app, method) method_route = getattr(app, method)

View file

@ -483,17 +483,12 @@ class TestRoutes:
["yyy_get_post", "static", "aaa_post"], ["yyy_get_post", "static", "aaa_post"],
invoke(["routes", "-s", "rule"]).output, invoke(["routes", "-s", "rule"]).output,
) )
match_order = [ match_order = [r.endpoint for r in app.url_map.iter_rules()]
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) self.expect_order(match_order, invoke(["routes", "-s", "match"]).output)
def test_all_methods(self, invoke): def test_all_methods(self, invoke):
output = invoke(["routes"]).output output = invoke(["routes"]).output
assert "HEAD" not in output assert "GET, HEAD, OPTIONS, POST" not in output
assert "OPTIONS" not in output
output = invoke(["routes", "--all-methods"]).output output = invoke(["routes", "--all-methods"]).output
assert "GET, HEAD, OPTIONS, POST" in output assert "GET, HEAD, OPTIONS, POST" in output

796
uv.lock generated

File diff suppressed because it is too large Load diff