Skip to content

feat: lazy attribute population. #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 56 additions & 63 deletions source/ftrack_api/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
import collections
from six.moves import collections_abc
import copy
import typing
import logging
import warnings
import functools

import ftrack_api.symbol
import ftrack_api.exception
import ftrack_api.collection
import ftrack_api.inspection
import ftrack_api.operation

from ftrack_api.symbol import NOT_SET
from ftrack_api.attribute_storage import get_entity_storage

logger = logging.getLogger(__name__)


Expand All @@ -25,6 +29,11 @@ def merge_references(function):
@functools.wraps(function)
def get_value(attribute, entity):
"""Merge the attribute with the local cache."""
mergable_types = (
ftrack_api.entity.base.Entity,
ftrack_api.collection.Collection,
ftrack_api.collection.MappedCollectionProxy,
)

if attribute.name not in entity._inflated:
# Only merge on first access to avoid
Expand All @@ -39,11 +48,7 @@ def get_value(attribute, entity):
local_value = attribute.get_local_value(entity)
if isinstance(
local_value,
(
ftrack_api.entity.base.Entity,
ftrack_api.collection.Collection,
ftrack_api.collection.MappedCollectionProxy,
),
mergable_types,
):
logger.debug("Merging local value for attribute {0}.".format(attribute))

Expand All @@ -57,11 +62,7 @@ def get_value(attribute, entity):
remote_value = attribute.get_remote_value(entity)
if isinstance(
remote_value,
(
ftrack_api.entity.base.Entity,
ftrack_api.collection.Collection,
ftrack_api.collection.MappedCollectionProxy,
),
mergable_types,
):
logger.debug(
"Merging remote value for attribute {0}.".format(attribute)
Expand Down Expand Up @@ -139,7 +140,7 @@ class Attribute(object):
def __init__(
self,
name,
default_value=ftrack_api.symbol.NOT_SET,
default_value=NOT_SET,
mutable=True,
computed=False,
):
Expand All @@ -164,29 +165,23 @@ def __init__(
self._computed = computed
self.default_value = default_value

self._local_key = "local"
self._remote_key = "remote"

def __repr__(self):
"""Return representation of entity."""
return "<{0}.{1}({2}) object at {3}>".format(
self.__module__, self.__class__.__name__, self.name, id(self)
)

def get_entity_storage(self, entity):
@staticmethod
def get_entity_storage(entity):
"""Return attribute storage on *entity* creating if missing."""
storage_key = "_ftrack_attribute_storage"
storage = getattr(entity, storage_key, None)
if storage is None:
storage = collections.defaultdict(
lambda: {
self._local_key: ftrack_api.symbol.NOT_SET,
self._remote_key: ftrack_api.symbol.NOT_SET,
}
)
setattr(entity, storage_key, storage)

return storage
warnings.warn(
"Use of Attribute.get_entity_storage is deprecated, use ftrack_api.attribute_storage"
".get_entity_storage function instead.",
DeprecationWarning,
)

return get_entity_storage(entity)

@property
def name(self):
Expand All @@ -211,24 +206,26 @@ def get_value(self, entity):
via the session and block until available.

"""
value = self.get_local_value(entity)
if value is not ftrack_api.symbol.NOT_SET:
return value
local_value, remote_remote = get_entity_storage(entity).get_local_remote_pair(
self.name
)

if local_value is not NOT_SET:
return local_value

value = self.get_remote_value(entity)
if value is not ftrack_api.symbol.NOT_SET:
return value
if remote_remote is not NOT_SET:
return remote_remote

if not entity.session.auto_populate:
return value
return remote_remote

self.populate_remote_value(entity)

return self.get_remote_value(entity)

def get_local_value(self, entity):
"""Return locally set value for *entity*."""
storage = self.get_entity_storage(entity)
return storage[self.name][self._local_key]
return get_entity_storage(entity).get_local(self.name)

def get_remote_value(self, entity):
"""Return remote value for *entity*.
Expand All @@ -238,22 +235,16 @@ def get_remote_value(self, entity):
Only return locally stored remote value, do not fetch from remote.

"""
storage = self.get_entity_storage(entity)
return storage[self.name][self._remote_key]
return get_entity_storage(entity).get_remote(self.name)

def set_local_value(self, entity, value):
"""Set local *value* for *entity*."""
if (
not self.mutable
and self.is_set(entity)
and value is not ftrack_api.symbol.NOT_SET
):
if not self.mutable and self.is_set(entity) and value is not NOT_SET:
raise ftrack_api.exception.ImmutableAttributeError(self)

old_value = self.get_local_value(entity)

storage = self.get_entity_storage(entity)
storage[self.name][self._local_key] = value
get_entity_storage(entity).set_local(self.name, value)

# Record operation.
if entity.session.record_operations:
Expand All @@ -275,8 +266,7 @@ def set_remote_value(self, entity, value):
Only set locally stored remote value, do not persist to remote.

"""
storage = self.get_entity_storage(entity)
storage[self.name][self._remote_key] = value
get_entity_storage(entity).set_remote(self.name, value)

def populate_remote_value(self, entity):
"""Populate remote value for *entity*."""
Expand All @@ -291,18 +281,22 @@ def is_modified(self, entity):
are the same on the remote.

"""
local_value = self.get_local_value(entity)
remote_value = self.get_remote_value(entity)
return (
local_value is not ftrack_api.symbol.NOT_SET and local_value != remote_value
local_value, remote_value = get_entity_storage(entity).get_local_remote_pair(
self.name
)

return local_value is not NOT_SET and local_value != remote_value

def is_set(self, entity):
"""Return whether a value is set for *entity*."""
local_value, remote_value = get_entity_storage(entity).get_local_remote_pair(
self.name
)

return any(
[
self.get_local_value(entity) is not ftrack_api.symbol.NOT_SET,
self.get_remote_value(entity) is not ftrack_api.symbol.NOT_SET,
local_value is not NOT_SET,
remote_value is not NOT_SET,
]
)

Expand Down Expand Up @@ -352,13 +346,14 @@ def is_modified(self, entity):
are the same on the remote.

"""
local_value = self.get_local_value(entity)
remote_value = self.get_remote_value(entity)
local_value, remote_value = get_entity_storage(entity).get_local_remote_pair(
self.name
)

if local_value is ftrack_api.symbol.NOT_SET:
if local_value is NOT_SET:
return False

if remote_value is ftrack_api.symbol.NOT_SET:
if remote_value is NOT_SET:
return True

if ftrack_api.inspection.identity(
Expand Down Expand Up @@ -400,9 +395,7 @@ def get_value(self, entity):
# mutated without side effects.
local_value = self.get_local_value(entity)
remote_value = self.get_remote_value(entity)
if local_value is ftrack_api.symbol.NOT_SET and isinstance(
remote_value, self.collection_class
):
if local_value is NOT_SET and isinstance(remote_value, self.collection_class):
try:
with entity.session.operation_recording(False):
self.set_local_value(entity, copy.copy(remote_value))
Expand All @@ -417,7 +410,7 @@ def get_value(self, entity):
# newly created entity for example. It *could* be done as a simple
# default value, but that would incur cost for every collection even
# when they are not modified before commit.
if value is ftrack_api.symbol.NOT_SET:
if value is NOT_SET:
try:
with entity.session.operation_recording(False):
self.set_local_value(
Expand All @@ -432,7 +425,7 @@ def get_value(self, entity):

def set_local_value(self, entity, value):
"""Set local *value* for *entity*."""
if value is not ftrack_api.symbol.NOT_SET:
if value is not NOT_SET:
value = self._adapt_to_collection(entity, value)
value.mutable = self.mutable

Expand All @@ -446,7 +439,7 @@ def set_remote_value(self, entity, value):
Only set locally stored remote value, do not persist to remote.

"""
if value is not ftrack_api.symbol.NOT_SET:
if value is not NOT_SET:
value = self._adapt_to_collection(entity, value)
value.mutable = False

Expand Down
53 changes: 53 additions & 0 deletions source/ftrack_api/attribute_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import typing
import collections

from ftrack_api.symbol import NOT_SET

if typing.TYPE_CHECKING:
from ftrack_api.entity.base import Entity

ENTITY_STORAGE_KEY = "ftrack_attribute_storage"

LOCAL_ENTITY_STORAGE_KEY = "local"
REMOTE_ENTITY_STORAGE_KEY = "remote"


class EntityStorage(collections.defaultdict):
"""Storage for entity attributes"""

def get(self, key: str) -> typing.Any:
local, remote = self.get_local_remote_pair(key)

return local if local is not NOT_SET else remote

def get_local(self, key: str) -> typing.Any:
return self[key][LOCAL_ENTITY_STORAGE_KEY]

def get_remote(self, key: str) -> typing.Any:
return self[key][REMOTE_ENTITY_STORAGE_KEY]

def get_local_remote_pair(self, key: str) -> typing.Tuple[typing.Any, typing.Any]:
"""Return local and remote values for *key*."""
return self[key][LOCAL_ENTITY_STORAGE_KEY], self[key][REMOTE_ENTITY_STORAGE_KEY]

def set_local(self, key: str, value: typing.Any) -> None:
self[key][LOCAL_ENTITY_STORAGE_KEY] = value

def set_remote(self, key: str, value: typing.Any) -> None:
self[key][REMOTE_ENTITY_STORAGE_KEY] = value


def get_entity_storage(entity: "Entity") -> EntityStorage:
"""Return attribute storage on *entity* creating if missing."""

storage = getattr(entity, ENTITY_STORAGE_KEY, None)
if storage is None:
storage = EntityStorage(
lambda: {
LOCAL_ENTITY_STORAGE_KEY: NOT_SET,
REMOTE_ENTITY_STORAGE_KEY: NOT_SET,
}
)
setattr(entity, ENTITY_STORAGE_KEY, storage)

return storage
6 changes: 3 additions & 3 deletions source/ftrack_api/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
import pickle

import ftrack_api.inspection
import ftrack_api.symbol
from ftrack_api.symbol import NOT_SET


class Cache(with_metaclass(abc.ABCMeta, object)):
Expand Down Expand Up @@ -184,7 +184,7 @@ def get(self, key):

"""
target_caches = []
value = ftrack_api.symbol.NOT_SET
value = NOT_SET

for cache in self.caches:
try:
Expand All @@ -195,7 +195,7 @@ def get(self, key):
else:
break

if value is ftrack_api.symbol.NOT_SET:
if value is NOT_SET:
raise KeyError(key)

# Set value on all higher level caches.
Expand Down
Loading