django-bulk-hooks 0.1.102__py3-none-any.whl → 0.1.104__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-bulk-hooks might be problematic. Click here for more details.

@@ -17,113 +17,118 @@ from django_bulk_hooks.queryset import HookQuerySet
17
17
 
18
18
 
19
19
  class BulkHookManager(models.Manager):
20
- # Default chunk sizes - can be overridden per model
21
- DEFAULT_CHUNK_SIZE = 200
22
- DEFAULT_RELATED_CHUNK_SIZE = 500 # Higher for related object fetching
23
-
24
- def __init__(self):
25
- super().__init__()
26
- self._chunk_size = self.DEFAULT_CHUNK_SIZE
27
- self._related_chunk_size = self.DEFAULT_RELATED_CHUNK_SIZE
28
- self._prefetch_related_fields = set()
29
- self._select_related_fields = set()
30
-
31
- def configure(
32
- self,
33
- chunk_size=None,
34
- related_chunk_size=None,
35
- select_related=None,
36
- prefetch_related=None,
37
- ):
38
- """
39
- Configure bulk operation parameters for this manager.
20
+ CHUNK_SIZE = 200
21
+
22
+ def get_queryset(self):
23
+ return HookQuerySet(self.model, using=self._db)
40
24
 
41
- Args:
42
- chunk_size: Number of objects to process in each bulk operation chunk
43
- related_chunk_size: Number of objects to fetch in each related object query
44
- select_related: List of fields to always select_related in bulk operations
45
- prefetch_related: List of fields to always prefetch_related in bulk operations
25
+ def _has_multi_table_inheritance(self, model_cls):
46
26
  """
47
- if chunk_size is not None:
48
- self._chunk_size = chunk_size
49
- if related_chunk_size is not None:
50
- self._related_chunk_size = related_chunk_size
51
- if select_related:
52
- self._select_related_fields.update(select_related)
53
- if prefetch_related:
54
- self._prefetch_related_fields.update(prefetch_related)
55
-
56
- def _load_originals_optimized(self, pks, fields_to_fetch=None):
27
+ Check if this model uses multi-table inheritance.
57
28
  """
58
- Optimized loading of original instances with smart batching and field selection.
29
+ return (
30
+ model_cls._meta.parents and
31
+ not all(parent._meta.abstract for parent in model_cls._meta.parents.values())
32
+ )
33
+
34
+ def _get_base_model(self, model_cls):
59
35
  """
60
- queryset = self.model.objects.filter(pk__in=pks)
61
-
62
- # Only select specific fields if provided and not empty
63
- if fields_to_fetch and len(fields_to_fetch) > 0:
64
- queryset = queryset.only("pk", *fields_to_fetch)
65
-
66
- # Apply configured related field optimizations
67
- if self._select_related_fields:
68
- queryset = queryset.select_related(*self._select_related_fields)
69
- if self._prefetch_related_fields:
70
- queryset = queryset.prefetch_related(*self._prefetch_related_fields)
71
-
72
- # Batch load in chunks to avoid memory issues
73
- all_originals = []
74
- for i in range(0, len(pks), self._related_chunk_size):
75
- chunk_pks = pks[i : i + self._related_chunk_size]
76
- chunk_originals = list(queryset.filter(pk__in=chunk_pks))
77
- all_originals.extend(chunk_originals)
78
-
79
- return all_originals
80
-
81
- def _get_fields_to_fetch(self, objs, fields):
36
+ Get the base model (first non-abstract parent or self).
82
37
  """
83
- Determine which fields need to be fetched based on what's being updated
84
- and what's needed for hooks.
38
+ base_model = model_cls
39
+ while base_model._meta.parents:
40
+ parent = next(iter(base_model._meta.parents.values()))
41
+ if not parent._meta.abstract:
42
+ base_model = parent
43
+ else:
44
+ break
45
+ return base_model
46
+
47
+ def _extract_base_objects(self, objs, model_cls):
85
48
  """
86
- fields_to_fetch = set(fields)
87
-
88
- # Add fields needed by registered hooks
89
- from django_bulk_hooks.registry import get_hooks
90
-
91
- hooks = get_hooks(self.model, "before_update") + get_hooks(
92
- self.model, "after_update"
93
- )
94
-
95
- for handler_cls, method_name, condition, _ in hooks:
96
- if condition:
97
- # If there's a condition, we need all fields it might access
98
- fields_to_fetch.update(condition.get_required_fields())
99
-
100
- # Filter out fields that don't exist on the model
101
- valid_fields = set()
102
- invalid_fields = set()
103
- for field_name in fields_to_fetch:
104
- try:
105
- self.model._meta.get_field(field_name)
106
- valid_fields.add(field_name)
107
- except Exception as e:
108
- # Field doesn't exist, skip it
109
- invalid_fields.add(field_name)
110
- import logging
111
-
112
- logger = logging.getLogger(__name__)
113
- logger.debug(
114
- f"Field '{field_name}' requested by hook condition but doesn't exist on {self.model.__name__}: {e}"
115
- )
49
+ Extract base model objects from inherited objects.
50
+ """
51
+ base_model = self._get_base_model(model_cls)
52
+ base_objects = []
53
+
54
+ for obj in objs:
55
+ base_obj = base_model()
56
+ for field in base_model._meta.fields:
57
+ if field.name != 'id' and hasattr(obj, field.name):
58
+ setattr(base_obj, field.name, getattr(obj, field.name))
59
+ base_objects.append(base_obj)
60
+
61
+ return base_objects
62
+
63
+ def _extract_child_objects(self, objs, model_cls):
64
+ """
65
+ Extract child model objects from inherited objects.
66
+ """
67
+ child_objects = []
68
+
69
+ for obj in objs:
70
+ child_obj = model_cls()
71
+ child_obj.pk = obj.pk # Set the same PK as base
72
+
73
+ # Copy only fields specific to this model
74
+ for field in model_cls._meta.fields:
75
+ if (field.name != 'id' and
76
+ field.model == model_cls and
77
+ hasattr(obj, field.name)):
78
+ setattr(child_obj, field.name, getattr(obj, field.name))
79
+
80
+ child_objects.append(child_obj)
81
+
82
+ return child_objects
83
+
84
+ def _bulk_create_inherited(self, objs, **kwargs):
85
+ """
86
+ Handle bulk create for inherited models by handling each table separately.
87
+ """
88
+ if not objs:
89
+ return []
90
+
91
+ model_cls = self.model
92
+ result = []
93
+
94
+ # Group objects by their actual class
95
+ objects_by_class = {}
96
+ for obj in objs:
97
+ obj_class = obj.__class__
98
+ if obj_class not in objects_by_class:
99
+ objects_by_class[obj_class] = []
100
+ objects_by_class[obj_class].append(obj)
101
+
102
+ for obj_class, class_objects in objects_by_class.items():
103
+ # Check if this class has multi-table inheritance
104
+ parent_models = [p for p in obj_class._meta.get_parent_list()
105
+ if not p._meta.abstract]
106
+
107
+ if not parent_models:
108
+ # No inheritance, use standard bulk_create
109
+ chunk_result = super(models.Manager, self).bulk_create(class_objects, **kwargs)
110
+ result.extend(chunk_result)
116
111
  continue
117
-
118
- if invalid_fields:
119
- import logging
120
- logger = logging.getLogger(__name__)
121
- logger.warning(
122
- f"Invalid fields requested for {self.model.__name__}: {invalid_fields}. "
123
- f"These fields were ignored to prevent errors."
124
- )
125
-
126
- return valid_fields
112
+
113
+ # Handle multi-table inheritance
114
+ # Step 1: Bulk create base objects without hooks
115
+ base_objects = self._extract_base_objects(class_objects, obj_class)
116
+ created_base = super(models.Manager, self).bulk_create(base_objects, **kwargs)
117
+
118
+ # Step 2: Update original objects with base IDs
119
+ for obj, base_obj in zip(class_objects, created_base):
120
+ obj.pk = base_obj.pk
121
+ obj._state.adding = False
122
+
123
+ # Step 3: Bulk create child objects without hooks
124
+ child_objects = self._extract_child_objects(class_objects, obj_class)
125
+ if child_objects:
126
+ # Use _base_manager to avoid recursion
127
+ obj_class._base_manager.bulk_create(child_objects, **kwargs)
128
+
129
+ result.extend(class_objects)
130
+
131
+ return result
127
132
 
128
133
  @transaction.atomic
129
134
  def bulk_update(
@@ -140,42 +145,36 @@ class BulkHookManager(models.Manager):
140
145
  )
141
146
 
142
147
  if not bypass_hooks:
143
- # Determine which fields we need to fetch
144
- fields_to_fetch = self._get_fields_to_fetch(objs, fields)
145
-
146
- # Load originals efficiently
147
- pks = [obj.pk for obj in objs if obj.pk is not None]
148
- originals = self._load_originals_optimized(pks, fields_to_fetch)
149
-
150
- # Create a mapping for quick lookup
151
- original_map = {obj.pk: obj for obj in originals}
152
-
153
- # Align originals with new instances
154
- aligned_originals = [original_map.get(obj.pk) for obj in objs]
148
+ # Load originals for hook comparison and ensure they match the order of new instances
149
+ original_map = {
150
+ obj.pk: obj for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
151
+ }
152
+ originals = [original_map.get(obj.pk) for obj in objs]
155
153
 
156
154
  ctx = HookContext(model_cls)
157
155
 
158
156
  # Run validation hooks first
159
157
  if not bypass_validation:
160
- engine.run(model_cls, VALIDATE_UPDATE, objs, aligned_originals, ctx=ctx)
158
+ engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
161
159
 
162
160
  # Then run business logic hooks
163
- engine.run(model_cls, BEFORE_UPDATE, objs, aligned_originals, ctx=ctx)
161
+ engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
164
162
 
165
163
  # Automatically detect fields that were modified during BEFORE_UPDATE hooks
166
- modified_fields = self._detect_modified_fields(objs, aligned_originals)
164
+ modified_fields = self._detect_modified_fields(objs, originals)
167
165
  if modified_fields:
166
+ # Convert to set for efficient union operation
168
167
  fields_set = set(fields)
169
168
  fields_set.update(modified_fields)
170
169
  fields = list(fields_set)
171
170
 
172
- # Process in chunks
173
- for i in range(0, len(objs), self._chunk_size):
174
- chunk = objs[i : i + self._chunk_size]
171
+ for i in range(0, len(objs), self.CHUNK_SIZE):
172
+ chunk = objs[i : i + self.CHUNK_SIZE]
173
+ # Call the base implementation to avoid re-triggering this method
175
174
  super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
176
175
 
177
176
  if not bypass_hooks:
178
- engine.run(model_cls, AFTER_UPDATE, objs, aligned_originals, ctx=ctx)
177
+ engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
179
178
 
180
179
  return objs
181
180
 
@@ -187,17 +186,11 @@ class BulkHookManager(models.Manager):
187
186
  if not original_instances:
188
187
  return set()
189
188
 
190
- # Create a mapping of pk to original instance for efficient lookup
191
- original_map = {obj.pk: obj for obj in original_instances if obj.pk is not None}
192
-
193
189
  modified_fields = set()
194
190
 
195
- for new_instance in new_instances:
196
- if new_instance.pk is None:
197
- continue
198
-
199
- original = original_map.get(new_instance.pk)
200
- if not original:
191
+ # Since original_instances is now ordered to match new_instances, we can zip them directly
192
+ for new_instance, original in zip(new_instances, original_instances):
193
+ if new_instance.pk is None or original is None:
201
194
  continue
202
195
 
203
196
  # Compare all fields to detect changes
@@ -224,42 +217,43 @@ class BulkHookManager(models.Manager):
224
217
 
225
218
  @transaction.atomic
226
219
  def bulk_create(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
227
- if not objs:
228
- return []
229
-
230
220
  model_cls = self.model
231
- result = []
232
221
 
233
222
  if any(not isinstance(obj, model_cls) for obj in objs):
234
223
  raise TypeError(
235
224
  f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
236
225
  )
237
226
 
227
+ # Check if this model uses multi-table inheritance
228
+ has_multi_table_inheritance = self._has_multi_table_inheritance(model_cls)
229
+
230
+ result = []
231
+
238
232
  if not bypass_hooks:
239
233
  ctx = HookContext(model_cls)
240
234
 
241
- # Process validation in chunks to avoid memory issues
235
+ # Run validation hooks first
242
236
  if not bypass_validation:
243
- for i in range(0, len(objs), self._chunk_size):
244
- chunk = objs[i : i + self._chunk_size]
245
- engine.run(model_cls, VALIDATE_CREATE, chunk, ctx=ctx)
237
+ engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
246
238
 
247
- # Process before_create hooks in chunks
248
- for i in range(0, len(objs), self._chunk_size):
249
- chunk = objs[i : i + self._chunk_size]
250
- engine.run(model_cls, BEFORE_CREATE, chunk, ctx=ctx)
239
+ # Then run business logic hooks
240
+ engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
251
241
 
252
242
  # Perform bulk create in chunks
253
- for i in range(0, len(objs), self._chunk_size):
254
- chunk = objs[i : i + self._chunk_size]
255
- created_chunk = super(models.Manager, self).bulk_create(chunk, **kwargs)
243
+ for i in range(0, len(objs), self.CHUNK_SIZE):
244
+ chunk = objs[i : i + self.CHUNK_SIZE]
245
+
246
+ if has_multi_table_inheritance:
247
+ # Use our multi-table bulk create
248
+ created_chunk = self._bulk_create_inherited(chunk, **kwargs)
249
+ else:
250
+ # Use Django's standard bulk create
251
+ created_chunk = super(models.Manager, self).bulk_create(chunk, **kwargs)
252
+
256
253
  result.extend(created_chunk)
257
254
 
258
255
  if not bypass_hooks:
259
- # Process after_create hooks in chunks
260
- for i in range(0, len(result), self._chunk_size):
261
- chunk = result[i : i + self._chunk_size]
262
- engine.run(model_cls, AFTER_CREATE, chunk, ctx=ctx)
256
+ engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
263
257
 
264
258
  return result
265
259
 
@@ -271,7 +265,6 @@ class BulkHookManager(models.Manager):
271
265
  return []
272
266
 
273
267
  model_cls = self.model
274
- chunk_size = batch_size or self._chunk_size
275
268
 
276
269
  if any(not isinstance(obj, model_cls) for obj in objs):
277
270
  raise TypeError(
@@ -281,25 +274,21 @@ class BulkHookManager(models.Manager):
281
274
  ctx = HookContext(model_cls)
282
275
 
283
276
  if not bypass_hooks:
284
- # Process hooks in chunks
285
- for i in range(0, len(objs), chunk_size):
286
- chunk = objs[i : i + chunk_size]
277
+ # Run validation hooks first
278
+ if not bypass_validation:
279
+ engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
287
280
 
288
- if not bypass_validation:
289
- engine.run(model_cls, VALIDATE_DELETE, chunk, ctx=ctx)
290
- engine.run(model_cls, BEFORE_DELETE, chunk, ctx=ctx)
281
+ # Then run business logic hooks
282
+ engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
291
283
 
292
- # Collect PKs and delete in chunks
293
284
  pks = [obj.pk for obj in objs if obj.pk is not None]
294
- for i in range(0, len(pks), chunk_size):
295
- chunk_pks = pks[i : i + chunk_size]
296
- model_cls._base_manager.filter(pk__in=chunk_pks).delete()
285
+
286
+ # Use base manager for the actual deletion to prevent recursion
287
+ # The hooks have already been fired above, so we don't need them again
288
+ model_cls._base_manager.filter(pk__in=pks).delete()
297
289
 
298
290
  if not bypass_hooks:
299
- # Process after_delete hooks in chunks
300
- for i in range(0, len(objs), chunk_size):
301
- chunk = objs[i : i + chunk_size]
302
- engine.run(model_cls, AFTER_DELETE, chunk, ctx=ctx)
291
+ engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
303
292
 
304
293
  return objs
305
294
 
@@ -14,35 +14,6 @@ from django_bulk_hooks.constants import (
14
14
  from django_bulk_hooks.context import HookContext
15
15
  from django_bulk_hooks.engine import run
16
16
  from django_bulk_hooks.manager import BulkHookManager
17
- from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
18
- from functools import wraps
19
- import contextlib
20
-
21
-
22
- @contextlib.contextmanager
23
- def patch_foreign_key_behavior():
24
- """
25
- Temporarily patches Django's foreign key descriptor to return None instead of raising
26
- RelatedObjectDoesNotExist when accessing an unset foreign key field.
27
- """
28
- original_get = ForwardManyToOneDescriptor.__get__
29
-
30
- @wraps(original_get)
31
- def safe_get(self, instance, cls=None):
32
- if instance is None:
33
- return self
34
- try:
35
- return original_get(self, instance, cls)
36
- except self.RelatedObjectDoesNotExist:
37
- return None
38
-
39
- # Patch the descriptor
40
- ForwardManyToOneDescriptor.__get__ = safe_get
41
- try:
42
- yield
43
- finally:
44
- # Restore original behavior
45
- ForwardManyToOneDescriptor.__get__ = original_get
46
17
 
47
18
 
48
19
  class HookModelMixin(models.Model):
@@ -57,70 +28,68 @@ class HookModelMixin(models.Model):
57
28
  This ensures that when Django calls clean() (like in admin forms),
58
29
  it triggers the VALIDATE_* hooks for validation only.
59
30
  """
60
- # Call Django's clean first
61
31
  super().clean()
62
32
 
63
- # Skip hook validation during admin form validation
64
- # This prevents RelatedObjectDoesNotExist errors when Django hasn't
65
- # fully set up the object's relationships yet
66
- if hasattr(self, '_state') and getattr(self._state, 'validating', False):
67
- return
68
-
69
33
  # Determine if this is a create or update operation
70
34
  is_create = self.pk is None
71
35
 
72
36
  if is_create:
73
37
  # For create operations, run VALIDATE_CREATE hooks for validation
74
38
  ctx = HookContext(self.__class__)
75
- with patch_foreign_key_behavior():
76
- run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
39
+ run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
77
40
  else:
78
41
  # For update operations, run VALIDATE_UPDATE hooks for validation
79
42
  try:
80
43
  old_instance = self.__class__.objects.get(pk=self.pk)
81
44
  ctx = HookContext(self.__class__)
82
- with patch_foreign_key_behavior():
83
- run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
45
+ run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
84
46
  except self.__class__.DoesNotExist:
85
47
  # If the old instance doesn't exist, treat as create
86
48
  ctx = HookContext(self.__class__)
87
- with patch_foreign_key_behavior():
88
- run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
49
+ run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
89
50
 
90
51
  def save(self, *args, **kwargs):
91
52
  is_create = self.pk is None
92
- ctx = HookContext(self.__class__)
93
53
 
94
- # Use a single context manager for all hooks
95
- with patch_foreign_key_behavior():
96
- if is_create:
97
- # For create operations
54
+ if is_create:
55
+ # For create operations, we don't have old records
56
+ ctx = HookContext(self.__class__)
57
+ run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
58
+
59
+ super().save(*args, **kwargs)
60
+
61
+ run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
62
+ else:
63
+ # For update operations, we need to get the old record
64
+ try:
65
+ old_instance = self.__class__.objects.get(pk=self.pk)
66
+ ctx = HookContext(self.__class__)
67
+ run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
68
+
69
+ super().save(*args, **kwargs)
70
+
71
+ run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
72
+ except self.__class__.DoesNotExist:
73
+ # If the old instance doesn't exist, treat as create
74
+ ctx = HookContext(self.__class__)
98
75
  run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
76
+
99
77
  super().save(*args, **kwargs)
78
+
100
79
  run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
101
- else:
102
- # For update operations
103
- try:
104
- old_instance = self.__class__.objects.get(pk=self.pk)
105
- run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
106
- super().save(*args, **kwargs)
107
- run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
108
- except self.__class__.DoesNotExist:
109
- # If the old instance doesn't exist, treat as create
110
- run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
111
- super().save(*args, **kwargs)
112
- run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
113
80
 
114
81
  return self
115
82
 
116
83
  def delete(self, *args, **kwargs):
117
84
  ctx = HookContext(self.__class__)
118
85
 
119
- # Use a single context manager for all hooks
120
- with patch_foreign_key_behavior():
121
- run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
122
- run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
123
- result = super().delete(*args, **kwargs)
124
- run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
125
-
86
+ # Run validation hooks first
87
+ run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
88
+
89
+ # Then run business logic hooks
90
+ run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
91
+
92
+ result = super().delete(*args, **kwargs)
93
+
94
+ run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
126
95
  return result
@@ -0,0 +1,16 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class Priority(IntEnum):
5
+ """
6
+ Named priorities for django-bulk-hooks hooks.
7
+
8
+ Lower values run earlier (higher priority).
9
+ Hooks are sorted in ascending order.
10
+ """
11
+
12
+ HIGHEST = 0 # runs first
13
+ HIGH = 25 # runs early
14
+ NORMAL = 50 # default ordering
15
+ LOW = 75 # runs later
16
+ LOWEST = 100 # runs last
@@ -18,8 +18,9 @@ class HookQuerySet(models.QuerySet):
18
18
  model_cls = self.model
19
19
  pks = [obj.pk for obj in instances]
20
20
 
21
- # Load originals for hook comparison
22
- originals = list(model_cls.objects.filter(pk__in=pks))
21
+ # Load originals for hook comparison and ensure they match the order of instances
22
+ original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
23
+ originals = [original_map.get(obj.pk) for obj in instances]
23
24
 
24
25
  # Apply field updates to instances
25
26
  for obj in instances:
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Union
3
3
 
4
- from django_bulk_hooks.enums import Priority
4
+ from django_bulk_hooks.priority import Priority
5
5
 
6
6
  _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
7
 
@@ -17,7 +17,8 @@ def register_hook(
17
17
 
18
18
 
19
19
  def get_hooks(model, event):
20
- return _hooks.get((model, event), [])
20
+ hooks = _hooks.get((model, event), [])
21
+ return hooks
21
22
 
22
23
 
23
24
  def list_all_hooks():