django-bulk-hooks 0.1.214__py3-none-any.whl → 0.1.216__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.
- django_bulk_hooks/conditions.py +15 -1
- django_bulk_hooks/context.py +14 -1
- django_bulk_hooks/engine.py +18 -1
- django_bulk_hooks/queryset.py +38 -10
- django_bulk_hooks/registry.py +12 -1
- {django_bulk_hooks-0.1.214.dist-info → django_bulk_hooks-0.1.216.dist-info}/METADATA +3 -3
- django_bulk_hooks-0.1.216.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.214.dist-info → django_bulk_hooks-0.1.216.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.214.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.214.dist-info → django_bulk_hooks-0.1.216.dist-info}/LICENSE +0 -0
django_bulk_hooks/conditions.py
CHANGED
|
@@ -66,11 +66,25 @@ class HasChanged(HookCondition):
|
|
|
66
66
|
self.has_changed = has_changed
|
|
67
67
|
|
|
68
68
|
def check(self, instance, original_instance=None):
|
|
69
|
+
print(f"DEBUG: HasChanged.check called for field '{self.field}' on instance {getattr(instance, 'pk', 'No PK')}")
|
|
70
|
+
print(f"DEBUG: Original instance: {getattr(original_instance, 'pk', 'No PK') if original_instance else 'None'}")
|
|
71
|
+
|
|
69
72
|
if not original_instance:
|
|
73
|
+
print(f"DEBUG: No original instance, returning False")
|
|
70
74
|
return False
|
|
75
|
+
|
|
71
76
|
current = resolve_dotted_attr(instance, self.field)
|
|
72
77
|
previous = resolve_dotted_attr(original_instance, self.field)
|
|
73
|
-
|
|
78
|
+
|
|
79
|
+
# Add more detailed debugging
|
|
80
|
+
print(f"DEBUG: Field '{self.field}' - current value: {current} (type: {type(current)})")
|
|
81
|
+
print(f"DEBUG: Field '{self.field}' - previous value: {previous} (type: {type(previous)})")
|
|
82
|
+
print(f"DEBUG: Values are equal: {current == previous}")
|
|
83
|
+
|
|
84
|
+
result = (current != previous) == self.has_changed
|
|
85
|
+
print(f"DEBUG: HasChanged result: current={current}, previous={previous}, has_changed={self.has_changed}, result={result}")
|
|
86
|
+
|
|
87
|
+
return result
|
|
74
88
|
|
|
75
89
|
|
|
76
90
|
class WasEqual(HookCondition):
|
django_bulk_hooks/context.py
CHANGED
|
@@ -12,9 +12,22 @@ def get_hook_queue():
|
|
|
12
12
|
return _hook_context.queue
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def set_bypass_hooks(bypass_hooks):
|
|
16
|
+
"""Set the current bypass_hooks state for the current thread."""
|
|
17
|
+
_hook_context.bypass_hooks = bypass_hooks
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_bypass_hooks():
|
|
21
|
+
"""Get the current bypass_hooks state for the current thread."""
|
|
22
|
+
return getattr(_hook_context, 'bypass_hooks', False)
|
|
23
|
+
|
|
24
|
+
|
|
15
25
|
class HookContext:
|
|
16
|
-
def __init__(self, model):
|
|
26
|
+
def __init__(self, model, bypass_hooks=False):
|
|
17
27
|
self.model = model
|
|
28
|
+
self.bypass_hooks = bypass_hooks
|
|
29
|
+
# Set the thread-local bypass state when creating a context
|
|
30
|
+
set_bypass_hooks(bypass_hooks)
|
|
18
31
|
|
|
19
32
|
@property
|
|
20
33
|
def is_executing(self):
|
django_bulk_hooks/engine.py
CHANGED
|
@@ -29,6 +29,12 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
29
29
|
print(f"DEBUG: Call stack (last 3 frames):")
|
|
30
30
|
for line in stack[-4:-1]: # Show last 3 frames before this one
|
|
31
31
|
print(f" {line.strip()}")
|
|
32
|
+
print(f"DEBUG: Total hooks found: {len(hooks)}")
|
|
33
|
+
|
|
34
|
+
# Check if we're in a bypass context
|
|
35
|
+
if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
|
|
36
|
+
print(f"DEBUG: Context has bypass_hooks=True, skipping hook execution")
|
|
37
|
+
return
|
|
32
38
|
|
|
33
39
|
# For BEFORE_* events, run model.clean() first for validation
|
|
34
40
|
if event.startswith("before_"):
|
|
@@ -41,6 +47,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
41
47
|
|
|
42
48
|
# Process hooks
|
|
43
49
|
for handler_cls, method_name, condition, priority in hooks:
|
|
50
|
+
print(f"DEBUG: Processing hook {handler_cls.__name__}.{method_name} with condition: {condition}")
|
|
44
51
|
handler_instance = handler_cls()
|
|
45
52
|
func = getattr(handler_instance, method_name)
|
|
46
53
|
|
|
@@ -52,9 +59,16 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
52
59
|
old_records or [None] * len(new_records),
|
|
53
60
|
strict=True,
|
|
54
61
|
):
|
|
55
|
-
if not condition
|
|
62
|
+
if not condition:
|
|
63
|
+
print(f"DEBUG: No condition, adding record {new.pk if hasattr(new, 'pk') else 'No PK'}")
|
|
56
64
|
to_process_new.append(new)
|
|
57
65
|
to_process_old.append(original)
|
|
66
|
+
else:
|
|
67
|
+
condition_result = condition.check(new, original)
|
|
68
|
+
print(f"DEBUG: Condition {condition.__class__.__name__} check result: {condition_result} for record {new.pk if hasattr(new, 'pk') else 'No PK'}")
|
|
69
|
+
if condition_result:
|
|
70
|
+
to_process_new.append(new)
|
|
71
|
+
to_process_old.append(original)
|
|
58
72
|
|
|
59
73
|
if to_process_new:
|
|
60
74
|
print(
|
|
@@ -66,4 +80,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
66
80
|
old_records=to_process_old if any(to_process_old) else None,
|
|
67
81
|
)
|
|
68
82
|
except Exception as e:
|
|
83
|
+
print(f"DEBUG: Hook execution failed: {e}")
|
|
69
84
|
raise
|
|
85
|
+
else:
|
|
86
|
+
print(f"DEBUG: No records to process for hook {handler_cls.__name__}.{method_name}")
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -72,9 +72,19 @@ class HookQuerySetMixin:
|
|
|
72
72
|
for field, value in kwargs.items():
|
|
73
73
|
setattr(obj, field, value)
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
# Check if we're in a bulk operation context to prevent double hook execution
|
|
76
|
+
from django_bulk_hooks.context import get_bypass_hooks
|
|
77
|
+
current_bypass_hooks = get_bypass_hooks()
|
|
78
|
+
|
|
79
|
+
# If we're in a bulk operation context, skip hooks to prevent double execution
|
|
80
|
+
if current_bypass_hooks:
|
|
81
|
+
print(f"DEBUG: update method skipping hooks due to bulk operation context")
|
|
82
|
+
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
83
|
+
else:
|
|
84
|
+
print(f"DEBUG: update method running hooks (standalone update)")
|
|
85
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
86
|
+
# Run BEFORE_UPDATE hooks only for standalone updates
|
|
87
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
78
88
|
|
|
79
89
|
# Use Django's built-in update logic directly
|
|
80
90
|
# Call the base QuerySet implementation to avoid recursion
|
|
@@ -101,8 +111,12 @@ class HookQuerySetMixin:
|
|
|
101
111
|
getattr(refreshed_instance, field.name),
|
|
102
112
|
)
|
|
103
113
|
|
|
104
|
-
# Run AFTER_UPDATE hooks
|
|
105
|
-
|
|
114
|
+
# Run AFTER_UPDATE hooks only for standalone updates
|
|
115
|
+
if not current_bypass_hooks:
|
|
116
|
+
print(f"DEBUG: update method running AFTER_UPDATE hooks")
|
|
117
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
118
|
+
else:
|
|
119
|
+
print(f"DEBUG: update method skipping AFTER_UPDATE hooks due to bulk operation context")
|
|
106
120
|
|
|
107
121
|
return update_count
|
|
108
122
|
|
|
@@ -161,10 +175,13 @@ class HookQuerySetMixin:
|
|
|
161
175
|
|
|
162
176
|
# Fire hooks before DB ops
|
|
163
177
|
if not bypass_hooks:
|
|
164
|
-
ctx = HookContext(model_cls)
|
|
178
|
+
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
165
179
|
if not bypass_validation:
|
|
166
180
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
167
181
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
182
|
+
else:
|
|
183
|
+
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
184
|
+
print(f"DEBUG: Set thread-local bypass_hooks=True for nested calls in bulk_create")
|
|
168
185
|
|
|
169
186
|
# For MTI models, we need to handle them specially
|
|
170
187
|
if is_mti:
|
|
@@ -219,6 +236,10 @@ class HookQuerySetMixin:
|
|
|
219
236
|
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
220
237
|
)
|
|
221
238
|
|
|
239
|
+
print(f"DEBUG: bulk_update called with bypass_hooks={bypass_hooks} (type: {type(bypass_hooks)}), bypass_validation={bypass_validation}")
|
|
240
|
+
print(f"DEBUG: bulk_update for model {model_cls.__name__} with {len(objs)} objects")
|
|
241
|
+
print(f"DEBUG: kwargs: {kwargs}")
|
|
242
|
+
|
|
222
243
|
# Check for MTI
|
|
223
244
|
is_mti = False
|
|
224
245
|
for parent in model_cls._meta.all_parents:
|
|
@@ -227,8 +248,7 @@ class HookQuerySetMixin:
|
|
|
227
248
|
break
|
|
228
249
|
|
|
229
250
|
if not bypass_hooks:
|
|
230
|
-
print(f"DEBUG:
|
|
231
|
-
# Load originals for hook comparison
|
|
251
|
+
print(f"DEBUG: bulk_update running hooks for {len(objs)} objects")
|
|
232
252
|
original_map = {
|
|
233
253
|
obj.pk: obj
|
|
234
254
|
for obj in model_cls._base_manager.filter(
|
|
@@ -237,7 +257,7 @@ class HookQuerySetMixin:
|
|
|
237
257
|
}
|
|
238
258
|
originals = [original_map.get(obj.pk) for obj in objs]
|
|
239
259
|
|
|
240
|
-
ctx = HookContext(model_cls)
|
|
260
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
241
261
|
|
|
242
262
|
# Run validation hooks first
|
|
243
263
|
if not bypass_validation:
|
|
@@ -253,7 +273,10 @@ class HookQuerySetMixin:
|
|
|
253
273
|
fields_set.update(modified_fields)
|
|
254
274
|
fields = list(fields_set)
|
|
255
275
|
else:
|
|
256
|
-
print(f"DEBUG:
|
|
276
|
+
print(f"DEBUG: bulk_update skipping hooks and setting bulk context to prevent double execution")
|
|
277
|
+
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
278
|
+
print(f"DEBUG: Set thread-local bypass_hooks=True to prevent nested update() calls from running hooks")
|
|
279
|
+
originals = [None] * len(objs) # Ensure originals is defined for after_update call
|
|
257
280
|
|
|
258
281
|
# Handle auto_now fields like Django's update_or_create does
|
|
259
282
|
fields_set = set(fields)
|
|
@@ -278,7 +301,12 @@ class HookQuerySetMixin:
|
|
|
278
301
|
for k, v in kwargs.items()
|
|
279
302
|
if k not in ["bypass_hooks", "bypass_validation"]
|
|
280
303
|
}
|
|
304
|
+
print(f"DEBUG: Calling Django's bulk_update with kwargs: {django_kwargs}")
|
|
305
|
+
# Call Django's bulk_update with hook suspension to prevent double execution
|
|
306
|
+
# Django's bulk_update internally calls .update() which would trigger our hooks again
|
|
307
|
+
print(f"DEBUG: About to call Django's bulk_update")
|
|
281
308
|
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
309
|
+
print(f"DEBUG: Django's bulk_update completed, result: {result}")
|
|
282
310
|
|
|
283
311
|
if not bypass_hooks:
|
|
284
312
|
print(
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -14,10 +14,21 @@ def register_hook(
|
|
|
14
14
|
hooks.append((handler_cls, method_name, condition, priority))
|
|
15
15
|
# keep sorted by priority
|
|
16
16
|
hooks.sort(key=lambda x: x[3])
|
|
17
|
+
print(f"DEBUG: Registered hook {handler_cls.__name__}.{method_name} for {model.__name__}.{event} with condition: {condition}")
|
|
18
|
+
print(f"DEBUG: Model class: {model} (id: {id(model)})")
|
|
19
|
+
print(f"DEBUG: Total hooks for {model.__name__}.{event}: {len(hooks)}")
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def get_hooks(model, event):
|
|
20
|
-
|
|
23
|
+
key = (model, event)
|
|
24
|
+
hooks = _hooks.get(key, [])
|
|
25
|
+
print(f"DEBUG: get_hooks called for {model.__name__}.{event}, found {len(hooks)} hooks")
|
|
26
|
+
print(f"DEBUG: Hook key: {key}")
|
|
27
|
+
print(f"DEBUG: Model class: {model} (id: {id(model)})")
|
|
28
|
+
print(f"DEBUG: All registered hook keys: {list(_hooks.keys())}")
|
|
29
|
+
for hook in hooks:
|
|
30
|
+
handler_cls, method_name, condition, priority = hook
|
|
31
|
+
print(f"DEBUG: Hook: {handler_cls.__name__}.{method_name} with condition: {condition}")
|
|
21
32
|
return hooks
|
|
22
33
|
|
|
23
34
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.216
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
-
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: django,bulk,hooks
|
|
8
7
|
Author: Konrad Beck
|
|
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Requires-Dist: Django (>=4.0)
|
|
16
|
+
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
|
+
django_bulk_hooks/conditions.py,sha256=0NEAtiIxS9DI4DcqvqGPaCJJR-5ldHqY9uQ9VuM2YYc,6865
|
|
3
|
+
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
+
django_bulk_hooks/context.py,sha256=_NbGWTq9s66g0vbFIaqN4GlIHWQmFg3EQ44qY8YvvEg,1537
|
|
5
|
+
django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=pZGN3-O7SRZeMq69DVjtAAU7CQzcm8sj8GZyTUctBdc,3140
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
+
django_bulk_hooks/models.py,sha256=TA2dBIA1nJBiYt6joefWkpFIQZWysF9kZlkBYvEe59c,4358
|
|
11
|
+
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=Lo0_BIBRX_ADDyXUCeCQ-jw4Kr3XaThrOQTaERMEe-s,34033
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=fbr6T9MidCs0oV6LhocLIVo3TGs2EazGTVqapoUbm2U,1436
|
|
14
|
+
django_bulk_hooks-0.1.216.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.216.dist-info/METADATA,sha256=AmuIXkrR8uVi8e88h74tUym5nS0WHkO0U_RJQu-uBwo,9061
|
|
16
|
+
django_bulk_hooks-0.1.216.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
django_bulk_hooks-0.1.216.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=mTvlLcttixbXRkTSNZU5VewkPUavbXRuD2BkJbVWMkw,6041
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
-
django_bulk_hooks/context.py,sha256=4IPuOX8TBAYBEMzN0RNHWgE6Giy2ZnR5uRXfd1cpIwk,1051
|
|
5
|
-
django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=aoeGCW9dne_5qB4-0YpndC3vnLGlLOYGwyWjmbFbZvg,2133
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
-
django_bulk_hooks/models.py,sha256=TA2dBIA1nJBiYt6joefWkpFIQZWysF9kZlkBYvEe59c,4358
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=AK4znACMv5tyZyFmPchreO8yn9xDERIkmerfiuSRURs,31941
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
|
|
14
|
-
django_bulk_hooks-0.1.214.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.214.dist-info/METADATA,sha256=S1UR9Sm-y6xTmf2Yi-QAIKwmt2p-dmqsJgJcrvPp2BE,9049
|
|
16
|
-
django_bulk_hooks-0.1.214.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
-
django_bulk_hooks-0.1.214.dist-info/RECORD,,
|
|
File without changes
|