• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

netzkolchose / django-computedfields / 16119908775

07 Jul 2025 02:33PM UTC coverage: 94.927% (+0.02%) from 94.903%
16119908775

Pull #178

github

web-flow
Merge 54bc1a9fc into 39d912552
Pull Request #178: default_on_create argument

475 of 516 branches covered (92.05%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

8 existing lines in 1 file now uncovered.

1153 of 1199 relevant lines covered (96.16%)

11.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.57
/computedfields/resolver.py
1
"""
2
Contains the resolver logic for automated computed field updates.
3
"""
4
import operator
12✔
5
from collections import OrderedDict
12✔
6
from functools import reduce
12✔
7
from itertools import zip_longest
12✔
8

9
from django.db import transaction
12✔
10
from django.db.models import QuerySet
12✔
11
from django.core.exceptions import FieldDoesNotExist
12✔
12

13
from .settings import settings
12✔
14
from .graph import ComputedModelsGraph, ComputedFieldsException, Graph, ModelGraph
12✔
15
from .helpers import proxy_to_base_model, slice_iterator, subquery_pk, are_same, modelname
12✔
16
from . import __version__
12✔
17

18
from fast_update.fast import fast_update
12✔
19

20
# typing imports
21
from typing import (Any, Callable, Dict, Generator, Iterable, List, Optional, Sequence, Set,
12✔
22
                    Tuple, Type, Union, cast, overload)
23
from typing_extensions import TypedDict
12✔
24
from django.db.models import Field, Model
12✔
25
from .graph import IComputedField, IDepends, IFkMap, ILocalMroMap, ILookupMap, _ST, _GT, F
12✔
26

27

28
class IM2mData(TypedDict):
12✔
29
    left: str
12✔
30
    right: str
12✔
31
IM2mMap = Dict[Type[Model], IM2mData]
12✔
32

33

34
MALFORMED_DEPENDS = """
12✔
35
Your depends keyword argument is malformed.
36

37
The depends keyword should either be None, an empty listing or
38
a listing of rules as depends=[rule1, rule2, .. ruleN].
39

40
A rule is formed as ('relation.path', ['list', 'of', 'fieldnames']) tuple.
41
The relation path either contains 'self' for fieldnames on the same model,
42
or a string as 'a.b.c', where 'a' is a relation on the current model
43
descending over 'b' to 'c' to pull fieldnames from 'c'. The denoted fieldnames
44
must be concrete fields on the rightmost model of the relation path.
45

46
Example:
47
depends=[
48
    ('self', ['name', 'status']),
49
    ('parent.color', ['value'])
50
]
51
This has 2 path rules - one for fields 'name' and 'status' on the same model,
52
and one to a field 'value' on a foreign model, which is accessible from
53
the current model through self -> parent -> color relation.
54
"""
55

56

57
class ResolverException(ComputedFieldsException):
12✔
58
    """
59
    Exception raised during model and field registration or dependency resolving.
60
    """
61

62

63
class Resolver:
12✔
64
    """
65
    Holds the needed data for graph calculations and runtime dependency resolving.
66

67
    Basic workflow:
68

69
        - On django startup a resolver gets instantiated early to track all project-wide
70
          model registrations and computed field decorations (collector phase).
71
        - On `app.ready` the computed fields are associated with their models to build
72
          a resolver-wide map of models with computed fields (``computed_models``).
73
        - After that the resolver maps are created (see `graph.ComputedModelsGraph`).
74
    """
75

76
    def __init__(self):
12✔
77
        # collector phase data
78
        #: Models from `class_prepared` signal hook during collector phase.
79
        self.models: Set[Type[Model]] = set()
12✔
80
        #: Computed fields found during collector phase.
81
        self.computedfields: Set[IComputedField] = set()
12✔
82

83
        # resolving phase data and final maps
84
        self._graph: Optional[ComputedModelsGraph] = None
12✔
85
        self._computed_models: Dict[Type[Model], Dict[str, IComputedField]] = {}
12✔
86
        self._map: ILookupMap = {}
12✔
87
        self._fk_map: IFkMap = {}
12✔
88
        self._local_mro: ILocalMroMap = {}
12✔
89
        self._m2m: IM2mMap = {}
12✔
90
        self._proxymodels: Dict[Type[Model], Type[Model]] = {}
12✔
91
        self.use_fastupdate: bool = settings.COMPUTEDFIELDS_FASTUPDATE
12✔
92
        self._batchsize: int = (settings.COMPUTEDFIELDS_BATCHSIZE_FAST
12✔
93
            if self.use_fastupdate else settings.COMPUTEDFIELDS_BATCHSIZE_BULK)
94

95
        # some internal states
96
        self._sealed: bool = False        # initial boot phase
12✔
97
        self._initialized: bool = False   # initialized (computed_models populated)?
12✔
98
        self._map_loaded: bool = False    # final stage with fully loaded maps
12✔
99

100
    def add_model(self, sender: Type[Model], **kwargs) -> None:
12✔
101
        """
102
        `class_prepared` signal hook to collect models during ORM registration.
103
        """
104
        if self._sealed:
12✔
105
            raise ResolverException('cannot add models on sealed resolver')
12✔
106
        self.models.add(sender)
12✔
107

108
    def add_field(self, field: IComputedField) -> None:
12✔
109
        """
110
        Collects fields from decoration stage of @computed.
111
        """
112
        if self._sealed:
12✔
113
            raise ResolverException('cannot add computed fields on sealed resolver')
12✔
114
        self.computedfields.add(field)
12✔
115

116
    def seal(self) -> None:
12✔
117
        """
118
        Seal the resolver, so no new models or computed fields can be added anymore.
119

120
        This marks the end of the collector phase and is a basic security measure
121
        to catch runtime model creations with computed fields.
122

123
        (Currently runtime creation of models with computed fields is not supported,
124
        trying to do so will raise an exception. This might change in future versions.)
125
        """
126
        self._sealed = True
12✔
127

128
    @property
12✔
129
    def models_with_computedfields(self) -> Generator[Tuple[Type[Model], Set[IComputedField]], None, None]:
12✔
130
        """
131
        Generator of tracked models with their computed fields.
132

133
        This cannot be accessed during the collector phase.
134
        """
135
        if not self._sealed:
12✔
136
            raise ResolverException('resolver must be sealed before accessing models or fields')
12✔
137

138
        field_ids: List[int] = [f.creation_counter for f in self.computedfields]
12✔
139
        for model in self.models:
12✔
140
            fields = set()
12✔
141
            for field in model._meta.fields:
12✔
142
                # for some reason the in ... check does not work for Django >= 3.2 anymore
143
                # workaround: check for _computed and the field creation_counter
144
                if hasattr(field, '_computed') and field.creation_counter in field_ids:
12✔
145
                    fields.add(field)
12✔
146
            if fields:
12✔
147
                yield (model, fields)
12✔
148

149
    @property
12✔
150
    def computedfields_with_models(self) -> Generator[Tuple[IComputedField, Set[Type[Model]]], None, None]:
12✔
151
        """
152
        Generator of tracked computed fields and their models.
153

154
        This cannot be accessed during the collector phase.
155
        """
156
        if not self._sealed:
12✔
157
            raise ResolverException('resolver must be sealed before accessing models or fields')
12✔
158

159
        for field in self.computedfields:
12✔
160
            models = set()
12✔
161
            for model in self.models:
12✔
162
                for f in model._meta.fields:
12✔
163
                    if hasattr(field, '_computed') and f.creation_counter == field.creation_counter:
12✔
164
                        models.add(model)
12✔
165
            yield (field, models)
12✔
166

167
    @property
12✔
168
    def computed_models(self) -> Dict[Type[Model], Dict[str, IComputedField]]:
12✔
169
        """
170
        Mapping of `ComputedFieldModel` models and their computed fields.
171

172
        The data is the single source of truth for the graph reduction and
173
        map creations. Thus it can be used to decide at runtime whether
174
        the active resolver a certain as a model with computed fields.
175
        
176
        .. NOTE::
177
        
178
            The resolver will only list models here, that actually have
179
            a computed field defined. A model derived from `ComputedFieldsModel`
180
            without a computed field will not be listed.
181
        """
182
        if self._initialized:
12✔
183
            return self._computed_models
12✔
184
        raise ResolverException('resolver is not properly initialized')
12✔
185

186
    def extract_computed_models(self) -> Dict[Type[Model], Dict[str, IComputedField]]:
12✔
187
        """
188
        Creates `computed_models` mapping from models and computed fields
189
        found in collector phase.
190
        """
191
        computed_models: Dict[Type[Model], Dict[str, IComputedField]] = {}
12✔
192
        for model, computedfields in self.models_with_computedfields:
12✔
193
            if not issubclass(model, _ComputedFieldsModelBase):
12✔
194
                raise ResolverException(f'{model} is not a subclass of ComputedFieldsModel')
12✔
195
            computed_models[model] = {}
12✔
196
            for field in computedfields:
12✔
197
                computed_models[model][field.name] = field
12✔
198

199
        return computed_models
12✔
200

201
    def initialize(self, models_only: bool = False) -> None:
12✔
202
        """
203
        Entrypoint for ``app.ready`` to seal the resolver and trigger
204
        the resolver map creation.
205

206
        Upon instantiation the resolver is in the collector phase, where it tracks
207
        model registrations and computed field decorations.
208

209
        After calling ``initialize`` no more models or fields can be registered
210
        to the resolver, and ``computed_models`` and the resolver maps get loaded.
211
        """
212
        # resolver must be sealed before doing any map calculations
213
        self.seal()
12✔
214
        self._computed_models = self.extract_computed_models()
12✔
215
        self._initialized = True
12✔
216
        if not models_only:
12✔
217
            self.load_maps()
12✔
218

219
    def load_maps(self, _force_recreation: bool = False) -> None:
12✔
220
        """
221
        Load all needed resolver maps. The steps are:
222

223
            - create intermodel graph of the dependencies
224
            - remove redundant paths with cycling check
225
            - create modelgraphs for local MRO
226
            - merge graphs to uniongraph with cycling check
227
            - create final resolver maps
228

229
                - `lookup_map`: intermodel dependencies as queryset access strings
230
                - `fk_map`: models with their contributing fk fields
231
                - `local_mro`: MRO of local computed fields per model
232
        """
233
        self._graph = ComputedModelsGraph(self.computed_models)
12✔
234
        if not getattr(settings, 'COMPUTEDFIELDS_ALLOW_RECURSION', False):
12✔
235
            self._graph.get_edgepaths()
12✔
236
            self._graph.get_uniongraph().get_edgepaths()
12✔
237
        self._map, self._fk_map = self._graph.generate_maps()
12✔
238
        self._local_mro = self._graph.generate_local_mro_map()
12✔
239
        self._extract_m2m_through()
12✔
240
        self._patch_proxy_models()
12✔
241
        self._map_loaded = True
12✔
242

243
    def _extract_m2m_through(self) -> None:
12✔
244
        """
245
        Creates M2M through model mappings with left/right field names.
246
        The map is used by the m2m_changed handler for faster name lookups.
247
        This cannot be pickled, thus is built for every resolver bootstrapping.
248
        """
249
        for model, fields in self.computed_models.items():
12✔
250
            for _, real_field in fields.items():
12✔
251
                depends = real_field._computed['depends']
12✔
252
                for path, _ in depends:
12✔
253
                    if path == 'self':
12✔
254
                        continue
12✔
255
                    cls: Type[Model] = model
12✔
256
                    for symbol in path.split('.'):
12✔
257
                        try:
12✔
258
                            rel: Any = cls._meta.get_field(symbol)
12✔
259
                        except FieldDoesNotExist:
12✔
260
                            descriptor = getattr(cls, symbol)
12✔
261
                            rel = getattr(descriptor, 'rel', None) or getattr(descriptor, 'related')
12✔
262
                        if rel.many_to_many:
12✔
263
                            if hasattr(rel, 'through'):
12✔
264
                                self._m2m[rel.through] = {
12✔
265
                                    'left': rel.remote_field.name, 'right': rel.name}
266
                            else:
267
                                self._m2m[rel.remote_field.through] = {
12✔
268
                                    'left': rel.name, 'right': rel.remote_field.name}
269
                        cls = rel.related_model
12✔
270

271
    def _patch_proxy_models(self) -> None:
12✔
272
        """
273
        Patch proxy models into the resolver maps.
274
        """
275
        for model in self.models:
12✔
276
            if model._meta.proxy:
12✔
277
                basemodel = proxy_to_base_model(model)
12✔
278
                if basemodel in self._map:
12✔
279
                    self._map[model] = self._map[basemodel]
12✔
280
                if basemodel in self._fk_map:
12✔
281
                    self._fk_map[model] = self._fk_map[basemodel]
12✔
282
                if basemodel in self._local_mro:
12✔
283
                    self._local_mro[model] = self._local_mro[basemodel]
12✔
284
                if basemodel in self._m2m:
12!
285
                    self._m2m[model] = self._m2m[basemodel]
×
286
                self._proxymodels[model] = basemodel or model
12✔
287

288
    def get_local_mro(
12✔
289
        self,
290
        model: Type[Model],
291
        update_fields: Optional[Iterable[str]] = None
292
    ) -> List[str]:
293
        """
294
        Return `MRO` for local computed field methods for a given set of `update_fields`.
295
        The returned list of fieldnames must be calculated in order to correctly update
296
        dependent computed field values in one pass.
297

298
        Returns computed fields as self dependent to simplify local field dependency calculation.
299
        """
300
        # TODO: investigate - memoization of update_fields result? (runs ~4 times faster)
301
        entry = self._local_mro.get(model)
12✔
302
        if not entry:
12✔
303
            return []
12✔
304
        if update_fields is None:
12✔
305
            return entry['base']
12✔
306
        update_fields = frozenset(update_fields)
12✔
307
        base = entry['base']
12✔
308
        fields = entry['fields']
12✔
309
        mro = 0
12✔
310
        for field in update_fields:
12✔
311
            mro |= fields.get(field, 0)
12✔
312
        return [name for pos, name in enumerate(base) if mro & (1 << pos)]
12✔
313

314
    def _querysets_for_update(
12✔
315
        self,
316
        model: Type[Model],
317
        instance: Union[Model, QuerySet],
318
        update_fields: Optional[Iterable[str]] = None,
319
        pk_list: bool = False,
320
        m2m: Optional[Model] = None
321
    ) -> Dict[Type[Model], List[Any]]:
322
        """
323
        Returns a mapping of all dependent models, dependent fields and a
324
        queryset containing all dependent objects.
325
        """
326
        final: Dict[Type[Model], List[Any]] = OrderedDict()
12✔
327
        modeldata = self._map.get(model)
12✔
328
        if not modeldata:
12✔
329
            return final
12✔
330
        if not update_fields:
12✔
331
            updates: Set[str] = set(modeldata.keys())
12✔
332
        else:
333
            updates = set()
12✔
334
            for fieldname in update_fields:
12✔
335
                if fieldname in modeldata:
12✔
336
                    updates.add(fieldname)
12✔
337
        subquery = '__in' if isinstance(instance, QuerySet) else ''
12✔
338

339
        # fix #100
340
        # mysql does not support 'LIMIT & IN/ALL/ANY/SOME subquery'
341
        # thus we extract pks explicitly instead
342
        # TODO: cleanup type mess here including this workaround
343
        if isinstance(instance, QuerySet):
12✔
344
            from django.db import connections
12✔
345
            if not instance.query.can_filter() and connections[instance.db].vendor == 'mysql':
12!
346
                instance = set(instance.values_list('pk', flat=True).iterator())
×
347

348
        model_updates: Dict[Type[Model], Tuple[Set[str], Set[str]]] = OrderedDict()
12✔
349
        for update in updates:
12✔
350
            # first aggregate fields and paths to cover
351
            # multiple comp field dependencies
352
            for model, resolver in modeldata[update].items():
12✔
353
                fields, paths = resolver
12✔
354
                m_fields, m_paths = model_updates.setdefault(model, (set(), set()))
12✔
355
                m_fields.update(fields)
12✔
356
                m_paths.update(paths)
12✔
357

358
        # generate narrowed down querysets for all cf dependencies
359
        for model, data in model_updates.items():
12✔
360
            fields, paths = data
12✔
361

362
            # queryset construction
363
            if m2m and self._proxymodels.get(type(m2m), type(m2m)) == model:
12✔
364
                # M2M optimization: got called through an M2M signal
365
                # narrow updates to the single signal instance
366
                queryset = model._base_manager.filter(pk=m2m.pk)
12✔
367
            else:
368
                queryset: Any = model._base_manager.none()
12✔
369
                query_pipe_method = self._choose_optimal_query_pipe_method(paths)
12✔
370
                queryset = reduce(
12✔
371
                    query_pipe_method,
372
                    (model._base_manager.filter(**{path+subquery: instance}) for path in paths),
373
                    queryset
374
                )
375
            if pk_list:
12✔
376
                # need pks for post_delete since the real queryset will be empty
377
                # after deleting the instance in question
378
                # since we need to interact with the db anyways
379
                # we can already drop empty results here
380
                queryset = set(queryset.values_list('pk', flat=True).iterator())
12✔
381
                if not queryset:
12✔
382
                    continue
12✔
383
            # FIXME: change to tuple or dict for narrower type
384
            final[model] = [queryset, fields]
12✔
385
        return final
12✔
386
    
387
    def _get_model(self, instance: Union[Model, QuerySet]) -> Type[Model]:
12✔
388
        return instance.model if isinstance(instance, QuerySet) else type(instance)
12✔
389

390
    def _choose_optimal_query_pipe_method(self, paths: Set[str]) -> Callable:
12✔
391
        """
392
            Choose optimal pipe method, to combine querystes.
393
            Returns `|` if there are only one element or the difference is only the fields name, on the same path.
394
            Otherwise, return union.
395
        """
396
        if len(paths) == 1:
12✔
397
            return operator.or_
12✔
398
        else:
399
            paths_by_parts = tuple(path.split("__") for path in paths)
12✔
400
            if are_same(*(len(path_in_parts) for path_in_parts in paths_by_parts)):
12✔
401
                max_depth = len(paths_by_parts[0]) - 1
12✔
402
                for depth, paths_parts in enumerate(zip(*paths_by_parts)):
12!
403
                    if are_same(*paths_parts):
12✔
404
                        pass
12✔
405
                    else:
406
                        if depth == max_depth:
12✔
407
                            return operator.or_
12✔
408
                        else:
409
                            break
12✔
410
        return lambda x, y: x.union(y)
12✔
411

412
    def preupdate_dependent(
12✔
413
        self,
414
        instance: Union[QuerySet, Model],
415
        model: Optional[Type[Model]] = None,
416
        update_fields: Optional[Iterable[str]] = None,
417
    ) -> Dict[Type[Model], List[Any]]:
418
        """
419
        Create a mapping of currently associated computed field records,
420
        that might turn dirty by a follow-up bulk action.
421

422
        Feed the mapping back to ``update_dependent`` as `old` argument
423
        after your bulk action to update de-associated computed field records as well.
424
        """
425
        return self._querysets_for_update(
12✔
426
            model or self._get_model(instance), instance, update_fields, pk_list=True)
427

428
    def update_dependent(
12✔
429
        self,
430
        instance: Union[QuerySet, Model],
431
        model: Optional[Type[Model]] = None,
432
        update_fields: Optional[Iterable[str]] = None,
433
        old: Optional[Dict[Type[Model], List[Any]]] = None,
434
        update_local: bool = True,
435
        querysize: Optional[int] = None
436
    ) -> None:
437
        """
438
        Updates all dependent computed fields on related models traversing
439
        the dependency tree as shown in the graphs.
440

441
        This is the main entry hook of the resolver to do updates on dependent
442
        computed fields during runtime. While this is done automatically for
443
        model instance actions from signal handlers, you have to call it yourself
444
        after changes done by bulk actions.
445

446
        To do that, simply call this function after the update with the queryset
447
        containing the changed objects:
448

449
            >>> Entry.objects.filter(pub_date__year=2010).update(comments_on=False)
450
            >>> update_dependent(Entry.objects.filter(pub_date__year=2010))
451

452
        This can also be used with ``bulk_create``. Since ``bulk_create``
453
        returns the objects in a python container, you have to create the queryset
454
        yourself, e.g. with pks:
455

456
            >>> objs = Entry.objects.bulk_create([
457
            ...     Entry(headline='This is a test'),
458
            ...     Entry(headline='This is only a test'),
459
            ... ])
460
            >>> pks = set(obj.pk for obj in objs)
461
            >>> update_dependent(Entry.objects.filter(pk__in=pks))
462

463
        .. NOTE::
464

465
            Getting pks from ``bulk_create`` is not supported by all database adapters.
466
            With a local computed field you can "cheat" here by providing a sentinel:
467

468
                >>> MyComputedModel.objects.bulk_create([
469
                ...     MyComputedModel(comp='SENTINEL'), # here or as default field value
470
                ...     MyComputedModel(comp='SENTINEL'),
471
                ... ])
472
                >>> update_dependent(MyComputedModel.objects.filter(comp='SENTINEL'))
473

474
            If the sentinel is beyond reach of the method result, this even ensures to update
475
            only the newly added records.
476

477
        `instance` can also be a single model instance. Since calling ``save`` on a model instance
478
        will trigger this function by the `post_save` signal already it should not be called
479
        for single instances, if they get saved anyway.
480

481
        `update_fields` can be used to indicate, that only certain fields on the queryset changed,
482
        which helps to further narrow down the records to be updated.
483

484
        Special care is needed, if a bulk action contains foreign key changes,
485
        that are part of a computed field dependency chain. To correctly handle that case,
486
        provide the result of ``preupdate_dependent`` as `old` argument like this:
487

488
                >>> # given: some computed fields model depends somehow on Entry.fk_field
489
                >>> old_relations = preupdate_dependent(Entry.objects.filter(pub_date__year=2010))
490
                >>> Entry.objects.filter(pub_date__year=2010).update(fk_field=new_related_obj)
491
                >>> update_dependent(Entry.objects.filter(pub_date__year=2010), old=old_relations)
492

493
        `update_local=False` disables model local computed field updates of the entry node. 
494
        (used as optimization during tree traversal). You should not disable it yourself.
495
        """
496
        _model = model or self._get_model(instance)
12✔
497

498
        # bulk_updater might change fields, ensure we have set/None
499
        _update_fields = None if update_fields is None else set(update_fields)
12✔
500

501
        # Note: update_local is always off for updates triggered from the resolver
502
        # but True by default to avoid accidentally skipping updates called by user
503
        if update_local and self.has_computedfields(_model):
12✔
504
            # We skip a transaction here in the same sense,
505
            # as local cf updates are not guarded either.
506
            queryset = instance if isinstance(instance, QuerySet) \
12✔
507
                else _model._base_manager.filter(pk__in=[instance.pk])
508
            self.bulk_updater(queryset, _update_fields, local_only=True, querysize=querysize)
12✔
509

510
        updates = self._querysets_for_update(_model, instance, _update_fields).values()
12✔
511
        if updates:
12✔
512
            with transaction.atomic():  # FIXME: place transaction only once in tree descent
12✔
513
                pks_updated: Dict[Type[Model], Set[Any]] = {}
12✔
514
                for queryset, fields in updates:
12✔
515
                    _pks = self.bulk_updater(queryset, fields, return_pks=True, querysize=querysize)
12✔
516
                    if _pks:
12✔
517
                        pks_updated[queryset.model] = _pks
12✔
518
                if old:
12✔
519
                    for model2, data in old.items():
12✔
520
                        pks, fields = data
12✔
521
                        queryset = model2.objects.filter(pk__in=pks-pks_updated.get(model2, set()))
12✔
522
                        self.bulk_updater(queryset, fields, querysize=querysize)
12✔
523

524
    def bulk_updater(
12✔
525
        self,
526
        queryset: QuerySet,
527
        update_fields: Optional[Set[str]] = None,
528
        return_pks: bool = False,
529
        local_only: bool = False,
530
        querysize: Optional[int] = None
531
    ) -> Optional[Set[Any]]:
532
        """
533
        Update local computed fields and descent in the dependency tree by calling
534
        ``update_dependent`` for dependent models.
535

536
        This method does the local field updates on `queryset`:
537

538
            - eval local `MRO` of computed fields
539
            - expand `update_fields`
540
            - apply optional `select_related` and `prefetch_related` rules to `queryset`
541
            - walk all records and recalculate fields in `update_fields`
542
            - aggregate changeset and save as batched `bulk_update` to the database
543

544
        By default this method triggers the update of dependent models by calling
545
        ``update_dependent`` with `update_fields` (next level of tree traversal).
546
        This can be suppressed by setting `local_only=True`.
547

548
        If `return_pks` is set, the method returns a set of altered pks of `queryset`.
549
        """
550
        model: Type[Model] = queryset.model
12✔
551

552
        # distinct issue workaround
553
        # the workaround is needed for already sliced/distinct querysets coming from outside
554
        # TODO: distinct is a major query perf smell, and is in fact only needed on back relations
555
        #       may need some rework in _querysets_for_update
556
        #       ideally we find a way to avoid it for forward relations
557
        #       also see #101
558
        if queryset.query.can_filter() and not queryset.query.distinct_fields:
12!
559
            if queryset.query.combinator != "union":
12✔
560
                queryset = queryset.distinct()
12✔
561
        else:
562
            queryset = model._base_manager.filter(pk__in=subquery_pk(queryset, queryset.db))
×
563

564
        # correct update_fields by local mro
565
        mro = self.get_local_mro(model, update_fields)
12✔
566
        fields: Any = set(mro)  # FIXME: narrow type once issue in django-stubs is resolved
12✔
567
        if update_fields:
12✔
568
            update_fields.update(fields)
12✔
569

570
        select = self.get_select_related(model, fields)
12✔
571
        prefetch = self.get_prefetch_related(model, fields)
12✔
572
        if select:
12✔
573
            queryset = queryset.select_related(*select)
12✔
574
        # fix #167: skip prefetch if union was used
575
        if prefetch and queryset.query.combinator != "union":
12✔
576
            queryset = queryset.prefetch_related(*prefetch)
12✔
577

578
        pks = []
12✔
579
        if fields:
12!
580
            q_size = self.get_querysize(model, fields, querysize)
12✔
581
            change: List[Model] = []
12✔
582
            for elem in slice_iterator(queryset, q_size):
12✔
583
                # note on the loop: while it is technically not needed to batch things here,
584
                # we still prebatch to not cause memory issues for very big querysets
585
                has_changed = False
12✔
586
                for comp_field in mro:
12✔
587
                    new_value = self._compute(elem, model, comp_field)
12✔
588
                    if new_value != getattr(elem, comp_field):
12✔
589
                        has_changed = True
12✔
590
                        setattr(elem, comp_field, new_value)
12✔
591
                if has_changed:
12✔
592
                    change.append(elem)
12✔
593
                    pks.append(elem.pk)
12✔
594
                if len(change) >= self._batchsize:
12!
595
                    self._update(model._base_manager.all(), change, fields)
×
596
                    change = []
×
597
            if change:
12✔
598
                self._update(model._base_manager.all(), change, fields)
12✔
599

600
        # trigger dependent comp field updates from changed records
601
        # other than before we exit the update tree early, if we have no changes at all
602
        # also cuts the update tree for recursive deps (tree-like)
603
        if not local_only and pks:
12✔
604
            self.update_dependent(model._base_manager.filter(pk__in=pks), model, fields, update_local=False)
12✔
605
        return set(pks) if return_pks else None
12✔
606
    
607
    def _update(self, queryset: QuerySet, change: Sequence[Any], fields: Sequence[str]) -> Union[int, None]:
12✔
608
        # we can skip batch_size here, as it already was batched in bulk_updater
609
        if self.use_fastupdate:
12!
610
            return fast_update(queryset, change, fields, None)
12✔
611
        return queryset.model._base_manager.bulk_update(change, fields)
×
612

613
    def _compute(self, instance: Model, model: Type[Model], fieldname: str) -> Any:
12✔
614
        """
615
        Returns the computed field value for ``fieldname``.
616
        Note that this is just a shorthand method for calling the underlying computed
617
        field method and does not deal with local MRO, thus should only be used,
618
        if the MRO is respected by other means.
619
        For quick inspection of a single computed field value, that gonna be written
620
        to the database, always use ``compute(fieldname)`` instead.
621
        """
622
        field = self._computed_models[model][fieldname]
12✔
623
        if instance._state.adding or not instance.pk:
12✔
624
            if field._computed['default_on_create']:
12✔
625
                return field.get_default()
12✔
626
        return field._computed['func'](instance)
12✔
627

628
    def compute(self, instance: Model, fieldname: str) -> Any:
12✔
629
        """
630
        Returns the computed field value for ``fieldname``. This method allows
631
        to inspect the new calculated value, that would be written to the database
632
        by a following ``save()``.
633

634
        Other than calling ``update_computedfields`` on an model instance this call
635
        is not destructive for old computed field values.
636
        """
637
        # Getting a single computed value prehand is quite complicated,
638
        # as we have to:
639
        # - resolve local MRO backwards (stored MRO data is optimized for forward deps)
640
        # - calc all local cfs, that the requested one depends on
641
        # - stack and rewind interim values, as we dont want to introduce side effects here
642
        #   (in fact the save/bulker logic might try to save db calls based on changes)
643
        mro = self.get_local_mro(type(instance), None)
12✔
644
        if not fieldname in mro:
12✔
645
            return getattr(instance, fieldname)
12✔
646
        entries = self._local_mro[type(instance)]['fields']
12✔
647
        pos = 1 << mro.index(fieldname)
12✔
648
        stack: List[Tuple[str, Any]] = []
12✔
649
        model = type(instance)
12✔
650
        for field in mro:
12!
651
            if field == fieldname:
12✔
652
                ret = self._compute(instance, model, fieldname)
12✔
653
                for field2, old in stack:
12✔
654
                    # reapply old stack values
655
                    setattr(instance, field2, old)
12✔
656
                return ret
12✔
657
            f_mro = entries.get(field, 0)
12✔
658
            if f_mro & pos:
12✔
659
                # append old value to stack for later rewinding
660
                # calc and set new value for field, if the requested one depends on it
661
                stack.append((field, getattr(instance, field)))
12✔
662
                setattr(instance, field, self._compute(instance, model, field))
12✔
663

664
    # TODO: the following 3 lookups are very expensive at runtime adding ~2s for 1M calls
665
    #       --> all need pregenerated lookup maps
666
    # Note: the same goes for get_local_mro and _queryset_for_update...
667
    def get_select_related(
12✔
668
        self,
669
        model: Type[Model],
670
        fields: Optional[Iterable[str]] = None
671
    ) -> Set[str]:
672
        """
673
        Get defined select_related rules for `fields` (all if none given).
674
        """
675
        if fields is None:
12!
676
            fields = self._computed_models[model].keys()
×
677
        select: Set[str] = set()
12✔
678
        for field in fields:
12✔
679
            select.update(self._computed_models[model][field]._computed['select_related'])
12✔
680
        return select
12✔
681

682
    def get_prefetch_related(
12✔
683
        self,
684
        model: Type[Model],
685
        fields: Optional[Iterable[str]] = None
686
    ) -> List:
687
        """
688
        Get defined prefetch_related rules for `fields` (all if none given).
689
        """
690
        if fields is None:
12!
691
            fields = self._computed_models[model].keys()
×
692
        prefetch: List[Any] = []
12✔
693
        for field in fields:
12✔
694
            prefetch.extend(self._computed_models[model][field]._computed['prefetch_related'])
12✔
695
        return prefetch
12✔
696

697
    def get_querysize(
12✔
698
        self,
699
        model: Type[Model],
700
        fields: Optional[Iterable[str]] = None,
701
        override: Optional[int] = None
702
    ) -> int:
703
        base = settings.COMPUTEDFIELDS_QUERYSIZE if override is None else override
12✔
704
        if fields is None:
12✔
705
            fields = self._computed_models[model].keys()
12✔
706
        return min(self._computed_models[model][f]._computed['querysize'] or base for f in fields)
12✔
707

708
    def get_contributing_fks(self) -> IFkMap:
12✔
709
        """
710
        Get a mapping of models and their local foreign key fields,
711
        that are part of a computed fields dependency chain.
712

713
        Whenever a bulk action changes one of the fields listed here, you have to create
714
        a listing of the associated  records with ``preupdate_dependent`` before doing
715
        the bulk change. After the bulk change feed the listing back to ``update_dependent``
716
        with the `old` argument.
717

718
        With ``COMPUTEDFIELDS_ADMIN = True`` in `settings.py` this mapping can also be
719
        inspected as admin view. 
720
        """
721
        if not self._map_loaded:  # pragma: no cover
722
            raise ResolverException('resolver has no maps loaded yet')
723
        return self._fk_map
12✔
724

725
    def _sanity_check(self, field: Field, depends: IDepends) -> None:
12✔
726
        """
727
        Basic type check for computed field arguments `field` and `depends`.
728
        This only checks for proper type alignment (most crude source of errors) to give
729
        devs an early startup error for misconfigured computed fields.
730
        More subtle errors like non-existing paths or fields are caught
731
        by the resolver during graph reduction yielding somewhat crytic error messages.
732

733
        There is another class of misconfigured computed fields we currently cannot
734
        find by any safety measures - if `depends` provides valid paths and fields,
735
        but the function operates on different dependencies. Currently it is the devs'
736
        responsibility to perfectly align `depends` entries with dependencies
737
        used by the function to avoid faulty update behavior.
738
        """
739
        if not isinstance(field, Field):
12!
740
                raise ResolverException('field argument is not a Field instance')
×
741
        for rule in depends:
12✔
742
            try:
12✔
743
                path, fieldnames = rule
12✔
744
            except ValueError:
×
745
                raise ResolverException(MALFORMED_DEPENDS)
×
746
            if not isinstance(path, str) or not all(isinstance(f, str) for f in fieldnames):
12!
747
                raise ResolverException(MALFORMED_DEPENDS)
×
748

749
    def computedfield_factory(
12✔
750
        self,
751
        field: 'Field[_ST, _GT]',
752
        compute: Callable[..., _ST],
753
        depends: Optional[IDepends] = None,
754
        select_related: Optional[Sequence[str]] = None,
755
        prefetch_related: Optional[Sequence[Any]] = None,
756
        querysize: Optional[int] = None,
757
        default_on_create: Optional[bool] = False
758
    ) -> 'Field[_ST, _GT]':
759
        """
760
        Factory for computed fields.
761

762
        The method gets exposed as ``ComputedField`` to allow a more declarative
763
        code style with better separation of field declarations and function
764
        implementations. It is also used internally for the ``computed`` decorator.
765
        Similar to the decorator, the ``compute`` function expects a single argument
766
        as model instance of the model it got applied to.
767

768
        Usage example:
769

770
        .. code-block:: python
771

772
            from computedfields.models import ComputedField
773

774
            def calc_mul(inst):
775
                return inst.a * inst.b
776

777
            class MyModel(ComputedFieldsModel):
778
                a = models.IntegerField()
779
                b = models.IntegerField()
780
                sum = ComputedField(
781
                    models.IntegerField(),
782
                    depends=[('self', ['a', 'b'])],
783
                    compute=lambda inst: inst.a + inst.b
784
                )
785
                mul = ComputedField(
786
                    models.IntegerField(),
787
                    depends=[('self', ['a', 'b'])],
788
                    compute=calc_mul
789
                )
790
        """
791
        self._sanity_check(field, depends or [])
12✔
792
        cf = cast('IComputedField[_ST, _GT]', field)
12✔
793
        cf._computed = {
12✔
794
            'func': compute,
795
            'depends': depends or [],
796
            'select_related': select_related or [],
797
            'prefetch_related': prefetch_related or [],
798
            'querysize': querysize,
799
            'default_on_create': default_on_create
800
        }
801
        cf.editable = False
12✔
802
        self.add_field(cf)
12✔
803
        return field
12✔
804

805
    def computed(
12✔
806
        self,
807
        field: 'Field[_ST, _GT]',
808
        depends: Optional[IDepends] = None,
809
        select_related: Optional[Sequence[str]] = None,
810
        prefetch_related: Optional[Sequence[Any]] = None,
811
        querysize: Optional[int] = None,
812
        default_on_create: Optional[bool] = False
813
    ) -> Callable[[Callable[..., _ST]], 'Field[_ST, _GT]']:
814
        """
815
        Decorator to create computed fields.
816

817
        `field` should be a model concrete field instance suitable to hold the result
818
        of the decorated method. The decorator expects a keyword argument `depends`
819
        to indicate dependencies to model fields (local or related).
820
        Listed dependencies will automatically update the computed field.
821

822
        Examples:
823

824
            - create a char field with no further dependencies (not very useful)
825

826
            .. code-block:: python
827

828
                @computed(models.CharField(max_length=32))
829
                def ...
830

831
            - create a char field with a dependency to the field ``name`` on a
832
              foreign key relation ``fk``
833

834
            .. code-block:: python
835

836
                @computed(models.CharField(max_length=32), depends=[('fk', ['name'])])
837
                def ...
838

839
        Dependencies should be listed as ``['relation_name', concrete_fieldnames]``.
840
        The relation can span serveral models, simply name the relation
841
        in python style with a dot (e.g. ``'a.b.c'``). A relation can be any of
842
        foreign key, m2m, o2o and their back relations. The fieldnames must point to
843
        concrete fields on the foreign model.
844

845
        .. NOTE::
846

847
            Dependencies to model local fields should be list with ``'self'`` as relation name.
848

849
        With `select_related` and `prefetch_related` you can instruct the dependency resolver
850
        to apply certain optimizations on the update queryset.
851

852
        .. NOTE::
853

854
            `select_related` and `prefetch_related` are stacked over computed fields
855
            of the same model during updates, that are marked for update.
856
            If your optimizations contain custom attributes (as with `to_attr` of a
857
            `Prefetch` object), these attributes will only be available on instances
858
            during updates from the resolver, never on newly constructed instances or
859
            model instances pulled by other means, unless you applied the same lookups manually.
860

861
            To keep the computed field methods working under any circumstances,
862
            it is a good idea not to rely on lookups with custom attributes,
863
            or to test explicitly for them in the method with an appropriate plan B.
864

865
        .. CAUTION::
866

867
            With the dependency resolver you can easily create recursive dependencies
868
            by accident. Imagine the following:
869

870
            .. code-block:: python
871

872
                class A(ComputedFieldsModel):
873
                    @computed(models.CharField(max_length=32), depends=[('b_set', ['comp'])])
874
                    def comp(self):
875
                        return ''.join(b.comp for b in self.b_set.all())
876

877
                class B(ComputedFieldsModel):
878
                    a = models.ForeignKey(A)
879

880
                    @computed(models.CharField(max_length=32), depends=[('a', ['comp'])])
881
                    def comp(self):
882
                        return a.comp
883

884
            Neither an object of `A` or `B` can be saved, since the ``comp`` fields depend on
885
            each other. While it is quite easy to spot for this simple case it might get tricky
886
            for more complicated dependencies. Therefore the dependency resolver tries
887
            to detect cyclic dependencies and might raise a ``CycleNodeException`` during
888
            startup.
889

890
            If you experience this in your project try to get in-depth cycle
891
            information, either by using the ``rendergraph`` management command or
892
            by directly accessing the graph objects:
893

894
            - intermodel dependency graph: ``active_resolver._graph``
895
            - mode local dependency graphs: ``active_resolver._graph.modelgraphs[your_model]``
896
            - union graph: ``active_resolver._graph.get_uniongraph()``
897

898
            Note that there is not graph object, when running with ``COMPUTEDFIELDS_MAP = True``.
899
            In that case either comment out that line `settings.py` and restart the server
900
            or build the graph at runtime with:
901

902
                >>> from computedfields.graph import ComputedModelsGraph
903
                >>> from computedfields.resolver import active_resolver
904
                >>> graph = ComputedModelsGraph(active_resolver.computed_models)
905

906
            Also see the graph documentation :ref:`here<graph>`.
907
        """
908
        def wrap(func: Callable[..., _ST]) -> 'Field[_ST, _GT]':
12✔
909
            return self.computedfield_factory(
12✔
910
                field,
911
                compute=func,
912
                depends=depends,
913
                select_related=select_related,
914
                prefetch_related=prefetch_related,
915
                querysize=querysize,
916
                default_on_create=default_on_create
917
            )
918
        return wrap
12✔
919

920
    @overload
12✔
921
    def precomputed(self, f: F) -> F:
12✔
UNCOV
922
        ...
×
923
    @overload
12✔
924
    def precomputed(self, skip_after: bool) -> Callable[[F], F]:
12✔
UNCOV
925
        ...
×
926
    def precomputed(self, *dargs, **dkwargs) -> Union[F, Callable[[F], F]]:
12✔
927
        """
928
        Decorator for custom ``save`` methods, that expect local computed fields
929
        to contain already updated values on enter.
930

931
        By default local computed field values are only calculated once by the
932
        ``ComputedFieldModel.save`` method after your own save method.
933

934
        By placing this decorator on your save method, the values will be updated
935
        before entering your method as well. Note that this comes for the price of
936
        doubled local computed field calculations (before and after your save method).
937
        
938
        To avoid a second recalculation, the decorator can be called with `skip_after=True`.
939
        Note that this might lead to desychronized computed field values, if you do late
940
        field changes in your save method without another resync afterwards.
941
        """
942
        skip: bool = False
12✔
943
        func: Optional[F] = None
12✔
944
        if dargs:
12✔
945
            if len(dargs) > 1 or not callable(dargs[0]) or dkwargs:
12!
UNCOV
946
                raise ResolverException('error in @precomputed declaration')
×
947
            func = dargs[0]
12✔
948
        else:
949
            skip = dkwargs.get('skip_after', False)
12✔
950
        
951
        def wrap(func: F) -> F:
12✔
952
            def _save(instance, *args, **kwargs):
12✔
953
                new_fields = self.update_computedfields(instance, kwargs.get('update_fields'))
12✔
954
                if new_fields:
12!
UNCOV
955
                    kwargs['update_fields'] = new_fields
×
956
                kwargs['skip_computedfields'] = skip
12✔
957
                return func(instance, *args, **kwargs)
12✔
958
            return cast(F, _save)
12✔
959
        
960
        return wrap(func) if func else wrap
12✔
961

962
    def update_computedfields(
12✔
963
        self,
964
        instance: Model,
965
        update_fields: Optional[Iterable[str]] = None
966
        ) -> Optional[Iterable[str]]:
967
        """
968
        Update values of local computed fields of `instance`.
969

970
        Other than calling ``compute`` on an instance, this call overwrites
971
        computed field values on the instance (destructive).
972

973
        Returns ``None`` or an updated set of field names for `update_fields`.
974
        The returned fields might contained additional computed fields, that also
975
        changed based on the input fields, thus should extend `update_fields`
976
        on a save call.
977
        """
978
        model = type(instance)
12✔
979
        if not self.has_computedfields(model):
12✔
980
            return update_fields
12✔
981
        cf_mro = self.get_local_mro(model, update_fields)
12✔
982
        if update_fields:
12✔
983
            update_fields = set(update_fields)
12✔
984
            update_fields.update(set(cf_mro))
12✔
985
        for fieldname in cf_mro:
12✔
986
            setattr(instance, fieldname, self._compute(instance, model, fieldname))
12✔
987
        if update_fields:
12✔
988
            return update_fields
12✔
989
        return None
12✔
990

991
    def has_computedfields(self, model: Type[Model]) -> bool:
12✔
992
        """
993
        Indicate whether `model` has computed fields.
994
        """
995
        return model in self._computed_models
12✔
996

997
    def get_computedfields(self, model: Type[Model]) -> Iterable[str]:
12✔
998
        """
999
        Get all computed fields on `model`.
1000
        """
1001
        return self._computed_models.get(model, {}).keys()
12✔
1002

1003
    def is_computedfield(self, model: Type[Model], fieldname: str) -> bool:
12✔
1004
        """
1005
        Indicate whether `fieldname` on `model` is a computed field.
1006
        """
1007
        return fieldname in self.get_computedfields(model)
12✔
1008

1009
    def get_graphs(self) -> Tuple[Graph, Dict[Type[Model], ModelGraph], Graph]:
12✔
1010
        """
1011
        Return a tuple of all graphs as
1012
        ``(intermodel_graph, {model: modelgraph, ...}, union_graph)``.
1013
        """
UNCOV
1014
        graph = self._graph
×
UNCOV
1015
        if not graph:
×
UNCOV
1016
            graph = ComputedModelsGraph(active_resolver.computed_models)
×
UNCOV
1017
            graph.get_edgepaths()
×
1018
            graph.get_uniongraph()
×
1019
        return (graph, graph.modelgraphs, graph.get_uniongraph())
×
1020

1021

1022
# active_resolver is currently treated as global singleton (used in imports)
1023
#: Currently active resolver.
1024
active_resolver = Resolver()
12✔
1025

1026
# BOOT_RESOLVER: resolver that holds all startup declarations and resolve maps
1027
# gets deactivated after startup, thus it is currently not possible to define
1028
# new computed fields and add their resolve rules at runtime
1029
# TODO: investigate on custom resolvers at runtime to be bootstrapped from BOOT_RESOLVER
1030
#: Resolver used during django bootstrapping.
1031
#: This is currently the same as `active_resolver` (treated as global singleton).
1032
BOOT_RESOLVER = active_resolver
12✔
1033

1034

1035
# placeholder class to test for correct model inheritance
1036
# during initial field resolving
1037
class _ComputedFieldsModelBase:
12✔
1038
    pass
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc