django-bulk-hooks 0.1.213__tar.gz → 0.1.215__tar.gz

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.

Files changed (18) hide show
  1. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/PKG-INFO +3 -3
  2. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/conditions.py +15 -1
  3. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/context.py +2 -1
  4. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/engine.py +18 -1
  5. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/queryset.py +13 -4
  6. django_bulk_hooks-0.1.215/django_bulk_hooks/registry.py +37 -0
  7. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/pyproject.toml +1 -1
  8. django_bulk_hooks-0.1.213/django_bulk_hooks/registry.py +0 -26
  9. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/LICENSE +0 -0
  10. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/README.md +0 -0
  11. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/__init__.py +0 -0
  12. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/constants.py +0 -0
  13. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/decorators.py +0 -0
  14. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/enums.py +0 -0
  15. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/manager.py +0 -0
  17. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/models.py +0 -0
  18. {django_bulk_hooks-0.1.213 → django_bulk_hooks-0.1.215}/django_bulk_hooks/priority.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.213
3
+ Version: 0.1.215
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
 
@@ -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):
@@ -13,8 +13,9 @@ def get_hook_queue():
13
13
 
14
14
 
15
15
  class HookContext:
16
- def __init__(self, model):
16
+ def __init__(self, model, bypass_hooks=False):
17
17
  self.model = model
18
+ self.bypass_hooks = bypass_hooks
18
19
 
19
20
  @property
20
21
  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}")
@@ -161,10 +161,12 @@ class HookQuerySetMixin:
161
161
 
162
162
  # Fire hooks before DB ops
163
163
  if not bypass_hooks:
164
- ctx = HookContext(model_cls)
164
+ ctx = HookContext(model_cls, bypass_hooks=False)
165
165
  if not bypass_validation:
166
166
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
167
167
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
168
+ else:
169
+ ctx = HookContext(model_cls, bypass_hooks=True)
168
170
 
169
171
  # For MTI models, we need to handle them specially
170
172
  if is_mti:
@@ -219,6 +221,10 @@ class HookQuerySetMixin:
219
221
  f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
220
222
  )
221
223
 
224
+ print(f"DEBUG: bulk_update called with bypass_hooks={bypass_hooks} (type: {type(bypass_hooks)}), bypass_validation={bypass_validation}")
225
+ print(f"DEBUG: bulk_update for model {model_cls.__name__} with {len(objs)} objects")
226
+ print(f"DEBUG: kwargs: {kwargs}")
227
+
222
228
  # Check for MTI
223
229
  is_mti = False
224
230
  for parent in model_cls._meta.all_parents:
@@ -237,7 +243,7 @@ class HookQuerySetMixin:
237
243
  }
238
244
  originals = [original_map.get(obj.pk) for obj in objs]
239
245
 
240
- ctx = HookContext(model_cls)
246
+ ctx = HookContext(model_cls, bypass_hooks=False)
241
247
 
242
248
  # Run validation hooks first
243
249
  if not bypass_validation:
@@ -254,6 +260,7 @@ class HookQuerySetMixin:
254
260
  fields = list(fields_set)
255
261
  else:
256
262
  print(f"DEBUG: bypass_hooks=True, skipping hooks for {len(objs)} objects")
263
+ ctx = HookContext(model_cls, bypass_hooks=True)
257
264
 
258
265
  # Handle auto_now fields like Django's update_or_create does
259
266
  fields_set = set(fields)
@@ -278,10 +285,12 @@ class HookQuerySetMixin:
278
285
  for k, v in kwargs.items()
279
286
  if k not in ["bypass_hooks", "bypass_validation"]
280
287
  }
288
+ print(f"DEBUG: Calling Django's bulk_update with kwargs: {django_kwargs}")
281
289
  # Call Django's bulk_update with hook suspension to prevent double execution
282
290
  # Django's bulk_update internally calls .update() which would trigger our hooks again
283
- with Hook.suspend():
284
- result = super().bulk_update(objs, fields, **django_kwargs)
291
+ print(f"DEBUG: About to call Django's bulk_update")
292
+ result = super().bulk_update(objs, fields, **django_kwargs)
293
+ print(f"DEBUG: Django's bulk_update completed, result: {result}")
285
294
 
286
295
  if not bypass_hooks:
287
296
  print(
@@ -0,0 +1,37 @@
1
+ from collections.abc import Callable
2
+ from typing import Union
3
+
4
+ from django_bulk_hooks.priority import Priority
5
+
6
+ _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
+
8
+
9
+ def register_hook(
10
+ model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
11
+ ):
12
+ key = (model, event)
13
+ hooks = _hooks.setdefault(key, [])
14
+ hooks.append((handler_cls, method_name, condition, priority))
15
+ # keep sorted by priority
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)}")
20
+
21
+
22
+ def get_hooks(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}")
32
+ return hooks
33
+
34
+
35
+ def list_all_hooks():
36
+ """Debug function to list all registered hooks"""
37
+ return _hooks
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.213"
3
+ version = "0.1.215"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"
@@ -1,26 +0,0 @@
1
- from collections.abc import Callable
2
- from typing import Union
3
-
4
- from django_bulk_hooks.priority import Priority
5
-
6
- _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
-
8
-
9
- def register_hook(
10
- model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
11
- ):
12
- key = (model, event)
13
- hooks = _hooks.setdefault(key, [])
14
- hooks.append((handler_cls, method_name, condition, priority))
15
- # keep sorted by priority
16
- hooks.sort(key=lambda x: x[3])
17
-
18
-
19
- def get_hooks(model, event):
20
- hooks = _hooks.get((model, event), [])
21
- return hooks
22
-
23
-
24
- def list_all_hooks():
25
- """Debug function to list all registered hooks"""
26
- return _hooks