Parcourir la source

Merge changes from Django to utils.dispatch

Ask Solem il y a 8 ans
Parent
commit
f780638d31

+ 7 - 4
celery/app/base.py

@@ -293,10 +293,13 @@ class Celery(object):
         # Signals
         if self.on_configure is None:
             # used to be a method pre 4.0
-            self.on_configure = Signal()
-        self.on_after_configure = Signal()
-        self.on_after_finalize = Signal()
-        self.on_after_fork = Signal()
+            self.on_configure = Signal(name='app.on_configure')
+        self.on_after_configure = Signal(
+            name='app.on_after_configure',
+            providing_args={'source'},
+        )
+        self.on_after_finalize = Signal(name='app.on_after_finalize')
+        self.on_after_fork = Signal(name='app.on_after_fork')
 
         self.on_init()
         _register_app(self)

+ 13 - 4
celery/contrib/testing/worker.py

@@ -8,12 +8,21 @@ from celery.result import allow_join_result, _set_task_join_will_block
 from celery.utils.dispatch import Signal
 from celery.utils.nodenames import anon_nodename
 
-test_worker_starting = Signal(providing_args=[])
-test_worker_started = Signal(providing_args=['worker', 'consumer'])
-test_worker_stopped = Signal(providing_args=['worker'])
-
 WORKER_LOGLEVEL = os.environ.get('WORKER_LOGLEVEL', 'error')
 
+test_worker_starting = Signal(
+    name='test_worker_starting',
+    providing_args={},
+)
+test_worker_started = Signal(
+    name='test_worker_started',
+    providing_args={'worker', 'consumer'},
+)
+test_worker_stopped = Signal(
+    name='test_worker_stopped',
+    providing_args={'worker'},
+)
+
 
 class TestWorkController(worker.WorkController):
     """Worker that can synchronize on being fully started."""

+ 2 - 2
celery/events/snapshot.py

@@ -26,8 +26,8 @@ class Polaroid(object):
     """Record event snapshots."""
 
     timer = None
-    shutter_signal = Signal(providing_args=('state',))
-    cleanup_signal = Signal()
+    shutter_signal = Signal(name='shutter_signal', providing_args={'state'})
+    cleanup_signal = Signal(name='cleanup_signal')
     clear_after = False
 
     _tref = None

+ 10 - 7
celery/loaders/base.py

@@ -11,6 +11,7 @@ import sys
 from datetime import datetime
 
 from kombu.utils import json
+from kombu.utils.objects import cached_property
 
 from celery import signals
 from celery.five import reraise, string_t
@@ -107,13 +108,7 @@ class BaseLoader(object):
 
     def import_default_modules(self):
         signals.import_modules.send(sender=self.app)
-        return [
-            self.import_task_module(m) for m in (
-                tuple(self.builtin_modules) +
-                tuple(maybe_list(self.app.conf.imports)) +
-                tuple(maybe_list(self.app.conf.include))
-            )
-        ]
+        return [self.import_task_module(m) for m in self.default_modules]
 
     def init_worker(self):
         if not self.worker_initialized:
@@ -227,6 +222,14 @@ class BaseLoader(object):
             mod.__name__ for mod in autodiscover_tasks(packages or (),
                                                        related_name) if mod)
 
+    @cached_property
+    def default_modules(self):
+        return (
+            tuple(self.builtin_modules) +
+            tuple(maybe_list(self.app.conf.imports)) +
+            tuple(maybe_list(self.app.conf.include))
+        )
+
     @property
     def conf(self):
         """Loader configuration."""

+ 111 - 55
celery/signals.py

@@ -26,60 +26,116 @@ __all__ = [
     'eventlet_pool_postshutdown', 'eventlet_pool_apply',
 ]
 
-before_task_publish = Signal(providing_args=[
-    'body', 'exchange', 'routing_key', 'headers', 'properties',
-    'declare', 'retry_policy',
-])
-after_task_publish = Signal(providing_args=[
-    'body', 'exchange', 'routing_key',
-])
-task_prerun = Signal(providing_args=['task_id', 'task', 'args', 'kwargs'])
-task_postrun = Signal(providing_args=[
-    'task_id', 'task', 'args', 'kwargs', 'retval',
-])
-task_success = Signal(providing_args=['result'])
-task_retry = Signal(providing_args=[
-    'request', 'reason', 'einfo',
-])
-task_failure = Signal(providing_args=[
-    'task_id', 'exception', 'args', 'kwargs', 'traceback', 'einfo',
-])
-task_revoked = Signal(providing_args=[
-    'request', 'terminated', 'signum', 'expired',
-])
-task_rejected = Signal(providing_args=[
-    'message', 'exc',
-])
-task_unknown = Signal(providing_args=[
-    'message', 'exc', 'name', 'id',
-])
+# - Task
+before_task_publish = Signal(
+    name='before_task_publish',
+    providing_args={
+        'body', 'exchange', 'routing_key', 'headers',
+        'properties', 'declare', 'retry_policy',
+    },
+)
+after_task_publish = Signal(
+    name='after_task_publish',
+    providing_args={'body', 'exchange', 'routing_key'},
+)
+task_prerun = Signal(
+    name='task_prerun',
+    providing_args={'task_id', 'task', 'args', 'kwargs'},
+)
+task_postrun = Signal(
+    name='task_postrun',
+    providing_args={'task_id', 'task', 'args', 'kwargs', 'retval'},
+)
+task_success = Signal(
+    name='task_success',
+    providing_args={'result'},
+)
+task_retry = Signal(
+    name='task_retry',
+    providing_args={'request', 'reason', 'einfo'},
+)
+task_failure = Signal(
+    name='task_failure',
+    providing_args={
+        'task_id', 'exception', 'args', 'kwargs', 'traceback', 'einfo',
+    },
+)
+task_revoked = Signal(
+    name='task_revoked',
+    providing_args={
+        'request', 'terminated', 'signum', 'expired',
+    },
+)
+task_rejected = Signal(
+    name='task_rejected',
+    providing_args={'message', 'exc'},
+)
+task_unknown = Signal(
+    name='task_unknown',
+    providing_args={'message', 'exc', 'name', 'id'},
+)
 #: Deprecated, use after_task_publish instead.
-task_sent = Signal(providing_args=[
-    'task_id', 'task', 'args', 'kwargs', 'eta', 'taskset',
-])
+task_sent = Signal(
+    name='task_sent',
+    providing_args={
+        'task_id', 'task', 'args', 'kwargs', 'eta', 'taskset',
+    },
+)
 
-celeryd_init = Signal(providing_args=['instance', 'conf', 'options'])
-celeryd_after_setup = Signal(providing_args=['instance', 'conf'])
-import_modules = Signal(providing_args=[])
-worker_init = Signal(providing_args=[])
-worker_process_init = Signal(providing_args=[])
-worker_process_shutdown = Signal(providing_args=[])
-worker_ready = Signal(providing_args=[])
-worker_shutdown = Signal(providing_args=[])
-setup_logging = Signal(providing_args=[
-    'loglevel', 'logfile', 'format', 'colorize',
-])
-after_setup_logger = Signal(providing_args=[
-    'logger', 'loglevel', 'logfile', 'format', 'colorize',
-])
-after_setup_task_logger = Signal(providing_args=[
-    'logger', 'loglevel', 'logfile', 'format', 'colorize',
-])
-beat_init = Signal(providing_args=[])
-beat_embedded_init = Signal(providing_args=[])
-heartbeat_sent = Signal(providing_args=[])
-eventlet_pool_started = Signal(providing_args=[])
-eventlet_pool_preshutdown = Signal(providing_args=[])
-eventlet_pool_postshutdown = Signal(providing_args=[])
-eventlet_pool_apply = Signal(providing_args=['target', 'args', 'kwargs'])
-user_preload_options = Signal(providing_args=['app', 'options'])
+# - Prorgam: `celery worker`
+celeryd_init = Signal(
+    name='celeryd_init',
+    providing_args={'instance', 'conf', 'options'},
+)
+celeryd_after_setup = Signal(
+    name='celeryd_after_setup',
+    providing_args={'instance', 'conf'},
+)
+
+# - Worker
+import_modules = Signal(name='import_modules')
+worker_init = Signal(name='worker_init')
+worker_process_init = Signal(name='worker_process_init')
+worker_process_shutdown = Signal(name='worker_process_shutdown')
+worker_ready = Signal(name='worker_ready')
+worker_shutdown = Signal(name='worker_shutdown')
+heartbeat_sent = Signal(name='heartbeat_sent')
+
+# - Logging
+setup_logging = Signal(
+    name='setup_logging',
+    providing_args={
+        'loglevel', 'logfile', 'format', 'colorize',
+    },
+)
+after_setup_logger = Signal(
+    name='after_setup_logger',
+    providing_args={
+        'logger', 'loglevel', 'logfile', 'format', 'colorize',
+    },
+)
+after_setup_task_logger = Signal(
+    name='after_setup_task_logger',
+    providing_args={
+        'logger', 'loglevel', 'logfile', 'format', 'colorize',
+    },
+)
+
+# - Beat
+beat_init = Signal(name='beat_init')
+beat_embedded_init = Signal(name='beat_embedded_init')
+
+# - Eventlet
+eventlet_pool_started = Signal(name='eventlet_pool_started')
+eventlet_pool_preshutdown = Signal(name='eventlet_pool_preshutdown')
+eventlet_pool_postshutdown = Signal(name='eventlet_pool_postshutdown')
+eventlet_pool_apply = Signal(
+    name='eventlet_pool_apply',
+    providing_args={'target', 'args', 'kwargs'},
+)
+
+# - Programs
+user_preload_options = Signal(
+    name='user_preload_options',
+    providing_args={'app', 'options'},
+)

+ 1 - 3
celery/task/base.py

@@ -218,9 +218,7 @@ class Task(BaseTask):
         return self._get_app().amqp.Producer(
             connection,
             exchange=exchange and Exchange(exchange, exchange_type),
-            routing_key=self.routing_key, **options
-            auto_declare=False,
-        )
+            routing_key=self.routing_key, auto_declare=False, **options)
 
     @classmethod
     def get_consumer(cls, connection=None, queues=None, **kwargs):

+ 255 - 0
celery/utils/dispatch/LICENSE.python

@@ -0,0 +1,255 @@
+A. HISTORY OF THE SOFTWARE
+==========================
+
+Python was created in the early 1990s by Guido van Rossum at Stichting
+Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands
+as a successor of a language called ABC.  Guido remains Python's
+principal author, although it includes many contributions from others.
+
+In 1995, Guido continued his work on Python at the Corporation for
+National Research Initiatives (CNRI, see http://www.cnri.reston.va.us)
+in Reston, Virginia where he released several versions of the
+software.
+
+In May 2000, Guido and the Python core development team moved to
+BeOpen.com to form the BeOpen PythonLabs team.  In October of the same
+year, the PythonLabs team moved to Digital Creations (now Zope
+Corporation, see http://www.zope.com).  In 2001, the Python Software
+Foundation (PSF, see http://www.python.org/psf/) was formed, a
+non-profit organization created specifically to own Python-related
+Intellectual Property.  Zope Corporation is a sponsoring member of
+the PSF.
+
+All Python releases are Open Source (see http://www.opensource.org for
+the Open Source Definition).  Historically, most, but not all, Python
+releases have also been GPL-compatible; the table below summarizes
+the various releases.
+
+    Release         Derived     Year        Owner       GPL-
+                    from                                compatible? (1)
+
+    0.9.0 thru 1.2              1991-1995   CWI         yes
+    1.3 thru 1.5.2  1.2         1995-1999   CNRI        yes
+    1.6             1.5.2       2000        CNRI        no
+    2.0             1.6         2000        BeOpen.com  no
+    1.6.1           1.6         2001        CNRI        yes (2)
+    2.1             2.0+1.6.1   2001        PSF         no
+    2.0.1           2.0+1.6.1   2001        PSF         yes
+    2.1.1           2.1+2.0.1   2001        PSF         yes
+    2.1.2           2.1.1       2002        PSF         yes
+    2.1.3           2.1.2       2002        PSF         yes
+    2.2 and above   2.1.1       2001-now    PSF         yes
+
+Footnotes:
+
+(1) GPL-compatible doesn't mean that we're distributing Python under
+    the GPL.  All Python licenses, unlike the GPL, let you distribute
+    a modified version without making your changes open source.  The
+    GPL-compatible licenses make it possible to combine Python with
+    other software that is released under the GPL; the others don't.
+
+(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
+    because its license has a choice of law clause.  According to
+    CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
+    is "not incompatible" with the GPL.
+
+Thanks to the many outside volunteers who have worked under Guido's
+direction to make these releases possible.
+
+
+B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
+===============================================================
+
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights
+Reserved" are retained in Python alone or in any derivative version prepared by
+Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee.  This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
+-------------------------------------------
+
+BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
+
+1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
+office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
+Individual or Organization ("Licensee") accessing and otherwise using
+this software in source or binary form and its associated
+documentation ("the Software").
+
+2. Subject to the terms and conditions of this BeOpen Python License
+Agreement, BeOpen hereby grants Licensee a non-exclusive,
+royalty-free, world-wide license to reproduce, analyze, test, perform
+and/or display publicly, prepare derivative works, distribute, and
+otherwise use the Software alone or in any derivative version,
+provided, however, that the BeOpen Python License is retained in the
+Software, alone or in any derivative version prepared by Licensee.
+
+3. BeOpen is making the Software available to Licensee on an "AS IS"
+basis.  BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
+SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
+AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
+DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+5. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+6. This License Agreement shall be governed by and interpreted in all
+respects by the law of the State of California, excluding conflict of
+law provisions.  Nothing in this License Agreement shall be deemed to
+create any relationship of agency, partnership, or joint venture
+between BeOpen and Licensee.  This License Agreement does not grant
+permission to use BeOpen trademarks or trade names in a trademark
+sense to endorse or promote products or services of Licensee, or any
+third party.  As an exception, the "BeOpen Python" logos available at
+http://www.pythonlabs.com/logos.html may be used according to the
+permissions granted on that web page.
+
+7. By copying, installing or otherwise using the software, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
+
+
+CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
+---------------------------------------
+
+1. This LICENSE AGREEMENT is between the Corporation for National
+Research Initiatives, having an office at 1895 Preston White Drive,
+Reston, VA 20191 ("CNRI"), and the Individual or Organization
+("Licensee") accessing and otherwise using Python 1.6.1 software in
+source or binary form and its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, CNRI
+hereby grants Licensee a nonexclusive, royalty-free, world-wide
+license to reproduce, analyze, test, perform and/or display publicly,
+prepare derivative works, distribute, and otherwise use Python 1.6.1
+alone or in any derivative version, provided, however, that CNRI's
+License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
+1995-2001 Corporation for National Research Initiatives; All Rights
+Reserved" are retained in Python 1.6.1 alone or in any derivative
+version prepared by Licensee.  Alternately, in lieu of CNRI's License
+Agreement, Licensee may substitute the following text (omitting the
+quotes): "Python 1.6.1 is made available subject to the terms and
+conditions in CNRI's License Agreement.  This Agreement together with
+Python 1.6.1 may be located on the Internet using the following
+unique, persistent identifier (known as a handle): 1895.22/1013.  This
+Agreement may also be obtained from a proxy server on the Internet
+using the following URL: http://hdl.handle.net/1895.22/1013".
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python 1.6.1 or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python 1.6.1.
+
+4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
+basis.  CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. This License Agreement shall be governed by the federal
+intellectual property law of the United States, including without
+limitation the federal copyright law, and, to the extent such
+U.S. federal law does not apply, by the law of the Commonwealth of
+Virginia, excluding Virginia's conflict of law provisions.
+Notwithstanding the foregoing, with regard to derivative works based
+on Python 1.6.1 that incorporate non-separable material that was
+previously distributed under the GNU General Public License (GPL), the
+law of the Commonwealth of Virginia shall govern this License
+Agreement only as to issues arising under or with respect to
+Paragraphs 4, 5, and 7 of this License Agreement.  Nothing in this
+License Agreement shall be deemed to create any relationship of
+agency, partnership, or joint venture between CNRI and Licensee.  This
+License Agreement does not grant permission to use CNRI trademarks or
+trade name in a trademark sense to endorse or promote products or
+services of Licensee, or any third party.
+
+8. By clicking on the "ACCEPT" button where indicated, or by copying,
+installing or otherwise using Python 1.6.1, Licensee agrees to be
+bound by the terms and conditions of this License Agreement.
+
+        ACCEPT
+
+
+CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
+--------------------------------------------------
+
+Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
+The Netherlands.  All rights reserved.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Stichting Mathematisch
+Centrum or CWI not be used in advertising or publicity pertaining to
+distribution of the software without specific, written prior
+permission.
+
+STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
+FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 8 - 7
celery/utils/dispatch/license.txt

@@ -4,23 +4,23 @@ PyDispatcher License:
 
     Copyright (c) 2001-2003, Patrick K. O'Brien and Contributors
     All rights reserved.
-
+    
     Redistribution and use in source and binary forms, with or without
     modification, are permitted provided that the following conditions
     are met:
-
+    
         Redistributions of source code must retain the above copyright
         notice, this list of conditions and the following disclaimer.
-
+    
         Redistributions in binary form must reproduce the above
         copyright notice, this list of conditions and the following
         disclaimer in the documentation and/or other materials
         provided with the distribution.
-
+    
         The name of Patrick K. O'Brien, or the name of any Contributor,
-        may not be used to endorse or promote products derived from this
+        may not be used to endorse or promote products derived from this 
         software without specific prior written permission.
-
+    
     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
@@ -32,4 +32,5 @@ PyDispatcher License:
     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-    OF THE POSSIBILITY OF SUCH DAMAGE.
+    OF THE POSSIBILITY OF SUCH DAMAGE. 
+

+ 0 - 286
celery/utils/dispatch/saferef.py

@@ -1,286 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Safe weakrefs, originally from :pypi:`pyDispatcher`.
-
-Provides a way to safely weakref any function, including bound methods (which
-aren't handled by the core weakref module).
-"""
-from __future__ import absolute_import, unicode_literals
-import sys
-import traceback
-import weakref
-from celery.five import python_2_unicode_compatible
-
-__all__ = ['safe_ref']
-
-PY3 = sys.version_info[0] == 3
-
-
-def safe_ref(target, on_delete=None):  # pragma: no cover
-    """Return a *safe* weak reference to a callable target.
-
-    Arguments:
-        target (Any): The object to be weakly referenced, if it's a
-            bound method reference, will create a :class:`BoundMethodWeakref`,
-            otherwise creates a simple :class:`weakref.ref`.
-
-        on_delete (Callable): If provided, will have a hard reference stored
-            to the callable to be called after the safe reference
-            goes out of scope with the reference object, (either a
-            :class:`weakref.ref` or a :class:`BoundMethodWeakref`) as argument.
-    """
-    if getattr(target, '__self__', None) is not None:
-        # Turn a bound method into a BoundMethodWeakref instance.
-        # Keep track of these instances for lookup by disconnect().
-        assert hasattr(target, '__func__'), \
-            """safe_ref target {0!r} has __self__, but no __func__: \
-            don't know how to create reference""".format(target)
-        return get_bound_method_weakref(target=target,
-                                        on_delete=on_delete)
-    if callable(on_delete):
-        return weakref.ref(target, on_delete)
-    else:
-        return weakref.ref(target)
-
-
-@python_2_unicode_compatible
-class BoundMethodWeakref(object):  # pragma: no cover
-    """'Safe' and reusable weak references to instance methods.
-
-    BoundMethodWeakref objects provide a mechanism for
-    referencing a bound method without requiring that the
-    method object itself (which is normally a transient
-    object) is kept alive.  Instead, the BoundMethodWeakref
-    object keeps weak references to both the object and the
-    function which together define the instance method.
-
-    Attributes:
-
-        key (str): the identity key for the reference, calculated
-            by the class's :meth:`calculate_key` method applied to the
-            target instance method.
-
-        deletion_methods (Sequence[Callable]): Callables taking
-            single argument, a reference to this object which
-            will be called when *either* the target object or
-            target function is garbage collected (i.e., when
-            this object becomes invalid).  These are specified
-            as the on_delete parameters of :func:`safe_ref` calls.
-
-        weak_self (weakref.ref): weak reference to the target object.
-
-        weak_fun (weakref.ref): weak reference to the target function
-
-        _all_instances (weakref.WeakValueDictionary):
-            class attribute pointing to all live
-            BoundMethodWeakref objects indexed by the class's
-            `calculate_key(target)` method applied to the target
-            objects.  This weak value dictionary is used to
-            short-circuit creation so that multiple references
-            to the same (object, function) pair produce the
-            same BoundMethodWeakref instance.
-    """
-
-    _all_instances = weakref.WeakValueDictionary()
-
-    def __new__(cls, target, on_delete=None, *arguments, **named):
-        """Create new instance or return current instance.
-
-        Note:
-            Basically this method of construction allows us to
-            short-circuit creation of references to already-
-            referenced instance methods.  The key corresponding
-            to the target is calculated, and if there's already
-            an existing reference, that is returned, with its
-            deletionMethods attribute updated.  Otherwise the
-            new instance is created and registered in the table
-            of already-referenced methods.
-        """
-        key = cls.calculate_key(target)
-        current = cls._all_instances.get(key)
-        if current is not None:
-            current.deletion_methods.append(on_delete)
-            return current
-        else:
-            base = super(BoundMethodWeakref, cls).__new__(cls)
-            cls._all_instances[key] = base
-            base.__init__(target, on_delete, *arguments, **named)
-            return base
-
-    def __init__(self, target, on_delete=None):
-        """Return a weak-reference-like instance for a bound method.
-
-        Arguments:
-            target (Any): The instance-method target for the weak
-                reference, must have `__self__` and `__func__` attributes
-                and be reconstructable via::
-
-                    target.__func__.__get__(target.__self__)
-
-                which is true of built-in instance methods.
-
-            on_delete (Callable): Optional callback which will be called
-                when this weak reference ceases to be valid
-                (i.e., either the object or the function is garbage
-                collected).  Should take a single argument,
-                which will be passed a pointer to this object.
-        """
-        def remove(weak, self=self):
-            """Set is_dead to true when method or instance is destroyed."""
-            methods = self.deletion_methods[:]
-            del(self.deletion_methods[:])
-            try:
-                del(self.__class__._all_instances[self.key])
-            except KeyError:
-                pass
-            for function in methods:
-                try:
-                    if callable(function):
-                        function(self)
-                except Exception as exc:
-                    try:
-                        traceback.print_exc()
-                    except AttributeError:
-                        print('Exception during saferef {0} cleanup function '
-                              '{1}: {2}'.format(self, function, exc))
-
-        self.deletion_methods = [on_delete]
-        self.key = self.calculate_key(target)
-        self.weak_self = weakref.ref(target.__self__, remove)
-        self.weak_fun = weakref.ref(target.__func__, remove)
-        self.self_name = str(target.__self__)
-        self.fun_name = str(target.__func__.__name__)
-
-    def calculate_key(cls, target):
-        """Calculate the reference key for this reference.
-
-        Returns:
-            Tuple[int, int]: Currently this is a two-tuple of
-                the `id()`'s of the target object and the target
-                function respectively.
-        """
-        return id(target.__self__), id(target.__func__)
-    calculate_key = classmethod(calculate_key)
-
-    def __str__(self):
-        return '{0}( {1}.{2} )'.format(
-            type(self).__name__,
-            self.self_name,
-            self.fun_name,
-        )
-
-    def __repr__(self):
-        return str(self)
-
-    def __bool__(self):
-        """Whether we're still a valid reference."""
-        return self() is not None
-    __nonzero__ = __bool__  # py2
-
-    if not PY3:
-        def __cmp__(self, other):
-            """Compare with another reference."""
-            if not isinstance(other, self.__class__):
-                return cmp(self.__class__, type(other))  # noqa
-            return cmp(self.key, other.key)              # noqa
-
-    def __call__(self):
-        """Return a strong reference to the bound method.
-
-        If the target cannot be retrieved, then will
-        return None, otherwise return a bound instance
-        method for our object and function.
-
-        Note:
-            You may call this method any number of times,
-            as it does not invalidate the reference.
-        """
-        target = self.weak_self()
-        if target is not None:
-            function = self.weak_fun()
-            if function is not None:
-                return function.__get__(target)
-
-
-class BoundNonDescriptorMethodWeakref(BoundMethodWeakref):  # pragma: no cover
-    """A specialized :class:`BoundMethodWeakref`.
-
-    For platforms where instance methods are not descriptors.
-
-    Warning:
-        It assumes that the function name and the target attribute name are
-        the same, instead of assuming that the function is a descriptor.
-        This approach is equally fast, but not 100% reliable because
-        functions can be stored on an attribute named differenty than the
-        function's name, such as in::
-
-            >>> class A(object):
-            ...     pass
-
-            >>> def foo(self):
-            ...     return 'foo'
-            >>> A.bar = foo
-
-        This shouldn't be a common use case.  So, on platforms where methods
-        aren't descriptors (e.g., Jython) this implementation has the
-        advantage of working in the most cases.
-    """
-
-    def __init__(self, target, on_delete=None):
-        """Return a weak-reference-like instance for a bound method.
-
-        Arguments:
-            target (Any): the instance-method target for the weak
-                reference, must have `__self__` and `__func__` attributes
-                and be reconstructable via::
-
-                    target.__func__.__get__(target.__self__)
-
-                which is true of built-in instance methods.
-
-            on_delete (Callable): Optional callback which will be called
-                when this weak reference ceases to be valid
-                (i.e., either the object or the function is garbage
-                collected).  Should take a single argument,
-                which will be passed a pointer to this object.
-        """
-        assert getattr(target.__self__, target.__name__) == target
-        super(BoundNonDescriptorMethodWeakref, self).__init__(target,
-                                                              on_delete)
-
-    def __call__(self):
-        """Return a strong reference to the bound method.
-
-        If the target cannot be retrieved, then will
-        return None, otherwise return a bound instance
-        method for our object and function.
-
-        Note:
-            You may call this method any number of times,
-            as it does not invalidate the reference.
-        """
-        target = self.weak_self()
-        if target is not None:
-            function = self.weak_fun()
-            if function is not None:
-                # Using curry() would be another option, but it erases the
-                # "signature" of the function.  That is, after a function is
-                # curried, the inspect module can't be used to determine how
-                # many arguments the function expects, nor what keyword
-                # arguments it supports, and pydispatcher needs this
-                # information.
-                return getattr(target, function.__name__)
-
-
-def get_bound_method_weakref(target, on_delete):  # pragma: no cover
-    """Instantiate the appropiate :class:`BoundMethodWeakRef`.
-
-    Depending on the details of the underlying class method
-    implementation.
-    """
-    if hasattr(target, '__get__'):
-        # target method is a descriptor, so the default implementation works:
-        return BoundMethodWeakref(target=target, on_delete=on_delete)
-    else:
-        # no luck, use the alternative implementation:
-        return BoundNonDescriptorMethodWeakref(
-            target=target, on_delete=on_delete)

+ 159 - 66
celery/utils/dispatch/signal.py

@@ -1,18 +1,25 @@
 # -*- coding: utf-8 -*-
 """Implementation of the Observer pattern."""
 from __future__ import absolute_import, unicode_literals
+import sys
+import threading
 import weakref
+import warnings
+from celery.exceptions import CDeprecationWarning
 from celery.five import python_2_unicode_compatible, range, text_t
 from celery.local import PromiseProxy, Proxy
+from celery.utils.functional import fun_accepts_kwargs
 from celery.utils.log import get_logger
-from . import saferef
+try:
+    from weakref import WeakMethod
+except ImportError:
+    from .weakref_backports import WeakMethod  # noqa
 
 __all__ = ['Signal']
 
+PY3 = sys.version_info[0] >= 3
 logger = get_logger(__name__)
 
-WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref)
-
 
 def _make_id(target):  # pragma: no cover
     if isinstance(target, Proxy):
@@ -23,25 +30,42 @@ def _make_id(target):  # pragma: no cover
     if hasattr(target, '__func__'):
         return (id(target.__self__), id(target.__func__))
     return id(target)
+NONE_ID = _make_id(None)
+
+NO_RECEIVERS = object()
 
 
 @python_2_unicode_compatible
 class Signal(object):  # pragma: no cover
-    """Observer pattern implementation.
+    """Create new signal.
 
-    Arguments:
+    Keyword Arguments:
         providing_args (List): A list of the arguments this signal can pass
             along in a :meth:`send` call.
+        use_caching (bool): Enable receiver cache.
+        name (str): Name of signal, used for debugging purposes.
     """
 
     #: Holds a dictionary of
     #: ``{receiverkey (id): weakref(receiver)}`` mappings.
     receivers = None
 
-    def __init__(self, providing_args=None):
+    def __init__(self, providing_args=None, use_caching=True, name=None):
         self.receivers = []
         self.providing_args = set(
             providing_args if providing_args is not None else [])
+        self.lock = threading.Lock()
+        self.use_caching = use_caching
+        self.name = name
+        # For convenience we create empty caches even if they are not used.
+        # A note about caching: if use_caching is defined, then for each
+        # distinct sender we cache the receivers that sender has in
+        # 'sender_receivers_cache'.  The cache is cleaned when .connect() or
+        # .disconnect() is called and populated on .send().
+        self.sender_receivers_cache = (
+            weakref.WeakKeyDictionary() if use_caching else {}
+        )
+        self._dead_receivers = False
 
     def _connect_proxy(self, fun, sender, weak, dispatch_uid):
         return self.connect(
@@ -57,8 +81,7 @@ class Signal(object):  # pragma: no cover
                 receive signals.  Receivers must be hashable objects.
 
                 if weak is :const:`True`, then receiver must be
-                weak-referenceable (more precisely :func:`saferef.safe_ref()`
-                must be able to create a reference to the receiver).
+                weak-referenceable.
 
                 Receivers must be able to accept keyword arguments.
 
@@ -67,7 +90,7 @@ class Signal(object):  # pragma: no cover
                 `dispatch_uid`.
 
             sender (Any): The sender to which the receiver should respond.
-                Must either be of type :class:`Signal`, or :const:`None` to
+                Must either be a Python object, or :const:`None` to
                 receive events from any sender.
 
             weak (bool): Whether to use weak references to the receiver.
@@ -82,39 +105,61 @@ class Signal(object):  # pragma: no cover
         def _handle_options(sender=None, weak=True, dispatch_uid=None):
 
             def _connect_signal(fun):
-                receiver = fun
-
-                if isinstance(sender, PromiseProxy):
-                    sender.__then__(
-                        self._connect_proxy, fun, sender, weak, dispatch_uid,
-                    )
-                    return fun
-
-                if dispatch_uid:
-                    lookup_key = (dispatch_uid, _make_id(sender))
-                else:
-                    lookup_key = (_make_id(receiver), _make_id(sender))
-
-                if weak:
-                    receiver = saferef.safe_ref(
-                        receiver, on_delete=self._remove_receiver,
-                    )
-
-                for r_key, _ in self.receivers:
-                    if r_key == lookup_key:
-                        break
-                else:
-                    self.receivers.append((lookup_key, receiver))
-
+                self._connect_signal(fun, sender, weak, dispatch_uid)
                 return fun
-
             return _connect_signal
 
         if args and callable(args[0]):
             return _handle_options(*args[1:], **kwargs)(args[0])
         return _handle_options(*args, **kwargs)
 
-    def disconnect(self, receiver=None, sender=None, weak=True,
+    def _connect_signal(self, receiver, sender, weak, dispatch_uid):
+        assert callable(receiver), 'Signal receivers must be callable'
+        if not fun_accepts_kwargs(receiver):
+            raise ValueError(
+                'Signal receiver must accept keyword arguments.')
+
+
+        if isinstance(sender, PromiseProxy):
+            sender.__then__(
+                self._connect_proxy, receiver, sender, weak, dispatch_uid,
+            )
+            return receiver
+
+        if dispatch_uid:
+            lookup_key = (dispatch_uid, _make_id(sender))
+        else:
+            lookup_key = (_make_id(receiver), _make_id(sender))
+
+        if weak:
+            ref = weakref.ref
+            receiver_object = receiver
+            # Check for bound methods
+            try:
+                receiver.__self__
+                receiver.__func__
+            except AttributeError:
+                pass
+            else:
+                ref = WeakMethod
+                receiver_object = receiver.__self__
+            if PY3:
+                receiver = ref(receiver)
+                weakref.finalize(receiver_object, self._remove_receiver)
+            else:
+                receiver = ref(receiver, self._remove_receiver)
+
+        with self.lock:
+            self._clear_dead_receivers()
+            for r_key, _ in self.receivers:
+                if r_key == lookup_key:
+                    break
+            else:
+                self.receivers.append((lookup_key, receiver))
+            self.sender_receivers_cache.clear()
+        return receiver
+
+    def disconnect(self, receiver=None, sender=None, weak=None,
                    dispatch_uid=None):
         """Disconnect receiver from sender for signal.
 
@@ -132,16 +177,29 @@ class Signal(object):  # pragma: no cover
             dispatch_uid (Hashable): The unique identifier of the receiver
                 to disconnect.
         """
+        if weak is not None:
+            warnings.warn(
+                'Passing `weak` to disconnect has no effect.',
+                CDeprecationWarning, stacklevel=2)
         if dispatch_uid:
             lookup_key = (dispatch_uid, _make_id(sender))
         else:
             lookup_key = (_make_id(receiver), _make_id(sender))
 
-        for index in range(len(self.receivers)):
-            (r_key, _) = self.receivers[index]
-            if r_key == lookup_key:
-                del self.receivers[index]
-                break
+        disconnected = False
+        with self.lock:
+            self._clear_dead_receivers()
+            for index in range(len(self.receivers)):
+                (r_key, _) = self.receivers[index]
+                if r_key == lookup_key:
+                    disconnected = True
+                    del self.receivers[index]
+                    break
+            self.sender_receivers_cache.clear()
+        return disconnected
+
+    def has_listeners(self, sender=None):
+        return bool(self._live_receivers(sender))
 
     def send(self, sender, **named):
         """Send signal from sender to all connected receivers.
@@ -159,53 +217,88 @@ class Signal(object):  # pragma: no cover
             List: of tuple pairs: `[(receiver, response), … ]`.
         """
         responses = []
-        if not self.receivers:
+        if not self.receivers or \
+                self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
             return responses
 
-        for receiver in self._live_receivers(_make_id(sender)):
+        for receiver in self._live_receivers(sender):
             try:
                 response = receiver(signal=self, sender=sender, **named)
             except Exception as exc:  # pylint: disable=broad-except
+                if not hasattr(exc, '__traceback__'):
+                    exc.__traceback__ = sys.exc_info()[2]
                 logger.exception(
                     'Signal handler %r raised: %r', receiver, exc)
+                responses.append((receiver, exc))
             else:
                 responses.append((receiver, response))
         return responses
-
-    def _live_receivers(self, senderkey):
+    send_robust = send  # Compat with Django interface.
+
+    def _clear_dead_receivers(self):
+        # Warning: caller is assumed to hold self.lock
+        if self._dead_receivers:
+            self._dead_receivers = False
+            new_receivers = []
+            for r in self.receivers:
+                if isinstance(r[1], weakref.ReferenceType) and r[1]() is None:
+                    continue
+                new_receivers.append(r)
+            self.receivers = new_receivers
+
+    def _live_receivers(self, sender):
         """Filter sequence of receivers to get resolved, live receivers.
 
         This checks for weak references and resolves them, then returning only
         live receivers.
         """
-        none_senderkey = _make_id(None)
-        receivers = []
-
-        for (_, r_senderkey), receiver in self.receivers:
-            if r_senderkey == none_senderkey or r_senderkey == senderkey:
-                if isinstance(receiver, WEAKREF_TYPES):
-                    # Dereference the weak reference.
-                    receiver = receiver()
-                    if receiver is not None:
+        receivers = None
+        if self.use_caching and not self._dead_receivers:
+            receivers = self.sender_receivers_cache.get(sender)
+            # We could end up here with NO_RECEIVERS even if we do check this
+            # case in .send() prior to calling _Live_receivers()  due to
+            # concurrent .send() call.
+            if receivers is NO_RECEIVERS:
+                return []
+        if receivers is None:
+            with self.lock:
+                self._clear_dead_receivers()
+                senderkey = _make_id(sender)
+                receivers = []
+                for (receiverkey, r_senderkey), receiver in self.receivers:
+                    if r_senderkey == NONE_ID or r_senderkey == senderkey:
                         receivers.append(receiver)
-                else:
-                    receivers.append(receiver)
-        return receivers
+                if self.use_caching:
+                    if not receivers:
+                        self.sender_receivers_cache[sender] = NO_RECEIVERS
+                    else:
+                        # Note: we must cache the weakref versions.
+                        self.sender_receivers_cache[sender] = receivers
+        non_weak_receivers = []
+        for receiver in receivers:
+            if isinstance(receiver, weakref.ReferenceType):
+                # Dereference the weak reference.
+                receiver = receiver()
+                if receiver is not None:
+                    non_weak_receivers.append(receiver)
+            else:
+                non_weak_receivers.append(receiver)
+        return non_weak_receivers
 
-    def _remove_receiver(self, receiver):
+    def _remove_receiver(self, receiver=None):
         """Remove dead receivers from connections."""
-        to_remove = []
-        for key, connected_receiver in self.receivers:
-            if connected_receiver == receiver:
-                to_remove.append(key)
-        for key in to_remove:
-            for idx, (r_key, _) in enumerate(self.receivers):
-                if r_key == key:
-                    del self.receivers[idx]
+        # Mark that the self..receivers first has dead weakrefs. If so,
+        # we will clean those up in connect, disconnect and _live_receivers
+        # while holding self.lock.  Note that doing the cleanup here isn't a
+        # good idea, _remove_receiver() will be called as a side effect of
+        # garbage collection, and so the call can happen wh ile we are already
+        # holding self.lock.
+        self._dead_receivers = True
 
     def __repr__(self):
         """``repr(signal)``."""
-        return '<Signal: {0}>'.format(type(self).__name__)
+        return '<{0}: {1} providing_args={2!r}>'.format(
+            type(self).__name__, self.name, self.providing_args)
 
     def __str__(self):
         """``str(signal)``."""

+ 70 - 0
celery/utils/dispatch/weakref_backports.py

@@ -0,0 +1,70 @@
+"""Weakref compatibility.
+
+weakref_backports is a partial backport of the weakref module for Python
+versions below 3.4.
+
+Copyright (C) 2013 Python Software Foundation, see LICENSE.python for details.
+
+The following changes were made to the original sources during backporting:
+
+* Added ``self`` to ``super`` calls.
+* Removed ``from None`` when raising exceptions.
+"""
+from __future__ import absolute_import, unicode_literals
+from weakref import ref
+
+
+class WeakMethod(ref):
+    """Weak reference to bound method.
+
+    A custom :class:`weakref.ref` subclass which simulates a weak reference
+    to a bound method, working around the lifetime problem of bound methods.
+    """
+
+    __slots__ = '_func_ref', '_meth_type', '_alive', '__weakref__'
+
+    def __new__(cls, meth, callback=None):
+        try:
+            obj = meth.__self__
+            func = meth.__func__
+        except AttributeError:
+            raise TypeError(
+                "Argument should be a bound method, not {0}".format(
+                    type(meth)))
+
+        def _cb(arg):
+            # The self-weakref trick is needed to avoid creating a
+            # reference cycle.
+            self = self_wr()
+            if self._alive:
+                self._alive = False
+                if callback is not None:
+                    callback(self)
+        self = ref.__new__(cls, obj, _cb)
+        self._func_ref = ref(func, _cb)
+        self._meth_type = type(meth)
+        self._alive = True
+        self_wr = ref(self)
+        return self
+
+    def __call__(self):
+        obj = super(WeakMethod, self).__call__()
+        func = self._func_ref()
+        if obj is not None and func is not None:
+            return self._meth_type(func, obj)
+
+    def __eq__(self, other):
+        if not isinstance(other, WeakMethod):
+            return False
+        if not self._alive or not other._alive:
+            return self is other
+        return ref.__eq__(self, other) and self._func_ref == other._func_ref
+
+    def __ne__(self, other):
+        if not isinstance(other, WeakMethod):
+            return True
+        if not self._alive or not other._alive:
+            return self is not other
+        return ref.__ne__(self, other) or self._func_ref != other._func_ref
+
+    __hash__ = ref.__hash__

+ 21 - 3
celery/utils/functional.py

@@ -2,10 +2,10 @@
 """Functional-style utilties."""
 from __future__ import absolute_import, print_function, unicode_literals
 
+import inspect
 import sys
 
 from functools import partial
-from inspect import isfunction
 from itertools import chain, islice
 
 from kombu.utils.functional import (
@@ -20,7 +20,7 @@ __all__ = [
     'LRUCache', 'is_list', 'maybe_list', 'memoize', 'mlazy', 'noop',
     'first', 'firstmethod', 'chunks', 'padlist', 'mattrgetter', 'uniq',
     'regen', 'dictfilter', 'lazy', 'maybe_evaluate', 'head_from_fun',
-    'maybe',
+    'maybe', 'fun_accepts_kwargs',
 ]
 
 IS_PY3 = sys.version_info[0] == 3
@@ -249,7 +249,7 @@ def head_from_fun(fun, bound=False, debug=False):
     # in pure-Python.  Instead we use exec to create a new function
     # with an empty body, meaning it has the same performance as
     # as just calling a function.
-    if not isfunction(fun) and hasattr(fun, '__call__'):
+    if not inspect.isfunction(fun) and hasattr(fun, '__call__'):
         name, fun = fun.__class__.__name__, fun.__call__
     else:
         name = fun.__name__
@@ -284,6 +284,24 @@ def fun_takes_argument(name, fun, position=None):
     )
 
 
+if IS_PY3:
+    def fun_accepts_kwargs(fun):
+        return any(
+            p for p in inspect.signature(fun).parameters.values()
+            if p.kind == p.VAR_KEYWORD
+        )
+else:
+    def fun_accepts_kwargs(fun):  # noqa
+        try:
+            argspec = inspect.getargspec(fun)
+        except TypeError:
+            try:
+                argspec = inspect.getargspec(fun.__call__)
+            except (TypeError, AttributeError):
+                return
+        return not argspec or argspec[2] is not None
+
+
 def maybe(typ, val):
     """Call typ on value if val is defined."""
     return typ(val) if val is not None else val

+ 0 - 11
docs/internals/reference/celery.utils.dispatch.saferef.rst

@@ -1,11 +0,0 @@
-==========================================================
- ``celery.utils.dispatch.saferef``
-==========================================================
-
-.. contents::
-    :local:
-.. currentmodule:: celery.utils.dispatch.saferef
-
-.. automodule:: celery.utils.dispatch.saferef
-    :members:
-    :undoc-members:

+ 1 - 3
t/unit/utils/test_dispatcher.py

@@ -46,11 +46,9 @@ class test_Signal:
 
     def _testIsClean(self, signal):
         """Assert that everything has been cleaned up automatically"""
+        assert not signal.has_listeners()
         assert signal.receivers == []
 
-        # force cleanup just in case
-        signal.receivers = []
-
     def test_exact(self):
         a_signal.connect(receiver_1_arg, sender=self)
         try:

+ 0 - 89
t/unit/utils/test_saferef.py

@@ -1,89 +0,0 @@
-from __future__ import absolute_import, unicode_literals
-
-from celery.five import range
-from celery.utils.dispatch.saferef import safe_ref
-
-
-class Class1(object):
-
-    def x(self):
-        pass
-
-
-def fun(obj):
-    pass
-
-
-class Class2(object):
-
-    def __call__(self, obj):
-        pass
-
-
-class test_safe_ref:
-
-    def setup(self):
-        ts = []
-        ss = []
-        for x in range(5000):
-            t = Class1()
-            ts.append(t)
-            s = safe_ref(t.x, self._closure)
-            ss.append(s)
-        ts.append(fun)
-        ss.append(safe_ref(fun, self._closure))
-        for x in range(30):
-            t = Class2()
-            ts.append(t)
-            s = safe_ref(t, self._closure)
-            ss.append(s)
-        self.ts = ts
-        self.ss = ss
-        self.closureCount = 0
-
-    def test_in(self):
-        """test_in
-
-        Test the "in" operator for safe references (cmp)
-
-        """
-        for t in self.ts[:50]:
-            assert safe_ref(t.x) in self.ss
-
-    def test_valid(self):
-        """test_value
-
-        Test that the references are valid (return instance methods)
-
-        """
-        for s in self.ss:
-            assert s()
-
-    def test_shortcircuit(self):
-        """test_shortcircuit
-
-        Test that creation short-circuits to reuse existing references
-
-        """
-        sd = {}
-        for s in self.ss:
-            sd[s] = 1
-        for t in self.ts:
-            if hasattr(t, 'x'):
-                assert safe_ref(t.x) in sd
-            else:
-                assert safe_ref(t) in sd
-
-    def test_representation(self):
-        """test_representation
-
-        Test that the reference object's representation works
-
-        XXX Doesn't currently check the results, just that no error
-            is raised
-        """
-        repr(self.ss[-1])
-
-    def _closure(self, ref):
-        """Dumb utility mechanism to increment deletion counter"""
-        self.closureCount += 1