'''
kabaret.flow.object
Defines the Object class, the base for all object in a flow, and
the Root class used to hold a flow graph.
'''
import logging
import six
from .exceptions import WIPException
from .exceptions import MissingRelationError, MissingChildError
from .value_store import MemoryValueStore
class _Manager(object):
def __init__(self, object, parent, name, value_store):
super(_Manager, self).__init__()
self.object = object
self.parent = parent
self.name = name
self.value_store = value_store
self.destroyed = False # TODO: use this to stop behavior after desctuction
if self.parent is not None:
self.parent._mng.set_child(self, object)
self.children = {}
#---OLD
def create_manager(self, object, parent, name):
raise WIPException('Is this used anywhere ?!?')
return self.__class__(object, parent, name)
def root(self):
if self.parent is None:
return self.object
return self.parent._mng.root()
#---OLD
def iter_parents(self):
raise WIPException('Is this used anywhere ?!?')
curr = self
while curr is not None:
parent = curr.parent
if parent is None:
raise StopIteration()
yield parent
curr = parent._mng
def oid(self):
# TODO: since we cant rename an item, and we use this quite a lot:
# shouldn't we bake it ?
# (it may help for Map.rows())
if self.parent is None:
return self.name
return '%s/%s' % (self.parent._mng.oid(), self.name)
def destroy(self):
logging.getLogger('kabaret.flow').debug('----> DESTROYING ' + self.oid() + '. Should we notify refs ?')
self.destroyed = True
self.parent = None
def set_child(self, mng, object):
try:
self.children[mng.name]
except:
self.children[mng.name] = object
else:
raise Exception('This child already exists !!!')
def relations(self):
return self.object._relations
#---OLD
def drop_child(self, name):
raise WIPException('This has been renamed destroy_child')
def destroy_child(self, name):
try:
old = self.children[name]
except KeyError:
raise MissingChildError(self.oid(), name)
else:
old._mng.destroy()
del self.children[name]
#---OLD
def children_names(self):
# relations must be used to find children names.
raise WIPException('This should not be useful')
try:
return self.object.children_names()
except AttributeError:
return self.children.keys()
#---OLD
def children_oids(self):
raise WIPException('This should not be useful')
oid = self.oid()
return ['%s/%s' % (oid, n) for n in self.children_names()]
#---OLD
def object_type_name(self):
raise WIPException('Is this used anywhere ?!?')
return self.object.__class__.__name__
def get_qualified_type_name(self, TYPE):
return '%s.%s' % (TYPE.__module__, TYPE.__name__)
def object_qualified_type_name(self):
return self.get_qualified_type_name(self.object.__class__)
def qualified_type_name_to_type(self, qualified_type_name):
# NB: str it because json tend to send unicodes here :/
chunks = str(qualified_type_name).rsplit('.', 1)
type_name = chunks.pop(-1)
module_path = chunks[0]
leaf_module_name = module_path.rsplit('.', 1)[-1]
module = __import__(module_path, None, None, leaf_module_name, 0)
try:
return getattr(module, type_name)
except AttributeError:
raise ImportError("Nothing named %s in module %s" %
(type_name, module_path))
#---OLD
def __len__(self):
raise WIPException('This should not be useful')
return len(self.children)
#---OLD
def __iter__(self):
raise WIPException('This should not be useful')
for name, object in self.children.iteritems():
yield name, object
#---OLD
def __contains__(self, object_name):
raise WIPException('This should not be useful')
return object_name in self.children
def has_related(self, relation_name):
return relation_name in [r.name for r in self.object._relations]
#---OLD
def get_child(self, name):
raise WIPException('This has been renamed to get_existing_child(name)')
def get_existing_child(self, name):
'''
Return the child object named 'name'.
This DOES NOT involve Relation instance and can only return already
existing children.
If no such child exists, a MissingChildError is raised.
'''
try:
return self.children[name]
except KeyError:
raise MissingChildError(self.oid(), name)
#---OLD
def get_typed_children(self, class_or_type_or_tuple):
raise WIPException('Is this used anywhere ?!?')
return [
relation.get_related(self.object)
for relation in self.object._relations
if issubclass(relation.related_type, class_or_type_or_tuple)
]
def get_object(self, oid):
'''
Returns the object with the given oid.
If oid is relative (does not start with /), returns a (grand) child.
The oid may contain . and .. has in posix paths
(do we really use that ? o.O)
This will evaluate the _Relation used by self.object and potential
override of __getattr__.
'''
if oid is None:
return self.object
# remove consecutive slashes (//) to avoid requesting root in
# the middle of the search
while '//' in oid:
oid = oid.replace('//', '/')
cuts = oid.split('/', 1)
try:
remain = cuts[1]
except IndexError:
remain = None
next = cuts[0]
if not next:
root = self.root()
return root._mng.get_object(remain)
if next == '.':
return self.get_object(remain)
if next == '..':
return self.parent._mng.get_object(remain)
if self.has_related(next):
# we must us getattr to have the relation do the job:
child = getattr(self.object, next)
else:
# maybe it's in a map
try:
self.object.get_mapped
except AttributeError:
raise MissingRelationError(self.oid(), next)
else:
child = self.object.get_mapped(next)
return child._mng.get_object(remain)
#---OLD
def set_default_value(self, value):
raise WIPException('! this should be in Value !')
self.default_value = value
#---OLD
def compute_value(self):
raise WIPException('! this should be in ComputedValue !')
if not self.is_dirty():
return
self.parent.compute_child_value(self.object)
def add_ref(self, object):
oid = object.oid()
refs = set(self.refs())
if oid in refs:
# Avoid doubles and extra refresh/events:
return
refs.add(object.oid())
refs = sorted(refs)
self.value_store.set(self.oid() + '.refs', refs)
def remove_ref(self, object):
# use set to cleanup duplicates (was possible in previous code)
refs = set(self.refs())
try:
refs.remove(object.oid())
except KeyError:
# This should not happen.
logging.getLogger('kabaret.flow').warning('Removing ref %r from %r: ref not in list!')
return
refs = sorted(refs)
self.value_store.set(self.oid() + '.refs', refs)
def refs(self):
try:
return self.value_store.get(self.oid() + '.refs')
except KeyError:
return []
def ref_objects(self):
return [self.get_object(ref_oid) for ref_oid in self.refs()]
def get_value(self, default):
try:
return self.value_store.get(self.oid())
except KeyError:
return default
def del_value(self):
self.value_store.delete(self.oid())
self._on_value_changed()
def set_value(self, value):
self.value_store.set(self.oid(), value)
self._on_value_changed()
#--- OLD
# def update_dict_value(self, **new_values):
# self.value_store.update(self.oid(), **new_values)
# self._on_value_changed()
def incr_value(self, by=1):
self.value_store.incr(self.oid(), by)
self._on_value_changed()
def decr_value(self, by=1):
self.value_store.decr(self.oid(), by)
self._on_value_changed()
# --- OSS
def oss_get(self):
return self.value_store.oss_get(self.oid())
def oss_get_range(self, first, last):
return self.value_store.oss_get_range(self.oid(), first, last)
def oss_has(self, member):
return self.value_store.oss_has(self.oid(), member)
def oss_add(self, member, score):
self.value_store.oss_add(self.oid(), member, score)
self._on_value_changed()
def oss_remove(self, member):
self.value_store.oss_remove(self.oid(), member)
self._on_value_changed()
def oss_len(self):
return self.value_store.oss_len(self.oid())
def oss_get_score(self, member, score):
return self.value_store.oss_get_score(self.oid(), member)
def oss_set_score(self, member, score):
self.value_store.oss_set_score(self.oid(), member, score)
self._on_value_changed()
# --- HASH
def get_hash_value(self):
return self.value_store.get(self.oid())
def hash_get_key(self, key):
return self.value_store.hash_get_key(self.oid(), key)
def hash_has_key(self, key):
return self.value_store.hash_has_key(self.oid(), key)
def del_hash_key(self, key):
self.value_store.del_hash_key(self.oid(), key)
self._on_value_changed()
def get_hash(self):
return self.value_store.get_hash(self.oid())
def get_hash_as_dict(self):
return self.value_store.get_hash_as_dict(self.oid())
def get_hash_keys(self):
return self.value_store.get_hash_keys(self.oid())
def get_hash_len(self):
return self.value_store.get_hash_len(self.oid())
def update_hash(self, mapping):
self.value_store.update_hash(self.oid(), mapping)
self._on_value_changed()
def set_hash(self, mapping):
self.value_store.set_hash(self.oid(), mapping)
self._on_value_changed()
def set_hash_key(self, key, value):
self.value_store.set_hash_key(self.oid(), key, value)
self._on_value_changed()
def _on_value_changed(self):
# use self.object.touch() and not self.touch()
# because object.touch() may have been overridden to implement
# touch propagation.
self.object.touch()
def touch(self):
self.root().object_touched(self.object)
def pformat(self, indent=0):
from .values import Value, Ref
if isinstance(self.object, Ref):
target = self.object.get()
if target is not None:
infos = "%s<%s>(%s)" % (
self.object.__class__.__name__,
target.__class__.__name__,
target._mng.oid()
)
else:
infos = "%s<%s>" % (
self.object.__class__.__name__, target.__class__.__name__)
elif isinstance(self.object, Value):
infos = "%s(%s)" % (
self.object.__class__.__name__, repr(self.object.get())[:50])
else:
infos = self.object.__class__.__name__
head = "%s%s = %s" % (
indent * 0 * ' ',
self.oid(),
infos,
)
i = indent + 1
body = '\n'.join([
o._mng.pformat(i) for n, o in sorted(self.children.items())
])
return head + (body and '\n' + body or '')
[docs]class _Relation(object):
'''
This is the base class of all relations.
You must use one of the subclasses.
Relations are descriptors managing the instantiation and the access
to object related to the relation owner.
You can configure the relation behavior using the :any:`ui()` and :any:`editor()`
methods.
'''
_RELATION_TYPE_NAME = None # see cls.relation_type_name()
MAX_INDEX = 100 # maximum automatic relation index.
_next_relation_index = 0
_GROUP_CONTEXT = []
@classmethod
def _generate_relation_index(cls):
# tricky stuff to order relations in declaration order [part 1]
# /!\ be sure to use _Relation class (not cls)
_Relation._next_relation_index += 1
return cls._next_relation_index
@classmethod
def reset_relation_indexes(cls):
# /!\ be sure to use _Relation class (not cls)
_Relation._next_relation_index = 0
@classmethod
def relation_type_name(cls):
'''
Used by clients to classify relations.
Returns cls._RELATION_TYPE_NAME. Subclass can set this
class attribute to alter the string returned here.
'''
return cls._RELATION_TYPE_NAME
@classmethod
def get_default_group(cls):
if not cls._GROUP_CONTEXT:
return None
return '.'.join(cls._GROUP_CONTEXT)
def __init__(self, related_type):
super(_Relation, self).__init__()
self.index = self.__class__._generate_relation_index()
self.group = None
self.name = None
self.related_type = related_type
self.used = False
self._ui = {
# missing key means use default: 'editor_type': None,
'icon': None,
'editor_type': None,
'editable': False,
'label': None,
'group': self.__class__.get_default_group(),
'hidden': False,
'tooltip': None,
}
[docs] def ui(
self,
icon=None,
editor=None,
editable=None,
label=None,
group=None,
hidden=None,
tooltip=None,
expanded=None,
expandable=None,
**editor_options
):
'''
Configure the relation GUI.
This method returns self so that you can chain
it in assigment:
::
meh = MyRelation('example').ui(label='Amazing')
'''
if icon is not None:
self._ui['icon'] = icon
if editor is not None:
self._ui['editor_type'] = editor
if editable is not None:
self._ui['editable'] = editable
if label is not None:
self._ui['label'] = label
if group is not None:
self._ui['group'] = group
if hidden is not None:
self._ui['hidden'] = hidden and True or False
if tooltip is not None:
self._ui['tooltip'] = tooltip
if expanded is not None:
self._ui['expanded'] = bool(expanded)
if expandable is not None:
self._ui['expandable'] = bool(expandable)
if editor_options:
self._ui.update(editor_options)
return self
def is_hidden(self, of=None):
ui = self.get_ui(of)
return ui.get('hidden', False)
def get_ui(self, of=None):
'''
:param of: the parent of the related object. If not None, the related
Object._fill_ui(ui) is used instead of the default value set on the
relation.
:return: dict with keys icon, editor_type, editable, label, hidden, tooltip
'''
ui = self._ui.copy()
if not ui.get('icon') and self.related_type is not None:
ui['icon'] = self.related_type.ICON
if of is not None:
related = self.get_related(of)
related._fill_ui(ui)
return ui
def set_related_type(self, related_type):
if self.used:
raise RuntimeError(
'Too late to change the related type: '
'some related object have already been created.'
)
self.related_type = related_type
def _create_object(self, parent):
raise NotImplementedError()
def get_related(self, of):
try:
return of._mng.get_existing_child(self.name)
except MissingChildError:
pass # let's create it
related = self._create_object(of)
self.used = True
return related
def __get__(self, o, t=None):
if o is None:
return self
return self.get_related(o)
class _RelationGroupContext:
def __init__(self, name):
self.name = name
def __enter__(self):
_Relation._GROUP_CONTEXT.append(self.name)
return self
def __exit__(self, type, value, tb):
_Relation._GROUP_CONTEXT.pop()
def group(name):
return _RelationGroupContext(name)
class ObjectType(type):
def __new__(cls, class_name, bases, class_dict):
# tricky stuff to order relations in declaration order [part 2]
_Relation.reset_relation_indexes()
default_relation_group = class_dict.get('LABEL', class_name)
if bases != (object,): # if it's not our most base class
if 'oid' in class_dict:
raise ValueError(
'Cannot create ObjectType %r with %r named "oid" (reserved name)' % (
class_name, class_dict['oid']
)
)
if 'name' in class_dict:
raise ValueError('Cannot create ObjectType %r with %r named "name" (reserved name)' % (
class_name, class_dict['name']))
else:
default_relation_group = None
relations = []
min_relation_index = 0
for base in bases:
try:
base_relations = base._relations
except AttributeError:
pass
else:
relations.extend(base_relations)
relation_indexes = [
r.index for r in base_relations if r.index > 0 and r.index < _Relation.MAX_INDEX]
if relation_indexes:
min_relation_index = max(
min_relation_index, max(relation_indexes))
for n, o in class_dict.items():
if isinstance(o, _Relation):
o.name = n
o.index += min_relation_index
if o.group is None:
# set default if no group defined
o.group = default_relation_group
if o.index > _Relation.MAX_INDEX:
raise Exception('Relation index for %r of ObjectType %r exceeded the max of %s.' % (
n, class_name, _Relation.MAX_INDEX))
relations.append(o)
# remove overridden relations by using only the last ones with same name:
relations = dict([(r.name, r) for r in relations])
def relation_sort_key(r):
return r.index
relations = sorted(
six.viewvalues(relations), # py 2+3 version of relations.values()
key=relation_sort_key
)
class_dict['_ref_type'] = None
class_dict['_relations'] = relations
return super(ObjectType, cls).__new__(cls, class_name, bases, class_dict)
[docs]class Object(six.with_metaclass(ObjectType, object)):
@classmethod
def _create_value_store(cls, parent, name):
'''
Subclasses may override this class method to return
the value store they want to use.
Default is to return None, which will lead to using the parent's value_store
'''
return None
ICON = 'object'
@classmethod
def ref_type(cls):
if cls._ref_type is not None:
return cls._ref_type
from .values import Ref
class MyTypeRef(Ref):
SOURCE_TYPE = cls
MyTypeRef.__name__ = cls.__name__ + 'Ref'
cls._ref_type = MyTypeRef
return cls._ref_type
[docs] @classmethod
def get_source_display(cls, oid):
'''
Returns the text to display when showing a Ref pointing to the object of type cls with
the id oid.
This is used by Connection relations' GUI.
'''
return oid
def __new__(cls, parent, name, **kwargs):
if parent is None:
# we are creating a root, the name must be none and kwargs must have a value_store key:
if name != '':
raise TypeError(
'Cannot create root (object parent == None) with a name that is not empty string.')
if 'value_store' not in kwargs:
raise TypeError(
'Cannot create root (object parent == None) without "value_store" keyword argument.')
value_store = kwargs['value_store']
manager_type = _Manager # kwargs.get('manager_type', _Manager)
else:
# we are creating an Object, name must not be empty or None and kwargs must be empty:
if parent is None:
raise TypeError(
'Cannot create an object without parent (Use a Root() as parent if needed).')
if not isinstance(parent, Object):
raise TypeError(
'parent must be an Object, not a %r' % (type(parent),))
if not name:
raise TypeError('Cannot create an object without name.')
if kwargs:
raise TypeError(
'Cannot create object with more arguments than "parent" and "name".')
# name must also be a valid python attribute name:
if '.' in name:
raise TypeError(
'Invalid object name %r (it must be a valid attribute name).' % (name,))
try:
exec(name + '=None') in {}
except Exception:
raise TypeError(
'Invalid object name %r (it must be a valid attribute name).' % (name,))
# the manager type is the same as the parent's manager
manager_type = parent._mng.__class__
# if not overriden, the value store is the the parent's one
value_store = cls._create_value_store(parent, name)
if value_store is None:
value_store = parent._mng.value_store
instance = super(Object, cls).__new__(cls)
instance._mng = manager_type(instance, parent, name, value_store)
return instance
def __init__(self, parent, name):
super(Object, self).__init__()
def name(self):
return self._mng.name
def oid(self):
return self._mng.oid()
def root(self):
return self._mng.root()
[docs] def touch(self):
'''
Force notification that the object has changed.
'''
self._mng.touch()
[docs] def child_value_changed(self, child_value):
'''
Called when a watched child Value has changed.
See relations.Param.watched() and values.Value.watch()
'''
raise NotImplementedError(
'missing child_value_changed(child_value) in %s' % (self.oid()))
[docs] def compute_child_value(self, child_value):
'''
Called when a ComputeValue child need its value to be computed.
This happens when a computed value was touched before someone
asks for its value.
You must set the result of the computation to the child_value.
'''
raise NotImplementedError(
'missing compute_child_value(child_value) in %s (child name: %r)' % (self.oid(), child_value.name()))
def _fill_ui(self, ui):
'''
Override this to dynamically alter the ui option set on the
relation holding this Object:
icon, editor_type, editable, label, hidden, tooltip
Default is to keep all default value.
:param ui: the ui info set on the relation holding this Object.
:return: None
'''
pass
#---OLD #FIXME: replace this by a _summary child or a Summary relation...
def summary(self):
return None
def log(self):
logging.getLogger('kabaret.flow').info(self._mng.pformat())
[docs]class SessionObject(Object):
'''
The SessionObject overrides its default value store
with a MemoryValueStore() (all value die when the session ends)
As the default value store is inherited by parent Objects, the
whole branch under this one will also be "in memory only".
'''
@classmethod
def _create_value_store(cls, parent, name):
return MemoryValueStore()
class Root(Object):
def __new__(cls, value_store):
return super(Root, cls).__new__(cls, None, '', value_store=value_store)
def __init__(self, value_store):
super(Root, self).__init__(None, '')
self._object_touched_handler = []
def get_mapped(self, name):
'''
Implementing this is needed to have Ref's working with
source_oid specified as absolute.
'''
raise NotImplementedError()
def add_object_touched_handler(self, handler):
self._object_touched_handler.append(handler)
def remove_object_touched_handler(self, handler):
self._object_touched_handler.remove(handler)
def object_touched(self, object):
'''
Called when any object under this root was touched.
'''
for handler in self._object_touched_handler:
handler(object)
[docs]class ThumbnailInfo(Object):
'''
This object defines the interface needed to provide "object image view" in
the GUI.
You define an object's thumbnail by defining its get_thumbnail_object()
method and returning a related ThumbnailInfo instance:
class MyObject(Object):
_thumbnail = Child(MyThumbnailIn)
'''
[docs] def is_resource(self):
'''
Must return True if your thumbnail source is a resource file.
If this returns True, does methods need to be implemented:
get_default_heigth()
get_label()
get_resource()
'''
raise NotImplementedError()
[docs] def get_resource(self):
'''
If is_resource() returns True, this must return a 2d string:
resource_folder_name, resource_name
'''
raise NotImplementedError()
[docs] def is_image(self):
'''
Must return True if your thumbnail source is a single image.
If this returns True, does methods need to be implemented:
get_default_heigth()
get_label()
get_path()
'''
raise NotImplementedError()
[docs] def is_sequence(self):
'''
Must return True if your thumbnail source is a sequence of images.
If this returns True, does methods need to be implemented:
get_default_heigth()
get_label()
get_path()
get_first_and_last()
'''
raise NotImplementedError()
[docs] def get_label(self):
'''
Returns the label to display on the thumbnail.
A value of None will use the basename of the thumbnail source file.
'''
return None
[docs] def get_path(self):
'''
Returns the path of the thumbnail source.
If is_sequence() returns True, the path must contain a formater for the
frame number:
/path/to/my/images.%04d.jpg
Supported images types are roughtly the same as a standard webbrowser.
(I.e: no EXP support here.)
'''
raise NotImplementedError()
[docs] def get_default_height(self):
'''
Returns the default height of the thumbnail when first show in GUI.
'''
return 50
[docs] def get_first_last_fps(self):
'''
If is_sequence() returns True, this must return the index of the
first and the last frames, and the frame per second rate
'''
raise NotImplementedError()