API Reference

models.py

class computedfields.models.ComputedFieldsModel(*args, **kwargs)[source]

Bases: _ComputedFieldsModelBase, Model

Abstract base class for models with computed fields. Overloads save to update local computed field values before they are written to the database.

All models containing a computed field must be derived from this class.

save(force_insert: bool = False, force_update: bool = False, using: str | None = None, update_fields: Iterable[str] | None = None, skip_computedfields: bool = False) None[source]

Overloaded to update computed field values before writing to the database.

The method understands a special argument skip_computedfields to skip the calculation of local computed fields. This is used by the @precomputed decorator to allow to skip a second field calculation with @precomputed(skip_after=True). If you use skip_computedfields yourself, make sure to synchronize computed fields yourself by other means, e.g. by calling update_computedfields before writing the instance or by a later update_dependent call.

computedfields.models.ComputedField(field: Field[_ST, _GT], compute: Callable[[...], _ST], depends: Sequence[Tuple[str, Sequence[str]]] | None = None, select_related: Sequence[str] | None = None, prefetch_related: Sequence[Any] | None = None, querysize: int | None = None) Field[_ST, _GT]

Convenient access to computedfield_factory.

computedfields.models.computed(field: Field[_ST, _GT], depends: Sequence[Tuple[str, Sequence[str]]] | None = None, select_related: Sequence[str] | None = None, prefetch_related: Sequence[Any] | None = None, querysize: int | None = None) Callable[[Callable[[...], _ST]], Field[_ST, _GT]]

Convenient access to the decorator @computed.

computedfields.models.precomputed(*dargs, **dkwargs) F | Callable[[F], F]

Convenient access to the decorator @precomputed.

computedfields.models.compute(instance: Model, fieldname: str) Any

Convenient access to compute.

computedfields.models.update_computedfields(instance: Model, update_fields: Iterable[str] | None = None) Iterable[str] | None

Convenient access to update_computedfields.

computedfields.models.update_dependent(instance: QuerySet | Model, model: Type[Model] | None = None, update_fields: Iterable[str] | None = None, old: Dict[Type[Model], List[Any]] | None = None, update_local: bool = True, querysize: int | None = None) None

Convenient access to update_dependent.

computedfields.models.preupdate_dependent(instance: QuerySet | Model, model: Type[Model] | None = None, update_fields: Iterable[str] | None = None) Dict[Type[Model], List[Any]]

Convenient access to preupdate_dependent.

computedfields.models.has_computedfields(model: Type[Model]) bool

Convenient access to has_computedfields.

computedfields.models.get_computedfields(model: Type[Model]) Iterable[str]

Convenient access to get_computedfields.

computedfields.models.is_computedfield(model: Type[Model], fieldname: str) bool

Convenient access to is_computedfield.

computedfields.models.get_contributing_fks() Dict[Type[Model], Set[str]]

Convenient access to get_contributing_fks.

class computedfields.models.ComputedFieldsAdminModel(*args, **kwargs)[source]

Bases: ContentType

Proxy model to list all ComputedFieldsModel models with their field dependencies in admin. This might be useful during development. To enable it, set COMPUTEDFIELDS_ADMIN = True in settings.py.

exception DoesNotExist

Bases: DoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

class computedfields.models.ContributingModelsModel(*args, **kwargs)[source]

Bases: ContentType

Proxy model to list all models in admin, that contain foreign key relations contributing to computed fields. This might be useful during development. To enable it, set COMPUTEDFIELDS_ADMIN = True in settings.py.

Note

A foreign key relation is considered contributing, if it is part of a computed field dependency in reverse direction.

exception DoesNotExist

Bases: DoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

resolver.py

Contains the resolver logic for automated computed field updates.

class computedfields.resolver.IM2mData[source]

Bases: TypedDict

exception computedfields.resolver.ResolverException[source]

Bases: ComputedFieldsException

Exception raised during model and field registration or dependency resolving.

class computedfields.resolver.Resolver[source]

Bases: object

Holds the needed data for graph calculations and runtime dependency resolving.

Basic workflow:

  • On django startup a resolver gets instantiated early to track all project-wide model registrations and computed field decorations (collector phase).

  • On app.ready the computed fields are associated with their models to build a resolver-wide map of models with computed fields (computed_models).

  • After that the resolver maps are created (see graph.ComputedModelsGraph).

models: Set[Type[Model]]

Models from class_prepared signal hook during collector phase.

computedfields: Set[IComputedField]

Computed fields found during collector phase.

add_model(sender: Type[Model], **kwargs) None[source]

class_prepared signal hook to collect models during ORM registration.

add_field(field: IComputedField) None[source]

Collects fields from decoration stage of @computed.

seal() None[source]

Seal the resolver, so no new models or computed fields can be added anymore.

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

(Currently runtime creation of models with computed fields is not supported, trying to do so will raise an exception. This might change in future versions.)

property models_with_computedfields: Generator[Tuple[Type[Model], Set[IComputedField]], None, None]

Generator of tracked models with their computed fields.

This cannot be accessed during the collector phase.

property computedfields_with_models: Generator[Tuple[IComputedField, Set[Type[Model]]], None, None]

Generator of tracked computed fields and their models.

This cannot be accessed during the collector phase.

property computed_models: Dict[Type[Model], Dict[str, IComputedField]]

Mapping of ComputedFieldModel models and their computed fields.

The data is the single source of truth for the graph reduction and map creations. Thus it can be used to decide at runtime whether the active resolver a certain as a model with computed fields.

Note

The resolver will only list models here, that actually have a computed field defined. A model derived from ComputedFieldsModel without a computed field will not be listed.

extract_computed_models() Dict[Type[Model], Dict[str, IComputedField]][source]

Creates computed_models mapping from models and computed fields found in collector phase.

initialize(models_only: bool = False) None[source]

Entrypoint for app.ready to seal the resolver and trigger the resolver map creation.

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

After calling initialize no more models or fields can be registered to the resolver, and computed_models and the resolver maps get loaded.

load_maps(_force_recreation: bool = False) None[source]

Load all needed resolver maps. The steps are:

  • create intermodel graph of the dependencies

  • remove redundant paths with cycling check

  • create modelgraphs for local MRO

  • merge graphs to uniongraph with cycling check

  • create final resolver maps

    • lookup_map: intermodel dependencies as queryset access strings

    • fk_map: models with their contributing fk fields

    • local_mro: MRO of local computed fields per model

get_local_mro(model: Type[Model], update_fields: Iterable[str] | None = None) List[str][source]

Return MRO for local computed field methods for a given set of update_fields. The returned list of fieldnames must be calculated in order to correctly update dependent computed field values in one pass.

Returns computed fields as self dependent to simplify local field dependency calculation.

preupdate_dependent(instance: QuerySet | Model, model: Type[Model] | None = None, update_fields: Iterable[str] | None = None) Dict[Type[Model], List[Any]][source]

Create a mapping of currently associated computed field records, that might turn dirty by a follow-up bulk action.

Feed the mapping back to update_dependent as old argument after your bulk action to update de-associated computed field records as well.

update_dependent(instance: QuerySet | Model, model: Type[Model] | None = None, update_fields: Iterable[str] | None = None, old: Dict[Type[Model], List[Any]] | None = None, update_local: bool = True, querysize: int | None = None) None[source]

Updates all dependent computed fields on related models traversing the dependency tree as shown in the graphs.

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

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

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

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

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

Note

Getting pks from bulk_create is not supported by all database adapters. With a local computed field you can “cheat” here by providing a sentinel:

>>> MyComputedModel.objects.bulk_create([
...     MyComputedModel(comp='SENTINEL'), # here or as default field value
...     MyComputedModel(comp='SENTINEL'),
... ])
>>> update_dependent(MyComputedModel.objects.filter(comp='SENTINEL'))

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

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

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

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

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

update_local=False disables model local computed field updates of the entry node. (used as optimization during tree traversal). You should not disable it yourself.

bulk_updater(queryset: QuerySet, update_fields: Set[str] | None = None, return_pks: bool = False, local_only: bool = False, querysize: int | None = None) Set[Any] | None[source]

Update local computed fields and descent in the dependency tree by calling update_dependent for dependent models.

This method does the local field updates on queryset:

  • eval local MRO of computed fields

  • expand update_fields

  • apply optional select_related and prefetch_related rules to queryset

  • walk all records and recalculate fields in update_fields

  • aggregate changeset and save as batched bulk_update to the database

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

If return_pks is set, the method returns a set of altered pks of queryset.

compute(instance: Model, fieldname: str) Any[source]

Returns the computed field value for fieldname. This method allows to inspect the new calculated value, that would be written to the database by a following save().

Other than calling update_computedfields on an model instance this call is not destructive for old computed field values.

Get defined select_related rules for fields (all if none given).

Get defined prefetch_related rules for fields (all if none given).

get_contributing_fks() Dict[Type[Model], Set[str]][source]

Get a mapping of models and their local foreign key fields, that are part of a computed fields dependency chain.

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

With COMPUTEDFIELDS_ADMIN = True in settings.py this mapping can also be inspected as admin view.

computedfield_factory(field: Field[_ST, _GT], compute: Callable[[...], _ST], depends: Sequence[Tuple[str, Sequence[str]]] | None = None, select_related: Sequence[str] | None = None, prefetch_related: Sequence[Any] | None = None, querysize: int | None = None) Field[_ST, _GT][source]

Factory for computed fields.

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

Usage example:

from computedfields.models import ComputedField

def calc_mul(inst):
    return inst.a * inst.b

class MyModel(ComputedFieldsModel):
    a = models.IntegerField()
    b = models.IntegerField()
    sum = ComputedField(
        models.IntegerField(),
        depends=[('self', ['a', 'b'])],
        compute=lambda inst: inst.a + inst.b
    )
    mul = ComputedField(
        models.IntegerField(),
        depends=[('self', ['a', 'b'])],
        compute=calc_mul
    )
computed(field: Field[_ST, _GT], depends: Sequence[Tuple[str, Sequence[str]]] | None = None, select_related: Sequence[str] | None = None, prefetch_related: Sequence[Any] | None = None, querysize: int | None = None) Callable[[Callable[[...], _ST]], Field[_ST, _GT]][source]

Decorator to create computed fields.

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

Examples:

  • create a char field with no further dependencies (not very useful)

@computed(models.CharField(max_length=32))
def ...
  • create a char field with a dependency to the field name on a foreign key relation fk

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

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

Note

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

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

Note

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

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

Caution

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

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

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

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

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

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

  • intermodel dependency graph: active_resolver._graph

  • mode local dependency graphs: active_resolver._graph.modelgraphs[your_model]

  • union graph: active_resolver._graph.get_uniongraph()

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

>>> from computedfields.graph import ComputedModelsGraph
>>> from computedfields.resolver import active_resolver
>>> graph = ComputedModelsGraph(active_resolver.computed_models)

Also see the graph documentation here.

precomputed(f: F) F[source]
precomputed(skip_after: bool) Callable[[F], F]

Decorator for custom save methods, that expect local computed fields to contain already updated values on enter.

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

By placing this decorator on your save method, the values will be updated before entering your method as well. Note that this comes for the price of doubled local computed field calculations (before and after your save method).

To avoid a second recalculation, the decorator can be called with skip_after=True. Note that this might lead to desychronized computed field values, if you do late field changes in your save method without another resync afterwards.

update_computedfields(instance: Model, update_fields: Iterable[str] | None = None) Iterable[str] | None[source]

Update values of local computed fields of instance.

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

Returns None or an updated set of field names for update_fields. The returned fields might contained additional computed fields, that also changed based on the input fields, thus should extend update_fields on a save call.

has_computedfields(model: Type[Model]) bool[source]

Indicate whether model has computed fields.

get_computedfields(model: Type[Model]) Iterable[str][source]

Get all computed fields on model.

is_computedfield(model: Type[Model], fieldname: str) bool[source]

Indicate whether fieldname on model is a computed field.

get_graphs() Tuple[Graph, Dict[Type[Model], ModelGraph], Graph][source]

Return a tuple of all graphs as (intermodel_graph, {model: modelgraph, ...}, union_graph).

computedfields.resolver.active_resolver = <computedfields.resolver.Resolver object>

Currently active resolver.

computedfields.resolver.BOOT_RESOLVER = <computedfields.resolver.Resolver object>

Resolver used during django bootstrapping. This is currently the same as active_resolver (treated as global singleton).

graph.py

Module containing the graph logic for the dependency resolver.

Upon application initialization a dependency graph of all project wide computed fields is created. The graph does a basic cycle check and removes redundant dependencies. Finally the dependencies are translated to the resolver map to be used later by update_dependent and in the signal handlers.

class computedfields.graph.IComputedData[source]

Bases: TypedDict

class computedfields.graph.IComputedField(verbose_name=None, name=None, primary_key=False, max_length=None, unique=False, blank=False, null=False, db_index=False, rel=None, default=<class 'django.db.models.fields.NOT_PROVIDED'>, editable=True, serialize=True, unique_for_date=None, unique_for_month=None, unique_for_year=None, choices=None, help_text='', db_column=None, db_tablespace=None, auto_created=False, validators=(), error_messages=None)[source]

Bases: Field, Generic[_ST, _GT]

class computedfields.graph.ICycleData[source]

Bases: TypedDict

class computedfields.graph.IDependsData[source]

Bases: TypedDict

class computedfields.graph.ILocalMroData[source]

Bases: TypedDict

class computedfields.graph.IResolvedDeps[source]

Bases: TypedDict

exception computedfields.graph.ComputedFieldsException[source]

Bases: Exception

Base exception raised from computed fields.

exception computedfields.graph.CycleException[source]

Bases: ComputedFieldsException

Exception raised during path linearization, if a cycle was found. Contains the found cycle either as list of edges or nodes in args.

exception computedfields.graph.CycleEdgeException[source]

Bases: CycleException

Exception raised during path linearization, if a cycle was found. Contains the found cycle as list of edges in args.

exception computedfields.graph.CycleNodeException[source]

Bases: CycleException

Exception raised during path linearization, if a cycle was found. Contains the found cycle as list of nodes in args.

class computedfields.graph.Edge(*args)[source]

Bases: object

Class for representing an edge in Graph. The instances are created as singletons, calling Edge('A', 'B') multiple times will always point to the same object.

class computedfields.graph.Node(*args)[source]

Bases: object

Class for representing a node in Graph. The instances are created as singletons, calling Node('A') multiple times will always point to the same object.

class computedfields.graph.Graph[source]

Bases: object

Simple directed graph implementation.

add_node(node: Node) None[source]

Add a node to the graph.

remove_node(node: Node) None[source]

Remove a node from the graph.

Warning

Removing edges containing the removed node is not implemented.

add_edge(edge: Edge) None[source]

Add an edge to the graph. Automatically inserts the associated nodes.

remove_edge(edge: Edge) None[source]

Removes an edge from the graph.

Warning

Does not remove leftover contained nodes.

get_dot(format: str = 'pdf', mark_edges: Dict[Edge, Dict[str, Any]] | None = None, mark_nodes: Dict[Node, Dict[str, Any]] | None = None)[source]

Returns the graphviz object of the graph (needs the graphviz package).

render(filename: PathLike | str | None = None, format: str = 'pdf', mark_edges: Dict[Edge, Dict[str, Any]] | None = None, mark_nodes: Dict[Node, Dict[str, Any]] | None = None) None[source]

Renders the graph to file (needs the graphviz package).

view(format: str = 'pdf', mark_edges: Dict[Edge, Dict[str, Any]] | None = None, mark_nodes: Dict[Node, Dict[str, Any]] | None = None) None[source]

Directly opens the graph in the associated desktop viewer (needs the graphviz package).

static edgepath_to_nodepath(path: Sequence[Edge]) List[Node][source]

Converts a list of edges to a list of nodes.

static nodepath_to_edgepath(path: Sequence[Node]) List[Edge][source]

Converts a list of nodes to a list of edges.

get_edgepaths() List[List[Edge]][source]

Returns a list of all edge paths. An edge path is represented as list of edges.

Might raise a CycleEdgeException. For in-depth cycle detection use edge_cycles, node_cycles` or get_cycles().

get_nodepaths() List[List[Node]][source]

Returns a list of all node paths. A node path is represented as list of nodes.

Might raise a CycleNodeException. For in-depth cycle detection use edge_cycles, node_cycles or get_cycles().

get_cycles() Dict[FrozenSet[Edge], ICycleData][source]

Gets all cycles in graph.

This is not optimised by any means, it simply walks the whole graph recursively and aborts as soon a seen edge gets entered again. Therefore use this and all dependent properties (edge_cycles and node_cycles) for in-depth cycle inspection only.

As a start node any node on the left side of an edge will be tested.

Returns a mapping of

{frozenset(<cycle edges>): {
    'entries': set(edges leading to the cycle),
    'path': list(cycle edges in last seen order)
}}

An edge in entries is not necessarily part of the cycle itself, but once entered it will lead to the cycle.

property edge_cycles: List[List[Edge]]

Returns all cycles as list of edge lists. Use this only for in-depth cycle inspection.

property node_cycles: List[List[Node]]

Returns all cycles as list of node lists. Use this only for in-depth cycle inspection.

property is_cyclefree: bool

True if the graph contains no cycles.

For faster calculation this property relies on path linearization instead of the more expensive full cycle detection. For in-depth cycle inspection use edge_cycles or node_cycles instead.

class computedfields.graph.ComputedModelsGraph(computed_models: Dict[Type[Model], Dict[str, IComputedField]])[source]

Bases: Graph

Class to convert the computed fields dependency strings into a graph and generate the final resolver functions.

Steps taken:

  • resolve_dependencies resolves the depends field strings to real model fields.

  • The dependencies are rearranged to adjacency lists for the underlying graph.

  • The graph does a cycle check and removes redundant edges to lower the database penalty.

  • In generate_lookup_map the path segments of remaining edges are collected into the final lookup map.

resolve_dependencies(computed_models: Dict[Type[Model], Dict[str, IComputedField]]) IResolvedDeps[source]

Converts depends rules into ORM lookups and checks the source fields’ existance.

Basic depends rules: - left side may contain any relation path accessible from an instance as 'a.b.c' - right side may contain real database source fields (never relations)

Deeper nested relations get automatically added to the resolver map:

  • fk relations are added on the model holding the fk field

  • reverse fk relations are added on related model holding the fk field

  • m2m fields and backrelations are added on the model directly, but only used for inter-model resolving, never for field lookups during save

generate_maps() Tuple[Dict[Type[Model], Dict[str, Dict[Type[Model], Tuple[Set[str], Set[str]]]]], Dict[Type[Model], Set[str]]][source]

Generates the final lookup map and the fk map.

Schematically the lookup map is a reversed adjacency list of every source model with its fields mapping to the target models with computed fields it would update through a certain filter string:

src_model:[src_field, ...] --> target_model:[(cf_field, filter_string), ...]

During runtime update_dependent will use the the information to create select querysets on the target_models (roughly):

qs = target_model._base_manager.filter(filter_string=src_model.instance)
qs |= target_model._base_manager.filter(filter_string2=src_model.instance)
...
bulk_updater(qs, cf_fields)

The fk map list all models with fk fieldnames, that contribute to computed fields.

Returns a tuple of (lookup_map, fk_map).

prepare_modelgraphs() None[source]

Helper to initialize model local subgraphs.

generate_local_mro_map() Dict[Type[Model], ILocalMroData][source]

Generate model local computed fields mro maps. Returns a mapping of models with local computed fields dependencies and their mro, example:

{
    modelX: {
        'base': ['c1', 'c2', 'c3'],
        'fields': {
            'name': ['c2', 'c3'],
            'c2': ['c2', 'c3']
        }
    }
}

In the example modelX would have 3 computed fields, where c2 somehow depends on the field name. c3 itself depends on changes to c2, thus a change to name should run c2 and c3 in that specific order.

base lists all computed fields in topological execution order (mro). It is also used at runtime to cover a full update of an instance (update_fields=None).

Note

Note that the actual values in fields are bitarrays to index positions of base, which allows quick field update merges at runtime by doing binary OR on the bitarrays.

get_uniongraph() Graph[source]

Build a union graph of intermodel dependencies and model local dependencies. This graph represents the final update cascades triggered by certain field updates. The union graph is needed to spot cycles introduced by model local dependencies, that otherwise might went unnoticed, example:

  • global dep graph (acyclic): A.comp --> B.comp, B.comp2 --> A.comp

  • modelgraph of B (acyclic): B.comp --> B.comp2

Here the resulting union graph is not a DAG anymore, since both subgraphs short-circuit to a cycle of A.comp --> B.comp --> B.comp2 --> A.comp.

class computedfields.graph.ModelGraph(model: Type[Model], local_dependencies: Dict[str, Set[str]], computed_fields: Dict[str, IComputedField])[source]

Bases: Graph

Graph to resolve model local computed field dependencies in right calculation order.

transitive_reduction() None[source]

Remove redundant single edges. Also checks for cycles. Note: Other than intermodel dependencies local dependencies must always be cyclefree.

get_topological_paths() Dict[Node, List[Node]][source]

Creates a map of all possible entry nodes and their topological update path (computed fields mro).

generate_field_paths(tpaths: Dict[Node, List[Node]]) Dict[str, List[str]][source]

Convert topological path node mapping into a mapping containing the fieldnames.

generate_local_mapping(field_paths: Dict[str, List[str]]) ILocalMroData[source]

Generates the final model local update table to be used during ComputedFieldsModel.save. Output is a mapping of local fields, that also update local computed fields, to a bitarray containing the computed fields mro, and the base topologcial order for a full update.

handlers.py

Module containing the database signal handlers.

The handlers are registered during application startup in apps.ready.

Note

The handlers are not registered in the managment commands makemigrations, migrate and help.

computedfields.handlers.get_old_handler(sender: Type[Model], instance: Model, **kwargs) None[source]

get_old_handler handler.

pre_save signal handler to spot incoming fk relation changes. This is needed to correctly update old relations after fk changes, that would contain dirty computed field values after a save. The actual updates on old relations are done during post_save. Skipped during fixtures.

computedfields.handlers.postsave_handler(sender: Type[Model], instance: Model, **kwargs) None[source]

post_save handler.

Directly updates dependent objects. Skipped during fixtures.

computedfields.handlers.predelete_handler(sender: Type[Model], instance: Model, **_) None[source]

pre_delete handler.

Gets all dependent objects as pk lists and saves them in thread local storage.

computedfields.handlers.postdelete_handler(sender: Type[Model], instance: Model, **kwargs) None[source]

post_delete handler.

Loads the dependent objects from the previously saved pk lists and updates them.

computedfields.handlers.merge_pk_maps(obj1: Dict[Type[Model], List[Any]], obj2: Dict[Type[Model], List[Any]]) Dict[Type[Model], List[Any]][source]

Merge pk map in obj2 on obj1. Updates obj1 inplace and also returns it.

computedfields.handlers.merge_qs_maps(obj1: Dict[Type[Model], List[Any]], obj2: Dict[Type[Model], List[Any]]) Dict[Type[Model], List[Any]][source]

Merge queryset map in obj2 on obj1. Updates obj1 inplace and also returns it.

computedfields.handlers.m2m_handler(sender: Type[Model], instance: Model, **kwargs) None[source]

m2m_change handler.

Works like the other handlers but on the corresponding m2m actions.

Note

The handler triggers updates for both ends of the m2m relation, which might lead to massive database interaction.

admin.py

class computedfields.admin.ComputedModelsAdmin(model, admin_site)[source]

Bases: ModelAdmin

Shows all ComputedFieldsModel models with their field dependencies in the admin. Also renders the dependency graphs if the graphviz package is installed.

actions = None
dependencies(inst)[source]

List dependencies for model.

computed_fields(inst)[source]

List computed fields for model.

local_computed_fields_mro(inst)[source]

List MRO for local computed fields on model.

name(obj)[source]

Resolve modelname, optionally with link.

modelgraph(inst)[source]

Link to show modelgraph.

render_graph(request, extra_context=None)[source]

Render intermodel graph view.

render_uniongraph(request, extra_context=None)[source]

Render union graph view.

render_modelgraph(request, modelid, extra_context=None)[source]

Render modelgraph view.

class computedfields.admin.ContributingModelsAdmin(model, admin_site)[source]

Bases: ModelAdmin

Shows models with cf contributing local fk fields.

actions = None
fk_fields(inst)[source]

List contributing fk field names.

name(obj)[source]

Resolve modelname, optionally with link.