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

netzkolchose / django-computedfields / 16098370026

06 Jul 2025 11:08AM UTC coverage: 94.895% (-0.008%) from 94.903%
16098370026

Pull #177

github

web-flow
Merge 997143b51 into 39d912552
Pull Request #177: through expansion on m2m fields

432 of 469 branches covered (92.11%)

Branch coverage included in aggregate %.

42 of 42 new or added lines in 3 files covered. (100.0%)

21 existing lines in 2 files now uncovered.

1111 of 1157 relevant lines covered (96.02%)

11.52 hits per line

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

92.96
/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

8
from django.db import transaction
12✔
9
from django.db.models import QuerySet
12✔
10

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

16
from fast_update.fast import fast_update
12✔
17

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

24

25
MALFORMED_DEPENDS = """
12✔
26
Your depends keyword argument is malformed.
27

28
The depends keyword should either be None, an empty listing or
29
a listing of rules as depends=[rule1, rule2, .. ruleN].
30

31
A rule is formed as ('relation.path', ['list', 'of', 'fieldnames']) tuple.
32
The relation path either contains 'self' for fieldnames on the same model,
33
or a string as 'a.b.c', where 'a' is a relation on the current model
34
descending over 'b' to 'c' to pull fieldnames from 'c'. The denoted fieldnames
35
must be concrete fields on the rightmost model of the relation path.
36

37
Example:
38
depends=[
39
    ('self', ['name', 'status']),
40
    ('parent.color', ['value'])
41
]
42
This has 2 path rules - one for fields 'name' and 'status' on the same model,
43
and one to a field 'value' on a foreign model, which is accessible from
44
the current model through self -> parent -> color relation.
45
"""
46

47

48
class ResolverException(ComputedFieldsException):
12✔
49
    """
50
    Exception raised during model and field registration or dependency resolving.
51
    """
52

53

54
class Resolver:
12✔
55
    """
56
    Holds the needed data for graph calculations and runtime dependency resolving.
57

58
    Basic workflow:
59

60
        - On django startup a resolver gets instantiated early to track all project-wide
61
          model registrations and computed field decorations (collector phase).
62
        - On `app.ready` the computed fields are associated with their models to build
63
          a resolver-wide map of models with computed fields (``computed_models``).
64
        - After that the resolver maps are created (see `graph.ComputedModelsGraph`).
65
    """
66

67
    def __init__(self):
12✔
68
        # collector phase data
69
        #: Models from `class_prepared` signal hook during collector phase.
70
        self.models: Set[Type[Model]] = set()
12✔
71
        #: Computed fields found during collector phase.
72
        self.computedfields: Set[IComputedField] = set()
12✔
73

74
        # resolving phase data and final maps
75
        self._graph: Optional[ComputedModelsGraph] = None
12✔
76
        self._computed_models: Dict[Type[Model], Dict[str, IComputedField]] = {}
12✔
77
        self._map: ILookupMap = {}
12✔
78
        self._fk_map: IFkMap = {}
12✔
79
        self._local_mro: ILocalMroMap = {}
12✔
80
        self._m2m: IM2mMap = {}
12✔
81
        self._proxymodels: Dict[Type[Model], Type[Model]] = {}
12✔
82
        self.use_fastupdate: bool = settings.COMPUTEDFIELDS_FASTUPDATE
12✔
83
        self._batchsize: int = (settings.COMPUTEDFIELDS_BATCHSIZE_FAST
12✔
84
            if self.use_fastupdate else settings.COMPUTEDFIELDS_BATCHSIZE_BULK)
85

86
        # some internal states
87
        self._sealed: bool = False        # initial boot phase
12✔
88
        self._initialized: bool = False   # initialized (computed_models populated)?
12✔
89
        self._map_loaded: bool = False    # final stage with fully loaded maps
12✔
90

91
    def add_model(self, sender: Type[Model], **kwargs) -> None:
12✔
92
        """
93
        `class_prepared` signal hook to collect models during ORM registration.
94
        """
95
        if self._sealed:
12✔
96
            raise ResolverException('cannot add models on sealed resolver')
12✔
97
        self.models.add(sender)
12✔
98

99
    def add_field(self, field: IComputedField) -> None:
12✔
100
        """
101
        Collects fields from decoration stage of @computed.
102
        """
103
        if self._sealed:
12✔
104
            raise ResolverException('cannot add computed fields on sealed resolver')
12✔
105
        self.computedfields.add(field)
12✔
106

107
    def seal(self) -> None:
12✔
108
        """
109
        Seal the resolver, so no new models or computed fields can be added anymore.
110

111
        This marks the end of the collector phase and is a basic security measure
112
        to catch runtime model creations with computed fields.
113

114
        (Currently runtime creation of models with computed fields is not supported,
115
        trying to do so will raise an exception. This might change in future versions.)
116
        """
117
        self._sealed = True
12✔
118

119
    @property
12✔
120
    def models_with_computedfields(self) -> Generator[Tuple[Type[Model], Set[IComputedField]], None, None]:
12✔
121
        """
122
        Generator of tracked models with their computed fields.
123

124
        This cannot be accessed during the collector phase.
125
        """
126
        if not self._sealed:
12✔
127
            raise ResolverException('resolver must be sealed before accessing models or fields')
12✔
128

129
        field_ids: List[int] = [f.creation_counter for f in self.computedfields]
12✔
130
        for model in self.models:
12✔
131
            fields = set()
12✔
132
            for field in model._meta.fields:
12✔
133
                # for some reason the in ... check does not work for Django >= 3.2 anymore
134
                # workaround: check for _computed and the field creation_counter
135
                if hasattr(field, '_computed') and field.creation_counter in field_ids:
12✔
136
                    fields.add(field)
12✔
137
            if fields:
12✔
138
                yield (model, fields)
12✔
139

140
    @property
12✔
141
    def computedfields_with_models(self) -> Generator[Tuple[IComputedField, Set[Type[Model]]], None, None]:
12✔
142
        """
143
        Generator of tracked computed fields and their models.
144

145
        This cannot be accessed during the collector phase.
146
        """
147
        if not self._sealed:
12✔
148
            raise ResolverException('resolver must be sealed before accessing models or fields')
12✔
149

150
        for field in self.computedfields:
12✔
151
            models = set()
12✔
152
            for model in self.models:
12✔
153
                for f in model._meta.fields:
12✔
154
                    if hasattr(field, '_computed') and f.creation_counter == field.creation_counter:
12✔
155
                        models.add(model)
12✔
156
            yield (field, models)
12✔
157

158
    @property
12✔
159
    def computed_models(self) -> Dict[Type[Model], Dict[str, IComputedField]]:
12✔
160
        """
161
        Mapping of `ComputedFieldModel` models and their computed fields.
162

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

177
    def extract_computed_models(self) -> Dict[Type[Model], Dict[str, IComputedField]]:
12✔
178
        """
179
        Creates `computed_models` mapping from models and computed fields
180
        found in collector phase.
181
        """
182
        computed_models: Dict[Type[Model], Dict[str, IComputedField]] = {}
12✔
183
        for model, computedfields in self.models_with_computedfields:
12✔
184
            if not issubclass(model, _ComputedFieldsModelBase):
12✔
185
                raise ResolverException(f'{model} is not a subclass of ComputedFieldsModel')
12✔
186
            computed_models[model] = {}
12✔
187
            for field in computedfields:
12✔
188
                computed_models[model][field.name] = field
12✔
189

190
        return computed_models
12✔
191

192
    def initialize(self, models_only: bool = False) -> None:
12✔
193
        """
194
        Entrypoint for ``app.ready`` to seal the resolver and trigger
195
        the resolver map creation.
196

197
        Upon instantiation the resolver is in the collector phase, where it tracks
198
        model registrations and computed field decorations.
199

200
        After calling ``initialize`` no more models or fields can be registered
201
        to the resolver, and ``computed_models`` and the resolver maps get loaded.
202
        """
203
        # resolver must be sealed before doing any map calculations
204
        self.seal()
12✔
205
        self._computed_models = self.extract_computed_models()
12✔
206
        self._initialized = True
12✔
207
        if not models_only:
12✔
208
            self.load_maps()
12✔
209

210
    def load_maps(self, _force_recreation: bool = False) -> None:
12✔
211
        """
212
        Load all needed resolver maps. The steps are:
213

214
            - create intermodel graph of the dependencies
215
            - remove redundant paths with cycling check
216
            - create modelgraphs for local MRO
217
            - merge graphs to uniongraph with cycling check
218
            - create final resolver maps
219

220
                - `lookup_map`: intermodel dependencies as queryset access strings
221
                - `fk_map`: models with their contributing fk fields
222
                - `local_mro`: MRO of local computed fields per model
223
        """
224
        self._graph = ComputedModelsGraph(self.computed_models)
12✔
225
        if not getattr(settings, 'COMPUTEDFIELDS_ALLOW_RECURSION', False):
12✔
226
            self._graph.get_edgepaths()
12✔
227
            self._graph.get_uniongraph().get_edgepaths()
12✔
228
        self._map, self._fk_map = self._graph.generate_maps()
12✔
229
        self._local_mro = self._graph.generate_local_mro_map()
12✔
230
        self._m2m = self._graph._m2m
12✔
231
        self._patch_proxy_models()
12✔
232
        self._map_loaded = True
12✔
233

234
    def _patch_proxy_models(self) -> None:
12✔
235
        """
236
        Patch proxy models into the resolver maps.
237
        """
238
        for model in self.models:
12✔
239
            if model._meta.proxy:
12✔
240
                basemodel = proxy_to_base_model(model)
12✔
241
                if basemodel in self._map:
12✔
242
                    self._map[model] = self._map[basemodel]
12✔
243
                if basemodel in self._fk_map:
12✔
244
                    self._fk_map[model] = self._fk_map[basemodel]
12✔
245
                if basemodel in self._local_mro:
12✔
246
                    self._local_mro[model] = self._local_mro[basemodel]
12✔
247
                if basemodel in self._m2m:
12!
UNCOV
248
                    self._m2m[model] = self._m2m[basemodel]
×
249
                self._proxymodels[model] = basemodel or model
12✔
250

251
    def get_local_mro(
12✔
252
        self,
253
        model: Type[Model],
254
        update_fields: Optional[Iterable[str]] = None
255
    ) -> List[str]:
256
        """
257
        Return `MRO` for local computed field methods for a given set of `update_fields`.
258
        The returned list of fieldnames must be calculated in order to correctly update
259
        dependent computed field values in one pass.
260

261
        Returns computed fields as self dependent to simplify local field dependency calculation.
262
        """
263
        # TODO: investigate - memoization of update_fields result? (runs ~4 times faster)
264
        entry = self._local_mro.get(model)
12✔
265
        if not entry:
12✔
266
            return []
12✔
267
        if update_fields is None:
12✔
268
            return entry['base']
12✔
269
        update_fields = frozenset(update_fields)
12✔
270
        base = entry['base']
12✔
271
        fields = entry['fields']
12✔
272
        mro = 0
12✔
273
        for field in update_fields:
12✔
274
            mro |= fields.get(field, 0)
12✔
275
        return [name for pos, name in enumerate(base) if mro & (1 << pos)]
12✔
276

277
    def _querysets_for_update(
12✔
278
        self,
279
        model: Type[Model],
280
        instance: Union[Model, QuerySet],
281
        update_fields: Optional[Iterable[str]] = None,
282
        pk_list: bool = False,
283
    ) -> Dict[Type[Model], List[Any]]:
284
        """
285
        Returns a mapping of all dependent models, dependent fields and a
286
        queryset containing all dependent objects.
287
        """
288
        final: Dict[Type[Model], List[Any]] = OrderedDict()
12✔
289
        modeldata = self._map.get(model)
12✔
290
        if not modeldata:
12✔
291
            return final
12✔
292
        if not update_fields:
12✔
293
            updates: Set[str] = set(modeldata.keys())
12✔
294
        else:
295
            updates = set()
12✔
296
            for fieldname in update_fields:
12✔
297
                if fieldname in modeldata:
12✔
298
                    updates.add(fieldname)
12✔
299
        subquery = '__in' if isinstance(instance, QuerySet) else ''
12✔
300

301
        # fix #100
302
        # mysql does not support 'LIMIT & IN/ALL/ANY/SOME subquery'
303
        # thus we extract pks explicitly instead
304
        # TODO: cleanup type mess here including this workaround
305
        if isinstance(instance, QuerySet):
12✔
306
            from django.db import connections
12✔
307
            if not instance.query.can_filter() and connections[instance.db].vendor == 'mysql':
12!
UNCOV
308
                instance = set(instance.values_list('pk', flat=True).iterator())
×
309

310
        model_updates: Dict[Type[Model], Tuple[Set[str], Set[str]]] = OrderedDict()
12✔
311
        for update in updates:
12✔
312
            # first aggregate fields and paths to cover
313
            # multiple comp field dependencies
314
            for model, resolver in modeldata[update].items():
12✔
315
                fields, paths = resolver
12✔
316
                m_fields, m_paths = model_updates.setdefault(model, (set(), set()))
12✔
317
                m_fields.update(fields)
12✔
318
                m_paths.update(paths)
12✔
319

320
        # generate narrowed down querysets for all cf dependencies
321
        for model, data in model_updates.items():
12✔
322
            fields, paths = data
12✔
323
            queryset: Any = model._base_manager.none()
12✔
324
            query_pipe_method = self._choose_optimal_query_pipe_method(paths)
12✔
325
            queryset = reduce(
12✔
326
                query_pipe_method,
327
                (model._base_manager.filter(**{path+subquery: instance}) for path in paths),
328
                queryset
329
            )
330
            if pk_list:
12✔
331
                # need pks for post_delete since the real queryset will be empty
332
                # after deleting the instance in question
333
                # since we need to interact with the db anyways
334
                # we can already drop empty results here
335
                queryset = set(queryset.values_list('pk', flat=True).iterator())
12✔
336
                if not queryset:
12✔
337
                    continue
12✔
338
            # FIXME: change to tuple or dict for narrower type
339
            final[model] = [queryset, fields]
12✔
340
        return final
12✔
341
    
342
    def _get_model(self, instance: Union[Model, QuerySet]) -> Type[Model]:
12✔
343
        return instance.model if isinstance(instance, QuerySet) else type(instance)
12✔
344

345
    def _choose_optimal_query_pipe_method(self, paths: Set[str]) -> Callable:
12✔
346
        """
347
            Choose optimal pipe method, to combine querystes.
348
            Returns `|` if there are only one element or the difference is only the fields name, on the same path.
349
            Otherwise, return union.
350
        """
351
        if len(paths) == 1:
12✔
352
            return operator.or_
12✔
353
        else:
354
            paths_by_parts = tuple(path.split("__") for path in paths)
12✔
355
            if are_same(*(len(path_in_parts) for path_in_parts in paths_by_parts)):
12✔
356
                max_depth = len(paths_by_parts[0]) - 1
12✔
357
                for depth, paths_parts in enumerate(zip(*paths_by_parts)):
12!
358
                    if are_same(*paths_parts):
12✔
359
                        pass
12✔
360
                    else:
361
                        if depth == max_depth:
12✔
362
                            return operator.or_
12✔
363
                        else:
364
                            break
12✔
365
        return lambda x, y: x.union(y)
12✔
366

367
    def preupdate_dependent(
12✔
368
        self,
369
        instance: Union[QuerySet, Model],
370
        model: Optional[Type[Model]] = None,
371
        update_fields: Optional[Iterable[str]] = None,
372
    ) -> Dict[Type[Model], List[Any]]:
373
        """
374
        Create a mapping of currently associated computed field records,
375
        that might turn dirty by a follow-up bulk action.
376

377
        Feed the mapping back to ``update_dependent`` as `old` argument
378
        after your bulk action to update de-associated computed field records as well.
379
        """
380
        return self._querysets_for_update(
12✔
381
            model or self._get_model(instance), instance, update_fields, pk_list=True)
382

383
    def update_dependent(
12✔
384
        self,
385
        instance: Union[QuerySet, Model],
386
        model: Optional[Type[Model]] = None,
387
        update_fields: Optional[Iterable[str]] = None,
388
        old: Optional[Dict[Type[Model], List[Any]]] = None,
389
        update_local: bool = True,
390
        querysize: Optional[int] = None
391
    ) -> None:
392
        """
393
        Updates all dependent computed fields on related models traversing
394
        the dependency tree as shown in the graphs.
395

396
        This is the main entry hook of the resolver to do updates on dependent
397
        computed fields during runtime. While this is done automatically for
398
        model instance actions from signal handlers, you have to call it yourself
399
        after changes done by bulk actions.
400

401
        To do that, simply call this function after the update with the queryset
402
        containing the changed objects:
403

404
            >>> Entry.objects.filter(pub_date__year=2010).update(comments_on=False)
405
            >>> update_dependent(Entry.objects.filter(pub_date__year=2010))
406

407
        This can also be used with ``bulk_create``. Since ``bulk_create``
408
        returns the objects in a python container, you have to create the queryset
409
        yourself, e.g. with pks:
410

411
            >>> objs = Entry.objects.bulk_create([
412
            ...     Entry(headline='This is a test'),
413
            ...     Entry(headline='This is only a test'),
414
            ... ])
415
            >>> pks = set(obj.pk for obj in objs)
416
            >>> update_dependent(Entry.objects.filter(pk__in=pks))
417

418
        .. NOTE::
419

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

423
                >>> MyComputedModel.objects.bulk_create([
424
                ...     MyComputedModel(comp='SENTINEL'), # here or as default field value
425
                ...     MyComputedModel(comp='SENTINEL'),
426
                ... ])
427
                >>> update_dependent(MyComputedModel.objects.filter(comp='SENTINEL'))
428

429
            If the sentinel is beyond reach of the method result, this even ensures to update
430
            only the newly added records.
431

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

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

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

443
                >>> # given: some computed fields model depends somehow on Entry.fk_field
444
                >>> old_relations = preupdate_dependent(Entry.objects.filter(pub_date__year=2010))
445
                >>> Entry.objects.filter(pub_date__year=2010).update(fk_field=new_related_obj)
446
                >>> update_dependent(Entry.objects.filter(pub_date__year=2010), old=old_relations)
447

448
        `update_local=False` disables model local computed field updates of the entry node. 
449
        (used as optimization during tree traversal). You should not disable it yourself.
450
        """
451
        _model = model or self._get_model(instance)
12✔
452

453
        # bulk_updater might change fields, ensure we have set/None
454
        _update_fields = None if update_fields is None else set(update_fields)
12✔
455

456
        # Note: update_local is always off for updates triggered from the resolver
457
        # but True by default to avoid accidentally skipping updates called by user
458
        if update_local and self.has_computedfields(_model):
12✔
459
            # We skip a transaction here in the same sense,
460
            # as local cf updates are not guarded either.
461
            queryset = instance if isinstance(instance, QuerySet) \
12✔
462
                else _model._base_manager.filter(pk__in=[instance.pk])
463
            self.bulk_updater(queryset, _update_fields, local_only=True, querysize=querysize)
12✔
464

465
        updates = self._querysets_for_update(_model, instance, _update_fields).values()
12✔
466
        if updates:
12✔
467
            with transaction.atomic():  # FIXME: place transaction only once in tree descent
12✔
468
                pks_updated: Dict[Type[Model], Set[Any]] = {}
12✔
469
                for queryset, fields in updates:
12✔
470
                    _pks = self.bulk_updater(queryset, fields, return_pks=True, querysize=querysize)
12✔
471
                    if _pks:
12✔
472
                        pks_updated[queryset.model] = _pks
12✔
473
                if old:
12✔
474
                    for model2, data in old.items():
12✔
475
                        pks, fields = data
12✔
476
                        queryset = model2.objects.filter(pk__in=pks-pks_updated.get(model2, set()))
12✔
477
                        self.bulk_updater(queryset, fields, querysize=querysize)
12✔
478

479
    def bulk_updater(
12✔
480
        self,
481
        queryset: QuerySet,
482
        update_fields: Optional[Set[str]] = None,
483
        return_pks: bool = False,
484
        local_only: bool = False,
485
        querysize: Optional[int] = None
486
    ) -> Optional[Set[Any]]:
487
        """
488
        Update local computed fields and descent in the dependency tree by calling
489
        ``update_dependent`` for dependent models.
490

491
        This method does the local field updates on `queryset`:
492

493
            - eval local `MRO` of computed fields
494
            - expand `update_fields`
495
            - apply optional `select_related` and `prefetch_related` rules to `queryset`
496
            - walk all records and recalculate fields in `update_fields`
497
            - aggregate changeset and save as batched `bulk_update` to the database
498

499
        By default this method triggers the update of dependent models by calling
500
        ``update_dependent`` with `update_fields` (next level of tree traversal).
501
        This can be suppressed by setting `local_only=True`.
502

503
        If `return_pks` is set, the method returns a set of altered pks of `queryset`.
504
        """
505
        model: Type[Model] = queryset.model
12✔
506

507
        # distinct issue workaround
508
        # the workaround is needed for already sliced/distinct querysets coming from outside
509
        # TODO: distinct is a major query perf smell, and is in fact only needed on back relations
510
        #       may need some rework in _querysets_for_update
511
        #       ideally we find a way to avoid it for forward relations
512
        #       also see #101
513
        if queryset.query.can_filter() and not queryset.query.distinct_fields:
12!
514
            if queryset.query.combinator != "union":
12✔
515
                queryset = queryset.distinct()
12✔
516
        else:
UNCOV
517
            queryset = model._base_manager.filter(pk__in=subquery_pk(queryset, queryset.db))
×
518

519
        # correct update_fields by local mro
520
        mro = self.get_local_mro(model, update_fields)
12✔
521
        fields: Any = set(mro)  # FIXME: narrow type once issue in django-stubs is resolved
12✔
522
        if update_fields:
12✔
523
            update_fields.update(fields)
12✔
524

525
        select = self.get_select_related(model, fields)
12✔
526
        prefetch = self.get_prefetch_related(model, fields)
12✔
527
        if select:
12✔
528
            queryset = queryset.select_related(*select)
12✔
529
        # fix #167: skip prefetch if union was used
530
        if prefetch and queryset.query.combinator != "union":
12✔
531
            queryset = queryset.prefetch_related(*prefetch)
12✔
532

533
        pks = []
12✔
534
        if fields:
12!
535
            q_size = self.get_querysize(model, fields, querysize)
12✔
536
            change: List[Model] = []
12✔
537
            for elem in slice_iterator(queryset, q_size):
12✔
538
                # note on the loop: while it is technically not needed to batch things here,
539
                # we still prebatch to not cause memory issues for very big querysets
540
                has_changed = False
12✔
541
                for comp_field in mro:
12✔
542
                    new_value = self._compute(elem, model, comp_field)
12✔
543
                    if new_value != getattr(elem, comp_field):
12✔
544
                        has_changed = True
12✔
545
                        setattr(elem, comp_field, new_value)
12✔
546
                if has_changed:
12✔
547
                    change.append(elem)
12✔
548
                    pks.append(elem.pk)
12✔
549
                if len(change) >= self._batchsize:
12!
UNCOV
550
                    self._update(model._base_manager.all(), change, fields)
×
551
                    change = []
×
552
            if change:
12✔
553
                self._update(model._base_manager.all(), change, fields)
12✔
554

555
        # trigger dependent comp field updates from changed records
556
        # other than before we exit the update tree early, if we have no changes at all
557
        # also cuts the update tree for recursive deps (tree-like)
558
        if not local_only and pks:
12✔
559
            self.update_dependent(model._base_manager.filter(pk__in=pks), model, fields, update_local=False)
12✔
560
        return set(pks) if return_pks else None
12✔
561
    
562
    def _update(self, queryset: QuerySet, change: Sequence[Any], fields: Sequence[str]) -> Union[int, None]:
12✔
563
        # we can skip batch_size here, as it already was batched in bulk_updater
564
        if self.use_fastupdate:
12!
565
            return fast_update(queryset, change, fields, None)
12✔
UNCOV
566
        return queryset.model._base_manager.bulk_update(change, fields)
×
567

568
    def _compute(self, instance: Model, model: Type[Model], fieldname: str) -> Any:
12✔
569
        """
570
        Returns the computed field value for ``fieldname``.
571
        Note that this is just a shorthand method for calling the underlying computed
572
        field method and does not deal with local MRO, thus should only be used,
573
        if the MRO is respected by other means.
574
        For quick inspection of a single computed field value, that gonna be written
575
        to the database, always use ``compute(fieldname)`` instead.
576
        """
577
        field = self._computed_models[model][fieldname]
12✔
578
        return field._computed['func'](instance)
12✔
579

580
    def compute(self, instance: Model, fieldname: str) -> Any:
12✔
581
        """
582
        Returns the computed field value for ``fieldname``. This method allows
583
        to inspect the new calculated value, that would be written to the database
584
        by a following ``save()``.
585

586
        Other than calling ``update_computedfields`` on an model instance this call
587
        is not destructive for old computed field values.
588
        """
589
        # Getting a single computed value prehand is quite complicated,
590
        # as we have to:
591
        # - resolve local MRO backwards (stored MRO data is optimized for forward deps)
592
        # - calc all local cfs, that the requested one depends on
593
        # - stack and rewind interim values, as we dont want to introduce side effects here
594
        #   (in fact the save/bulker logic might try to save db calls based on changes)
595
        mro = self.get_local_mro(type(instance), None)
12✔
596
        if not fieldname in mro:
12✔
597
            return getattr(instance, fieldname)
12✔
598
        entries = self._local_mro[type(instance)]['fields']
12✔
599
        pos = 1 << mro.index(fieldname)
12✔
600
        stack: List[Tuple[str, Any]] = []
12✔
601
        model = type(instance)
12✔
602
        for field in mro:
12!
603
            if field == fieldname:
12✔
604
                ret = self._compute(instance, model, fieldname)
12✔
605
                for field2, old in stack:
12✔
606
                    # reapply old stack values
607
                    setattr(instance, field2, old)
12✔
608
                return ret
12✔
609
            f_mro = entries.get(field, 0)
12✔
610
            if f_mro & pos:
12✔
611
                # append old value to stack for later rewinding
612
                # calc and set new value for field, if the requested one depends on it
613
                stack.append((field, getattr(instance, field)))
12✔
614
                setattr(instance, field, self._compute(instance, model, field))
12✔
615

616
    # TODO: the following 3 lookups are very expensive at runtime adding ~2s for 1M calls
617
    #       --> all need pregenerated lookup maps
618
    # Note: the same goes for get_local_mro and _queryset_for_update...
619
    def get_select_related(
12✔
620
        self,
621
        model: Type[Model],
622
        fields: Optional[Iterable[str]] = None
623
    ) -> Set[str]:
624
        """
625
        Get defined select_related rules for `fields` (all if none given).
626
        """
627
        if fields is None:
12!
UNCOV
628
            fields = self._computed_models[model].keys()
×
629
        select: Set[str] = set()
12✔
630
        for field in fields:
12✔
631
            select.update(self._computed_models[model][field]._computed['select_related'])
12✔
632
        return select
12✔
633

634
    def get_prefetch_related(
12✔
635
        self,
636
        model: Type[Model],
637
        fields: Optional[Iterable[str]] = None
638
    ) -> List:
639
        """
640
        Get defined prefetch_related rules for `fields` (all if none given).
641
        """
642
        if fields is None:
12!
UNCOV
643
            fields = self._computed_models[model].keys()
×
644
        prefetch: List[Any] = []
12✔
645
        for field in fields:
12✔
646
            prefetch.extend(self._computed_models[model][field]._computed['prefetch_related'])
12✔
647
        return prefetch
12✔
648

649
    def get_querysize(
12✔
650
        self,
651
        model: Type[Model],
652
        fields: Optional[Iterable[str]] = None,
653
        override: Optional[int] = None
654
    ) -> int:
655
        base = settings.COMPUTEDFIELDS_QUERYSIZE if override is None else override
12✔
656
        if fields is None:
12✔
657
            fields = self._computed_models[model].keys()
12✔
658
        return min(self._computed_models[model][f]._computed['querysize'] or base for f in fields)
12✔
659

660
    def get_contributing_fks(self) -> IFkMap:
12✔
661
        """
662
        Get a mapping of models and their local foreign key fields,
663
        that are part of a computed fields dependency chain.
664

665
        Whenever a bulk action changes one of the fields listed here, you have to create
666
        a listing of the associated  records with ``preupdate_dependent`` before doing
667
        the bulk change. After the bulk change feed the listing back to ``update_dependent``
668
        with the `old` argument.
669

670
        With ``COMPUTEDFIELDS_ADMIN = True`` in `settings.py` this mapping can also be
671
        inspected as admin view. 
672
        """
673
        if not self._map_loaded:  # pragma: no cover
674
            raise ResolverException('resolver has no maps loaded yet')
675
        return self._fk_map
12✔
676

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

685
        There is another class of misconfigured computed fields we currently cannot
686
        find by any safety measures - if `depends` provides valid paths and fields,
687
        but the function operates on different dependencies. Currently it is the devs'
688
        responsibility to perfectly align `depends` entries with dependencies
689
        used by the function to avoid faulty update behavior.
690
        """
691
        if not isinstance(field, Field):
12!
UNCOV
692
                raise ResolverException('field argument is not a Field instance')
×
693
        for rule in depends:
12✔
694
            try:
12✔
695
                path, fieldnames = rule
12✔
UNCOV
696
            except ValueError:
×
697
                raise ResolverException(MALFORMED_DEPENDS)
×
698
            if not isinstance(path, str) or not all(isinstance(f, str) for f in fieldnames):
12!
UNCOV
699
                raise ResolverException(MALFORMED_DEPENDS)
×
700

701
    def computedfield_factory(
12✔
702
        self,
703
        field: 'Field[_ST, _GT]',
704
        compute: Callable[..., _ST],
705
        depends: Optional[IDepends] = None,
706
        select_related: Optional[Sequence[str]] = None,
707
        prefetch_related: Optional[Sequence[Any]] = None,
708
        querysize: Optional[int] = None
709
    ) -> 'Field[_ST, _GT]':
710
        """
711
        Factory for computed fields.
712

713
        The method gets exposed as ``ComputedField`` to allow a more declarative
714
        code style with better separation of field declarations and function
715
        implementations. It is also used internally for the ``computed`` decorator.
716
        Similar to the decorator, the ``compute`` function expects a single argument
717
        as model instance of the model it got applied to.
718

719
        Usage example:
720

721
        .. code-block:: python
722

723
            from computedfields.models import ComputedField
724

725
            def calc_mul(inst):
726
                return inst.a * inst.b
727

728
            class MyModel(ComputedFieldsModel):
729
                a = models.IntegerField()
730
                b = models.IntegerField()
731
                sum = ComputedField(
732
                    models.IntegerField(),
733
                    depends=[('self', ['a', 'b'])],
734
                    compute=lambda inst: inst.a + inst.b
735
                )
736
                mul = ComputedField(
737
                    models.IntegerField(),
738
                    depends=[('self', ['a', 'b'])],
739
                    compute=calc_mul
740
                )
741
        """
742
        self._sanity_check(field, depends or [])
12✔
743
        cf = cast('IComputedField[_ST, _GT]', field)
12✔
744
        cf._computed = {
12✔
745
            'func': compute,
746
            'depends': depends or [],
747
            'select_related': select_related or [],
748
            'prefetch_related': prefetch_related or [],
749
            'querysize': querysize
750
        }
751
        cf.editable = False
12✔
752
        self.add_field(cf)
12✔
753
        return field
12✔
754

755
    def computed(
12✔
756
        self,
757
        field: 'Field[_ST, _GT]',
758
        depends: Optional[IDepends] = None,
759
        select_related: Optional[Sequence[str]] = None,
760
        prefetch_related: Optional[Sequence[Any]] = None,
761
        querysize: Optional[int] = None
762
    ) -> Callable[[Callable[..., _ST]], 'Field[_ST, _GT]']:
763
        """
764
        Decorator to create computed fields.
765

766
        `field` should be a model concrete field instance suitable to hold the result
767
        of the decorated method. The decorator expects a keyword argument `depends`
768
        to indicate dependencies to model fields (local or related).
769
        Listed dependencies will automatically update the computed field.
770

771
        Examples:
772

773
            - create a char field with no further dependencies (not very useful)
774

775
            .. code-block:: python
776

777
                @computed(models.CharField(max_length=32))
778
                def ...
779

780
            - create a char field with a dependency to the field ``name`` on a
781
              foreign key relation ``fk``
782

783
            .. code-block:: python
784

785
                @computed(models.CharField(max_length=32), depends=[('fk', ['name'])])
786
                def ...
787

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

794
        .. NOTE::
795

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

798
        With `select_related` and `prefetch_related` you can instruct the dependency resolver
799
        to apply certain optimizations on the update queryset.
800

801
        .. NOTE::
802

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

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

814
        .. CAUTION::
815

816
            With the dependency resolver you can easily create recursive dependencies
817
            by accident. Imagine the following:
818

819
            .. code-block:: python
820

821
                class A(ComputedFieldsModel):
822
                    @computed(models.CharField(max_length=32), depends=[('b_set', ['comp'])])
823
                    def comp(self):
824
                        return ''.join(b.comp for b in self.b_set.all())
825

826
                class B(ComputedFieldsModel):
827
                    a = models.ForeignKey(A)
828

829
                    @computed(models.CharField(max_length=32), depends=[('a', ['comp'])])
830
                    def comp(self):
831
                        return a.comp
832

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

839
            If you experience this in your project try to get in-depth cycle
840
            information, either by using the ``rendergraph`` management command or
841
            by directly accessing the graph objects:
842

843
            - intermodel dependency graph: ``active_resolver._graph``
844
            - mode local dependency graphs: ``active_resolver._graph.modelgraphs[your_model]``
845
            - union graph: ``active_resolver._graph.get_uniongraph()``
846

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

851
                >>> from computedfields.graph import ComputedModelsGraph
852
                >>> from computedfields.resolver import active_resolver
853
                >>> graph = ComputedModelsGraph(active_resolver.computed_models)
854

855
            Also see the graph documentation :ref:`here<graph>`.
856
        """
857
        def wrap(func: Callable[..., _ST]) -> 'Field[_ST, _GT]':
12✔
858
            return self.computedfield_factory(
12✔
859
                field,
860
                compute=func,
861
                depends=depends,
862
                select_related=select_related,
863
                prefetch_related=prefetch_related,
864
                querysize=querysize
865
            )
866
        return wrap
12✔
867

868
    @overload
12✔
869
    def precomputed(self, f: F) -> F:
12✔
UNCOV
870
        ...
×
871
    @overload
12✔
872
    def precomputed(self, skip_after: bool) -> Callable[[F], F]:
12✔
UNCOV
873
        ...
×
874
    def precomputed(self, *dargs, **dkwargs) -> Union[F, Callable[[F], F]]:
12✔
875
        """
876
        Decorator for custom ``save`` methods, that expect local computed fields
877
        to contain already updated values on enter.
878

879
        By default local computed field values are only calculated once by the
880
        ``ComputedFieldModel.save`` method after your own save method.
881

882
        By placing this decorator on your save method, the values will be updated
883
        before entering your method as well. Note that this comes for the price of
884
        doubled local computed field calculations (before and after your save method).
885
        
886
        To avoid a second recalculation, the decorator can be called with `skip_after=True`.
887
        Note that this might lead to desychronized computed field values, if you do late
888
        field changes in your save method without another resync afterwards.
889
        """
890
        skip: bool = False
12✔
891
        func: Optional[F] = None
12✔
892
        if dargs:
12✔
893
            if len(dargs) > 1 or not callable(dargs[0]) or dkwargs:
12!
UNCOV
894
                raise ResolverException('error in @precomputed declaration')
×
895
            func = dargs[0]
12✔
896
        else:
897
            skip = dkwargs.get('skip_after', False)
12✔
898
        
899
        def wrap(func: F) -> F:
12✔
900
            def _save(instance, *args, **kwargs):
12✔
901
                new_fields = self.update_computedfields(instance, kwargs.get('update_fields'))
12✔
902
                if new_fields:
12!
UNCOV
903
                    kwargs['update_fields'] = new_fields
×
904
                kwargs['skip_computedfields'] = skip
12✔
905
                return func(instance, *args, **kwargs)
12✔
906
            return cast(F, _save)
12✔
907
        
908
        return wrap(func) if func else wrap
12✔
909

910
    def update_computedfields(
12✔
911
        self,
912
        instance: Model,
913
        update_fields: Optional[Iterable[str]] = None
914
        ) -> Optional[Iterable[str]]:
915
        """
916
        Update values of local computed fields of `instance`.
917

918
        Other than calling ``compute`` on an instance, this call overwrites
919
        computed field values on the instance (destructive).
920

921
        Returns ``None`` or an updated set of field names for `update_fields`.
922
        The returned fields might contained additional computed fields, that also
923
        changed based on the input fields, thus should extend `update_fields`
924
        on a save call.
925
        """
926
        model = type(instance)
12✔
927
        if not self.has_computedfields(model):
12✔
928
            return update_fields
12✔
929
        cf_mro = self.get_local_mro(model, update_fields)
12✔
930
        if update_fields:
12✔
931
            update_fields = set(update_fields)
12✔
932
            update_fields.update(set(cf_mro))
12✔
933
        for fieldname in cf_mro:
12✔
934
            setattr(instance, fieldname, self._compute(instance, model, fieldname))
12✔
935
        if update_fields:
12✔
936
            return update_fields
12✔
937
        return None
12✔
938

939
    def has_computedfields(self, model: Type[Model]) -> bool:
12✔
940
        """
941
        Indicate whether `model` has computed fields.
942
        """
943
        return model in self._computed_models
12✔
944

945
    def get_computedfields(self, model: Type[Model]) -> Iterable[str]:
12✔
946
        """
947
        Get all computed fields on `model`.
948
        """
949
        return self._computed_models.get(model, {}).keys()
12✔
950

951
    def is_computedfield(self, model: Type[Model], fieldname: str) -> bool:
12✔
952
        """
953
        Indicate whether `fieldname` on `model` is a computed field.
954
        """
955
        return fieldname in self.get_computedfields(model)
12✔
956

957
    def get_graphs(self) -> Tuple[Graph, Dict[Type[Model], ModelGraph], Graph]:
12✔
958
        """
959
        Return a tuple of all graphs as
960
        ``(intermodel_graph, {model: modelgraph, ...}, union_graph)``.
961
        """
UNCOV
962
        graph = self._graph
×
UNCOV
963
        if not graph:
×
UNCOV
964
            graph = ComputedModelsGraph(active_resolver.computed_models)
×
UNCOV
965
            graph.get_edgepaths()
×
UNCOV
966
            graph.get_uniongraph()
×
UNCOV
967
        return (graph, graph.modelgraphs, graph.get_uniongraph())
×
968

969

970
# active_resolver is currently treated as global singleton (used in imports)
971
#: Currently active resolver.
972
active_resolver = Resolver()
12✔
973

974
# BOOT_RESOLVER: resolver that holds all startup declarations and resolve maps
975
# gets deactivated after startup, thus it is currently not possible to define
976
# new computed fields and add their resolve rules at runtime
977
# TODO: investigate on custom resolvers at runtime to be bootstrapped from BOOT_RESOLVER
978
#: Resolver used during django bootstrapping.
979
#: This is currently the same as `active_resolver` (treated as global singleton).
980
BOOT_RESOLVER = active_resolver
12✔
981

982

983
# placeholder class to test for correct model inheritance
984
# during initial field resolving
985
class _ComputedFieldsModelBase:
12✔
986
    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