Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/testing.py: 58%
106 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
1from __future__ import annotations
3import typing as t
4from contextlib import contextmanager
5from contextlib import ExitStack
6from copy import copy
7from types import TracebackType
8from urllib.parse import urlsplit
10import werkzeug.test
11from click.testing import CliRunner
12from werkzeug.test import Client
13from werkzeug.wrappers import Request as BaseRequest
15from .cli import ScriptInfo
16from .sessions import SessionMixin
18if t.TYPE_CHECKING: # pragma: no cover
19 from werkzeug.test import TestResponse
21 from .app import Flask
24class EnvironBuilder(werkzeug.test.EnvironBuilder):
25 """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the
26 application.
28 :param app: The Flask application to configure the environment from.
29 :param path: URL path being requested.
30 :param base_url: Base URL where the app is being served, which
31 ``path`` is relative to. If not given, built from
32 :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
33 :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
34 :param subdomain: Subdomain name to append to :data:`SERVER_NAME`.
35 :param url_scheme: Scheme to use instead of
36 :data:`PREFERRED_URL_SCHEME`.
37 :param json: If given, this is serialized as JSON and passed as
38 ``data``. Also defaults ``content_type`` to
39 ``application/json``.
40 :param args: other positional arguments passed to
41 :class:`~werkzeug.test.EnvironBuilder`.
42 :param kwargs: other keyword arguments passed to
43 :class:`~werkzeug.test.EnvironBuilder`.
44 """
46 def __init__(
47 self,
48 app: Flask,
49 path: str = "/",
50 base_url: str | None = None,
51 subdomain: str | None = None,
52 url_scheme: str | None = None,
53 *args: t.Any,
54 **kwargs: t.Any,
55 ) -> None:
56 assert not (base_url or subdomain or url_scheme) or (
57 base_url is not None
58 ) != bool(
59 subdomain or url_scheme
60 ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
62 if base_url is None:
63 http_host = app.config.get("SERVER_NAME") or "localhost"
64 app_root = app.config["APPLICATION_ROOT"]
66 if subdomain:
67 http_host = f"{subdomain}.{http_host}"
69 if url_scheme is None:
70 url_scheme = app.config["PREFERRED_URL_SCHEME"]
72 url = urlsplit(path)
73 base_url = (
74 f"{url.scheme or url_scheme}://{url.netloc or http_host}"
75 f"/{app_root.lstrip('/')}"
76 )
77 path = url.path
79 if url.query:
80 sep = b"?" if isinstance(url.query, bytes) else "?"
81 path += sep + url.query
83 self.app = app
84 super().__init__(path, base_url, *args, **kwargs)
86 def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
87 """Serialize ``obj`` to a JSON-formatted string.
89 The serialization will be configured according to the config associated
90 with this EnvironBuilder's ``app``.
91 """
92 return self.app.json.dumps(obj, **kwargs)
95class FlaskClient(Client):
96 """Works like a regular Werkzeug test client but has knowledge about
97 Flask's contexts to defer the cleanup of the request context until
98 the end of a ``with`` block. For general information about how to
99 use this class refer to :class:`werkzeug.test.Client`.
101 .. versionchanged:: 0.12
102 `app.test_client()` includes preset default environment, which can be
103 set after instantiation of the `app.test_client()` object in
104 `client.environ_base`.
106 Basic usage is outlined in the :doc:`/testing` chapter.
107 """
109 application: Flask
111 def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
112 super().__init__(*args, **kwargs)
113 self.preserve_context = False
114 self._new_contexts: list[t.ContextManager[t.Any]] = []
115 self._context_stack = ExitStack()
116 self.environ_base = {
117 "REMOTE_ADDR": "127.0.0.1",
118 "HTTP_USER_AGENT": f"werkzeug/{werkzeug.__version__}",
119 }
121 @contextmanager
122 def session_transaction(
123 self, *args: t.Any, **kwargs: t.Any
124 ) -> t.Generator[SessionMixin, None, None]:
125 """When used in combination with a ``with`` statement this opens a
126 session transaction. This can be used to modify the session that
127 the test client uses. Once the ``with`` block is left the session is
128 stored back.
130 ::
132 with client.session_transaction() as session:
133 session['value'] = 42
135 Internally this is implemented by going through a temporary test
136 request context and since session handling could depend on
137 request variables this function accepts the same arguments as
138 :meth:`~flask.Flask.test_request_context` which are directly
139 passed through.
140 """
141 if self._cookies is None:
142 raise TypeError(
143 "Cookies are disabled. Create a client with 'use_cookies=True'."
144 )
146 app = self.application
147 ctx = app.test_request_context(*args, **kwargs)
148 self._add_cookies_to_wsgi(ctx.request.environ)
150 with ctx:
151 sess = app.session_interface.open_session(app, ctx.request)
153 if sess is None:
154 raise RuntimeError("Session backend did not open a session.")
156 yield sess
157 resp = app.response_class()
159 if app.session_interface.is_null_session(sess):
160 return
162 with ctx:
163 app.session_interface.save_session(app, sess, resp)
165 self._update_cookies_from_response(
166 ctx.request.host.partition(":")[0],
167 ctx.request.path,
168 resp.headers.getlist("Set-Cookie"),
169 )
171 def _copy_environ(self, other):
172 out = {**self.environ_base, **other}
174 if self.preserve_context:
175 out["werkzeug.debug.preserve_context"] = self._new_contexts.append
177 return out
179 def _request_from_builder_args(self, args, kwargs):
180 kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {}))
181 builder = EnvironBuilder(self.application, *args, **kwargs)
183 try:
184 return builder.get_request()
185 finally:
186 builder.close()
188 def open(
189 self,
190 *args: t.Any,
191 buffered: bool = False,
192 follow_redirects: bool = False,
193 **kwargs: t.Any,
194 ) -> TestResponse:
195 if args and isinstance(
196 args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest)
197 ):
198 if isinstance(args[0], werkzeug.test.EnvironBuilder):
199 builder = copy(args[0])
200 builder.environ_base = self._copy_environ(builder.environ_base or {})
201 request = builder.get_request()
202 elif isinstance(args[0], dict):
203 request = EnvironBuilder.from_environ(
204 args[0], app=self.application, environ_base=self._copy_environ({})
205 ).get_request()
206 else:
207 # isinstance(args[0], BaseRequest)
208 request = copy(args[0])
209 request.environ = self._copy_environ(request.environ)
210 else:
211 # request is None
212 request = self._request_from_builder_args(args, kwargs)
214 # Pop any previously preserved contexts. This prevents contexts
215 # from being preserved across redirects or multiple requests
216 # within a single block.
217 self._context_stack.close()
219 response = super().open(
220 request,
221 buffered=buffered,
222 follow_redirects=follow_redirects,
223 )
224 response.json_module = self.application.json # type: ignore[assignment]
226 # Re-push contexts that were preserved during the request.
227 while self._new_contexts:
228 cm = self._new_contexts.pop()
229 self._context_stack.enter_context(cm)
231 return response
233 def __enter__(self) -> FlaskClient:
234 if self.preserve_context:
235 raise RuntimeError("Cannot nest client invocations")
236 self.preserve_context = True
237 return self
239 def __exit__(
240 self,
241 exc_type: type | None,
242 exc_value: BaseException | None,
243 tb: TracebackType | None,
244 ) -> None:
245 self.preserve_context = False
246 self._context_stack.close()
249class FlaskCliRunner(CliRunner):
250 """A :class:`~click.testing.CliRunner` for testing a Flask app's
251 CLI commands. Typically created using
252 :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
253 """
255 def __init__(self, app: Flask, **kwargs: t.Any) -> None:
256 self.app = app
257 super().__init__(**kwargs)
259 def invoke( # type: ignore
260 self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any
261 ) -> t.Any:
262 """Invokes a CLI command in an isolated environment. See
263 :meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for
264 full method documentation. See :ref:`testing-cli` for examples.
266 If the ``obj`` argument is not given, passes an instance of
267 :class:`~flask.cli.ScriptInfo` that knows how to load the Flask
268 app being tested.
270 :param cli: Command object to invoke. Default is the app's
271 :attr:`~flask.app.Flask.cli` group.
272 :param args: List of strings to invoke the command with.
274 :return: a :class:`~click.testing.Result` object.
275 """
276 if cli is None:
277 cli = self.app.cli # type: ignore
279 if "obj" not in kwargs:
280 kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
282 return super().invoke(cli, args, **kwargs)