django-bulk-hooks 0.1.235__py3-none-any.whl → 0.1.237__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.

@@ -22,6 +22,22 @@ def get_bypass_hooks():
22
22
  return getattr(_hook_context, 'bypass_hooks', False)
23
23
 
24
24
 
25
+ # Thread-local storage for passing per-object field values from bulk_update -> update
26
+ def set_bulk_update_value_map(value_map):
27
+ """Store a mapping of {pk: {field_name: value}} for the current thread.
28
+
29
+ This allows the internal update() call (triggered by Django's bulk_update)
30
+ to populate in-memory instances with the concrete values that will be
31
+ written to the database, instead of Django expression objects like Case/Cast.
32
+ """
33
+ _hook_context.bulk_update_value_map = value_map
34
+
35
+
36
+ def get_bulk_update_value_map():
37
+ """Retrieve the mapping {pk: {field_name: value}} for the current thread, if any."""
38
+ return getattr(_hook_context, 'bulk_update_value_map', None)
39
+
40
+
25
41
  class HookContext:
26
42
  def __init__(self, model, bypass_hooks=False):
27
43
  self.model = model
@@ -1,4 +1,5 @@
1
1
  import logging
2
+
2
3
  from django.db import models, transaction
3
4
  from django.db.models import AutoField, Case, Field, Value, When
4
5
 
@@ -16,7 +17,11 @@ from django_bulk_hooks.constants import (
16
17
  VALIDATE_DELETE,
17
18
  VALIDATE_UPDATE,
18
19
  )
19
- from django_bulk_hooks.context import HookContext
20
+ from django_bulk_hooks.context import (
21
+ HookContext,
22
+ get_bulk_update_value_map,
23
+ set_bulk_update_value_map,
24
+ )
20
25
 
21
26
 
22
27
  class HookQuerySetMixin:
@@ -71,14 +76,21 @@ class HookQuerySetMixin:
71
76
  )
72
77
 
73
78
  # Apply field updates to instances
79
+ # If a per-object value map exists (from bulk_update), prefer it over kwargs
80
+ per_object_values = get_bulk_update_value_map()
74
81
  for obj in instances:
75
- for field, value in kwargs.items():
76
- setattr(obj, field, value)
82
+ if per_object_values and obj.pk in per_object_values:
83
+ for field, value in per_object_values[obj.pk].items():
84
+ setattr(obj, field, value)
85
+ else:
86
+ for field, value in kwargs.items():
87
+ setattr(obj, field, value)
77
88
 
78
89
  # Check if we're in a bulk operation context to prevent double hook execution
79
90
  from django_bulk_hooks.context import get_bypass_hooks
91
+
80
92
  current_bypass_hooks = get_bypass_hooks()
81
-
93
+
82
94
  # If we're in a bulk operation context, skip hooks to prevent double execution
83
95
  if current_bypass_hooks:
84
96
  logger.debug("update: skipping hooks (bulk context)")
@@ -91,6 +103,44 @@ class HookQuerySetMixin:
91
103
  # Then run BEFORE_UPDATE hooks
92
104
  engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
93
105
 
106
+ # Persist any additional field mutations made by BEFORE_UPDATE hooks.
107
+ # Build CASE statements per modified field not already present in kwargs.
108
+ modified_fields = self._detect_modified_fields(instances, originals)
109
+ extra_fields = [f for f in modified_fields if f not in kwargs]
110
+ if extra_fields:
111
+ case_statements = {}
112
+ for field_name in extra_fields:
113
+ try:
114
+ field_obj = model_cls._meta.get_field(field_name)
115
+ except Exception:
116
+ # Skip unknown fields
117
+ continue
118
+
119
+ when_statements = []
120
+ for obj in instances:
121
+ obj_pk = getattr(obj, "pk", None)
122
+ if obj_pk is None:
123
+ continue
124
+ value = getattr(obj, field_name)
125
+ # Normalize relation values to instance or pk consistently
126
+ if getattr(field_obj, "is_relation", False):
127
+ # If a model instance is provided, use it; else use as-is
128
+ # Value() with output_field handles conversion
129
+ pass
130
+
131
+ when_statements.append(
132
+ When(pk=obj_pk, then=Value(value, output_field=field_obj))
133
+ )
134
+
135
+ if when_statements:
136
+ case_statements[field_name] = Case(
137
+ *when_statements, output_field=field_obj
138
+ )
139
+
140
+ # Merge extra CASE updates into kwargs for DB update
141
+ if case_statements:
142
+ kwargs = {**kwargs, **case_statements}
143
+
94
144
  # Use Django's built-in update logic directly
95
145
  # Call the base QuerySet implementation to avoid recursion
96
146
  update_count = super().update(**kwargs)
@@ -180,12 +230,12 @@ class HookQuerySetMixin:
180
230
 
181
231
  # Fire hooks before DB ops
182
232
  if not bypass_hooks:
183
- ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
233
+ ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
184
234
  if not bypass_validation:
185
235
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
186
236
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
187
237
  else:
188
- ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
238
+ ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
189
239
  logger.debug("bulk_create bypassed hooks")
190
240
 
191
241
  # For MTI models, we need to handle them specially
@@ -241,7 +291,9 @@ class HookQuerySetMixin:
241
291
  f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
242
292
  )
243
293
 
244
- logger.debug(f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}")
294
+ logger.debug(
295
+ f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
296
+ )
245
297
 
246
298
  # Check for MTI
247
299
  is_mti = False
@@ -257,7 +309,9 @@ class HookQuerySetMixin:
257
309
  else:
258
310
  logger.debug("bulk_update: hooks bypassed")
259
311
  ctx = HookContext(model_cls, bypass_hooks=True)
260
- originals = [None] * len(objs) # Ensure originals is defined for after_update call
312
+ originals = [None] * len(
313
+ objs
314
+ ) # Ensure originals is defined for after_update call
261
315
 
262
316
  # Handle auto_now fields like Django's update_or_create does
263
317
  fields_set = set(fields)
@@ -283,7 +337,27 @@ class HookQuerySetMixin:
283
337
  if k not in ["bypass_hooks", "bypass_validation"]
284
338
  }
285
339
  logger.debug("Calling Django bulk_update")
286
- result = super().bulk_update(objs, fields, **django_kwargs)
340
+ # Build a per-object concrete value map to avoid leaking expressions into hooks
341
+ value_map = {}
342
+ for obj in objs:
343
+ if obj.pk is None:
344
+ continue
345
+ field_values = {}
346
+ for field_name in fields:
347
+ # Capture raw values assigned on the object (not expressions)
348
+ field_values[field_name] = getattr(obj, field_name)
349
+ if field_values:
350
+ value_map[obj.pk] = field_values
351
+
352
+ # Make the value map available to the subsequent update() call
353
+ if value_map:
354
+ set_bulk_update_value_map(value_map)
355
+
356
+ try:
357
+ result = super().bulk_update(objs, fields, **django_kwargs)
358
+ finally:
359
+ # Always clear after the internal update() path finishes
360
+ set_bulk_update_value_map(None)
287
361
  logger.debug(f"Django bulk_update done: {result}")
288
362
 
289
363
  # Note: We don't run AFTER_UPDATE hooks here to prevent double execution
@@ -315,18 +389,16 @@ class HookQuerySetMixin:
315
389
  if field.name == "id":
316
390
  continue
317
391
 
318
- new_value = getattr(new_instance, field.name)
319
- original_value = getattr(original, field.name)
320
-
321
392
  # Handle different field types appropriately
322
393
  if field.is_relation:
323
- # For foreign keys, compare the pk values
324
- new_pk = new_value.pk if new_value else None
325
- original_pk = original_value.pk if original_value else None
394
+ # Compare by raw id values to catch cases where only <fk>_id was set
395
+ new_pk = getattr(new_instance, field.attname, None)
396
+ original_pk = getattr(original, field.attname, None)
326
397
  if new_pk != original_pk:
327
398
  modified_fields.add(field.name)
328
399
  else:
329
- # For regular fields, use direct comparison
400
+ new_value = getattr(new_instance, field.name)
401
+ original_value = getattr(original, field.name)
330
402
  if new_value != original_value:
331
403
  modified_fields.add(field.name)
332
404
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.235
3
+ Version: 0.1.237
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  Home-page: https://github.com/AugendLimited/django-bulk-hooks
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
2
2
  django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
3
3
  django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
- django_bulk_hooks/context.py,sha256=_NbGWTq9s66g0vbFIaqN4GlIHWQmFg3EQ44qY8YvvEg,1537
4
+ django_bulk_hooks/context.py,sha256=jlLsqGZbj__J0-iBUp1D6jTrlDEiX3qIo0XlywW4D9I,2244
5
5
  django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
6
6
  django_bulk_hooks/engine.py,sha256=t_kvgex6_iZEFc5LK-srBTZPe-1bdlYdip5LfWOc6lc,2411
7
7
  django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,
9
9
  django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
10
10
  django_bulk_hooks/models.py,sha256=exnXYVKEVbYAXhChCP8VdWTnKCnm9DiTcokEIBee1I0,4350
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=YSDCMAf24YLeLJrRKCDYwbZInP2mK_Tcuw3EHLDkv_w,32605
12
+ django_bulk_hooks/queryset.py,sha256=5utEjCnrmJ7tbE2OrDgWf0LU6SCGYoJ1OecmaQW9HVM,35767
13
13
  django_bulk_hooks/registry.py,sha256=8UuhniiH5ChSeOKV1UUbqTEiIu25bZXvcHmkaRbxmME,1131
14
- django_bulk_hooks-0.1.235.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.235.dist-info/METADATA,sha256=_AvaKqJPklOzTxh8VnSpQCG8z8B2sPoltcq3phrz56A,9049
16
- django_bulk_hooks-0.1.235.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
- django_bulk_hooks-0.1.235.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.237.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.237.dist-info/METADATA,sha256=_jfrUKLLRuYMbka2ojsBYkINVC4kg8jbb5zVvwOhS8M,9049
16
+ django_bulk_hooks-0.1.237.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
+ django_bulk_hooks-0.1.237.dist-info/RECORD,,