Split the App and Blueprint into Sansio and IO parts

This follows a similar structure in Werkzeug and allows for async
based IO projects, specifically Quart, to base themselves on
Flask.

Note that the globals, and signals are specific to Flask and hence
specific to Flask's IO. This means they cannot be moved to the sansio
part of the codebase.
This commit is contained in:
pgjones 2023-06-11 15:03:45 +01:00
parent a64588f87a
commit 0ec7f713d6
10 changed files with 1621 additions and 1352 deletions

View file

@ -1,7 +1,5 @@
from . import json as json from . import json as json
from .app import Flask as Flask from .app import Flask as Flask
from .app import Request as Request
from .app import Response as Response
from .blueprints import Blueprint as Blueprint from .blueprints import Blueprint as Blueprint
from .config import Config as Config from .config import Config as Config
from .ctx import after_this_request as after_this_request from .ctx import after_this_request as after_this_request
@ -37,5 +35,7 @@ from .templating import render_template as render_template
from .templating import render_template_string as render_template_string from .templating import render_template_string as render_template_string
from .templating import stream_template as stream_template from .templating import stream_template as stream_template
from .templating import stream_template_string as stream_template_string from .templating import stream_template_string as stream_template_string
from .wrappers import Request as Request
from .wrappers import Response as Response
__version__ = "3.0.0.dev" __version__ = "3.0.0.dev"

1478
src/flask/app.py Normal file

File diff suppressed because it is too large Load diff

91
src/flask/blueprints.py Normal file
View file

@ -0,0 +1,91 @@
from __future__ import annotations
import os
import typing as t
from datetime import timedelta
from .globals import current_app
from .helpers import send_from_directory
from .sansio.blueprints import Blueprint as SansioBlueprint
from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa
if t.TYPE_CHECKING: # pragma: no cover
from .wrappers import Response
class Blueprint(SansioBlueprint):
def get_send_file_max_age(self, filename: str | None) -> int | None:
"""Used by :func:`send_file` to determine the ``max_age`` cache
value for a given file path if it wasn't passed.
By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
the configuration of :data:`~flask.current_app`. This defaults
to ``None``, which tells the browser to use conditional requests
instead of a timed cache, which is usually preferable.
Note this is a duplicate of the same method in the Flask
class.
.. versionchanged:: 2.0
The default configuration is ``None`` instead of 12 hours.
.. versionadded:: 0.9
"""
value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
if value is None:
return None
if isinstance(value, timedelta):
return int(value.total_seconds())
return value
def send_static_file(self, filename: str) -> Response:
"""The view function used to serve files from
:attr:`static_folder`. A route is automatically registered for
this view at :attr:`static_url_path` if :attr:`static_folder` is
set.
Note this is a duplicate of the same method in the Flask
class.
.. versionadded:: 0.5
"""
if not self.has_static_folder:
raise RuntimeError("'static_folder' must be set to serve static_files.")
# send_file only knows to call get_send_file_max_age on the app,
# call it here so it works for blueprints too.
max_age = self.get_send_file_max_age(filename)
return send_from_directory(
t.cast(str, self.static_folder), filename, max_age=max_age
)
def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for
reading.
For example, if the file ``schema.sql`` is next to the file
``app.py`` where the ``Flask`` app is defined, it can be opened
with:
.. code-block:: python
with app.open_resource("schema.sql") as f:
conn.executescript(f.read())
:param resource: Path to the resource relative to
:attr:`root_path`.
:param mode: Open the file in this mode. Only reading is
supported, valid values are "r" (or "rt") and "rb".
Note this is a duplicate of the same method in the Flask
class.
"""
if mode not in {"r", "rt", "rb"}:
raise ValueError("Resources can only be opened for reading.")
return open(os.path.join(self.root_path, resource), mode)

View file

@ -2,9 +2,9 @@ from __future__ import annotations
import typing as t import typing as t
from .app import Flask
from .blueprints import Blueprint from .blueprints import Blueprint
from .globals import request_ctx from .globals import request_ctx
from .sansio.app import App
class UnexpectedUnicodeError(AssertionError, UnicodeError): class UnexpectedUnicodeError(AssertionError, UnicodeError):
@ -113,7 +113,7 @@ def _dump_loader_info(loader) -> t.Generator:
yield f"{key}: {value!r}" yield f"{key}: {value!r}"
def explain_template_loading_attempts(app: Flask, template, attempts) -> None: def explain_template_loading_attempts(app: App, template, attempts) -> None:
"""This should help developers understand what failed""" """This should help developers understand what failed"""
info = [f"Locating template {template!r}:"] info = [f"Locating template {template!r}:"]
total_found = 0 total_found = 0
@ -122,7 +122,7 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None:
blueprint = request_ctx.request.blueprint blueprint = request_ctx.request.blueprint
for idx, (loader, srcobj, triple) in enumerate(attempts): for idx, (loader, srcobj, triple) in enumerate(attempts):
if isinstance(srcobj, Flask): if isinstance(srcobj, App):
src_info = f"application {srcobj.import_name!r}" src_info = f"application {srcobj.import_name!r}"
elif isinstance(srcobj, Blueprint): elif isinstance(srcobj, Blueprint):
src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"

View file

@ -11,7 +11,7 @@ from datetime import date
from werkzeug.http import http_date from werkzeug.http import http_date
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from ..app import Flask from ..sansio.app import App
from ..wrappers import Response from ..wrappers import Response
@ -34,7 +34,7 @@ class JSONProvider:
.. versionadded:: 2.2 .. versionadded:: 2.2
""" """
def __init__(self, app: Flask) -> None: def __init__(self, app: App) -> None:
self._app = weakref.proxy(app) self._app = weakref.proxy(app)
def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:

View file

@ -9,7 +9,7 @@ from werkzeug.local import LocalProxy
from .globals import request from .globals import request
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask from .sansio.app import App
@LocalProxy @LocalProxy
@ -52,7 +52,7 @@ default_handler.setFormatter(
) )
def create_logger(app: Flask) -> logging.Logger: def create_logger(app: App) -> logging.Logger:
"""Get the Flask app's logger and configure it if needed. """Get the Flask app's logger and configure it if needed.
The logger name will be the same as The logger name will be the same as

File diff suppressed because it is too large Load diff

View file

@ -5,14 +5,14 @@ import typing as t
from collections import defaultdict from collections import defaultdict
from functools import update_wrapper from functools import update_wrapper
from . import typing as ft from .. import typing as ft
from .scaffold import _endpoint_from_view_func from .scaffold import _endpoint_from_view_func
from .scaffold import _sentinel from .scaffold import _sentinel
from .scaffold import Scaffold from .scaffold import Scaffold
from .scaffold import setupmethod from .scaffold import setupmethod
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask from .app import App
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable)
@ -41,7 +41,7 @@ class BlueprintSetupState:
def __init__( def __init__(
self, self,
blueprint: Blueprint, blueprint: Blueprint,
app: Flask, app: App,
options: t.Any, options: t.Any,
first_registration: bool, first_registration: bool,
) -> None: ) -> None:
@ -244,7 +244,7 @@ class Blueprint(Scaffold):
self.record(update_wrapper(wrapper, func)) self.record(update_wrapper(wrapper, func))
def make_setup_state( def make_setup_state(
self, app: Flask, options: dict, first_registration: bool = False self, app: App, options: dict, first_registration: bool = False
) -> BlueprintSetupState: ) -> BlueprintSetupState:
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
object that is later passed to the register callback functions. object that is later passed to the register callback functions.
@ -270,7 +270,7 @@ class Blueprint(Scaffold):
raise ValueError("Cannot register a blueprint on itself") raise ValueError("Cannot register a blueprint on itself")
self._blueprints.append((blueprint, options)) self._blueprints.append((blueprint, options))
def register(self, app: Flask, options: dict) -> None: def register(self, app: App, options: dict) -> None:
"""Called by :meth:`Flask.register_blueprint` to register all """Called by :meth:`Flask.register_blueprint` to register all
views and callbacks registered on the blueprint with the views and callbacks registered on the blueprint with the
application. Creates a :class:`.BlueprintSetupState` and calls application. Creates a :class:`.BlueprintSetupState` and calls
@ -323,7 +323,7 @@ class Blueprint(Scaffold):
if self.has_static_folder: if self.has_static_folder:
state.add_url_rule( state.add_url_rule(
f"{self.static_url_path}/<path:filename>", f"{self.static_url_path}/<path:filename>",
view_func=self.send_static_file, view_func=self.send_static_file, # type: ignore[attr-defined]
endpoint="static", endpoint="static",
) )

View file

@ -6,7 +6,6 @@ import pathlib
import sys import sys
import typing as t import typing as t
from collections import defaultdict from collections import defaultdict
from datetime import timedelta
from functools import update_wrapper from functools import update_wrapper
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
@ -14,15 +13,10 @@ from werkzeug.exceptions import default_exceptions
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.utils import cached_property from werkzeug.utils import cached_property
from . import typing as ft from .. import typing as ft
from .cli import AppGroup from ..cli import AppGroup
from .globals import current_app from ..helpers import get_root_path
from .helpers import get_root_path from ..templating import _default_template_ctx_processor
from .helpers import send_from_directory
from .templating import _default_template_ctx_processor
if t.TYPE_CHECKING: # pragma: no cover
from .wrappers import Response
# a singleton sentinel value for parameter defaults # a singleton sentinel value for parameter defaults
_sentinel = object() _sentinel = object()
@ -276,48 +270,6 @@ class Scaffold:
self._static_url_path = value self._static_url_path = value
def get_send_file_max_age(self, filename: str | None) -> int | None:
"""Used by :func:`send_file` to determine the ``max_age`` cache
value for a given file path if it wasn't passed.
By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
the configuration of :data:`~flask.current_app`. This defaults
to ``None``, which tells the browser to use conditional requests
instead of a timed cache, which is usually preferable.
.. versionchanged:: 2.0
The default configuration is ``None`` instead of 12 hours.
.. versionadded:: 0.9
"""
value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
if value is None:
return None
if isinstance(value, timedelta):
return int(value.total_seconds())
return value
def send_static_file(self, filename: str) -> Response:
"""The view function used to serve files from
:attr:`static_folder`. A route is automatically registered for
this view at :attr:`static_url_path` if :attr:`static_folder` is
set.
.. versionadded:: 0.5
"""
if not self.has_static_folder:
raise RuntimeError("'static_folder' must be set to serve static_files.")
# send_file only knows to call get_send_file_max_age on the app,
# call it here so it works for blueprints too.
max_age = self.get_send_file_max_age(filename)
return send_from_directory(
t.cast(str, self.static_folder), filename, max_age=max_age
)
@cached_property @cached_property
def jinja_loader(self) -> FileSystemLoader | None: def jinja_loader(self) -> FileSystemLoader | None:
"""The Jinja loader for this object's templates. By default this """The Jinja loader for this object's templates. By default this
@ -331,29 +283,6 @@ class Scaffold:
else: else:
return None return None
def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for
reading.
For example, if the file ``schema.sql`` is next to the file
``app.py`` where the ``Flask`` app is defined, it can be opened
with:
.. code-block:: python
with app.open_resource("schema.sql") as f:
conn.executescript(f.read())
:param resource: Path to the resource relative to
:attr:`root_path`.
:param mode: Open the file in this mode. Only reading is
supported, valid values are "r" (or "rt") and "rb".
"""
if mode not in {"r", "rt", "rb"}:
raise ValueError("Resources can only be opened for reading.")
return open(os.path.join(self.root_path, resource), mode)
def _method_route( def _method_route(
self, self,
method: str, method: str,

View file

@ -17,7 +17,8 @@ from .signals import template_rendered
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask from .app import Flask
from .scaffold import Scaffold from .sansio.app import App
from .sansio.scaffold import Scaffold
def _default_template_ctx_processor() -> dict[str, t.Any]: def _default_template_ctx_processor() -> dict[str, t.Any]:
@ -41,7 +42,7 @@ class Environment(BaseEnvironment):
name of the blueprint to referenced templates if necessary. name of the blueprint to referenced templates if necessary.
""" """
def __init__(self, app: Flask, **options: t.Any) -> None: def __init__(self, app: App, **options: t.Any) -> None:
if "loader" not in options: if "loader" not in options:
options["loader"] = app.create_global_jinja_loader() options["loader"] = app.create_global_jinja_loader()
BaseEnvironment.__init__(self, **options) BaseEnvironment.__init__(self, **options)
@ -53,7 +54,7 @@ class DispatchingJinjaLoader(BaseLoader):
the blueprint folders. the blueprint folders.
""" """
def __init__(self, app: Flask) -> None: def __init__(self, app: App) -> None:
self.app = app self.app = app
def get_source( # type: ignore def get_source( # type: ignore