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

@@ -24,9 +24,8 @@ class HookQuerySet(models.QuerySet):
24
24
  objs = list(self)
25
25
  if not objs:
26
26
  return 0
27
- # Use our bulk_delete method to handle hooks properly
28
- self.bulk_delete(objs)
29
- return len(objs)
27
+ # Call the base QuerySet implementation to avoid recursion
28
+ return super().bulk_delete(objs)
30
29
 
31
30
  @transaction.atomic
32
31
  def update(self, **kwargs):
@@ -34,28 +33,31 @@ class HookQuerySet(models.QuerySet):
34
33
  if not instances:
35
34
  return 0
36
35
 
36
+ model_cls = self.model
37
+ pks = [obj.pk for obj in instances]
38
+
39
+ # Load originals for hook comparison and ensure they match the order of instances
40
+ # Use the base manager to avoid recursion
41
+ original_map = {obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)}
42
+ originals = [original_map.get(obj.pk) for obj in instances]
43
+
37
44
  # Apply field updates to instances
38
45
  for obj in instances:
39
46
  for field, value in kwargs.items():
40
47
  setattr(obj, field, value)
41
48
 
42
- # Use our bulk_update method to handle hooks properly
43
- self.bulk_update(instances, list(kwargs.keys()))
44
- return len(instances)
49
+ # Run BEFORE_UPDATE hooks
50
+ ctx = HookContext(model_cls)
51
+ engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
45
52
 
46
- def save(self, obj):
47
- """
48
- Save a single object using the appropriate bulk operation.
49
- """
50
- if obj.pk:
51
- # Use bulk_update for existing objects
52
- self.bulk_update(
53
- [obj], [field.name for field in obj._meta.fields if field.name != "id"]
54
- )
55
- else:
56
- # Use bulk_create for new objects
57
- self.bulk_create([obj])
58
- return obj
53
+ # Use Django's built-in update logic directly
54
+ # Call the base QuerySet implementation to avoid recursion
55
+ update_count = super().update(**kwargs)
56
+
57
+ # Run AFTER_UPDATE hooks
58
+ engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
59
+
60
+ return update_count
59
61
 
60
62
  @transaction.atomic
61
63
  def bulk_create(
@@ -112,30 +114,35 @@ class HookQuerySet(models.QuerySet):
112
114
 
113
115
  # Fire hooks before DB ops
114
116
  if not bypass_hooks:
117
+ print(f"DEBUG: Firing BEFORE_CREATE hooks for {model_cls}")
118
+ print(f"DEBUG: Number of objects: {len(objs)}")
119
+ print(f"DEBUG: Object types: {[type(obj) for obj in objs]}")
120
+ print(f"DEBUG: QuerySet type: {type(self)}")
121
+ print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
115
122
  ctx = HookContext(model_cls)
116
123
  if not bypass_validation:
124
+ print(f"DEBUG: Running VALIDATE_CREATE hooks")
117
125
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
126
+ print(f"DEBUG: Running BEFORE_CREATE hooks")
118
127
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
128
+ else:
129
+ print(f"DEBUG: Skipping hooks due to bypass_hooks=True for {model_cls}")
119
130
 
120
131
  # For MTI models, we need to handle them specially
121
132
  if is_mti:
122
133
  # Use our MTI-specific logic
123
134
  # Filter out custom parameters that Django's bulk_create doesn't accept
124
135
  mti_kwargs = {
125
- "batch_size": batch_size,
126
- "ignore_conflicts": ignore_conflicts,
127
- "update_conflicts": update_conflicts,
128
- "update_fields": update_fields,
129
- "unique_fields": unique_fields,
136
+ 'batch_size': batch_size,
137
+ 'ignore_conflicts': ignore_conflicts,
138
+ 'update_conflicts': update_conflicts,
139
+ 'update_fields': update_fields,
140
+ 'unique_fields': unique_fields,
130
141
  }
131
142
  # Remove custom hook kwargs if present in self.bulk_create signature
132
143
  result = self._mti_bulk_create(
133
144
  objs,
134
- **{
135
- k: v
136
- for k, v in mti_kwargs.items()
137
- if k not in ["bypass_hooks", "bypass_validation"]
138
- },
145
+ **{k: v for k, v in mti_kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
139
146
  )
140
147
  else:
141
148
  # For single-table models, use Django's built-in bulk_create
@@ -144,7 +151,6 @@ class HookQuerySet(models.QuerySet):
144
151
 
145
152
  # Use Django's original QuerySet to avoid recursive calls
146
153
  from django.db.models import QuerySet
147
-
148
154
  original_qs = QuerySet(model_cls, using=self.db)
149
155
  result = original_qs.bulk_create(
150
156
  objs,
@@ -157,19 +163,37 @@ class HookQuerySet(models.QuerySet):
157
163
 
158
164
  # Fire AFTER_CREATE hooks
159
165
  if not bypass_hooks:
166
+ print(f"DEBUG: Firing AFTER_CREATE hooks for {model_cls}")
167
+ print(f"DEBUG: Number of objects: {len(objs)}")
168
+ print(f"DEBUG: QuerySet type: {type(self)}")
169
+ print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
160
170
  engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
171
+ else:
172
+ print(f"DEBUG: Skipping AFTER_CREATE hooks due to bypass_hooks=True for {model_cls}")
161
173
 
162
174
  return result
163
175
 
164
176
  @transaction.atomic
165
- def bulk_update(
166
- self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
167
- ):
177
+ def bulk_update(self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs):
168
178
  """
169
179
  Bulk update objects in the database.
170
180
  """
181
+ import inspect
182
+ print(f"DEBUG: QuerySet.bulk_update called with:")
183
+ print(f" - self: {type(self)}")
184
+ print(f" - objs: {type(objs)}")
185
+ print(f" - fields: {fields}")
186
+ print(f" - bypass_hooks: {bypass_hooks}")
187
+ print(f" - bypass_validation: {bypass_validation}")
188
+ print(f" - kwargs: {kwargs}")
189
+ print(f"DEBUG: Method signature: {inspect.signature(self.bulk_update)}")
190
+
171
191
  model_cls = self.model
172
-
192
+ print(f"DEBUG: Model class: {model_cls}")
193
+ print(f"DEBUG: bypass_hooks value: {bypass_hooks}")
194
+ print(f"DEBUG: QuerySet type: {type(self)}")
195
+ print(f"DEBUG: Is this HookQuerySet? {isinstance(self, HookQuerySet)}")
196
+
173
197
  if not objs:
174
198
  return []
175
199
 
@@ -183,9 +207,7 @@ class HookQuerySet(models.QuerySet):
183
207
  # Use the base manager to avoid recursion
184
208
  original_map = {
185
209
  obj.pk: obj
186
- for obj in model_cls._base_manager.filter(
187
- pk__in=[obj.pk for obj in objs]
188
- )
210
+ for obj in model_cls._base_manager.filter(pk__in=[obj.pk for obj in objs])
189
211
  }
190
212
  originals = [original_map.get(obj.pk) for obj in objs]
191
213
 
@@ -197,7 +219,12 @@ class HookQuerySet(models.QuerySet):
197
219
 
198
220
  # Then run business logic hooks
199
221
  if not bypass_hooks:
222
+ print(f"DEBUG: Firing BEFORE_UPDATE hooks for {model_cls}")
223
+ print(f"DEBUG: Number of objects: {len(objs)}")
224
+ print(f"DEBUG: Object types: {[type(obj) for obj in objs]}")
200
225
  engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
226
+ else:
227
+ print(f"DEBUG: Skipping hooks due to bypass_hooks=True for {model_cls}")
201
228
 
202
229
  # Automatically detect fields that were modified during BEFORE_UPDATE hooks
203
230
  modified_fields = self._detect_modified_fields(objs, originals)
@@ -212,15 +239,15 @@ class HookQuerySet(models.QuerySet):
212
239
 
213
240
  # Call the base implementation to avoid re-triggering this method
214
241
  # Filter out custom parameters that Django's bulk_update doesn't accept
215
- django_kwargs = {
216
- k: v
217
- for k, v in kwargs.items()
218
- if k not in ["bypass_hooks", "bypass_validation"]
219
- }
242
+ django_kwargs = {k: v for k, v in kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
220
243
  super().bulk_update(chunk, fields, **django_kwargs)
221
244
 
222
245
  if not bypass_hooks:
246
+ print(f"DEBUG: Firing AFTER_UPDATE hooks for {model_cls}")
247
+ print(f"DEBUG: Number of objects: {len(objs)}")
223
248
  engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
249
+ else:
250
+ print(f"DEBUG: Skipping AFTER_UPDATE hooks due to bypass_hooks=True for {model_cls}")
224
251
 
225
252
  return objs
226
253
 
@@ -250,23 +277,9 @@ class HookQuerySet(models.QuerySet):
250
277
 
251
278
  pks = [obj.pk for obj in objs if obj.pk is not None]
252
279
 
253
- # Use Django's base manager to perform the actual deletion
254
- # This avoids recursion and uses Django's built-in delete logic
255
- from django.db.models import QuerySet
256
-
257
- base_qs = QuerySet(model_cls, using=self.db)
258
-
259
- # Delete in batches if batch_size is specified
260
- if batch_size:
261
- for i in range(0, len(objs), batch_size):
262
- batch = objs[i : i + batch_size]
263
- batch_pks = [obj.pk for obj in batch if obj.pk is not None]
264
- if batch_pks:
265
- base_qs.filter(pk__in=batch_pks).delete()
266
- else:
267
- # Delete all at once
268
- if pks:
269
- base_qs.filter(pk__in=pks).delete()
280
+ # Call the base QuerySet implementation to avoid recursion
281
+ # The hooks have already been fired above, so we don't need them again
282
+ super().bulk_delete(objs, batch_size=batch_size)
270
283
 
271
284
  if not bypass_hooks:
272
285
  engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
@@ -337,11 +350,7 @@ class HookQuerySet(models.QuerySet):
337
350
  Sets auto_now_add/auto_now fields for each model in the chain.
338
351
  """
339
352
  # Remove custom hook kwargs before passing to Django internals
340
- django_kwargs = {
341
- k: v
342
- for k, v in kwargs.items()
343
- if k not in ["bypass_hooks", "bypass_validation"]
344
- }
353
+ django_kwargs = {k: v for k, v in kwargs.items() if k not in ['bypass_hooks', 'bypass_validation']}
345
354
  if inheritance_chain is None:
346
355
  inheritance_chain = self._get_inheritance_chain()
347
356
 
@@ -374,9 +383,9 @@ class HookQuerySet(models.QuerySet):
374
383
 
375
384
  # Step 1: Do O(n) normal inserts into parent tables to get primary keys back
376
385
  # Get bypass_hooks from kwargs
377
- bypass_hooks = kwargs.get("bypass_hooks", False)
378
- bypass_validation = kwargs.get("bypass_validation", False)
379
-
386
+ bypass_hooks = kwargs.get('bypass_hooks', False)
387
+ bypass_validation = kwargs.get('bypass_validation', False)
388
+
380
389
  for obj in batch:
381
390
  parent_instances = {}
382
391
  current_parent = None
@@ -384,35 +393,32 @@ class HookQuerySet(models.QuerySet):
384
393
  parent_obj = self._create_parent_instance(
385
394
  obj, model_class, current_parent
386
395
  )
387
-
396
+
388
397
  # Fire parent hooks if not bypassed
389
398
  if not bypass_hooks:
390
399
  ctx = HookContext(model_class)
391
400
  if not bypass_validation:
392
401
  engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
393
402
  engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
394
-
403
+
395
404
  # Use Django's base manager to create the object and get PKs back
396
405
  # This bypasses hooks and the MTI exception
397
406
  field_values = {
398
407
  field.name: getattr(parent_obj, field.name)
399
408
  for field in model_class._meta.local_fields
400
- if hasattr(parent_obj, field.name)
401
- and getattr(parent_obj, field.name) is not None
409
+ if hasattr(parent_obj, field.name) and getattr(parent_obj, field.name) is not None
402
410
  }
403
- created_obj = model_class._base_manager.using(self.db).create(
404
- **field_values
405
- )
406
-
411
+ created_obj = model_class._base_manager.using(self.db).create(**field_values)
412
+
407
413
  # Update the parent_obj with the created object's PK
408
414
  parent_obj.pk = created_obj.pk
409
415
  parent_obj._state.adding = False
410
416
  parent_obj._state.db = self.db
411
-
417
+
412
418
  # Fire AFTER_CREATE hooks for parent
413
419
  if not bypass_hooks:
414
420
  engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
415
-
421
+
416
422
  parent_instances[model_class] = parent_obj
417
423
  current_parent = parent_obj
418
424
  parent_objects_map[id(obj)] = parent_instances
@@ -430,10 +436,10 @@ class HookQuerySet(models.QuerySet):
430
436
  if all_child_objects:
431
437
  # Get the base manager's queryset
432
438
  base_qs = child_model._base_manager.using(self.db)
433
-
439
+
434
440
  # Use Django's exact approach: call _prepare_for_bulk_create then partition
435
441
  base_qs._prepare_for_bulk_create(all_child_objects)
436
-
442
+
437
443
  # Implement our own partition since itertools.partition might not be available
438
444
  objs_without_pk, objs_with_pk = [], []
439
445
  for obj in all_child_objects:
@@ -441,50 +447,44 @@ class HookQuerySet(models.QuerySet):
441
447
  objs_with_pk.append(obj)
442
448
  else:
443
449
  objs_without_pk.append(obj)
444
-
450
+
445
451
  # Use Django's internal _batched_insert method
446
452
  opts = child_model._meta
447
453
  # For child models in MTI, we need to include the foreign key to the parent
448
454
  # but exclude the primary key since it's inherited
449
-
455
+
450
456
  # Include all local fields except generated ones
451
457
  # We need to include the foreign key to the parent (business_ptr)
452
458
  fields = [f for f in opts.local_fields if not f.generated]
453
-
459
+
454
460
  with transaction.atomic(using=self.db, savepoint=False):
455
- if objs_with_pk:
456
- returned_columns = base_qs._batched_insert(
457
- objs_with_pk,
458
- fields,
459
- batch_size=len(objs_with_pk), # Use actual batch size
460
- )
461
- for obj_with_pk, results in zip(objs_with_pk, returned_columns):
462
- for result, field in zip(results, opts.db_returning_fields):
463
- if field != opts.pk:
464
- setattr(obj_with_pk, field.attname, result)
465
- for obj_with_pk in objs_with_pk:
466
- obj_with_pk._state.adding = False
467
- obj_with_pk._state.db = self.db
468
-
469
- if objs_without_pk:
470
- # For objects without PK, we still need to exclude primary key fields
471
- fields = [
472
- f
473
- for f in fields
474
- if not isinstance(f, AutoField) and not f.primary_key
475
- ]
476
- returned_columns = base_qs._batched_insert(
477
- objs_without_pk,
478
- fields,
479
- batch_size=len(objs_without_pk), # Use actual batch size
480
- )
481
- for obj_without_pk, results in zip(
482
- objs_without_pk, returned_columns
483
- ):
484
- for result, field in zip(results, opts.db_returning_fields):
485
- setattr(obj_without_pk, field.attname, result)
486
- obj_without_pk._state.adding = False
487
- obj_without_pk._state.db = self.db
461
+ if objs_with_pk:
462
+ returned_columns = base_qs._batched_insert(
463
+ objs_with_pk,
464
+ fields,
465
+ batch_size=len(objs_with_pk), # Use actual batch size
466
+ )
467
+ for obj_with_pk, results in zip(objs_with_pk, returned_columns):
468
+ for result, field in zip(results, opts.db_returning_fields):
469
+ if field != opts.pk:
470
+ setattr(obj_with_pk, field.attname, result)
471
+ for obj_with_pk in objs_with_pk:
472
+ obj_with_pk._state.adding = False
473
+ obj_with_pk._state.db = self.db
474
+
475
+ if objs_without_pk:
476
+ # For objects without PK, we still need to exclude primary key fields
477
+ fields = [f for f in fields if not isinstance(f, AutoField) and not f.primary_key]
478
+ returned_columns = base_qs._batched_insert(
479
+ objs_without_pk,
480
+ fields,
481
+ batch_size=len(objs_without_pk), # Use actual batch size
482
+ )
483
+ for obj_without_pk, results in zip(objs_without_pk, returned_columns):
484
+ for result, field in zip(results, opts.db_returning_fields):
485
+ setattr(obj_without_pk, field.attname, result)
486
+ obj_without_pk._state.adding = False
487
+ obj_without_pk._state.db = self.db
488
488
 
489
489
  # Step 3: Update original objects with generated PKs and state
490
490
  pk_field_name = child_model._meta.pk.name
@@ -537,19 +537,15 @@ class HookQuerySet(models.QuerySet):
537
537
  value = getattr(source_obj, field.name, None)
538
538
  if value is not None:
539
539
  setattr(child_obj, field.name, value)
540
-
540
+
541
541
  # Set parent links for MTI
542
542
  for parent_model, parent_instance in parent_instances.items():
543
543
  parent_link = child_model._meta.get_ancestor_link(parent_model)
544
544
  if parent_link:
545
545
  # Set both the foreign key value (the ID) and the object reference
546
546
  # This follows Django's pattern in _set_pk_val
547
- setattr(
548
- child_obj, parent_link.attname, parent_instance.pk
549
- ) # Set the foreign key value
550
- setattr(
551
- child_obj, parent_link.name, parent_instance
552
- ) # Set the object reference
547
+ setattr(child_obj, parent_link.attname, parent_instance.pk) # Set the foreign key value
548
+ setattr(child_obj, parent_link.name, parent_instance) # Set the object reference
553
549
 
554
550
  # Handle auto_now_add and auto_now fields like Django does
555
551
  for field in child_model._meta.local_fields:
@@ -562,4 +558,4 @@ class HookQuerySet(models.QuerySet):
562
558
  elif hasattr(field, "auto_now") and field.auto_now:
563
559
  field.pre_save(child_obj, add=True)
564
560
 
565
- return child_obj
561
+ return child_obj
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.171
3
+ Version: 0.1.173
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
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,
9
9
  django_bulk_hooks/manager.py,sha256=OSzW8eVzknLV1WCvZcBkWMz9x_Vjq4bJM8raVXKiZvI,5085
10
10
  django_bulk_hooks/models.py,sha256=7fnx5xd4HWXfLVlFhhiRzR92JRWFEuxgk6aSWLEsyJg,3996
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=dHfZDjOPi5G4kY8VrC-M3ThjXTrh4Ik_7DN8sev-4I8,23826
12
+ django_bulk_hooks/queryset.py,sha256=83LqDkcLuNKi05p-W9qnJ_qmNrf2JuOaoPNYnEfwcSQ,25455
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.171.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.171.dist-info/METADATA,sha256=it1WojkivQKXX8ubgT-SFxiKfrKaV9GXh8rC8azUfL4,6939
16
- django_bulk_hooks-0.1.171.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
- django_bulk_hooks-0.1.171.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.173.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.173.dist-info/METADATA,sha256=yUU38QGdgYzCwOo6OJVDHsWp6UDyHM1A3qJoTgwtrng,6939
16
+ django_bulk_hooks-0.1.173.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
+ django_bulk_hooks-0.1.173.dist-info/RECORD,,