Skip to content

Instantly share code, notes, and snippets.

@amercader
Created May 22, 2020 12:02
Show Gist options
  • Save amercader/9c3524af1ef78bd2062cd136d954614f to your computer and use it in GitHub Desktop.
Save amercader/9c3524af1ef78bd2062cd136d954614f to your computer and use it in GitHub Desktop.
diff --git a/ckan/lib/signals.py b/ckan/lib/signals.py
index 681de8b92..22b4cc635 100644
--- a/ckan/lib/signals.py
+++ b/ckan/lib/signals.py
@@ -26,16 +26,16 @@ request processing happens.
request_finished = ckan.signal(u'request_finished')
"""This signal is sent right before the response is sent to the
client.
-
"""
register_blueprint = ckan.signal(u'register_blueprint')
-"""Blueprint for dataset/resoruce/group/organization is going to be
-registered inside application.
+"""This signal is sent when a blueprint for dataset/resource/group/organization
+is going to be registered inside the application.
"""
resource_download = ckan.signal(u'resource_download')
-"""File from uploaded resource will be sent to user.
+"""This signal is sent just before a file from an uploaded resource is sent
+to the user.
"""
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index 3acefc060..bb84a0489 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -1870,26 +1870,11 @@ class ISignal(Interface):
def get_signal_subscriptions(self):
"""Return a mapping of signals to their listeners.
- Resulting dict must be composed of the signal bound to the
- collection of listeners::
-
- def get_signal_subscriptions(self):
- from ckan.lib.signals import (
- request_started, register_blueprint
- )
-
- return {
- request_started: [request_listener],
- register_blueprint: [
- first_blueprint_listener,
- second_blueprint_listener
- ]
- }
-
- Note, keys are not stirngs, they are instances of
- ``blinker.Signal``. It better to use alias from
- ``ckan.plugins.toolkit`` if you want to support wider set of
- CKAN versions::
+ Note that keys are not strings, they are instances of
+ ``blinker.Signal``. When using signals provided by CKAN core, it is better
+ to use the references from the :doc:`plugins toolkit <plugins-toolkit>`
+ for better future compatibility. Values should be a list of listener
+ functions::
def get_signal_subscriptions(self):
import ckan.plugins.toolkit as tk
@@ -1906,23 +1891,24 @@ class ISignal(Interface):
]
}
- Listener is a callable, that accepts one mandatory
- argument(sender) and an arbitrary number of named
- arguments(context). The most optimal signature for the
- listener is ``def(sender, **kwargs)`` and one should use it in
- most cases(unless you are 100% sure you need something else).
-
- Sender will be different, depending on the signal and mostly
- it should be used for coditional applying of listener. For
- example, ``register_blueprint`` signal is sent every time,
- custom dataset/group/organization blueprint is registered
- inside application(using
- :class:`ckan.plugins.interfaces.IDatasetForm` or
- :class:`ckan.plugins.interfaces.IGroupForm`). Depending on
- kind of blueprint, sender may be 'dataset', 'group',
+ Listeners are callables that accept one mandatory
+ argument (``sender``) and an arbitrary number of
+ named arguments (text). The best signature for a listener is
+ ``def(sender, **kwargs)``.
+
+ The ``sender`` argument will be different depending on the signal
+ and will be generally used to conditionally executing code on the
+ listener. For example, the ``register_blueprint`` signal is sent every
+ time a custom dataset/group/organization blueprint is registered
+ (using :class:`ckan.plugins.interfaces.IDatasetForm`
+ or :class:`ckan.plugins.interfaces.IGroupForm`). Depending on
+ the kind of blueprint, ``sender`` may be 'dataset', 'group',
'organization' or 'resource'. If you want to do some work only
for 'dataset' blueprints, you may end up with something similar to::
+
+ import ckan.plugins.toolkit as tk
+
def dataset_blueprint_listener(sender, **kwargs):
if sender != 'dataset':
return
@@ -1932,7 +1918,6 @@ class ISignal(Interface):
plugins.implements(plugins.ISignal)
def get_signal_subscriptions(self):
- import ckan.plugins.toolkit as tk
return {
tk.signals.register_blueprint: [
@@ -1940,12 +1925,15 @@ class ISignal(Interface):
]
}
- Because this use-case is so popular, there is additional form
- of listener registration. Instead of plain callables, one can
+ Because this is a really common use case, there is additional form
+ of listener registration supported. Instead of just callables, one can
use dictionaries of form ``{'receiver': CALLABLE, 'sender':
- DESIRED_SENDER}``. Following code snippet is identical to the
+ DESIRED_SENDER}``. The following code snippet has the same effect than the
previous one::
+
+ import ckan.plugins.toolkit as tk
+
def dataset_blueprint_listener(sender, **kwargs):
# do something..
@@ -1953,7 +1941,6 @@ class ISignal(Interface):
plugins.implements(plugins.ISignal)
def get_signal_subscriptions(self):
- import ckan.plugins.toolkit as tk
return {
tk.signals.register_blueprint: [{
@@ -1962,40 +1949,40 @@ class ISignal(Interface):
}]
}
+ The two forms of registration can be mixed when multiple listeners are
+ registered, callables and dictionaries with ``receiver``/``sender`` keys::
- Even though it possible to change mutable arguments inside the
- listener, or return something from it, the original(and main)
- purpose of signals - performing some side effects, like
- logging, triggering of background jobs, calls to external
- services. Any mutation or attempt to change CKAN behavior
- through signals is very risky and may lead to numerous bugs in
- the future. So, never modify arguments of signal listener and
- treat them like constants.
-
- Also, always check the presence of the value inside context -
- signals will change with time as surrounding code changes, so
- some options may disappear.
-
- Finally, when multiple listeners are registered, plain
- callables and dictionaries with ``receiver``/``sender`` keys
- can be mixed::
+ import ckan.plugins.toolkit as tk
- def log_action(sender):
- log.info("Action call: %s" % action)
+ def log_registration(sender, **kwargs):
+ log.info("Log something")
class ExamplePlugin(plugins.SingletonPlugin)
plugins.implements(plugins.ISignal)
def get_signal_subscriptions(self):
return {
- p.toolkit.signals.before_action: [
- log_action,
- {'receiver': log_action, 'sender': 'help_show'}
+ tk.signals.request_started: [
+ log_registration,
+ {'receiver': log_registration, 'sender': 'dataset'}
]
}
+ Even though it is possible to change mutable arguments inside the
+ listener, or return something from it, the main purpose of signals
+ is the triggering of side effects, like logging, starting background
+ jobs, calls to external services, etc.
+
+ Any mutation or attempt to change CKAN behavior through signals should
+ be considered unsafe and may lead to hard to track bugs in
+ the future. So never modify the arguments of signal listener and
+ treat them as constants.
+
+ Always check for the presence of the desired value inside the received
+ context (named arguments). Arguments passed to
+ signals may change over time, and some arguments may disappear.
+
:returns: mapping of subscriptions to signals
:rtype: dict
-
"""
return {}
diff --git a/doc/extensions/signals.rst b/doc/extensions/signals.rst
index 5a3bb3452..c79ab5699 100644
--- a/doc/extensions/signals.rst
+++ b/doc/extensions/signals.rst
@@ -1,15 +1,17 @@
Signals
=======
-Starting v3.0, CKAN comes with built-in signal support, provided by
+.. versionadded:: 2.9
+
+Starting from CKAN 2.9, CKAN comes with built-in signal support, powered by
`blinker <https://pythonhosted.org/blinker/>`_.
The same library is used by `Flask
<https://flask.palletsprojects.com/en/1.1.x/signals/>`_ and anything
-written in Flask docs also applies to CKAN. Probably, the most
+written in the Flask documentation also applies to CKAN. Probably, the most
important point:
-.. note:: Flask comes with a couple of signals and other extensions
+ 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
@@ -23,10 +25,10 @@ important point:
:mod:`ckan.lib.signals` provides two namespaces for signals: ``ckan``
-and ``ckanext``. All core signals resides in ``ckan``, while signals
+and ``ckanext``. All core signals reside in ``ckan``, while signals
from extensions (``datastore``, ``datapusher``, third-party
-extensions) are registered under ``ckanext``. It's only a
-recommendation and nothing prevents developers from creating and using
+extensions) are registered under ``ckanext``. This is a recommended pattern
+and nothing prevents developers from creating and using
their own namespaces.
Signal subscribers **MUST** always be defined as callable accepting
@@ -36,22 +38,22 @@ arguments::
def subscriber(sender, **kwargs):
...
-CKAN core doesn't make any guarantees as for concrete named arguments
+CKAN core doesn't make any guarantees as for the concrete named arguments
that will be passed to subscriber. For particular CKAN version one can
use signlal-listing below as a reference, but in future versions
signature may change. In additon, any event can be fired by
-third-party plugin, so it would be safer to check whether particular
-argument is available inisde `kwargs`.
+a third-party plugin, so it is always safer to check whether a particular
+argument is available inisde the provided `kwargs`.
-Even though it possible to register subscribers using decorators::
+Even though it is possible to register subscribers using decorators::
@p.toolkit.signals.before_action.connect
def action_subscriber(sender, **kwargs):
pass
-recommended approach is using
-:class:`ckan.plugins.interfaces.ISignal`, in order to give CKAN more
-control over subscriptions available depending on enabled plugins::
+the recommended approach is to use the
+:class:`ckan.plugins.interfaces.ISignal` interface, in order to give CKAN more
+control over the subscriptions available depending on the enabled plugins::
class ExampleISignalPlugin(p.SingletonPlugin):
p.implements(p.ISignal)
@@ -67,19 +69,18 @@ control over subscriptions available depending on enabled plugins::
]
}
-.. warning:: Arguments passed to subscribers in no case should be
- modified. Use them only for doing some extra work and
- don't ever try to change existing CKAN behavior using
- subscribers. If one need to alter CKAN behavior,
- :mod:`ckan.plugins.interfaces` must be used instead.
+.. warning:: Arguments passed to subscribers should never be
+ modified. Use subscribers only to trigger side effects and
+ not to change existing CKAN behavior. If one needs to alter
+ CKAN behavior use :mod:`ckan.plugins.interfaces` instead.
-There are a number of built-in signals in CKAN(listing available in
-the end of the page). All of them are created inside one of the
+There are a number of built-in signals in CKAN (check the list at the bottom
+of the page). All of them are created inside one of the
available namespaces: ``ckan`` and ``ckanext``. For simplicity sake,
all built in signals have aliases inside ``ckan.lib.signals`` (or
``ckan.plugins.toolkit.signals``, or ``ckantoolkit.signals``), but you
-always can get signals directly from corresponding namespace(though,
-don't use this ability, unless you are familiar with ``blinker``
+can always get signals directly from corresponding the namespace
+(you shouldn't use this directly unless you are familiar with the ``blinker``
library)::
from ckan.lib.signals import (
@@ -91,8 +92,8 @@ library)::
This information may be quite handy, if you want to define custom
signals inside your extension. Just use ``ckanext`` namespace and call
-its method ``signal`` in order to create new(or get existing)
-signal. In order to avoid name collisions and unexpected behavior,
+its method ``signal`` in order to create a new signal (or get an existing one).
+In order to avoid name collisions and unexpected behavior,
always use your plugin's name as prefix for the signal.::
# ckanext-custom/ckanext/custom/signals.py
@@ -104,7 +105,7 @@ always use your plugin's name as prefix for the signal.::
# after this, you can notify subscribers using following code:
custom_signal_happened.send(SENDER, ARG1=VALUE1, ARG2=VALUE2, ...)
-Now, everyone, who are using your extension can subscirbe to your
+From now on, everyone who is using your extension can subscribe to your
signal from another extension::
# ckanext-ext/ckanext/ext/plugin.py
@@ -125,13 +126,12 @@ signal from another extension::
There is a small problem in snippet above. If ``ckanext-custom`` is
not installed, you'll get ``ImportError``. This is perfectly fine if
you are sure that you are using ``ckanext-custom``, but may be a
-problem for some general-use plugin. In order to avoid problem, either
-use ``try/except`` block, or take signals from ``ckanext`` namespace
-instead::
+problem for some general-use plugin. To avoid this, import signals from
+the ``ckanext`` namespace instead::
# ckanext-ext/ckanext/ext/plugin.py
import ckan.plugins as p
- from ckanext.ext import listeners # here you'll define listeners
+ from ckanext.ext import listeners
class ExtPlugin(p.SingletonPlugin):
p.implements(p.ISignal)
@@ -147,11 +147,10 @@ instead::
]
}
-All signals are singletons inside namespace and, if ``ckanext-custom``
-is installed, you'll get existing signal, otherwise you'll create new
-signal, that is never sent. I.e., your subscription will work only
-when ``ckanext-custom`` available and do nothing(and don't consume
-resources) otherwise.
+All signals are singletons inside their namespace. If ``ckanext-custom``
+is installed, you'll get its existing signal, otherwise you'll create a new
+signal that is never sent. So your subscription will work only
+when ``ckanext-custom`` is available and do nothing otherwise.
:py:mod:`ckan.lib.signals` contains a few core signals for
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment