Flask
Advanced tools
+125
| Async with Gevent | ||
| ================= | ||
| `Gevent`_ patches Python's standard library to run within special async workers | ||
| called `greenlets`_. Gevent has existed since long before Python's native | ||
| asyncio was available, and Flask has always worked with it. | ||
| .. _gevent: https://www.gevent.org | ||
| .. _greenlets: https://greenlet.readthedocs.io | ||
| Gevent is a reliable way to handle numerous, long lived, concurrent connections, | ||
| and to achieve similar capabilities to ASGI and asyncio. This works without | ||
| needing to write ``async def`` or ``await`` anywhere, but relies on gevent and | ||
| greenlet's low level manipulation of the Python interpreter. | ||
| Deciding whether you should use gevent with Flask, or `Quart`_, or something | ||
| else, is ultimately up to understanding the specific needs of your project. | ||
| .. _quart: https://quart.palletsprojects.com | ||
| Enabling gevent | ||
| --------------- | ||
| You need to apply gevent's patching as early as possible in your code. This | ||
| enables gevent's underlying event loop and converts many Python internals to run | ||
| inside it. Add the following at the top of your project's module or top | ||
| ``__init__.py``: | ||
| .. code-block:: python | ||
| import gevent.monkey | ||
| gevent.monkey.patch_all() | ||
| When deploying in production, use :doc:`/deploying/gunicorn` or | ||
| :doc:`/deploying/uwsgi` with a gevent worker, as described on those pages. | ||
| To run concurrent tasks within your own code, such as views, use | ||
| |gevent.spawn|_: | ||
| .. |gevent.spawn| replace:: ``gevent.spawn()`` | ||
| .. _gevent.spawn: https://www.gevent.org/api/gevent.html#gevent.spawn | ||
| .. code-block:: python | ||
| @app.post("/send") | ||
| def send_email(): | ||
| gevent.spawn(email.send, to="example@example.example", text="example") | ||
| return "Email is being sent." | ||
| If you need to access :data:`request` or other Flask context globals within the | ||
| spawned function, decorate the function with :func:`.stream_with_context` or | ||
| :func:`.copy_current_request_context`. Prefer passing the exact data you need | ||
| when spawning the function, rather than using the decorators. | ||
| .. note:: | ||
| When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 | ||
| is required. | ||
| .. _gevent-asyncio: | ||
| Combining with ``async``/``await`` | ||
| ---------------------------------- | ||
| Gevent's patching does not interact well with Flask's built-in asyncio support. | ||
| If you want to use Gevent and asyncio in the same app, you'll need to override | ||
| :meth:`flask.Flask.async_to_sync` to run async functions inside gevent. | ||
| .. code-block:: python | ||
| import gevent.monkey | ||
| gevent.monkey.patch_all() | ||
| import asyncio | ||
| from flask import Flask, request | ||
| loop = asyncio.EventLoop() | ||
| gevent.spawn(loop.run_forever) | ||
| class GeventFlask(Flask): | ||
| def async_to_sync(self, func): | ||
| def run(*args, **kwargs): | ||
| coro = func(*args, **kwargs) | ||
| future = asyncio.run_coroutine_threadsafe(coro, loop) | ||
| return future.result() | ||
| return run | ||
| app = GeventFlask(__name__) | ||
| @app.get("/") | ||
| async def greet(): | ||
| await asyncio.sleep(1) | ||
| return f"Hello, {request.args.get("name", "World")}!" | ||
| This starts an asyncio event loop in a gevent worker. Async functions are | ||
| scheduled on that event loop. This may still have limitations, and may need to | ||
| be modified further when using other asyncio implementations. | ||
| libuv | ||
| ~~~~~ | ||
| `libuv`_ is another event loop implementation that `gevent supports`_. There's | ||
| also a project called `uvloop`_ that enables libuv in asyncio. If you want to | ||
| use libuv, use gevent's support, not uvloop. It may be possible to further | ||
| modify the ``async_to_sync`` code from the previous section to work with uvloop, | ||
| but that's not currently known. | ||
| .. _libuv: https://libuv.org/ | ||
| .. _gevent supports: https://www.gevent.org/loop_impls.html | ||
| .. _uvloop: https://uvloop.readthedocs.io/ | ||
| To enable gevent's libuv support, add the following at the *very* top of your | ||
| code, before ``gevent.monkey.patch_all()``: | ||
| .. code-block:: python | ||
| import gevent | ||
| gevent.config.loop = "libuv" | ||
| import gevent.monkey | ||
| gevent.monkey.patch_all() |
+11
-17
@@ -26,9 +26,3 @@ .. _async_await: | ||
| .. admonition:: Using ``async`` with greenlet | ||
| When using gevent or eventlet to serve an application or patch the | ||
| runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is | ||
| required. | ||
| Performance | ||
@@ -82,11 +76,11 @@ ----------- | ||
| It has also already been possible to run Flask with Gevent or Eventlet | ||
| to get many of the benefits of async request handling. These libraries | ||
| patch low-level Python functions to accomplish this, whereas ``async``/ | ||
| ``await`` and ASGI use standard, modern Python capabilities. Deciding | ||
| whether you should use Flask, Quart, or something else is ultimately up | ||
| to understanding the specific needs of your project. | ||
| It has also already been possible to :doc:`run Flask with Gevent </gevent>` to | ||
| get many of the benefits of async request handling. Gevent patches low-level | ||
| Python functions to accomplish this, whereas ``async``/``await`` and ASGI use | ||
| standard, modern Python capabilities. Deciding whether you should use gevent | ||
| with Flask, or Quart, or something else is ultimately up to understanding the | ||
| specific needs of your project. | ||
| .. _Quart: https://github.com/pallets/quart | ||
| .. _ASGI: https://asgi.readthedocs.io/en/latest/ | ||
| .. _Quart: https://quart.palletsprojects.com | ||
| .. _ASGI: https://asgi.readthedocs.io | ||
@@ -125,4 +119,4 @@ | ||
| At the moment Flask only supports :mod:`asyncio`. It's possible to | ||
| override :meth:`flask.Flask.ensure_sync` to change how async functions | ||
| are wrapped to use a different library. | ||
| At the moment Flask only supports :mod:`asyncio`. It's possible to override | ||
| :meth:`flask.Flask.ensure_sync` to change how async functions are wrapped to use | ||
| a different library. See :ref:`gevent-asyncio` for an example. |
@@ -0,80 +1,8 @@ | ||
| :orphan: | ||
| eventlet | ||
| ======== | ||
| Prefer using :doc:`gunicorn` with eventlet workers rather than using | ||
| `eventlet`_ directly. Gunicorn provides a much more configurable and | ||
| production-tested server. | ||
| `Eventlet is no longer maintained.`__ Use :doc:`/deploying/gevent` instead. | ||
| `eventlet`_ allows writing asynchronous, coroutine-based code that looks | ||
| like standard synchronous Python. It uses `greenlet`_ to enable task | ||
| switching without writing ``async/await`` or using ``asyncio``. | ||
| :doc:`gevent` is another library that does the same thing. Certain | ||
| dependencies you have, or other considerations, may affect which of the | ||
| two you choose to use. | ||
| eventlet provides a WSGI server that can handle many connections at once | ||
| instead of one per worker process. You must actually use eventlet in | ||
| your own code to see any benefit to using the server. | ||
| .. _eventlet: https://eventlet.net/ | ||
| .. _greenlet: https://greenlet.readthedocs.io/en/latest/ | ||
| Installing | ||
| ---------- | ||
| When using eventlet, greenlet>=1.0 is required, otherwise context locals | ||
| such as ``request`` will not work as expected. When using PyPy, | ||
| PyPy>=7.3.7 is required. | ||
| Create a virtualenv, install your application, then install | ||
| ``eventlet``. | ||
| .. code-block:: text | ||
| $ cd hello-app | ||
| $ python -m venv .venv | ||
| $ . .venv/bin/activate | ||
| $ pip install . # install your application | ||
| $ pip install eventlet | ||
| Running | ||
| ------- | ||
| To use eventlet to serve your application, write a script that imports | ||
| its ``wsgi.server``, as well as your app or app factory. | ||
| .. code-block:: python | ||
| :caption: ``wsgi.py`` | ||
| import eventlet | ||
| from eventlet import wsgi | ||
| from hello import create_app | ||
| app = create_app() | ||
| wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app) | ||
| .. code-block:: text | ||
| $ python wsgi.py | ||
| (x) wsgi starting up on http://127.0.0.1:8000 | ||
| Binding Externally | ||
| ------------------ | ||
| eventlet should not be run as root because it would cause your | ||
| application code to run as root, which is not secure. However, this | ||
| means it will not be possible to bind to port 80 or 443. Instead, a | ||
| reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used | ||
| in front of eventlet. | ||
| You can bind to all external IPs on a non-privileged port by using | ||
| ``0.0.0.0`` in the server arguments shown in the previous section. | ||
| Don't do this when using a reverse proxy setup, otherwise it will be | ||
| possible to bypass the proxy. | ||
| ``0.0.0.0`` is not a valid address to navigate to, you'd use a specific | ||
| IP address in your browser. | ||
| __ https://eventlet.readthedocs.io |
@@ -10,11 +10,8 @@ gevent | ||
| like standard synchronous Python. It uses `greenlet`_ to enable task | ||
| switching without writing ``async/await`` or using ``asyncio``. | ||
| switching without writing ``async/await`` or using ``asyncio``. This is | ||
| not the same as Python's ``async/await``, or the ASGI server spec. | ||
| :doc:`eventlet` is another library that does the same thing. Certain | ||
| dependencies you have, or other considerations, may affect which of the | ||
| two you choose to use. | ||
| gevent provides a WSGI server that can handle many connections at once | ||
| instead of one per worker process. You must actually use gevent in your | ||
| own code to see any benefit to using the server. | ||
| instead of one per worker process. See :doc:`/gevent` for more | ||
| information about enabling it in your application. | ||
@@ -28,4 +25,3 @@ .. _gevent: https://www.gevent.org/ | ||
| When using gevent, greenlet>=1.0 is required, otherwise context locals | ||
| such as ``request`` will not work as expected. When using PyPy, | ||
| When using gevent, greenlet>=1.0 is required. When using PyPy, | ||
| PyPy>=7.3.7 is required. | ||
@@ -32,0 +28,0 @@ |
@@ -11,3 +11,3 @@ Gunicorn | ||
| or compilation. | ||
| * It has built-in async worker support using gevent or eventlet. | ||
| * It has built-in async worker support using gevent. | ||
@@ -97,16 +97,15 @@ This page outlines the basics of running Gunicorn. Be sure to read its | ||
| Async with gevent or eventlet | ||
| ----------------------------- | ||
| Async with gevent | ||
| ----------------- | ||
| The default sync worker is appropriate for many use cases. If you need | ||
| asynchronous support, Gunicorn provides workers using either `gevent`_ | ||
| or `eventlet`_. This is not the same as Python's ``async/await``, or the | ||
| ASGI server spec. You must actually use gevent/eventlet in your own code | ||
| to see any benefit to using the workers. | ||
| The default sync worker is appropriate for most use cases. If you need numerous, | ||
| long running, concurrent connections, Gunicorn provides an asynchronous worker | ||
| using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI | ||
| server spec. See :doc:`/gevent` for more information about enabling it in your | ||
| application. | ||
| When using either gevent or eventlet, greenlet>=1.0 is required, | ||
| otherwise context locals such as ``request`` will not work as expected. | ||
| When using PyPy, PyPy>=7.3.7 is required. | ||
| .. _gevent: https://www.gevent.org/ | ||
| To use gevent: | ||
| When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is | ||
| required. | ||
@@ -120,14 +119,1 @@ .. code-block:: text | ||
| Booting worker with pid: x | ||
| To use eventlet: | ||
| .. code-block:: text | ||
| $ gunicorn -k eventlet 'hello:create_app()' | ||
| Starting gunicorn 20.1.0 | ||
| Listening at: http://127.0.0.1:8000 (x) | ||
| Using worker: eventlet | ||
| Booting worker with pid: x | ||
| .. _gevent: https://www.gevent.org/ | ||
| .. _eventlet: https://eventlet.net/ |
@@ -39,3 +39,2 @@ Deploying to Production | ||
| gevent | ||
| eventlet | ||
| asgi | ||
@@ -42,0 +41,0 @@ |
@@ -122,12 +122,13 @@ uWSGI | ||
| The default sync worker is appropriate for many use cases. If you need | ||
| asynchronous support, uWSGI provides a `gevent`_ worker. This is not the | ||
| same as Python's ``async/await``, or the ASGI server spec. You must | ||
| actually use gevent in your own code to see any benefit to using the | ||
| worker. | ||
| The default sync worker is appropriate for most use cases. If you need numerous, | ||
| long running, concurrent connections, uWSGI provides an asynchronous worker | ||
| using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI | ||
| server spec. See :doc:`/gevent` for more information about enabling it in your | ||
| application. | ||
| When using gevent, greenlet>=1.0 is required, otherwise context locals | ||
| such as ``request`` will not work as expected. When using PyPy, | ||
| PyPy>=7.3.7 is required. | ||
| .. _gevent: https://www.gevent.org/ | ||
| When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is | ||
| required. | ||
| .. code-block:: text | ||
@@ -144,4 +145,1 @@ | ||
| *** running gevent loop engine [addr:x] *** | ||
| .. _gevent: https://www.gevent.org/ |
+1
-0
@@ -64,2 +64,3 @@ .. rst-class:: hide-header | ||
| deploying/index | ||
| gevent | ||
| async-await | ||
@@ -66,0 +67,0 @@ |
@@ -54,5 +54,4 @@ Installation | ||
| You may choose to use gevent or eventlet with your application. In this | ||
| case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is | ||
| required. | ||
| You may choose to use :doc:`/gevent` with your application. In this case, | ||
| greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is required. | ||
@@ -59,0 +58,0 @@ These are not minimum supported versions, they only indicate the first |
@@ -139,3 +139,4 @@ JavaScript, ``fetch``, and JSON | ||
| sending JSON data, the ``Content-Type: application/json`` header must be | ||
| sent as well, otherwise Flask will return a 400 error. | ||
| sent as well, otherwise Flask will return a 415 Unsupported Media Type | ||
| error. | ||
@@ -248,4 +249,5 @@ .. code-block:: javascript | ||
| :data:`~flask.request` object to decode the request's body as JSON. If | ||
| the body is not valid JSON, or the ``Content-Type`` header is not set to | ||
| ``application/json``, a 400 Bad Request error will be raised. | ||
| the body is not valid JSON, a 400 Bad Request error will be raised. If | ||
| the ``Content-Type`` header is not set to ``application/json``, a 415 | ||
| Unsupported Media Type error will be raised. | ||
@@ -252,0 +254,0 @@ .. code-block:: python |
@@ -59,6 +59,3 @@ .. currentmodule:: flask | ||
| # ensure the instance folder exists | ||
| try: | ||
| os.makedirs(app.instance_path) | ||
| except OSError: | ||
| pass | ||
| os.makedirs(app.instance_path, exist_ok=True) | ||
@@ -65,0 +62,0 @@ # a simple page that says hello |
@@ -24,6 +24,3 @@ import os | ||
| # ensure the instance folder exists | ||
| try: | ||
| os.makedirs(app.instance_path) | ||
| except OSError: | ||
| pass | ||
| os.makedirs(app.instance_path, exist_ok=True) | ||
@@ -30,0 +27,0 @@ @app.route("/hello") |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: Flask | ||
| Version: 3.1.2 | ||
| Version: 3.1.3 | ||
| Summary: A simple framework for building complex web applications. | ||
@@ -5,0 +5,0 @@ Maintainer-email: Pallets <contact@palletsprojects.com> |
+9
-5
| [project] | ||
| name = "Flask" | ||
| version = "3.1.2" | ||
| version = "3.1.3" | ||
| description = "A simple framework for building complex web applications." | ||
@@ -45,3 +45,3 @@ readme = "README.md" | ||
| "pallets-sphinx-themes", | ||
| "sphinx", | ||
| "sphinx<9", | ||
| "sphinx-tabs", | ||
@@ -62,3 +62,3 @@ "sphinxcontrib-log-cabinet", | ||
| "asgiref", | ||
| "greenlet ; python_version < '3.11'", | ||
| "greenlet", | ||
| "pytest", | ||
@@ -89,3 +89,3 @@ "python-dotenv", | ||
| [build-system] | ||
| requires = ["flit_core<4"] | ||
| requires = ["flit_core>=3.11,<4"] | ||
| build-backend = "flit_core.buildapi" | ||
@@ -172,4 +172,8 @@ | ||
| [tool.codespell] | ||
| ignore-words-list = "te" | ||
| [tool.tox] | ||
| env_list = [ | ||
| "py3.14", "py3.14t", | ||
| "py3.13", "py3.12", "py3.11", "py3.10", "py3.9", | ||
@@ -199,3 +203,3 @@ "pypy3.11", | ||
| description = "pytest on minimum dependency versions" | ||
| base_python = ["3.13"] | ||
| base_python = ["3.14"] | ||
| commands = [ | ||
@@ -202,0 +206,0 @@ [ |
+4
-4
@@ -278,3 +278,3 @@ from __future__ import annotations | ||
| host=static_host, | ||
| view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 | ||
| view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore | ||
| ) | ||
@@ -1322,4 +1322,4 @@ | ||
| if not self.session_interface.is_null_session(ctx.session): | ||
| self.session_interface.save_session(self, ctx.session, response) | ||
| if not self.session_interface.is_null_session(ctx._session): | ||
| self.session_interface.save_session(self, ctx._session, response) | ||
@@ -1517,3 +1517,3 @@ return response | ||
| response = self.handle_exception(e) | ||
| except: # noqa: B001 | ||
| except: | ||
| error = sys.exc_info()[1] | ||
@@ -1520,0 +1520,0 @@ raise |
+16
-6
@@ -327,3 +327,3 @@ from __future__ import annotations | ||
| self.flashes: list[tuple[str, str]] | None = None | ||
| self.session: SessionMixin | None = session | ||
| self._session: SessionMixin | None = session | ||
| # Functions that should be executed after the request on the response | ||
@@ -355,3 +355,3 @@ # object. These will be called before the regular "after_request" | ||
| request=self.request, | ||
| session=self.session, | ||
| session=self._session, | ||
| ) | ||
@@ -369,2 +369,12 @@ | ||
| @property | ||
| def session(self) -> SessionMixin: | ||
| """The session data associated with this request. Not available until | ||
| this context has been pushed. Accessing this property, also accessed by | ||
| the :data:`~flask.session` proxy, sets :attr:`.SessionMixin.accessed`. | ||
| """ | ||
| assert self._session is not None, "The session has not yet been opened." | ||
| self._session.accessed = True | ||
| return self._session | ||
| def push(self) -> None: | ||
@@ -387,8 +397,8 @@ # Before we push the request context we have to ensure that there | ||
| # pushed, otherwise stream_with_context loses the session. | ||
| if self.session is None: | ||
| if self._session is None: | ||
| session_interface = self.app.session_interface | ||
| self.session = session_interface.open_session(self.app, self.request) | ||
| self._session = session_interface.open_session(self.app, self.request) | ||
| if self.session is None: | ||
| self.session = session_interface.make_null_session(self.app) | ||
| if self._session is None: | ||
| self._session = session_interface.make_null_session(self.app) | ||
@@ -395,0 +405,0 @@ # Match the request URL after loading the session, so that the |
@@ -536,3 +536,3 @@ from __future__ import annotations | ||
| def select_jinja_autoescape(self, filename: str) -> bool: | ||
| def select_jinja_autoescape(self, filename: str | None) -> bool: | ||
| """Returns ``True`` if autoescaping should be active for the given | ||
@@ -539,0 +539,0 @@ template name. If no template name is given, returns `True`. |
+12
-26
@@ -30,3 +30,3 @@ from __future__ import annotations | ||
| """This reflects the ``'_permanent'`` key in the dict.""" | ||
| return self.get("_permanent", False) | ||
| return self.get("_permanent", False) # type: ignore[no-any-return] | ||
@@ -47,8 +47,13 @@ @permanent.setter | ||
| #: Some implementations can detect when session data is read or | ||
| #: written and set this when that happens. The mixin default is hard | ||
| #: coded to ``True``. | ||
| accessed = True | ||
| accessed = False | ||
| """Indicates if the session was accessed, even if it was not modified. This | ||
| is set when the session object is accessed through the request context, | ||
| including the global :data:`.session` proxy. A ``Vary: cookie`` header will | ||
| be added if this is ``True``. | ||
| .. versionchanged:: 3.1.3 | ||
| This is tracked by the request context. | ||
| """ | ||
| class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): | ||
@@ -70,31 +75,12 @@ """Base class for sessions based on signed cookies. | ||
| #: When data is read or written, this is set to ``True``. Used by | ||
| # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` | ||
| #: header, which allows caching proxies to cache different pages for | ||
| #: different users. | ||
| accessed = False | ||
| def __init__( | ||
| self, | ||
| initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, | ||
| initial: c.Mapping[str, t.Any] | None = None, | ||
| ) -> None: | ||
| def on_update(self: te.Self) -> None: | ||
| self.modified = True | ||
| self.accessed = True | ||
| super().__init__(initial, on_update) | ||
| def __getitem__(self, key: str) -> t.Any: | ||
| self.accessed = True | ||
| return super().__getitem__(key) | ||
| def get(self, key: str, default: t.Any = None) -> t.Any: | ||
| self.accessed = True | ||
| return super().get(key, default) | ||
| def setdefault(self, key: str, default: t.Any = None) -> t.Any: | ||
| self.accessed = True | ||
| return super().setdefault(key, default) | ||
| class NullSession(SecureCookieSession): | ||
@@ -113,3 +99,3 @@ """Class used to generate nicer error messages if sessions are not | ||
| __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # noqa: B950 | ||
| __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail | ||
| del _fail | ||
@@ -116,0 +102,0 @@ |
@@ -25,4 +25,4 @@ from __future__ import annotations | ||
| def _default_template_ctx_processor() -> dict[str, t.Any]: | ||
| """Default template context processor. Injects `request`, | ||
| `session` and `g`. | ||
| """Default template context processor. Replaces the ``request`` and ``g`` | ||
| proxies with their concrete objects for faster access. | ||
| """ | ||
@@ -36,3 +36,4 @@ appctx = _cv_app.get(None) | ||
| rv["request"] = reqctx.request | ||
| rv["session"] = reqctx.session | ||
| # The session proxy cannot be replaced, accessing it gets | ||
| # RequestContext.session, which sets session.accessed. | ||
| return rv | ||
@@ -39,0 +40,0 @@ |
+38
-17
@@ -23,2 +23,4 @@ import gc | ||
| import flask | ||
| from flask.globals import request_ctx | ||
| from flask.testing import FlaskClient | ||
@@ -235,25 +237,44 @@ require_cpython_gc = pytest.mark.skipif( | ||
| def test_session(app, client): | ||
| @app.route("/set", methods=["POST"]) | ||
| def set(): | ||
| assert not flask.session.accessed | ||
| assert not flask.session.modified | ||
| def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None: | ||
| @app.post("/") | ||
| def do_set(): | ||
| flask.session["value"] = flask.request.form["value"] | ||
| assert flask.session.accessed | ||
| assert flask.session.modified | ||
| return "value set" | ||
| @app.route("/get") | ||
| def get(): | ||
| assert not flask.session.accessed | ||
| assert not flask.session.modified | ||
| v = flask.session.get("value", "None") | ||
| assert flask.session.accessed | ||
| assert not flask.session.modified | ||
| return v | ||
| @app.get("/") | ||
| def do_get(): | ||
| return flask.session.get("value", "None") | ||
| assert client.post("/set", data={"value": "42"}).data == b"value set" | ||
| assert client.get("/get").data == b"42" | ||
| @app.get("/nothing") | ||
| def do_nothing() -> str: | ||
| return "" | ||
| with client: | ||
| rv = client.get("/nothing") | ||
| assert "cookie" not in rv.vary | ||
| assert not request_ctx._session.accessed | ||
| assert not request_ctx._session.modified | ||
| with client: | ||
| rv = client.post(data={"value": "42"}) | ||
| assert rv.text == "value set" | ||
| assert "cookie" in rv.vary | ||
| assert request_ctx._session.accessed | ||
| assert request_ctx._session.modified | ||
| with client: | ||
| rv = client.get() | ||
| assert rv.text == "42" | ||
| assert "cookie" in rv.vary | ||
| assert request_ctx._session.accessed | ||
| assert not request_ctx._session.modified | ||
| with client: | ||
| rv = client.get("/nothing") | ||
| assert rv.text == "" | ||
| assert "cookie" not in rv.vary | ||
| assert not request_ctx._session.accessed | ||
| assert not request_ctx._session.modified | ||
| def test_session_path(app, client): | ||
@@ -260,0 +281,0 @@ app.config.update(APPLICATION_ROOT="/foo") |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
1885259
7.73%222
0.45%13889
0.09%