Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/flask/testing.py: 62%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import importlib.metadata
4import typing as t
5from contextlib import contextmanager
6from contextlib import ExitStack
7from copy import copy
8from types import TracebackType
9from urllib.parse import urlsplit
11import werkzeug.test
12from click.testing import CliRunner
13from click.testing import Result
14from werkzeug.test import Client
15from werkzeug.wrappers import Request as BaseRequest
17from .cli import ScriptInfo
18from .sessions import SessionMixin
20if t.TYPE_CHECKING: # pragma: no cover
21 from _typeshed.wsgi import WSGIEnvironment
22 from werkzeug.test import TestResponse
24 from .app import Flask
27class EnvironBuilder(werkzeug.test.EnvironBuilder):
28 """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the
29 application.
31 :param app: The Flask application to configure the environment from.
32 :param path: URL path being requested.
33 :param base_url: Base URL where the app is being served, which
34 ``path`` is relative to. If not given, built from
35 :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
36 :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
37 :param subdomain: Subdomain name to append to :data:`SERVER_NAME`.
38 :param url_scheme: Scheme to use instead of
39 :data:`PREFERRED_URL_SCHEME`.
40 :param json: If given, this is serialized as JSON and passed as
41 ``data``. Also defaults ``content_type`` to
42 ``application/json``.
43 :param args: other positional arguments passed to
44 :class:`~werkzeug.test.EnvironBuilder`.
45 :param kwargs: other keyword arguments passed to
46 :class:`~werkzeug.test.EnvironBuilder`.
47 """
49 def __init__(
50 self,
51 app: Flask,
52 path: str = "/",
53 base_url: str | None = None,
54 subdomain: str | None = None,
55 url_scheme: str | None = None,
56 *args: t.Any,
57 **kwargs: t.Any,
58 ) -> None:
59 assert not (base_url or subdomain or url_scheme) or (
60 base_url is not None
61 ) != bool(subdomain or url_scheme), (
62 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
63 )
65 if base_url is None:
66 http_host = app.config.get("SERVER_NAME") or "localhost"
67 app_root = app.config["APPLICATION_ROOT"]
69 if subdomain:
70 http_host = f"{subdomain}.{http_host}"
72 if url_scheme is None:
73 url_scheme = app.config["PREFERRED_URL_SCHEME"]
75 url = urlsplit(path)
76 base_url = (
77 f"{url.scheme or url_scheme}://{url.netloc or http_host}"
78 f"/{app_root.lstrip('/')}"
79 )
80 path = url.path
82 if url.query:
83 path = f"{path}?{url.query}"
85 self.app = app
86 super().__init__(path, base_url, *args, **kwargs)
88 def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
89 """Serialize ``obj`` to a JSON-formatted string.
91 The serialization will be configured according to the config associated
92 with this EnvironBuilder's ``app``.
93 """
94 return self.app.json.dumps(obj, **kwargs)
97_werkzeug_version = ""
100def _get_werkzeug_version() -> str:
101 global _werkzeug_version
103 if not _werkzeug_version:
104 _werkzeug_version = importlib.metadata.version("werkzeug")
106 return _werkzeug_version
109class FlaskClient(Client):
110 """Works like a regular Werkzeug test client but has knowledge about
111 Flask's contexts to defer the cleanup of the request context until
112 the end of a ``with`` block. For general information about how to
113 use this class refer to :class:`werkzeug.test.Client`.
115 .. versionchanged:: 0.12
116 `app.test_client()` includes preset default environment, which can be
117 set after instantiation of the `app.test_client()` object in
118 `client.environ_base`.
120 Basic usage is outlined in the :doc:`/testing` chapter.
121 """
123 application: Flask
125 def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
126 super().__init__(*args, **kwargs)
127 self.preserve_context = False
128 self._new_contexts: list[t.ContextManager[t.Any]] = []
129 self._context_stack = ExitStack()
130 self.environ_base = {
131 "REMOTE_ADDR": "127.0.0.1",
132 "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}",
133 }
135 @contextmanager
136 def session_transaction(
137 self, *args: t.Any, **kwargs: t.Any
138 ) -> t.Iterator[SessionMixin]:
139 """When used in combination with a ``with`` statement this opens a
140 session transaction. This can be used to modify the session that
141 the test client uses. Once the ``with`` block is left the session is
142 stored back.
144 ::
146 with client.session_transaction() as session:
147 session['value'] = 42
149 Internally this is implemented by going through a temporary test
150 request context and since session handling could depend on
151 request variables this function accepts the same arguments as
152 :meth:`~flask.Flask.test_request_context` which are directly
153 passed through.
154 """
155 if self._cookies is None:
156 raise TypeError(
157 "Cookies are disabled. Create a client with 'use_cookies=True'."
158 )
160 app = self.application
161 ctx = app.test_request_context(*args, **kwargs)
162 self._add_cookies_to_wsgi(ctx.request.environ)
164 with ctx:
165 sess = app.session_interface.open_session(app, ctx.request)
167 if sess is None:
168 raise RuntimeError("Session backend did not open a session.")
170 yield sess
171 resp = app.response_class()
173 if app.session_interface.is_null_session(sess):
174 return
176 with ctx:
177 app.session_interface.save_session(app, sess, resp)
179 self._update_cookies_from_response(
180 ctx.request.host.partition(":")[0],
181 ctx.request.path,
182 resp.headers.getlist("Set-Cookie"),
183 )
185 def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment:
186 out = {**self.environ_base, **other}
188 if self.preserve_context:
189 out["werkzeug.debug.preserve_context"] = self._new_contexts.append
191 return out
193 def _request_from_builder_args(
194 self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any]
195 ) -> BaseRequest:
196 kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {}))
197 builder = EnvironBuilder(self.application, *args, **kwargs)
199 try:
200 return builder.get_request()
201 finally:
202 builder.close()
204 def open(
205 self,
206 *args: t.Any,
207 buffered: bool = False,
208 follow_redirects: bool = False,
209 **kwargs: t.Any,
210 ) -> TestResponse:
211 if args and isinstance(
212 args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest)
213 ):
214 if isinstance(args[0], werkzeug.test.EnvironBuilder):
215 builder = copy(args[0])
216 builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type]
217 request = builder.get_request()
218 elif isinstance(args[0], dict):
219 request = EnvironBuilder.from_environ(
220 args[0], app=self.application, environ_base=self._copy_environ({})
221 ).get_request()
222 else:
223 # isinstance(args[0], BaseRequest)
224 request = copy(args[0])
225 request.environ = self._copy_environ(request.environ)
226 else:
227 # request is None
228 request = self._request_from_builder_args(args, kwargs)
230 # Pop any previously preserved contexts. This prevents contexts
231 # from being preserved across redirects or multiple requests
232 # within a single block.
233 self._context_stack.close()
235 response = super().open(
236 request,
237 buffered=buffered,
238 follow_redirects=follow_redirects,
239 )
240 response.json_module = self.application.json # type: ignore[assignment]
242 # Re-push contexts that were preserved during the request.
243 while self._new_contexts:
244 cm = self._new_contexts.pop()
245 self._context_stack.enter_context(cm)
247 return response
249 def __enter__(self) -> FlaskClient:
250 if self.preserve_context:
251 raise RuntimeError("Cannot nest client invocations")
252 self.preserve_context = True
253 return self
255 def __exit__(
256 self,
257 exc_type: type | None,
258 exc_value: BaseException | None,
259 tb: TracebackType | None,
260 ) -> None:
261 self.preserve_context = False
262 self._context_stack.close()
265class FlaskCliRunner(CliRunner):
266 """A :class:`~click.testing.CliRunner` for testing a Flask app's
267 CLI commands. Typically created using
268 :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
269 """
271 def __init__(self, app: Flask, **kwargs: t.Any) -> None:
272 self.app = app
273 super().__init__(**kwargs)
275 def invoke( # type: ignore
276 self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any
277 ) -> Result:
278 """Invokes a CLI command in an isolated environment. See
279 :meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for
280 full method documentation. See :ref:`testing-cli` for examples.
282 If the ``obj`` argument is not given, passes an instance of
283 :class:`~flask.cli.ScriptInfo` that knows how to load the Flask
284 app being tested.
286 :param cli: Command object to invoke. Default is the app's
287 :attr:`~flask.app.Flask.cli` group.
288 :param args: List of strings to invoke the command with.
290 :return: a :class:`~click.testing.Result` object.
291 """
292 if cli is None:
293 cli = self.app.cli
295 if "obj" not in kwargs:
296 kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
298 return super().invoke(cli, args, **kwargs)