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.

@@ -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
- return (current != previous) == self.has_changed
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):
@@ -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):
@@ -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 or condition.check(new, original):
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}")
@@ -72,9 +72,19 @@ class HookQuerySetMixin:
72
72
  for field, value in kwargs.items():
73
73
  setattr(obj, field, value)
74
74
 
75
- # Run BEFORE_UPDATE hooks
76
- ctx = HookContext(model_cls)
77
- engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
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
- engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
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: bypass_hooks=False, running hooks for {len(objs)} objects")
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: bypass_hooks=True, skipping hooks for {len(objs)} objects")
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(
@@ -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
- hooks = _hooks.get((model, event), [])
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
1
+ Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.214
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,