django-bulk-hooks 0.1.231__py3-none-any.whl → 0.1.233__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/__init__.py +1 -12
- django_bulk_hooks/conditions.py +30 -33
- django_bulk_hooks/context.py +15 -43
- django_bulk_hooks/decorators.py +8 -111
- django_bulk_hooks/engine.py +41 -127
- django_bulk_hooks/enums.py +13 -10
- django_bulk_hooks/handler.py +73 -40
- django_bulk_hooks/manager.py +101 -123
- django_bulk_hooks/models.py +15 -51
- django_bulk_hooks/priority.py +6 -6
- django_bulk_hooks/queryset.py +181 -306
- django_bulk_hooks/registry.py +24 -191
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/METADATA +16 -32
- django_bulk_hooks-0.1.233.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.231.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/LICENSE +0 -0
django_bulk_hooks/__init__.py
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
from django_bulk_hooks.handler import Hook
|
|
2
2
|
from django_bulk_hooks.manager import BulkHookManager
|
|
3
|
-
from django_bulk_hooks.decorators import hook
|
|
4
|
-
from django_bulk_hooks.priority import Priority
|
|
5
|
-
from django_bulk_hooks.context import HookContext
|
|
6
|
-
from django_bulk_hooks.models import HookModelMixin
|
|
7
3
|
|
|
8
|
-
__all__ = [
|
|
9
|
-
"Hook",
|
|
10
|
-
"hook",
|
|
11
|
-
"Priority",
|
|
12
|
-
"HookContext",
|
|
13
|
-
"HookModelMixin",
|
|
14
|
-
"BulkHookManager",
|
|
15
|
-
]
|
|
4
|
+
__all__ = ["BulkHookManager", "Hook"]
|
django_bulk_hooks/conditions.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Any, Optional
|
|
3
2
|
|
|
4
3
|
logger = logging.getLogger(__name__)
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
def resolve_dotted_attr(instance
|
|
6
|
+
def resolve_dotted_attr(instance, dotted_path):
|
|
8
7
|
"""
|
|
9
8
|
Recursively resolve a dotted attribute path, e.g., "type.category".
|
|
10
|
-
|
|
11
|
-
Returns None if any intermediate value is None or missing.
|
|
12
9
|
"""
|
|
13
10
|
for attr in dotted_path.split("."):
|
|
14
11
|
if instance is None:
|
|
@@ -18,29 +15,29 @@ def resolve_dotted_attr(instance: Any, dotted_path: str) -> Any:
|
|
|
18
15
|
|
|
19
16
|
|
|
20
17
|
class HookCondition:
|
|
21
|
-
def check(self, instance
|
|
18
|
+
def check(self, instance, original_instance=None):
|
|
22
19
|
raise NotImplementedError
|
|
23
20
|
|
|
24
|
-
def __call__(self, instance
|
|
21
|
+
def __call__(self, instance, original_instance=None):
|
|
25
22
|
return self.check(instance, original_instance)
|
|
26
23
|
|
|
27
|
-
def __and__(self, other
|
|
24
|
+
def __and__(self, other):
|
|
28
25
|
return AndCondition(self, other)
|
|
29
26
|
|
|
30
|
-
def __or__(self, other
|
|
27
|
+
def __or__(self, other):
|
|
31
28
|
return OrCondition(self, other)
|
|
32
29
|
|
|
33
|
-
def __invert__(self)
|
|
30
|
+
def __invert__(self):
|
|
34
31
|
return NotCondition(self)
|
|
35
32
|
|
|
36
33
|
|
|
37
34
|
class IsNotEqual(HookCondition):
|
|
38
|
-
def __init__(self, field
|
|
35
|
+
def __init__(self, field, value, only_on_change=False):
|
|
39
36
|
self.field = field
|
|
40
37
|
self.value = value
|
|
41
38
|
self.only_on_change = only_on_change
|
|
42
39
|
|
|
43
|
-
def check(self, instance
|
|
40
|
+
def check(self, instance, original_instance=None):
|
|
44
41
|
current = resolve_dotted_attr(instance, self.field)
|
|
45
42
|
if self.only_on_change:
|
|
46
43
|
if original_instance is None:
|
|
@@ -52,12 +49,12 @@ class IsNotEqual(HookCondition):
|
|
|
52
49
|
|
|
53
50
|
|
|
54
51
|
class IsEqual(HookCondition):
|
|
55
|
-
def __init__(self, field
|
|
52
|
+
def __init__(self, field, value, only_on_change=False):
|
|
56
53
|
self.field = field
|
|
57
54
|
self.value = value
|
|
58
55
|
self.only_on_change = only_on_change
|
|
59
56
|
|
|
60
|
-
def check(self, instance
|
|
57
|
+
def check(self, instance, original_instance=None):
|
|
61
58
|
current = resolve_dotted_attr(instance, self.field)
|
|
62
59
|
if self.only_on_change:
|
|
63
60
|
if original_instance is None:
|
|
@@ -69,11 +66,11 @@ class IsEqual(HookCondition):
|
|
|
69
66
|
|
|
70
67
|
|
|
71
68
|
class HasChanged(HookCondition):
|
|
72
|
-
def __init__(self, field
|
|
69
|
+
def __init__(self, field, has_changed=True):
|
|
73
70
|
self.field = field
|
|
74
71
|
self.has_changed = has_changed
|
|
75
72
|
|
|
76
|
-
def check(self, instance
|
|
73
|
+
def check(self, instance, original_instance=None):
|
|
77
74
|
if not original_instance:
|
|
78
75
|
return False
|
|
79
76
|
|
|
@@ -88,7 +85,7 @@ class HasChanged(HookCondition):
|
|
|
88
85
|
|
|
89
86
|
|
|
90
87
|
class WasEqual(HookCondition):
|
|
91
|
-
def __init__(self, field
|
|
88
|
+
def __init__(self, field, value, only_on_change=False):
|
|
92
89
|
"""
|
|
93
90
|
Check if a field's original value was `value`.
|
|
94
91
|
If only_on_change is True, only return True when the field has changed away from that value.
|
|
@@ -97,7 +94,7 @@ class WasEqual(HookCondition):
|
|
|
97
94
|
self.value = value
|
|
98
95
|
self.only_on_change = only_on_change
|
|
99
96
|
|
|
100
|
-
def check(self, instance
|
|
97
|
+
def check(self, instance, original_instance=None):
|
|
101
98
|
if original_instance is None:
|
|
102
99
|
return False
|
|
103
100
|
previous = resolve_dotted_attr(original_instance, self.field)
|
|
@@ -109,7 +106,7 @@ class WasEqual(HookCondition):
|
|
|
109
106
|
|
|
110
107
|
|
|
111
108
|
class ChangesTo(HookCondition):
|
|
112
|
-
def __init__(self, field
|
|
109
|
+
def __init__(self, field, value):
|
|
113
110
|
"""
|
|
114
111
|
Check if a field's value has changed to `value`.
|
|
115
112
|
Only returns True when original value != value and current value == value.
|
|
@@ -117,7 +114,7 @@ class ChangesTo(HookCondition):
|
|
|
117
114
|
self.field = field
|
|
118
115
|
self.value = value
|
|
119
116
|
|
|
120
|
-
def check(self, instance
|
|
117
|
+
def check(self, instance, original_instance=None):
|
|
121
118
|
if original_instance is None:
|
|
122
119
|
return False
|
|
123
120
|
previous = resolve_dotted_attr(original_instance, self.field)
|
|
@@ -126,70 +123,70 @@ class ChangesTo(HookCondition):
|
|
|
126
123
|
|
|
127
124
|
|
|
128
125
|
class IsGreaterThan(HookCondition):
|
|
129
|
-
def __init__(self, field
|
|
126
|
+
def __init__(self, field, value):
|
|
130
127
|
self.field = field
|
|
131
128
|
self.value = value
|
|
132
129
|
|
|
133
|
-
def check(self, instance
|
|
130
|
+
def check(self, instance, original_instance=None):
|
|
134
131
|
current = resolve_dotted_attr(instance, self.field)
|
|
135
132
|
return current is not None and current > self.value
|
|
136
133
|
|
|
137
134
|
|
|
138
135
|
class IsGreaterThanOrEqual(HookCondition):
|
|
139
|
-
def __init__(self, field
|
|
136
|
+
def __init__(self, field, value):
|
|
140
137
|
self.field = field
|
|
141
138
|
self.value = value
|
|
142
139
|
|
|
143
|
-
def check(self, instance
|
|
140
|
+
def check(self, instance, original_instance=None):
|
|
144
141
|
current = resolve_dotted_attr(instance, self.field)
|
|
145
142
|
return current is not None and current >= self.value
|
|
146
143
|
|
|
147
144
|
|
|
148
145
|
class IsLessThan(HookCondition):
|
|
149
|
-
def __init__(self, field
|
|
146
|
+
def __init__(self, field, value):
|
|
150
147
|
self.field = field
|
|
151
148
|
self.value = value
|
|
152
149
|
|
|
153
|
-
def check(self, instance
|
|
150
|
+
def check(self, instance, original_instance=None):
|
|
154
151
|
current = resolve_dotted_attr(instance, self.field)
|
|
155
152
|
return current is not None and current < self.value
|
|
156
153
|
|
|
157
154
|
|
|
158
155
|
class IsLessThanOrEqual(HookCondition):
|
|
159
|
-
def __init__(self, field
|
|
156
|
+
def __init__(self, field, value):
|
|
160
157
|
self.field = field
|
|
161
158
|
self.value = value
|
|
162
159
|
|
|
163
|
-
def check(self, instance
|
|
160
|
+
def check(self, instance, original_instance=None):
|
|
164
161
|
current = resolve_dotted_attr(instance, self.field)
|
|
165
162
|
return current is not None and current <= self.value
|
|
166
163
|
|
|
167
164
|
|
|
168
165
|
class AndCondition(HookCondition):
|
|
169
|
-
def __init__(self, cond1
|
|
166
|
+
def __init__(self, cond1, cond2):
|
|
170
167
|
self.cond1 = cond1
|
|
171
168
|
self.cond2 = cond2
|
|
172
169
|
|
|
173
|
-
def check(self, instance
|
|
170
|
+
def check(self, instance, original_instance=None):
|
|
174
171
|
return self.cond1.check(instance, original_instance) and self.cond2.check(
|
|
175
172
|
instance, original_instance
|
|
176
173
|
)
|
|
177
174
|
|
|
178
175
|
|
|
179
176
|
class OrCondition(HookCondition):
|
|
180
|
-
def __init__(self, cond1
|
|
177
|
+
def __init__(self, cond1, cond2):
|
|
181
178
|
self.cond1 = cond1
|
|
182
179
|
self.cond2 = cond2
|
|
183
180
|
|
|
184
|
-
def check(self, instance
|
|
181
|
+
def check(self, instance, original_instance=None):
|
|
185
182
|
return self.cond1.check(instance, original_instance) or self.cond2.check(
|
|
186
183
|
instance, original_instance
|
|
187
184
|
)
|
|
188
185
|
|
|
189
186
|
|
|
190
187
|
class NotCondition(HookCondition):
|
|
191
|
-
def __init__(self, cond
|
|
188
|
+
def __init__(self, cond):
|
|
192
189
|
self.cond = cond
|
|
193
190
|
|
|
194
|
-
def check(self, instance
|
|
191
|
+
def check(self, instance, original_instance=None):
|
|
195
192
|
return not self.cond.check(instance, original_instance)
|
django_bulk_hooks/context.py
CHANGED
|
@@ -1,61 +1,36 @@
|
|
|
1
1
|
import threading
|
|
2
|
-
from
|
|
3
|
-
from django_bulk_hooks.handler import hook_vars
|
|
2
|
+
from collections import deque
|
|
3
|
+
from django_bulk_hooks.handler import hook_vars
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
_hook_context = threading.local()
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def get_hook_queue()
|
|
10
|
-
""
|
|
11
|
-
|
|
9
|
+
def get_hook_queue():
|
|
10
|
+
if not hasattr(_hook_context, "queue"):
|
|
11
|
+
_hook_context.queue = deque()
|
|
12
|
+
return _hook_context.queue
|
|
12
13
|
|
|
13
|
-
This proxies to the centralized queue in the handler module to avoid
|
|
14
|
-
divergent queues.
|
|
15
|
-
"""
|
|
16
|
-
return _handler_get_hook_queue()
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
def set_bypass_hooks(bypass_hooks: bool) -> None:
|
|
15
|
+
def set_bypass_hooks(bypass_hooks):
|
|
20
16
|
"""Set the current bypass_hooks state for the current thread."""
|
|
21
17
|
_hook_context.bypass_hooks = bypass_hooks
|
|
22
18
|
|
|
23
19
|
|
|
24
|
-
def get_bypass_hooks()
|
|
20
|
+
def get_bypass_hooks():
|
|
25
21
|
"""Get the current bypass_hooks state for the current thread."""
|
|
26
22
|
return getattr(_hook_context, 'bypass_hooks', False)
|
|
27
23
|
|
|
28
24
|
|
|
29
25
|
class HookContext:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- On construction sets thread-local bypass state;
|
|
35
|
-
when used as a context manager, restores previous state on exit.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self, model: type, bypass_hooks: bool = False):
|
|
39
|
-
self.model: type = model
|
|
40
|
-
self.bypass_hooks: bool = bypass_hooks
|
|
41
|
-
self._prev_bypass: Optional[bool] = None
|
|
42
|
-
# Preserve legacy behavior: set bypass state at construction
|
|
26
|
+
def __init__(self, model, bypass_hooks=False):
|
|
27
|
+
self.model = model
|
|
28
|
+
self.bypass_hooks = bypass_hooks
|
|
29
|
+
# Set the thread-local bypass state when creating a context
|
|
43
30
|
set_bypass_hooks(bypass_hooks)
|
|
44
31
|
|
|
45
|
-
def __enter__(self) -> "HookContext":
|
|
46
|
-
self._prev_bypass = get_bypass_hooks()
|
|
47
|
-
set_bypass_hooks(self.bypass_hooks)
|
|
48
|
-
return self
|
|
49
|
-
|
|
50
|
-
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
51
|
-
# Restore previous bypass state
|
|
52
|
-
if self._prev_bypass is not None:
|
|
53
|
-
set_bypass_hooks(self._prev_bypass)
|
|
54
|
-
# Do not suppress exceptions
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
32
|
@property
|
|
58
|
-
def is_executing(self)
|
|
33
|
+
def is_executing(self):
|
|
59
34
|
"""
|
|
60
35
|
Check if we're currently in a hook execution context.
|
|
61
36
|
Similar to Salesforce's Trigger.isExecuting.
|
|
@@ -64,18 +39,15 @@ class HookContext:
|
|
|
64
39
|
return hasattr(hook_vars, 'event') and hook_vars.event is not None
|
|
65
40
|
|
|
66
41
|
@property
|
|
67
|
-
def current_event(self)
|
|
42
|
+
def current_event(self):
|
|
68
43
|
"""
|
|
69
44
|
Get the current hook event being executed.
|
|
70
45
|
"""
|
|
71
46
|
return getattr(hook_vars, 'event', None)
|
|
72
47
|
|
|
73
48
|
@property
|
|
74
|
-
def execution_depth(self)
|
|
49
|
+
def execution_depth(self):
|
|
75
50
|
"""
|
|
76
51
|
Get the current execution depth to detect deep recursion.
|
|
77
52
|
"""
|
|
78
53
|
return getattr(hook_vars, 'depth', 0)
|
|
79
|
-
|
|
80
|
-
def __repr__(self) -> str:
|
|
81
|
-
return f"HookContext(model={getattr(self.model, '__name__', self.model)!r}, bypass_hooks={self.bypass_hooks})"
|
django_bulk_hooks/decorators.py
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Decorators for defining and optimizing hook handlers.
|
|
3
|
-
|
|
4
|
-
Notes:
|
|
5
|
-
- Hook registration occurs at import time; importing modules that define Hook
|
|
6
|
-
subclasses or use @hook will register handlers in the global registry.
|
|
7
|
-
- The preload helpers below are safe, in-place optimizations to avoid N+1s.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
1
|
import inspect
|
|
11
2
|
from functools import wraps
|
|
12
|
-
from typing import Any, Callable, Optional
|
|
13
3
|
|
|
14
4
|
from django.core.exceptions import FieldDoesNotExist
|
|
15
5
|
from django_bulk_hooks.enums import DEFAULT_PRIORITY
|
|
16
6
|
from django_bulk_hooks.registry import register_hook
|
|
17
7
|
|
|
18
8
|
|
|
19
|
-
def hook(event
|
|
9
|
+
def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
|
|
20
10
|
"""
|
|
21
11
|
Decorator to annotate a method with multiple hooks hook registrations.
|
|
22
12
|
If no priority is provided, uses Priority.NORMAL (50).
|
|
23
13
|
"""
|
|
24
14
|
|
|
25
|
-
def decorator(fn
|
|
15
|
+
def decorator(fn):
|
|
26
16
|
if not hasattr(fn, "hooks_hooks"):
|
|
27
17
|
fn.hooks_hooks = []
|
|
28
18
|
fn.hooks_hooks.append((model, event, condition, priority))
|
|
@@ -31,7 +21,7 @@ def hook(event: str, *, model: type, condition: Optional[Callable] = None, prior
|
|
|
31
21
|
return decorator
|
|
32
22
|
|
|
33
23
|
|
|
34
|
-
def select_related(*related_fields
|
|
24
|
+
def select_related(*related_fields):
|
|
35
25
|
"""
|
|
36
26
|
Decorator that preloads related fields in-place on `new_records`, before the hook logic runs.
|
|
37
27
|
|
|
@@ -40,14 +30,11 @@ def select_related(*related_fields: str):
|
|
|
40
30
|
- Populates Django's relation cache to avoid extra queries
|
|
41
31
|
"""
|
|
42
32
|
|
|
43
|
-
def decorator(func
|
|
44
|
-
# No-op if no fields specified
|
|
45
|
-
if not related_fields:
|
|
46
|
-
return func
|
|
33
|
+
def decorator(func):
|
|
47
34
|
sig = inspect.signature(func)
|
|
48
35
|
|
|
49
36
|
@wraps(func)
|
|
50
|
-
def wrapper(*args
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
51
38
|
bound = sig.bind_partial(*args, **kwargs)
|
|
52
39
|
bound.apply_defaults()
|
|
53
40
|
|
|
@@ -118,7 +105,7 @@ def select_related(*related_fields: str):
|
|
|
118
105
|
return decorator
|
|
119
106
|
|
|
120
107
|
|
|
121
|
-
def bulk_hook(model_cls
|
|
108
|
+
def bulk_hook(model_cls, event, when=None, priority=None):
|
|
122
109
|
"""
|
|
123
110
|
Decorator to register a bulk hook for a model.
|
|
124
111
|
|
|
@@ -128,24 +115,13 @@ def bulk_hook(model_cls: type, event: str, when: Optional[Callable] = None, prio
|
|
|
128
115
|
when: Optional condition for when the hook should run
|
|
129
116
|
priority: Optional priority for hook execution order
|
|
130
117
|
"""
|
|
131
|
-
def decorator(func
|
|
132
|
-
# Deprecation: prefer @hook(event, model=...) on a Hook subclass
|
|
133
|
-
try:
|
|
134
|
-
import warnings
|
|
135
|
-
warnings.warn(
|
|
136
|
-
"bulk_hook is deprecated; use @hook(event, model=..., condition=..., priority=...) on a Hook subclass",
|
|
137
|
-
DeprecationWarning,
|
|
138
|
-
stacklevel=2,
|
|
139
|
-
)
|
|
140
|
-
except Exception:
|
|
141
|
-
pass
|
|
142
|
-
|
|
118
|
+
def decorator(func):
|
|
143
119
|
# Create a simple handler class for the function
|
|
144
120
|
class FunctionHandler:
|
|
145
121
|
def __init__(self):
|
|
146
122
|
self.func = func
|
|
147
123
|
|
|
148
|
-
def handle(self, new_instances
|
|
124
|
+
def handle(self, new_instances, original_instances):
|
|
149
125
|
return self.func(new_instances, original_instances)
|
|
150
126
|
|
|
151
127
|
# Register the hook using the registry
|
|
@@ -159,82 +135,3 @@ def bulk_hook(model_cls: type, event: str, when: Optional[Callable] = None, prio
|
|
|
159
135
|
)
|
|
160
136
|
return func
|
|
161
137
|
return decorator
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def prefetch_related(*related_fields: str):
|
|
165
|
-
"""
|
|
166
|
-
Decorator that prefetches related collections on `new_records` in-place,
|
|
167
|
-
populating Django's prefetch cache to avoid extra queries in hooks.
|
|
168
|
-
|
|
169
|
-
- Supports many-to-many and one-to-many relationships
|
|
170
|
-
- Preserves instance identity; does not replace objects
|
|
171
|
-
- Uses the base manager to avoid recursive hook triggering
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
def decorator(func: Callable):
|
|
175
|
-
# No-op if no fields specified
|
|
176
|
-
if not related_fields:
|
|
177
|
-
return func
|
|
178
|
-
|
|
179
|
-
sig = inspect.signature(func)
|
|
180
|
-
|
|
181
|
-
@wraps(func)
|
|
182
|
-
def wrapper(*args: Any, **kwargs: Any):
|
|
183
|
-
bound = sig.bind_partial(*args, **kwargs)
|
|
184
|
-
bound.apply_defaults()
|
|
185
|
-
|
|
186
|
-
if "new_records" not in bound.arguments:
|
|
187
|
-
raise TypeError(
|
|
188
|
-
"@prefetch_related requires a 'new_records' argument in the decorated function"
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
new_records = bound.arguments["new_records"]
|
|
192
|
-
|
|
193
|
-
if not isinstance(new_records, list):
|
|
194
|
-
raise TypeError(
|
|
195
|
-
f"@prefetch_related expects a list of model instances, got {type(new_records)}"
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
if not new_records:
|
|
199
|
-
return func(*args, **kwargs)
|
|
200
|
-
|
|
201
|
-
model_cls = new_records[0].__class__
|
|
202
|
-
ids_to_fetch = [obj.pk for obj in new_records if getattr(obj, "pk", None)]
|
|
203
|
-
|
|
204
|
-
if ids_to_fetch:
|
|
205
|
-
# Validate fields (no dotted notation)
|
|
206
|
-
for field in related_fields:
|
|
207
|
-
if "." in field:
|
|
208
|
-
raise ValueError(
|
|
209
|
-
f"@prefetch_related does not support nested fields like '{field}'"
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
fetched_map = {
|
|
213
|
-
obj.pk: obj
|
|
214
|
-
for obj in (
|
|
215
|
-
model_cls._base_manager.filter(pk__in=ids_to_fetch)
|
|
216
|
-
.prefetch_related(*related_fields)
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for obj in new_records:
|
|
221
|
-
preloaded = fetched_map.get(obj.pk)
|
|
222
|
-
if preloaded is None:
|
|
223
|
-
continue
|
|
224
|
-
# Copy prefetch cache entries from the preloaded instance
|
|
225
|
-
src_cache = getattr(preloaded, "_prefetched_objects_cache", {})
|
|
226
|
-
if not src_cache:
|
|
227
|
-
continue
|
|
228
|
-
dst_cache = getattr(obj, "_prefetched_objects_cache", None)
|
|
229
|
-
if dst_cache is None:
|
|
230
|
-
obj._prefetched_objects_cache = {}
|
|
231
|
-
dst_cache = obj._prefetched_objects_cache
|
|
232
|
-
for field in related_fields:
|
|
233
|
-
if field in src_cache and field not in dst_cache:
|
|
234
|
-
dst_cache[field] = src_cache[field]
|
|
235
|
-
|
|
236
|
-
return func(*bound.args, **bound.kwargs)
|
|
237
|
-
|
|
238
|
-
return wrapper
|
|
239
|
-
|
|
240
|
-
return decorator
|