Skip to content

Commit 94dfdf5

Browse files
committed
Added list_signals command.
1 parent b43ff2c commit 94dfdf5

File tree

4 files changed

+124
-0
lines changed

4 files changed

+124
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
# Based on https://gist.github.com/voldmar/1264102
3+
# and https://gist.github.com/runekaagaard/2eecf0a8367959dc634b7866694daf2c
4+
5+
import gc
6+
import inspect
7+
import ctypes
8+
import weakref
9+
from collections import defaultdict
10+
11+
from django.core.management.base import BaseCommand
12+
from django.db.models.signals import (
13+
ModelSignal, pre_init, post_init, pre_save, post_save, pre_delete,
14+
post_delete, m2m_changed, pre_migrate, post_migrate
15+
)
16+
from django.utils.encoding import force_str
17+
18+
19+
MSG = '{module}.{name} #{line}'
20+
21+
SIGNAL_NAMES = {
22+
pre_init: 'pre_init',
23+
post_init: 'post_init',
24+
pre_save: 'pre_save',
25+
post_save: 'post_save',
26+
pre_delete: 'pre_delete',
27+
post_delete: 'post_delete',
28+
m2m_changed: 'm2m_changed',
29+
pre_migrate: 'pre_migrate',
30+
post_migrate: 'post_migrate',
31+
}
32+
33+
34+
class Command(BaseCommand):
35+
help = 'List all signals by model and signal type'
36+
37+
def handle(self, *args, **options):
38+
signals = [obj for obj in gc.get_objects() if isinstance(obj, ModelSignal)]
39+
models = defaultdict(lambda: defaultdict(list))
40+
41+
for signal in signals:
42+
signal_name = SIGNAL_NAMES.get(signal, 'unknown')
43+
for receiver in signal.receivers:
44+
lookup, receiver = receiver
45+
if isinstance(receiver, weakref.ReferenceType):
46+
receiver = receiver()
47+
if receiver is None:
48+
continue
49+
receiver_id, sender_id = lookup
50+
51+
model = ctypes.cast(sender_id, ctypes.py_object).value
52+
if model:
53+
models[model][signal_name].append(MSG.format(
54+
name=receiver.__name__,
55+
module=receiver.__module__,
56+
line=inspect.getsourcelines(receiver)[1],
57+
path=inspect.getsourcefile(receiver))
58+
)
59+
60+
output = []
61+
for key in sorted(models.keys(), key=str):
62+
verbose_name = force_str(key._meta.verbose_name)
63+
output.append('{}.{} ({})'.format(
64+
key.__module__, key.__name__, verbose_name))
65+
for signal_name in sorted(models[key].keys()):
66+
lines = models[key][signal_name]
67+
output.append(' {}'.format(signal_name))
68+
for line in lines:
69+
output.append(' {}'.format(line))
70+
71+
return '\n'.join(output)

docs/list_signals.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
list_signals
2+
============
3+
4+
:synopsis: Lists all signals defined in the project grouped by model and
5+
signal type
6+
7+
8+
Example Usage
9+
-------------
10+
11+
With *django-extensions* installed you can review all defined handlers using
12+
*list_signals* command::
13+
14+
# As above but non-interactive
15+
$ ./manage.py list_signals
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -*- coding: utf-8 -*-
2+
import re
3+
from io import StringIO
4+
5+
from django.test import TestCase
6+
from django.core.management import call_command
7+
8+
9+
class ListSignalsTests(TestCase):
10+
"""Tests for list_signals command."""
11+
12+
def setUp(self):
13+
self.out = StringIO()
14+
15+
def test_should_print_all_signals(self):
16+
expected_result = '''django.contrib.sites.models.Site (site)
17+
pre_delete
18+
django.contrib.sites.models.clear_site_cache #
19+
pre_save
20+
django.contrib.sites.models.clear_site_cache #
21+
tests.testapp.models.HasOwnerModel (has owner model)
22+
pre_save
23+
tests.testapp.models.dummy_handler #
24+
'''
25+
26+
call_command('list_signals', stdout=self.out)
27+
28+
# Strip line numbers to make the test less brittle
29+
out = re.sub(r'(?<=#)\d+', '', self.out.getvalue(), re.M)
30+
self.assertIn(expected_result, out)

tests/testapp/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.db import models
55
from django.contrib.auth import get_user_model
6+
from django.db.models.signals import pre_save
67

78
from django_extensions.db.fields import AutoSlugField, ModificationDateTimeField, RandomCharField, ShortUUIDField
89
from django_extensions.db.fields.json import JSONField
@@ -492,3 +493,10 @@ def has_defaults(self, one=1, two='Two', true=True, false=False, none=None):
492493

493494
class Meta:
494495
app_label = 'django_extensions'
496+
497+
498+
def dummy_handler(sender, instance, **kwargs):
499+
pass
500+
501+
502+
pre_save.connect(dummy_handler, sender=HasOwnerModel)

0 commit comments

Comments
 (0)