Skip to content

Commit 1558d30

Browse files
authored
Merge pull request #181 from netzkolchose/signals
signals
2 parents ae4feb3 + e542f2f commit 1558d30

File tree

6 files changed

+485
-2
lines changed

6 files changed

+485
-2
lines changed

computedfields/handlers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.db import transaction
1414
from .resolver import active_resolver
1515
from .settings import settings
16+
from .signals import resolver_start, resolver_exit
1617

1718
# typing imports
1819
from typing import Iterable, Type, cast
@@ -97,13 +98,15 @@ def postdelete_handler(sender: Type[Model], instance: Model, **kwargs) -> None:
9798
# after deletion we can update the associated computed fields
9899
updates = get_DELETES().pop(instance, None)
99100
if updates:
101+
resolver_start.send(sender=active_resolver)
100102
with transaction.atomic():
101103
for model, [pks, fields] in updates.items():
102104
active_resolver.bulk_updater(
103105
model._base_manager.filter(pk__in=pks),
104106
fields,
105107
querysize=settings.COMPUTEDFIELDS_QUERYSIZE
106108
)
109+
resolver_exit.send(sender=active_resolver)
107110

108111
# M2M tests: test_full.tests.test05_m2m test_full.tests.test06_m2mback test_full.tests.test_43.TestBetterM2M test_full.tests.test_m2m_advanced test_full.tests.test_norelated.TestNoReverse test_full.tests.test_proxymodels.TestProxyModelsM2M
109112
def m2m_handler(sender: Type[Model], instance: Model, **kwargs) -> None:
@@ -139,13 +142,15 @@ def m2m_handler(sender: Type[Model], instance: Model, **kwargs) -> None:
139142
elif action == 'post_remove':
140143
old = get_M2M_REMOVE().pop(instance, None)
141144
if old:
145+
resolver_start.send(sender=active_resolver)
142146
with transaction.atomic():
143147
for _model, [pks, update_fields] in old.items():
144148
active_resolver.bulk_updater(
145149
_model._base_manager.filter(pk__in=pks),
146150
update_fields,
147151
querysize=settings.COMPUTEDFIELDS_QUERYSIZE
148152
)
153+
resolver_exit.send(sender=active_resolver)
149154

150155
elif action == 'pre_clear':
151156
get_M2M_CLEAR()[instance] = active_resolver.preupdate_dependent(
@@ -155,10 +160,12 @@ def m2m_handler(sender: Type[Model], instance: Model, **kwargs) -> None:
155160
elif action == 'post_clear':
156161
old = get_M2M_CLEAR().pop(instance, None)
157162
if old:
163+
resolver_start.send(sender=active_resolver)
158164
with transaction.atomic():
159165
for _model, [pks, update_fields] in old.items():
160166
active_resolver.bulk_updater(
161167
_model._base_manager.filter(pk__in=pks),
162168
update_fields,
163169
querysize=settings.COMPUTEDFIELDS_QUERYSIZE
164170
)
171+
resolver_exit.send(sender=active_resolver)

computedfields/resolver.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .graph import ComputedModelsGraph, ComputedFieldsException, Graph, ModelGraph, IM2mMap
1414
from .helpers import proxy_to_base_model, slice_iterator, subquery_pk, are_same
1515
from . import __version__
16+
from .signals import resolver_start, resolver_exit, resolver_update
1617

1718
from fast_update.fast import fast_update
1819

@@ -391,7 +392,8 @@ def update_dependent(
391392
update_fields: Optional[Iterable[str]] = None,
392393
old: Optional[Dict[Type[Model], List[Any]]] = None,
393394
update_local: bool = True,
394-
querysize: Optional[int] = None
395+
querysize: Optional[int] = None,
396+
_is_recursive: bool = False
395397
) -> None:
396398
"""
397399
Updates all dependent computed fields on related models traversing
@@ -472,6 +474,8 @@ def update_dependent(
472474

473475
updates = self._querysets_for_update(_model, instance, _update_fields).values()
474476
if updates:
477+
if not _is_recursive:
478+
resolver_start.send(sender=self)
475479
with transaction.atomic(): # FIXME: place transaction only once in tree descent
476480
pks_updated: Dict[Type[Model], Set[Any]] = {}
477481
for queryset, fields in updates:
@@ -483,6 +487,8 @@ def update_dependent(
483487
pks, fields = data
484488
queryset = model2.objects.filter(pk__in=pks-pks_updated.get(model2, set()))
485489
self.bulk_updater(queryset, fields, querysize=querysize)
490+
if not _is_recursive:
491+
resolver_exit.send(sender=self)
486492

487493
def bulk_updater(
488494
self,
@@ -560,11 +566,20 @@ def bulk_updater(
560566
if change:
561567
self._update(model._base_manager.all(), change, fields)
562568

569+
if pks:
570+
resolver_update.send(sender=self, model=model, fields=fields, pks=pks)
571+
563572
# trigger dependent comp field updates from changed records
564573
# other than before we exit the update tree early, if we have no changes at all
565574
# also cuts the update tree for recursive deps (tree-like)
566575
if not local_only and pks:
567-
self.update_dependent(model._base_manager.filter(pk__in=pks), model, fields, update_local=False)
576+
self.update_dependent(
577+
instance=model._base_manager.filter(pk__in=pks),
578+
model=model,
579+
update_fields=fields,
580+
update_local=False,
581+
_is_recursive=True
582+
)
568583
return set(pks) if return_pks else None
569584

570585
def _update(self, queryset: QuerySet, change: Sequence[Any], fields: Sequence[str]) -> Union[int, None]:

computedfields/signals.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.dispatch import Signal
2+
3+
4+
# Signal sent upon start of the resolver. `sender` points to the resolver instance.
5+
resolver_start = Signal()
6+
"""Signal sent upon start of a tree update. `sender` points to the resolver instance."""
7+
8+
9+
# Signal sent after a bulk update by the resolver.
10+
resolver_update = Signal()
11+
"""
12+
Signal sent after a bulk update on a model.
13+
14+
Arguments sent with this signal:
15+
16+
- `sender`
17+
Resolver instance responsible for the updates.
18+
- `model`
19+
The model class.
20+
- `fields`
21+
Set of computed field names, that were updated.
22+
- `pks`
23+
List of model instance pks, that were updated.
24+
25+
26+
Note that this signal is sent immediately after the bulk update within the whole
27+
(recursive) dependency tree update done by the resolver. Furthermore your handler
28+
will be called under the update's transaction umbrella.
29+
30+
To not disrupt the resolver's tree update, you must avoid any raising code pattern
31+
in your handler code. Database interactions should be avoided, as the state is not
32+
fully resynced yet.
33+
34+
Also refer to the manual on how to use this signal in a safe way.
35+
"""
36+
37+
38+
# Signal sent upon exit of the resolver. `sender` points to the resolver instance.
39+
resolver_exit = Signal()
40+
"""Signal sent upon exit of a tree update. `sender` points to the resolver instance."""

docs/manual.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,53 @@ we could listen to. You have 2 options to work around this issue:
425425
update_dependent(Entry.objects.filter(pk__in=pks), update_fields=['blog'], old=old)
426426
427427
428+
Signals
429+
-------
430+
431+
Version 0.3.0 introduced 3 custom signals sent by the resolver:
432+
433+
- `resolver_start` indicates the start of a resolver update
434+
- `resolver_update` sent immediately after a model's bulk update
435+
- `resolver_exit` indicates the exit of a resolver update
436+
437+
The interesting signal is `resolver_update`, as it allows you to introspect,
438+
which computed fields on which records were updated. But since that signal is sent right away from deep within
439+
of the resolver's tree update, it comes with a few caveats:
440+
441+
- still in resolver DFS recursion
442+
- database not fully resynced yet
443+
- still under the resolver's transaction umbrella
444+
445+
446+
.. WARNING::
447+
448+
To not compromise the resolver's DFS update, you should not use any complicated or likely-to-raise code
449+
in your `resolver_update` handler. Also database interactions, especially calls to `update_dependent`,
450+
should be avoided.
451+
452+
Instead collect the interesting data points and wait for the `resolver_exit` signal.
453+
After that you can safely do your processing, example:
454+
455+
.. CODE:: python
456+
457+
from computedfields.signals import resolver_update, resolver_exit
458+
459+
def collect_updates(sender, model, fields, pks):
460+
# filter for updates of ModelXY.comp
461+
if model == ModelXY and 'comp' in fields:
462+
# only collect updated pks here
463+
store_somewhere(pks)
464+
resolver_update.connect(collect_updates)
465+
466+
def on_resolver_exit(sender):
467+
# retrieve updated pks for ModelXY.comp
468+
pks = retrieve_again()
469+
# here it is safe to do all nasty things
470+
# without compromising the resolver update
471+
likely_to_fail(ModelXY.objects.filter(pk__pks))
472+
resolver_exit.connect(on_resolver_exit)
473+
474+
428475
f-expressions
429476
-------------
430477

docs/reference.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ admin.py
3838
.. automodule:: computedfields.admin
3939
:members:
4040
:show-inheritance:
41+
42+
43+
signals.py
44+
----------
45+
46+
.. automodule:: computedfields.signals
47+
:members:
48+
:show-inheritance:

0 commit comments

Comments
 (0)