Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/testing.py: 61%
112 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 07:17 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 07:17 +0000
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 werkzeug.test import Client
14from werkzeug.wrappers import Request as BaseRequest
16from .cli import ScriptInfo
17from .sessions import SessionMixin
19if t.TYPE_CHECKING: # pragma: no cover
20 from werkzeug.test import TestResponse
22 from .app import Flask
25class EnvironBuilder(werkzeug.test.EnvironBuilder):
26 """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the
27 application.
29 :param app: The Flask application to configure the environment from.
30 :param path: URL path being requested.
31 :param base_url: Base URL where the app is being served, which
32 ``path`` is relative to. If not given, built from
33 :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
34 :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
35 :param subdomain: Subdomain name to append to :data:`SERVER_NAME`.
36 :param url_scheme: Scheme to use instead of
37 :data:`PREFERRED_URL_SCHEME`.
38 :param json: If given, this is serialized as JSON and passed as
39 ``data``. Also defaults ``content_type`` to
40 ``application/json``.
41 :param args: other positional arguments passed to
42 :class:`~werkzeug.test.EnvironBuilder`.
43 :param kwargs: other keyword arguments passed to
44 :class:`~werkzeug.test.EnvironBuilder`.
45 """
47 def __init__(
48 self,
49 app: Flask,
50 path: str = "/",
51 base_url: str | None = None,
52 subdomain: str | None = None,
53 url_scheme: str | None = None,
54 *args: t.Any,
55 **kwargs: t.Any,
56 ) -> None:
57 assert not (base_url or subdomain or url_scheme) or (
58 base_url is not None
59 ) != bool(
60 subdomain or url_scheme
61 ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
63 if base_url is None:
64 http_host = app.config.get("SERVER_NAME") or "localhost"
65 app_root = app.config["APPLICATION_ROOT"]
67 if subdomain:
68 http_host = f"{subdomain}.{http_host}"
70 if url_scheme is None:
71 url_scheme = app.config["PREFERRED_URL_SCHEME"]
73 url = urlsplit(path)
74 base_url = (
75 f"{url.scheme or url_scheme}://{url.netloc or http_host}"
76 f"/{app_root.lstrip('/')}"
77 )
78 path = url.path
80 if url.query:
81 sep = b"?" if isinstance(url.query, bytes) else "?"
82 path += sep + url.query
84 self.app = app
85 super().__init__(path, base_url, *args, **kwargs)
87 def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
88 """Serialize ``obj`` to a JSON-formatted string.
90 The serialization will be configured according to the config associated
91 with this EnvironBuilder's ``app``.
92 """
93 return self.app.json.dumps(obj, **kwargs)
96_werkzeug_version = ""
99def _get_werkzeug_version() -> str:
100 global _werkzeug_version
102 if not _werkzeug_version:
103 _werkzeug_version = importlib.metadata.version("werkzeug")
105 return _werkzeug_version
108class FlaskClient(Client):
109 """Works like a regular Werkzeug test client but has knowledge about
110 Flask's contexts to defer the cleanup of the request context until
111 the end of a ``with`` block. For general information about how to
112 use this class refer to :class:`werkzeug.test.Client`.
114 .. versionchanged:: 0.12
115 `app.test_client()` includes preset default environment, which can be
116 set after instantiation of the `app.test_client()` object in
117 `client.environ_base`.
119 Basic usage is outlined in the :doc:`/testing` chapter.
120 """
122 application: Flask
124 def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
125 super().__init__(*args, **kwargs)
126 self.preserve_context = False
127 self._new_contexts: list[t.ContextManager[t.Any]] = []
128 self._context_stack = ExitStack()
129 self.environ_base = {
130 "REMOTE_ADDR": "127.0.0.1",
131 "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}",
132 }
134 @contextmanager
135 def session_transaction(
136 self, *args: t.Any, **kwargs: t.Any
137 ) -> t.Generator[SessionMixin, None, None]:
138 """When used in combination with a ``with`` statement this opens a
139 session transaction. This can be used to modify the session that
140 the test client uses. Once the ``with`` block is left the session is
141 stored back.
143 ::
145 with client.session_transaction() as session:
146 session['value'] = 42
148 Internally this is implemented by going through a temporary test
149 request context and since session handling could depend on
150 request variables this function accepts the same arguments as
151 :meth:`~flask.Flask.test_request_context` which are directly
152 passed through.
153 """
154 if self._cookies is None:
155 raise TypeError(
156 "Cookies are disabled. Create a client with 'use_cookies=True'."
157 )
159 app = self.application
160 ctx = app.test_request_context(*args, **kwargs)
161 self._add_cookies_to_wsgi(ctx.request.environ)
163 with ctx:
164 sess = app.session_interface.open_session(app, ctx.request)
166 if sess is None:
167 raise RuntimeError("Session backend did not open a session.")
169 yield sess
170 resp = app.response_class()
172 if app.session_interface.is_null_session(sess):
173 return
175 with ctx:
176 app.session_interface.save_session(app, sess, resp)
178 self._update_cookies_from_response(
179 ctx.request.host.partition(":")[0],
180 ctx.request.path,
181 resp.headers.getlist("Set-Cookie"),
182 )
184 def _copy_environ(self, other):
185 out = {**self.environ_base, **other}
187 if self.preserve_context:
188 out["werkzeug.debug.preserve_context"] = self._new_contexts.append
190 return out
192 def _request_from_builder_args(self, args, kwargs):
193 kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {}))
194 builder = EnvironBuilder(self.application, *args, **kwargs)
196 try:
197 return builder.get_request()
198 finally:
199 builder.close()
201 def open(
202 self,
203 *args: t.Any,
204 buffered: bool = False,
205 follow_redirects: bool = False,
206 **kwargs: t.Any,
207 ) -> TestResponse:
208 if args and isinstance(
209 args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest)
210 ):
211 if isinstance(args[0], werkzeug.test.EnvironBuilder):
212 builder = copy(args[0])
213 builder.environ_base = self._copy_environ(builder.environ_base or {})
214 request = builder.get_request()
215 elif isinstance(args[0], dict):
216 request = EnvironBuilder.from_environ(
217 args[0], app=self.application, environ_base=self._copy_environ({})
218 ).get_request()
219 else:
220 # isinstance(args[0], BaseRequest)
221 request = copy(args[0])
222 request.environ = self._copy_environ(request.environ)
223 else:
224 # request is None
225 request = self._request_from_builder_args(args, kwargs)
227 # Pop any previously preserved contexts. This prevents contexts
228 # from being preserved across redirects or multiple requests
229 # within a single block.
230 self._context_stack.close()
232 response = super().open(
233 request,
234 buffered=buffered,
235 follow_redirects=follow_redirects,
236 )
237 response.json_module = self.application.json # type: ignore[assignment]
239 # Re-push contexts that were preserved during the request.
240 while self._new_contexts:
241 cm = self._new_contexts.pop()
242 self._context_stack.enter_context(cm)
244 return response
246 def __enter__(self) -> FlaskClient:
247 if self.preserve_context:
248 raise RuntimeError("Cannot nest client invocations")
249 self.preserve_context = True
250 return self
252 def __exit__(
253 self,
254 exc_type: type | None,
255 exc_value: BaseException | None,
256 tb: TracebackType | None,
257 ) -> None:
258 self.preserve_context = False
259 self._context_stack.close()
262class FlaskCliRunner(CliRunner):
263 """A :class:`~click.testing.CliRunner` for testing a Flask app's
264 CLI commands. Typically created using
265 :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
266 """
268 def __init__(self, app: Flask, **kwargs: t.Any) -> None:
269 self.app = app
270 super().__init__(**kwargs)
272 def invoke( # type: ignore
273 self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any
274 ) -> t.Any:
275 """Invokes a CLI command in an isolated environment. See
276 :meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for
277 full method documentation. See :ref:`testing-cli` for examples.
279 If the ``obj`` argument is not given, passes an instance of
280 :class:`~flask.cli.ScriptInfo` that knows how to load the Flask
281 app being tested.
283 :param cli: Command object to invoke. Default is the app's
284 :attr:`~flask.app.Flask.cli` group.
285 :param args: List of strings to invoke the command with.
287 :return: a :class:`~click.testing.Result` object.
288 """
289 if cli is None:
290 cli = self.app.cli # type: ignore
292 if "obj" not in kwargs:
293 kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
295 return super().invoke(cli, args, **kwargs)