diff --git a/CHANGES.rst b/CHANGES.rst index 0948bc19..77190adf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,6 +28,8 @@ Unreleased - The ``app.got_first_request`` property is deprecated. :pr:`4997` - The ``locked_cached_property`` decorator is deprecated. Use a lock inside the decorated function if locking is needed. :issue:`4993` +- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The + ``signals_available`` attribute is deprecated. :issue:`5056` - Remove uses of locks that could cause requests to block each other very briefly. :issue:`4993` - Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. diff --git a/docs/api.rst b/docs/api.rst index 8729f6cd..043beb07 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -333,14 +333,9 @@ Useful Internals Signals ------- -.. versionadded:: 0.6 +Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. -.. data:: signals.signals_available - - ``True`` if the signaling system is available. This is the case - when `blinker`_ is installed. - -The following signals exist in Flask: +.. _blinker: https://blinker.readthedocs.io/ .. data:: template_rendered @@ -507,7 +502,6 @@ The following signals exist in Flask: .. versionadded:: 0.10 - .. data:: message_flashed This signal is sent when the application is flashing a message. The @@ -525,22 +519,10 @@ The following signals exist in Flask: .. versionadded:: 0.10 -.. class:: signals.Namespace +.. data:: signals.signals_available - An alias for :class:`blinker.base.Namespace` if blinker is available, - otherwise a dummy class that creates fake signals. This class is - available for Flask extensions that want to provide the same fallback - system as Flask itself. - - .. method:: signal(name, doc=None) - - Creates a new signal for this namespace if blinker is available, - otherwise returns a fake signal that has a send method that will - do nothing but will fail with a :exc:`RuntimeError` for all other - operations, including connecting. - - -.. _blinker: https://pypi.org/project/blinker/ + .. deprecated:: 2.3 + Will be removed in Flask 2.4. Signals are always available Class-Based Views diff --git a/docs/appcontext.rst b/docs/appcontext.rst index a4ae3861..5509a9a7 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -140,10 +140,8 @@ Accessing ``db`` will call ``get_db`` internally, in the same way that Events and Signals ------------------ -The application will call functions registered with -:meth:`~Flask.teardown_appcontext` when the application context is -popped. +The application will call functions registered with :meth:`~Flask.teardown_appcontext` +when the application context is popped. -If :data:`~signals.signals_available` is true, the following signals are -sent: :data:`appcontext_pushed`, :data:`appcontext_tearing_down`, and -:data:`appcontext_popped`. +The following signals are sent: :data:`appcontext_pushed`, +:data:`appcontext_tearing_down`, and :data:`appcontext_popped`. diff --git a/docs/installation.rst b/docs/installation.rst index 52f5f3ed..0c9b1a47 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -24,12 +24,14 @@ These distributions will be installed automatically when installing Flask. to protect Flask's session cookie. * `Click`_ is a framework for writing command line applications. It provides the ``flask`` command and allows adding custom management commands. +* `Blinker`_ provides support for :doc:`signals`. .. _Werkzeug: https://palletsprojects.com/p/werkzeug/ .. _Jinja: https://palletsprojects.com/p/jinja/ .. _MarkupSafe: https://palletsprojects.com/p/markupsafe/ .. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ .. _Click: https://palletsprojects.com/p/click/ +.. _Blinker: https://blinker.readthedocs.io/ Optional dependencies @@ -38,13 +40,11 @@ Optional dependencies These distributions will not be installed automatically. Flask will detect and use them if you install them. -* `Blinker`_ provides support for :doc:`signals`. * `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` commands. * `Watchdog`_ provides a faster, more efficient reloader for the development server. -.. _Blinker: https://blinker.readthedocs.io/en/stable/ .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme .. _watchdog: https://pythonhosted.org/watchdog/ diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 70ea13e3..4f1846a3 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -204,21 +204,16 @@ contexts until the ``with`` block exits. Signals ~~~~~~~ -If :data:`~signals.signals_available` is true, the following signals are -sent: +The following signals are sent: -#. :data:`request_started` is sent before the - :meth:`~Flask.before_request` functions are called. - -#. :data:`request_finished` is sent after the - :meth:`~Flask.after_request` functions are called. - -#. :data:`got_request_exception` is sent when an exception begins to - be handled, but before an :meth:`~Flask.errorhandler` is looked up or - called. - -#. :data:`request_tearing_down` is sent after the - :meth:`~Flask.teardown_request` functions are called. +#. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions + are called. +#. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions + are called. +#. :data:`got_request_exception` is sent when an exception begins to be handled, but + before an :meth:`~Flask.errorhandler` is looked up or called. +#. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request` + functions are called. .. _notes-on-proxies: diff --git a/docs/signals.rst b/docs/signals.rst index 27630de6..3ba12a5a 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -1,33 +1,28 @@ Signals ======= -.. versionadded:: 0.6 +Signals are a lightweight way to notify subscribers of certain events during the +lifecycle of the application and each request. When an event occurs, it emits the +signal, which calls each subscriber. -Starting with Flask 0.6, there is integrated support for signalling in -Flask. This support is provided by the excellent `blinker`_ library and -will gracefully fall back if it is not available. +Signals are implemented by the `Blinker`_ library. See its documentation for detailed +information. Flask provides some built-in signals. Extensions may provide their own. -What are signals? Signals help you decouple applications by sending -notifications when actions occur elsewhere in the core framework or -another Flask extensions. In short, signals allow certain senders to -notify subscribers that something happened. +Many signals mirror Flask's decorator-based callbacks with similar names. For example, +the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` +decorator. The advantage of signals over handlers is that they can be subscribed to +temporarily, and can't directly affect the application. This is useful for testing, +metrics, auditing, and more. For example, if you want to know what templates were +rendered at what parts of what requests, there is a signal that will notify you of that +information. -Flask comes with a couple of signals and other extensions might provide -more. Also keep in mind that signals are intended to notify subscribers -and should not encourage subscribers to modify data. You will notice that -there are signals that appear to do the same thing like some of the -builtin decorators do (eg: :data:`~flask.request_started` is very similar -to :meth:`~flask.Flask.before_request`). However, there are differences in -how they work. The core :meth:`~flask.Flask.before_request` handler, for -example, is executed in a specific order and is able to abort the request -early by returning a response. In contrast all signal handlers are -executed in undefined order and do not modify any data. -The big advantage of signals over handlers is that you can safely -subscribe to them for just a split second. These temporary -subscriptions are helpful for unit testing for example. Say you want to -know what templates were rendered as part of a request: signals allow you -to do exactly that. +Core Signals +------------ + +See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` +page also describes the order that signals and decorators execute. + Subscribing to Signals ---------------------- @@ -99,11 +94,6 @@ The example above would then look like this:: ... template, context = templates[0] -.. admonition:: Blinker API Changes - - The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker - with version 1.1. - Creating Signals ---------------- @@ -123,12 +113,6 @@ The name for the signal here makes it unique and also simplifies debugging. You can access the name of the signal with the :attr:`~blinker.base.NamedSignal.name` attribute. -.. admonition:: For Extension Developers - - If you are writing a Flask extension and you want to gracefully degrade for - missing blinker installations, you can do so by using the - :class:`flask.signals.Namespace` class. - .. _signals-sending: Sending Signals @@ -170,7 +154,7 @@ in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. Decorator Based Signal Subscriptions ------------------------------------ -With Blinker 1.1 you can also easily subscribe to signals by using the new +You can also easily subscribe to signals by using the :meth:`~blinker.base.NamedSignal.connect_via` decorator:: from flask import template_rendered @@ -179,10 +163,5 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new def when_template_rendered(sender, template, context, **extra): print(f'Template {template.name} is rendered with {context}') -Core Signals ------------- - -Take a look at :ref:`core-signals-list` for a list of all builtin signals. - .. _blinker: https://pypi.org/project/blinker/ diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml index ce326ea7..e74415b8 100644 --- a/examples/javascript/pyproject.toml +++ b/examples/javascript/pyproject.toml @@ -11,7 +11,7 @@ dependencies = ["flask"] Documentation = "https://flask.palletsprojects.com/patterns/jquery/" [project.optional-dependencies] -test = ["pytest", "blinker"] +test = ["pytest"] [build-system] requires = ["setuptools"] diff --git a/pyproject.toml b/pyproject.toml index b80ad3ae..d8b293a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "Jinja2>=3.0", "itsdangerous>=2.0", "click>=8.0", + "blinker>=1.6.2", "importlib-metadata>=3.6.0; python_version < '3.10'", ] dynamic = ["version"] @@ -90,7 +91,6 @@ warn_unused_ignores = true [[tool.mypy.overrides]] module = [ "asgiref.*", - "blinker.*", "dotenv.*", "cryptography.*", "importlib_metadata", diff --git a/requirements/build.txt b/requirements/build.txt index e9cdf9da..1566dc07 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -11,5 +11,3 @@ packaging==23.0 # via build pyproject-hooks==1.0.0 # via build -tomli==2.0.1 - # via build diff --git a/requirements/tests-pallets-min.in b/requirements/tests-pallets-min.in index 6c8a55d9..c2916d81 100644 --- a/requirements/tests-pallets-min.in +++ b/requirements/tests-pallets-min.in @@ -3,3 +3,4 @@ Jinja2==3.0.0 MarkupSafe==2.0.0 itsdangerous==2.0.0 click==8.0.0 +blinker==1.6.2 diff --git a/requirements/tests-pallets-min.txt b/requirements/tests-pallets-min.txt index 64f0e1ce..baf4cf47 100644 --- a/requirements/tests-pallets-min.txt +++ b/requirements/tests-pallets-min.txt @@ -1,10 +1,12 @@ -# SHA1:4de7d9e6254a945fd97ec10880dd23b6cd43b70d +# SHA1:575f86f45391b662630a6080f0a12676215eb0cf # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # +blinker==1.6.2 + # via -r requirements/tests-pallets-min.in click==8.0.0 # via -r requirements/tests-pallets-min.in itsdangerous==2.0.0 diff --git a/requirements/tests.in b/requirements/tests.in index 87f72802..25f6b5bd 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -1,5 +1,4 @@ pytest asgiref -blinker greenlet ; python_version < "3.11" python-dotenv>=1; python_version >= "3.8" diff --git a/requirements/tests.txt b/requirements/tests.txt index fc9273ca..27ca92fc 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,4 @@ -# SHA1:30698f5f4f9cba5088318306829a15b0dc123b38 +# SHA1:3c8dde35aba20388b22430b17974af8ef8205b4f # # This file is autogenerated by pip-compile-multi # To update, run: @@ -7,12 +7,6 @@ # asgiref==3.6.0 # via -r requirements/tests.in -blinker==1.6.1 - # via -r requirements/tests.in -exceptiongroup==1.1.1 - # via pytest -greenlet==2.0.2 ; python_version < "3.11" - # via -r requirements/tests.in iniconfig==2.0.0 # via pytest packaging==23.0 @@ -23,7 +17,3 @@ pytest==7.3.0 # via -r requirements/tests.in python-dotenv==1.0.0 ; python_version >= "3.8" # via -r requirements/tests.in -tomli==2.0.1 - # via pytest -typing-extensions==4.5.0 - # via blinker diff --git a/requirements/typing.txt b/requirements/typing.txt index cec76ee3..b75f5308 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -15,8 +15,6 @@ mypy-extensions==1.0.0 # via mypy pycparser==2.21 # via cffi -tomli==2.0.1 - # via mypy types-contextvars==2.4.7.2 # via -r requirements/typing.in types-dataclasses==0.6.6 diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 2361bdb4..4441b95a 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -32,7 +32,6 @@ from .signals import message_flashed as message_flashed from .signals import request_finished as request_finished from .signals import request_started as request_started from .signals import request_tearing_down as request_tearing_down -from .signals import signals_available as signals_available from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string @@ -89,4 +88,15 @@ def __getattr__(name): ) return Markup + if name == "signals_available": + import warnings + + warnings.warn( + "'signals_available' is deprecated and will be removed in Flask 2.4." + " Signals are always available", + DeprecationWarning, + stacklevel=2, + ) + return True + raise AttributeError(name) diff --git a/src/flask/signals.py b/src/flask/signals.py index 2c6d6469..d79f21f9 100644 --- a/src/flask/signals.py +++ b/src/flask/signals.py @@ -1,49 +1,13 @@ +from __future__ import annotations + import typing as t +import warnings -try: - from blinker import Namespace +from blinker import Namespace - signals_available = True -except ImportError: - signals_available = False - - class Namespace: # type: ignore - def signal(self, name: str, doc: t.Optional[str] = None) -> "_FakeSignal": - return _FakeSignal(name, doc) - - class _FakeSignal: - """If blinker is unavailable, create a fake class with the same - interface that allows sending of signals but will fail with an - error on anything else. Instead of doing anything on send, it - will just ignore the arguments and do nothing instead. - """ - - def __init__(self, name: str, doc: t.Optional[str] = None) -> None: - self.name = name - self.__doc__ = doc - - def send(self, *args: t.Any, **kwargs: t.Any) -> t.Any: - pass - - def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.Any: - raise RuntimeError( - "Signalling support is unavailable because the blinker" - " library is not installed." - ) from None - - connect = connect_via = connected_to = temporarily_connected_to = _fail - disconnect = _fail - has_receivers_for = receivers_for = _fail - del _fail - - -# The namespace for code signals. If you are not Flask code, do -# not put signals in here. Create your own namespace instead. +# This namespace is only for signals provided by Flask itself. _signals = Namespace() - -# Core signals. For usage examples grep the source code or consult -# the API documentation in docs/api.rst as well as docs/signals.rst template_rendered = _signals.signal("template-rendered") before_render_template = _signals.signal("before-render-template") request_started = _signals.signal("request-started") @@ -54,3 +18,16 @@ appcontext_tearing_down = _signals.signal("appcontext-tearing-down") appcontext_pushed = _signals.signal("appcontext-pushed") appcontext_popped = _signals.signal("appcontext-popped") message_flashed = _signals.signal("message-flashed") + + +def __getattr__(name: str) -> t.Any: + if name == "signals_available": + warnings.warn( + "The 'signals_available' attribute is deprecated and will be removed in" + " Flask 2.4. Signals are always available.", + DeprecationWarning, + stacklevel=2, + ) + return True + + raise AttributeError(name) diff --git a/tests/test_signals.py b/tests/test_signals.py index 8aa69836..6174fe83 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,16 +1,5 @@ -import pytest - -try: - import blinker -except ImportError: - blinker = None - import flask -pytestmark = pytest.mark.skipif( - blinker is None, reason="Signals require the blinker library." -) - def test_template_rendered(app, client): @app.route("/") diff --git a/tests/test_testing.py b/tests/test_testing.py index 14c5ade0..8703e047 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -10,11 +10,6 @@ from flask.json import jsonify from flask.testing import EnvironBuilder from flask.testing import FlaskCliRunner -try: - import blinker -except ImportError: - blinker = None - def test_environ_defaults_from_config(app, client): app.config["SERVER_NAME"] = "example.com:1234" @@ -285,7 +280,6 @@ def test_json_request_and_response(app, client): assert rv.get_json() == json_data -@pytest.mark.skipif(blinker is None, reason="blinker is not installed") def test_client_json_no_app_context(app, client): @app.route("/hello", methods=["POST"]) def hello():