diff options
| author | Simon Charette <charette.s@gmail.com> | 2013-11-04 23:11:51 -0500 |
|---|---|---|
| committer | Simon Charette <charette.s@gmail.com> | 2013-11-24 17:51:22 -0500 |
| commit | eb38257e5199ca06b8b32f82dd50f64fd6b0d98d (patch) | |
| tree | 218b5face70be2baaffaabd3e5dac009a227727e /django | |
| parent | 03bc0a8ac5b465a9a1ec88dedb0a2f4bd91dd547 (diff) | |
Fixed #21391 -- Allow model signals to lazily reference their senders.
Diffstat (limited to 'django')
| -rw-r--r-- | django/core/management/validation.py | 30 | ||||
| -rw-r--r-- | django/db/models/signals.py | 68 |
2 files changed, 88 insertions, 10 deletions
diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0a976d72a1..5bf9413c20 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -1,5 +1,6 @@ import collections import sys +import types from django.conf import settings from django.core.management.color import color_style @@ -25,7 +26,7 @@ def get_validation_errors(outfile, app=None): validates all models of all installed apps. Writes errors, if any, to outfile. Returns number of errors. """ - from django.db import models, connection + from django.db import connection, models from django.db.models.loading import get_app_errors from django.db.models.deletion import SET_NULL, SET_DEFAULT @@ -363,6 +364,8 @@ def get_validation_errors(outfile, app=None): for it in opts.index_together: validate_local_fields(e, opts, "index_together", it) + validate_model_signals(e) + return len(e.errors) @@ -382,3 +385,28 @@ def validate_local_fields(e, opts, field_name, fields): e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name)) if f not in opts.local_fields: e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name)) + + +def validate_model_signals(e): + """Ensure lazily referenced model signals senders are installed.""" + from django.db import models + + for name in dir(models.signals): + obj = getattr(models.signals, name) + if isinstance(obj, models.signals.ModelSignal): + for reference, receivers in obj.unresolved_references.items(): + for receiver, _, _ in receivers: + # The receiver is either a function or an instance of class + # defining a `__call__` method. + if isinstance(receiver, types.FunctionType): + description = "The `%s` function" % receiver.__name__ + else: + description = "An instance of the `%s` class" % receiver.__class__.__name__ + e.add( + receiver.__module__, + "%s was connected to the `%s` signal " + "with a lazy reference to the '%s' sender, " + "which has not been installed." % ( + description, name, '.'.join(reference) + ) + ) diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 6b7605839c..6b011c2099 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -1,20 +1,70 @@ +from collections import defaultdict + +from django.db.models.loading import get_model from django.dispatch import Signal +from django.utils import six + class_prepared = Signal(providing_args=["class"]) -pre_init = Signal(providing_args=["instance", "args", "kwargs"], use_caching=True) -post_init = Signal(providing_args=["instance"], use_caching=True) -pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"], - use_caching=True) -post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True) +class ModelSignal(Signal): + """ + Signal subclass that allows the sender to be lazily specified as a string + of the `app_label.ModelName` form. + """ + + def __init__(self, *args, **kwargs): + super(ModelSignal, self).__init__(*args, **kwargs) + self.unresolved_references = defaultdict(list) + class_prepared.connect(self._resolve_references) + + def _resolve_references(self, sender, **kwargs): + opts = sender._meta + reference = (opts.app_label, opts.object_name) + try: + receivers = self.unresolved_references.pop(reference) + except KeyError: + pass + else: + for receiver, weak, dispatch_uid in receivers: + super(ModelSignal, self).connect( + receiver, sender=sender, weak=weak, dispatch_uid=dispatch_uid + ) + + def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): + if isinstance(sender, six.string_types): + try: + app_label, object_name = sender.split('.') + except ValueError: + raise ValueError( + "Specified sender must either be a model or a " + "model name of the 'app_label.ModelName' form." + ) + sender = get_model(app_label, object_name, only_installed=False) + if sender is None: + reference = (app_label, object_name) + self.unresolved_references[reference].append( + (receiver, weak, dispatch_uid) + ) + return + super(ModelSignal, self).connect( + receiver, sender=sender, weak=weak, dispatch_uid=dispatch_uid + ) -pre_delete = Signal(providing_args=["instance", "using"], use_caching=True) -post_delete = Signal(providing_args=["instance", "using"], use_caching=True) +pre_init = ModelSignal(providing_args=["instance", "args", "kwargs"], use_caching=True) +post_init = ModelSignal(providing_args=["instance"], use_caching=True) + +pre_save = ModelSignal(providing_args=["instance", "raw", "using", "update_fields"], + use_caching=True) +post_save = ModelSignal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True) + +pre_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True) +post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True) + +m2m_changed = ModelSignal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) pre_migrate = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"]) pre_syncdb = pre_migrate post_migrate = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"]) post_syncdb = post_migrate - -m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) |
