Spaces:
Running
Running
from __future__ import annotations | |
import importlib.metadata | |
import typing as t | |
from contextlib import contextmanager | |
from contextlib import ExitStack | |
from copy import copy | |
from types import TracebackType | |
from urllib.parse import urlsplit | |
import werkzeug.test | |
from click.testing import CliRunner | |
from werkzeug.test import Client | |
from werkzeug.wrappers import Request as BaseRequest | |
from .cli import ScriptInfo | |
from .sessions import SessionMixin | |
if t.TYPE_CHECKING: # pragma: no cover | |
from _typeshed.wsgi import WSGIEnvironment | |
from werkzeug.test import TestResponse | |
from .app import Flask | |
class EnvironBuilder(werkzeug.test.EnvironBuilder): | |
"""An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the | |
application. | |
:param app: The Flask application to configure the environment from. | |
:param path: URL path being requested. | |
:param base_url: Base URL where the app is being served, which | |
``path`` is relative to. If not given, built from | |
:data:`PREFERRED_URL_SCHEME`, ``subdomain``, | |
:data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. | |
:param subdomain: Subdomain name to append to :data:`SERVER_NAME`. | |
:param url_scheme: Scheme to use instead of | |
:data:`PREFERRED_URL_SCHEME`. | |
:param json: If given, this is serialized as JSON and passed as | |
``data``. Also defaults ``content_type`` to | |
``application/json``. | |
:param args: other positional arguments passed to | |
:class:`~werkzeug.test.EnvironBuilder`. | |
:param kwargs: other keyword arguments passed to | |
:class:`~werkzeug.test.EnvironBuilder`. | |
""" | |
def __init__( | |
self, | |
app: Flask, | |
path: str = "/", | |
base_url: str | None = None, | |
subdomain: str | None = None, | |
url_scheme: str | None = None, | |
*args: t.Any, | |
**kwargs: t.Any, | |
) -> None: | |
assert not (base_url or subdomain or url_scheme) or ( | |
base_url is not None | |
) != bool( | |
subdomain or url_scheme | |
), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' | |
if base_url is None: | |
http_host = app.config.get("SERVER_NAME") or "localhost" | |
app_root = app.config["APPLICATION_ROOT"] | |
if subdomain: | |
http_host = f"{subdomain}.{http_host}" | |
if url_scheme is None: | |
url_scheme = app.config["PREFERRED_URL_SCHEME"] | |
url = urlsplit(path) | |
base_url = ( | |
f"{url.scheme or url_scheme}://{url.netloc or http_host}" | |
f"/{app_root.lstrip('/')}" | |
) | |
path = url.path | |
if url.query: | |
path = f"{path}?{url.query}" | |
self.app = app | |
super().__init__(path, base_url, *args, **kwargs) | |
def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore | |
"""Serialize ``obj`` to a JSON-formatted string. | |
The serialization will be configured according to the config associated | |
with this EnvironBuilder's ``app``. | |
""" | |
return self.app.json.dumps(obj, **kwargs) | |
_werkzeug_version = "" | |
def _get_werkzeug_version() -> str: | |
global _werkzeug_version | |
if not _werkzeug_version: | |
_werkzeug_version = importlib.metadata.version("werkzeug") | |
return _werkzeug_version | |
class FlaskClient(Client): | |
"""Works like a regular Werkzeug test client but has knowledge about | |
Flask's contexts to defer the cleanup of the request context until | |
the end of a ``with`` block. For general information about how to | |
use this class refer to :class:`werkzeug.test.Client`. | |
.. versionchanged:: 0.12 | |
`app.test_client()` includes preset default environment, which can be | |
set after instantiation of the `app.test_client()` object in | |
`client.environ_base`. | |
Basic usage is outlined in the :doc:`/testing` chapter. | |
""" | |
application: Flask | |
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: | |
super().__init__(*args, **kwargs) | |
self.preserve_context = False | |
self._new_contexts: list[t.ContextManager[t.Any]] = [] | |
self._context_stack = ExitStack() | |
self.environ_base = { | |
"REMOTE_ADDR": "127.0.0.1", | |
"HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", | |
} | |
def session_transaction( | |
self, *args: t.Any, **kwargs: t.Any | |
) -> t.Iterator[SessionMixin]: | |
"""When used in combination with a ``with`` statement this opens a | |
session transaction. This can be used to modify the session that | |
the test client uses. Once the ``with`` block is left the session is | |
stored back. | |
:: | |
with client.session_transaction() as session: | |
session['value'] = 42 | |
Internally this is implemented by going through a temporary test | |
request context and since session handling could depend on | |
request variables this function accepts the same arguments as | |
:meth:`~flask.Flask.test_request_context` which are directly | |
passed through. | |
""" | |
if self._cookies is None: | |
raise TypeError( | |
"Cookies are disabled. Create a client with 'use_cookies=True'." | |
) | |
app = self.application | |
ctx = app.test_request_context(*args, **kwargs) | |
self._add_cookies_to_wsgi(ctx.request.environ) | |
with ctx: | |
sess = app.session_interface.open_session(app, ctx.request) | |
if sess is None: | |
raise RuntimeError("Session backend did not open a session.") | |
yield sess | |
resp = app.response_class() | |
if app.session_interface.is_null_session(sess): | |
return | |
with ctx: | |
app.session_interface.save_session(app, sess, resp) | |
self._update_cookies_from_response( | |
ctx.request.host.partition(":")[0], | |
ctx.request.path, | |
resp.headers.getlist("Set-Cookie"), | |
) | |
def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: | |
out = {**self.environ_base, **other} | |
if self.preserve_context: | |
out["werkzeug.debug.preserve_context"] = self._new_contexts.append | |
return out | |
def _request_from_builder_args( | |
self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] | |
) -> BaseRequest: | |
kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) | |
builder = EnvironBuilder(self.application, *args, **kwargs) | |
try: | |
return builder.get_request() | |
finally: | |
builder.close() | |
def open( | |
self, | |
*args: t.Any, | |
buffered: bool = False, | |
follow_redirects: bool = False, | |
**kwargs: t.Any, | |
) -> TestResponse: | |
if args and isinstance( | |
args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) | |
): | |
if isinstance(args[0], werkzeug.test.EnvironBuilder): | |
builder = copy(args[0]) | |
builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] | |
request = builder.get_request() | |
elif isinstance(args[0], dict): | |
request = EnvironBuilder.from_environ( | |
args[0], app=self.application, environ_base=self._copy_environ({}) | |
).get_request() | |
else: | |
# isinstance(args[0], BaseRequest) | |
request = copy(args[0]) | |
request.environ = self._copy_environ(request.environ) | |
else: | |
# request is None | |
request = self._request_from_builder_args(args, kwargs) | |
# Pop any previously preserved contexts. This prevents contexts | |
# from being preserved across redirects or multiple requests | |
# within a single block. | |
self._context_stack.close() | |
response = super().open( | |
request, | |
buffered=buffered, | |
follow_redirects=follow_redirects, | |
) | |
response.json_module = self.application.json # type: ignore[assignment] | |
# Re-push contexts that were preserved during the request. | |
while self._new_contexts: | |
cm = self._new_contexts.pop() | |
self._context_stack.enter_context(cm) | |
return response | |
def __enter__(self) -> FlaskClient: | |
if self.preserve_context: | |
raise RuntimeError("Cannot nest client invocations") | |
self.preserve_context = True | |
return self | |
def __exit__( | |
self, | |
exc_type: type | None, | |
exc_value: BaseException | None, | |
tb: TracebackType | None, | |
) -> None: | |
self.preserve_context = False | |
self._context_stack.close() | |
class FlaskCliRunner(CliRunner): | |
"""A :class:`~click.testing.CliRunner` for testing a Flask app's | |
CLI commands. Typically created using | |
:meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. | |
""" | |
def __init__(self, app: Flask, **kwargs: t.Any) -> None: | |
self.app = app | |
super().__init__(**kwargs) | |
def invoke( # type: ignore | |
self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any | |
) -> t.Any: | |
"""Invokes a CLI command in an isolated environment. See | |
:meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for | |
full method documentation. See :ref:`testing-cli` for examples. | |
If the ``obj`` argument is not given, passes an instance of | |
:class:`~flask.cli.ScriptInfo` that knows how to load the Flask | |
app being tested. | |
:param cli: Command object to invoke. Default is the app's | |
:attr:`~flask.app.Flask.cli` group. | |
:param args: List of strings to invoke the command with. | |
:return: a :class:`~click.testing.Result` object. | |
""" | |
if cli is None: | |
cli = self.app.cli | |
if "obj" not in kwargs: | |
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) | |
return super().invoke(cli, args, **kwargs) | |