Skip to content

Commit 4240ace

Browse files
committed
Merge remote-tracking branch 'origin/2.0.x'
2 parents d575de5 + f3551c8 commit 4240ace

14 files changed

+202
-163
lines changed

CHANGES.rst

+16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ Version 2.0.1
1313

1414
Unreleased
1515

16+
- Re-add the ``filename`` parameter in ``send_from_directory``. The
17+
``filename`` parameter has been renamed to ``path``, the old name
18+
is deprecated. :pr:`4019`
19+
- Mark top-level names as exported so type checking understands
20+
imports in user projects. :issue:`4024`
21+
- Fix type annotation for ``g`` and inform mypy that it is a namespace
22+
object that has arbitrary attributes. :issue:`4020`
23+
- Fix some types that weren't available in Python 3.6.0. :issue:`4040`
24+
- Improve typing for ``send_file``, ``send_from_directory``, and
25+
``get_send_file_max_age``. :issue:`4044`, :pr:`4026`
26+
- Show an error when a blueprint name contains a dot. The ``.`` has
27+
special meaning, it is used to separate (nested) blueprint names and
28+
the endpoint name. :issue:`4041`
29+
- Combine URL prefixes when nesting blueprints that were created with
30+
a ``url_prefix`` value. :issue:`4037`
31+
1632

1733
Version 2.0.0
1834
-------------

CONTRIBUTING.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ First time setup
129129
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
130130
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
131131
.. _GitHub account: https://github.com/join
132-
.. _Fork: https://github.com/pallets/jinja/fork
132+
.. _Fork: https://github.com/pallets/flask/fork
133133
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
134134

135135

docs/deploying/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Hosted options
1616

1717
- `Deploying Flask on Heroku <https://devcenter.heroku.com/articles/getting-started-with-python>`_
1818
- `Deploying Flask on Google App Engine <https://cloud.google.com/appengine/docs/standard/python3/runtime>`_
19+
- `Deploying Flask on Google Cloud Run <https://cloud.google.com/run/docs/quickstarts/build-and-deploy/python>`_
1920
- `Deploying Flask on AWS Elastic Beanstalk <https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create-deploy-python-flask.html>`_
2021
- `Deploying on Azure (IIS) <https://docs.microsoft.com/en-us/azure/app-service/containers/how-to-configure-python>`_
2122
- `Deploying on PythonAnywhere <https://help.pythonanywhere.com/pages/Flask/>`_

src/flask/__init__.py

+41-41
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
11
from markupsafe import escape
22
from markupsafe import Markup
3-
from werkzeug.exceptions import abort
4-
from werkzeug.utils import redirect
3+
from werkzeug.exceptions import abort as abort
4+
from werkzeug.utils import redirect as redirect
55

6-
from . import json
7-
from .app import Flask
8-
from .app import Request
9-
from .app import Response
10-
from .blueprints import Blueprint
11-
from .config import Config
12-
from .ctx import after_this_request
13-
from .ctx import copy_current_request_context
14-
from .ctx import has_app_context
15-
from .ctx import has_request_context
16-
from .globals import _app_ctx_stack
17-
from .globals import _request_ctx_stack
18-
from .globals import current_app
19-
from .globals import g
20-
from .globals import request
21-
from .globals import session
22-
from .helpers import flash
23-
from .helpers import get_flashed_messages
24-
from .helpers import get_template_attribute
25-
from .helpers import make_response
26-
from .helpers import safe_join
27-
from .helpers import send_file
28-
from .helpers import send_from_directory
29-
from .helpers import stream_with_context
30-
from .helpers import url_for
31-
from .json import jsonify
32-
from .signals import appcontext_popped
33-
from .signals import appcontext_pushed
34-
from .signals import appcontext_tearing_down
35-
from .signals import before_render_template
36-
from .signals import got_request_exception
37-
from .signals import message_flashed
38-
from .signals import request_finished
39-
from .signals import request_started
40-
from .signals import request_tearing_down
41-
from .signals import signals_available
42-
from .signals import template_rendered
43-
from .templating import render_template
44-
from .templating import render_template_string
6+
from . import json as json
7+
from .app import Flask as Flask
8+
from .app import Request as Request
9+
from .app import Response as Response
10+
from .blueprints import Blueprint as Blueprint
11+
from .config import Config as Config
12+
from .ctx import after_this_request as after_this_request
13+
from .ctx import copy_current_request_context as copy_current_request_context
14+
from .ctx import has_app_context as has_app_context
15+
from .ctx import has_request_context as has_request_context
16+
from .globals import _app_ctx_stack as _app_ctx_stack
17+
from .globals import _request_ctx_stack as _request_ctx_stack
18+
from .globals import current_app as current_app
19+
from .globals import g as g
20+
from .globals import request as request
21+
from .globals import session as session
22+
from .helpers import flash as flash
23+
from .helpers import get_flashed_messages as get_flashed_messages
24+
from .helpers import get_template_attribute as get_template_attribute
25+
from .helpers import make_response as make_response
26+
from .helpers import safe_join as safe_join
27+
from .helpers import send_file as send_file
28+
from .helpers import send_from_directory as send_from_directory
29+
from .helpers import stream_with_context as stream_with_context
30+
from .helpers import url_for as url_for
31+
from .json import jsonify as jsonify
32+
from .signals import appcontext_popped as appcontext_popped
33+
from .signals import appcontext_pushed as appcontext_pushed
34+
from .signals import appcontext_tearing_down as appcontext_tearing_down
35+
from .signals import before_render_template as before_render_template
36+
from .signals import got_request_exception as got_request_exception
37+
from .signals import message_flashed as message_flashed
38+
from .signals import request_finished as request_finished
39+
from .signals import request_started as request_started
40+
from .signals import request_tearing_down as request_tearing_down
41+
from .signals import signals_available as signals_available
42+
from .signals import template_rendered as template_rendered
43+
from .templating import render_template as render_template
44+
from .templating import render_template_string as render_template_string
4545

4646
__version__ = "2.1.0.dev0"

src/flask/app.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from .wrappers import Response
7373

7474
if t.TYPE_CHECKING:
75+
import typing_extensions as te
7576
from .blueprints import Blueprint
7677
from .testing import FlaskClient
7778
from .testing import FlaskCliRunner
@@ -1441,7 +1442,7 @@ def log_exception(
14411442
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
14421443
)
14431444

1444-
def raise_routing_exception(self, request: Request) -> t.NoReturn:
1445+
def raise_routing_exception(self, request: Request) -> "te.NoReturn":
14451446
"""Exceptions that are recording during routing are reraised with
14461447
this method. During debug we are not reraising redirect requests
14471448
for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising

src/flask/blueprints.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ def __init__(
188188
template_folder=template_folder,
189189
root_path=root_path,
190190
)
191+
192+
if "." in name:
193+
raise ValueError("'name' may not contain a dot '.' character.")
194+
191195
self.name = name
192196
self.url_prefix = url_prefix
193197
self.subdomain = subdomain
@@ -256,7 +260,7 @@ def register(self, app: "Flask", options: dict) -> None:
256260
"""Called by :meth:`Flask.register_blueprint` to register all
257261
views and callbacks registered on the blueprint with the
258262
application. Creates a :class:`.BlueprintSetupState` and calls
259-
each :meth:`record` callbackwith it.
263+
each :meth:`record` callback with it.
260264
261265
:param app: The application this blueprint is being registered
262266
with.
@@ -340,13 +344,17 @@ def extend(bp_dict, parent_dict):
340344
app.cli.add_command(self.cli)
341345

342346
for blueprint, bp_options in self._blueprints:
343-
url_prefix = options.get("url_prefix", "")
344-
if "url_prefix" in bp_options:
345-
url_prefix = (
346-
url_prefix.rstrip("/") + "/" + bp_options["url_prefix"].lstrip("/")
347+
bp_options = bp_options.copy()
348+
bp_url_prefix = bp_options.get("url_prefix")
349+
350+
if bp_url_prefix is None:
351+
bp_url_prefix = blueprint.url_prefix
352+
353+
if state.url_prefix is not None and bp_url_prefix is not None:
354+
bp_options["url_prefix"] = (
355+
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
347356
)
348357

349-
bp_options["url_prefix"] = url_prefix
350358
bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "."
351359
blueprint.register(app, bp_options)
352360

@@ -360,12 +368,12 @@ def add_url_rule(
360368
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
361369
the :func:`url_for` function is prefixed with the name of the blueprint.
362370
"""
363-
if endpoint:
364-
assert "." not in endpoint, "Blueprint endpoints should not contain dots"
365-
if view_func and hasattr(view_func, "__name__"):
366-
assert (
367-
"." not in view_func.__name__
368-
), "Blueprint view function name should not contain dots"
371+
if endpoint and "." in endpoint:
372+
raise ValueError("'endpoint' may not contain a dot '.' character.")
373+
374+
if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
375+
raise ValueError("'view_func' name may not contain a dot '.' character.")
376+
369377
self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options))
370378

371379
def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable:

src/flask/ctx.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ class _AppCtxGlobals:
4141
.. versionadded:: 0.10
4242
"""
4343

44+
# Define attr methods to let mypy know this is a namespace object
45+
# that has arbitrary attributes.
46+
47+
def __getattr__(self, name: str) -> t.Any:
48+
try:
49+
return self.__dict__[name]
50+
except KeyError:
51+
raise AttributeError(name) from None
52+
53+
def __setattr__(self, name: str, value: t.Any) -> None:
54+
self.__dict__[name] = value
55+
56+
def __delattr__(self, name: str) -> None:
57+
try:
58+
del self.__dict__[name]
59+
except KeyError:
60+
raise AttributeError(name) from None
61+
4462
def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any:
4563
"""Get an attribute by name, or a default value. Like
4664
:meth:`dict.get`.
@@ -78,10 +96,10 @@ def setdefault(self, name: str, default: t.Any = None) -> t.Any:
7896
"""
7997
return self.__dict__.setdefault(name, default)
8098

81-
def __contains__(self, item: t.Any) -> bool:
99+
def __contains__(self, item: str) -> bool:
82100
return item in self.__dict__
83101

84-
def __iter__(self) -> t.Iterator:
102+
def __iter__(self) -> t.Iterator[str]:
85103
return iter(self.__dict__)
86104

87105
def __repr__(self) -> str:

src/flask/globals.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
if t.TYPE_CHECKING:
88
from .app import Flask
9-
from .ctx import AppContext
9+
from .ctx import _AppCtxGlobals
1010
from .sessions import SessionMixin
1111
from .wrappers import Request
1212

@@ -53,5 +53,7 @@ def _find_app():
5353
_app_ctx_stack = LocalStack()
5454
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
5555
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
56-
session: "SessionMixin" = LocalProxy(partial(_lookup_req_object, "session")) # type: ignore # noqa: B950
57-
g: "AppContext" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore
56+
session: "SessionMixin" = LocalProxy( # type: ignore
57+
partial(_lookup_req_object, "session")
58+
)
59+
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore

src/flask/helpers.py

+43-21
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import typing as t
66
import warnings
7+
from datetime import datetime
78
from datetime import timedelta
89
from functools import update_wrapper
910
from threading import RLock
@@ -436,14 +437,16 @@ def get_flashed_messages(
436437

437438

438439
def _prepare_send_file_kwargs(
439-
download_name=None,
440-
attachment_filename=None,
441-
etag=None,
442-
add_etags=None,
443-
max_age=None,
444-
cache_timeout=None,
445-
**kwargs,
446-
):
440+
download_name: t.Optional[str] = None,
441+
attachment_filename: t.Optional[str] = None,
442+
etag: t.Optional[t.Union[bool, str]] = None,
443+
add_etags: t.Optional[t.Union[bool]] = None,
444+
max_age: t.Optional[
445+
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
446+
] = None,
447+
cache_timeout: t.Optional[int] = None,
448+
**kwargs: t.Any,
449+
) -> t.Dict[str, t.Any]:
447450
if attachment_filename is not None:
448451
warnings.warn(
449452
"The 'attachment_filename' parameter has been renamed to"
@@ -482,23 +485,25 @@ def _prepare_send_file_kwargs(
482485
max_age=max_age,
483486
use_x_sendfile=current_app.use_x_sendfile,
484487
response_class=current_app.response_class,
485-
_root_path=current_app.root_path,
488+
_root_path=current_app.root_path, # type: ignore
486489
)
487490
return kwargs
488491

489492

490493
def send_file(
491-
path_or_file,
492-
mimetype=None,
493-
as_attachment=False,
494-
download_name=None,
495-
attachment_filename=None,
496-
conditional=True,
497-
etag=True,
498-
add_etags=None,
499-
last_modified=None,
500-
max_age=None,
501-
cache_timeout=None,
494+
path_or_file: t.Union[os.PathLike, str, t.BinaryIO],
495+
mimetype: t.Optional[str] = None,
496+
as_attachment: bool = False,
497+
download_name: t.Optional[str] = None,
498+
attachment_filename: t.Optional[str] = None,
499+
conditional: bool = True,
500+
etag: t.Union[bool, str] = True,
501+
add_etags: t.Optional[bool] = None,
502+
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
503+
max_age: t.Optional[
504+
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
505+
] = None,
506+
cache_timeout: t.Optional[int] = None,
502507
):
503508
"""Send the contents of a file to the client.
504509
@@ -642,7 +647,12 @@ def safe_join(directory: str, *pathnames: str) -> str:
642647
return path
643648

644649

645-
def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response":
650+
def send_from_directory(
651+
directory: t.Union[os.PathLike, str],
652+
path: t.Union[os.PathLike, str],
653+
filename: t.Optional[str] = None,
654+
**kwargs: t.Any,
655+
) -> "Response":
646656
"""Send a file from within a directory using :func:`send_file`.
647657
648658
.. code-block:: python
@@ -666,12 +676,24 @@ def download_file(name):
666676
``directory``.
667677
:param kwargs: Arguments to pass to :func:`send_file`.
668678
679+
.. versionchanged:: 2.0
680+
``path`` replaces the ``filename`` parameter.
681+
669682
.. versionadded:: 2.0
670683
Moved the implementation to Werkzeug. This is now a wrapper to
671684
pass some Flask-specific arguments.
672685
673686
.. versionadded:: 0.5
674687
"""
688+
if filename is not None:
689+
warnings.warn(
690+
"The 'filename' parameter has been renamed to 'path'. The"
691+
" old name will be removed in Flask 2.1.",
692+
DeprecationWarning,
693+
stacklevel=2,
694+
)
695+
path = filename
696+
675697
return werkzeug.utils.send_from_directory( # type: ignore
676698
directory, path, **_prepare_send_file_kwargs(**kwargs)
677699
)

src/flask/scaffold.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def static_url_path(self, value: t.Optional[str]) -> None:
288288

289289
self._static_url_path = value
290290

291-
def get_send_file_max_age(self, filename: str) -> t.Optional[int]:
291+
def get_send_file_max_age(self, filename: t.Optional[str]) -> t.Optional[int]:
292292
"""Used by :func:`send_file` to determine the ``max_age`` cache
293293
value for a given file path if it wasn't passed.
294294

src/flask/sessions.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .json.tag import TaggedJSONSerializer
1313

1414
if t.TYPE_CHECKING:
15+
import typing_extensions as te
1516
from .app import Flask
1617
from .wrappers import Request, Response
1718

@@ -92,7 +93,7 @@ class NullSession(SecureCookieSession):
9293
but fail on setting.
9394
"""
9495

95-
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
96+
def _fail(self, *args: t.Any, **kwargs: t.Any) -> "te.NoReturn":
9697
raise RuntimeError(
9798
"The session is unavailable because no secret "
9899
"key was set. Set the secret_key on the "

0 commit comments

Comments
 (0)