#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
from collections import defaultdict, deque, namedtuple
import logging
import re
from traits.api import (
HasStrictTraits, Disallow, Property, Str, Enum, ReadOnly, Any,
)
from enaml.utils import make_dispatcher, id_generator
from .trait_types import EnamlEvent
logger = logging.getLogger(__name__)
#: The dispatch function for action dispatching.
dispatch_action = make_dispatcher('on_action_', logger)
#: A namedtuple which contains information about a child change event.
#: The `old` slot will be the ordered list of old children. The `new`
#: slot will be the ordered list of new children.
ChildrenEvent = namedtuple('ChildrenEvent', 'old new')
#: A namedtuple which contains information about a parent change event.
#: The `old` slot will be the old parent and the `new` slot will be
#: the new parent.
ParentEvent = namedtuple('ParentEvent', 'old new')
#: The identifier generator for object instances.
object_id_generator = id_generator('o_')
[docs]class ChildrenEventContext(object):
""" A context manager which will emit a child event on an Object.
This context manager will automatically emit the child event on an
Object when the context is exited. This context manager can also be
safetly nested; only the top-level context for a given object will
emit the child event, effectively collapsing all transient state.
"""
#: Class level storage for tracking nested context managers.
_counters = defaultdict(int)
[docs] def __init__(self, parent):
""" Initialize a ChildrenEventContext.
Parameters
----------
parent : Object or None
The Object on which to emit a child event on context exit.
To make it easier for reparenting operations, the parent
can be None.
"""
self._parent = parent
[docs] def __enter__(self):
""" Enter the children event context.
This method will snap the current child state of the parent and
use it to diff the state on context exit.
"""
parent = self._parent
counters = self._counters
count = counters[parent]
counters[parent] = count + 1
if count == 0 and parent is not None:
self._old = parent._children
[docs] def __exit__(self, exc_type, exc_value, traceback):
""" Exit the children event context.
If this context manager is the top-level manager for the parent
object *and* no exception occured in the context, then a child
event will be emitted on the parent. Any exception raised during
the context is propagated.
"""
parent = self._parent
counters = self._counters
counters[parent] -= 1
if counters[parent] == 0:
del counters[parent]
if exc_type is None and parent is not None:
old = self._old
new = parent._children
if old != new:
evt = ChildrenEvent(old, new)
parent.children_event(evt)
[docs]class Object(HasStrictTraits):
""" The most base class of the Enaml object hierarchy.
An Enaml Object provides supports parent-children relationships and
provides facilities for initializing, navigating, searching, and
destroying the tree. It also contains methods for sending messages
between objects when the object is part of a session.
"""
#: An optional name to give to this object to assist in finding it
#: in the tree (see . the 'find' method. Note that there is no
#: guarantee of uniqueness for an object `name`. It is left to the
#: developer to choose an appropriate name.
name = Str
#: A read-only property which returns the object's parent. This
#: will be an instance Object or None if there is no parent. A
#: strong reference is kept to the parent object.
parent = Property(fget=lambda self: self._parent)
#: A read-only property which returns the objects children. This
#: will be an iterable of Object instances. A strong reference is
#: kept to all child objects.
children = Property(fget=lambda self: self._children)
#: An event fired when an the oject has been initialized. It is
#: emitted once during an object's lifetime, when the object is
#: initialized by a Session.
initialized = EnamlEvent
#: An event fired when an object has been activated. It is emitted
#: once during an object's lifetime, when the object is activated
#: by a Session.
activated = EnamlEvent
#: An event fired when an object is being destroyed. This event
#: is fired once during the object lifetime, just before the
#: object is removed from the tree structure.
destroyed = EnamlEvent
#: A read-only property which returns the object's session. This
#: will be an instance of Session or None if there is no session.
#: A strong reference is kept to the session object. This value
#: should not be manipulated by user code.
session = Property(fget=lambda self: self._session)
#: A read-only value which returns the object's identifier. This
#: will be computed the first time it is requested. The default
#: value is guaranteed to be unique for the current process. The
#: initial value may be supplied by user code if more control is
#: required, with proper care that the value is a unique string.
object_id = ReadOnly
def _object_id_default(self):
return object_id_generator.next()
#: The current state of the object in terms of its lifetime within
#: a session. This value should not be manipulated by user code.
state = Enum(
'inactive', 'initializing', 'initialized', 'activating', 'active',
'destroying', 'destroyed',
)
#: A read-only property which is True if the object is inactive.
is_inactive = Property(fget=lambda self: self.state == 'inactive')
#: A read-only property which is True if the object is initializing.
is_initializing = Property(fget=lambda self: self.state == 'initializing')
#: A read-only property which is True if the object is initialized.
is_initialized = Property(fget=lambda self: self.state == 'initialized')
#: A read-only property which is True if the object is activating.
is_activating = Property(fget=lambda self: self.state == 'activating')
#: A read-only property which is True if the object is active.
is_active = Property(fget=lambda self: self.state == 'active')
#: A read-only property which is True if the object is destroying.
is_destroying = Property(fget=lambda self: self.state == 'destroying')
#: A read-only property which is True if the object is destroyed.
is_destroyed = Property(fget=lambda self: self.state == 'destroyed')
#: Private storage traits. These should *never* be manipulated by
#: user code. For performance reasons, these are not type-checked.
_parent = Any # Object or None
_children = Any # tuple of Object
_session = Any # Session or None
[docs] def __init__(self, parent=None, **kwargs):
""" Initialize an Object.
Parameters
----------
parent : Object or None, optional
The Object instance which is the parent of this object, or
None if the object has no parent. Defaults to None.
**kwargs
Additional keyword arguments to apply to the object after
the parent has been set.
"""
super(Object, self).__init__()
self._parent = None
self._children = ()
if parent is not None:
self.set_parent(parent)
if kwargs:
# `trait_set` is slow, don't use it here.
for key, value in kwargs.iteritems():
setattr(self, key, value)
#--------------------------------------------------------------------------
# Lifetime API
#--------------------------------------------------------------------------
[docs] def initialize(self):
""" Called by a Session to initialize the object tree.
This method is called by a Session object to allow the object
tree to perform initialization before the object is activated
for messaging.
"""
self.state = 'initializing'
self.pre_initialize()
for child in self.children:
child.initialize()
self.state = 'initialized'
self.post_initialize()
[docs] def pre_initialize(self):
""" Called during the initialization pass before any children
are initialized.
The object `state` during this call will be 'initializing'.
"""
pass
[docs] def post_initialize(self):
""" Called during the initialization pass after all children
have been initialized.
The object `state` during this call will be 'initialized'. The
default implementation of this method emits the `initialized`
event.
"""
self.initialized()
[docs] def activate(self, session):
""" Called by a Session to activate the object tree.
This method is called by a Session object to activate the object
tree for messaging.
Parameters
----------
session : Session
The session to use for messaging with this object tree.
"""
self.state = 'activating'
self.pre_activate(session)
self._session = session
session.register(self)
for child in self._children:
child.activate(session)
self.state = 'active'
self.post_activate(session)
[docs] def pre_activate(self, session):
""" Called during the activation pass before any children are
activated.
The object `state` during this call will be 'activating'.
Parameters
----------
session : Session
The session to use for messaging with this object tree.
"""
pass
[docs] def post_activate(self, session):
""" Called during the activation pass after all children are
activated.
The object `state` during this call will be 'active'. The
default implementation emits the `activated` event.
Parameters
----------
session : Session
The session to use for messaging with this object tree.
"""
self.activated()
[docs] def destroy(self):
""" Destroy this object and all of its children recursively.
This will emit the `destroyed` event before any change to the
object tree is made. After this returns, the object should be
considered invalid and should no longer be used.
"""
# Only send the destroy message if the object's parent is not
# being destroyed. This reduces the number of messages since
# the automatic destruction of children is assumed.
parent = self._parent
if parent is None or not parent.is_destroying:
self.send_action('destroy', {})
self.state = 'destroying'
self.pre_destroy()
if self._children:
for child in self._children:
child.destroy()
self._children = ()
if parent is not None:
if parent.is_destroying:
self._parent = None
else:
self.set_parent(None)
session = self._session
if session is not None:
session.unregister(self)
self.state = 'destroyed'
self.post_destroy()
[docs] def pre_destroy(self):
""" Called during the destruction pass before any children are
destroyed.
The object `state` during this call will be 'destroying'. The
default implementation emits the `destroyed` event.
"""
self.destroyed()
[docs] def post_destroy(self):
""" Called during the destruction pass after all children are
destroyed.
The object `state` during this call will be 'destroyed'. This
allows subclasses to perform cleanup once the object has been
fully removed from the hierarchy.
"""
pass
#--------------------------------------------------------------------------
# Parenting API
#--------------------------------------------------------------------------
[docs] def set_parent(self, parent):
""" Set the parent for this object.
If the parent is not None, the child will be appended to the end
of the parent's children. If the parent is already the parent of
this object, then this method is a no-op. If this object already
has a parent, then it will be properly reparented.
Parameters
----------
parent : Object or None
The Object instance to use for the parent, or None if this
object should be unparented.
Notes
-----
It is the responsibility of the caller to intialize and activate
the object if it is reparented dynamically at runtime and should
be involved with a session.
"""
old_parent = self._parent
if parent is old_parent:
return
if parent is self:
raise ValueError('cannot use `self` as Object parent')
if parent is not None and not isinstance(parent, Object):
raise TypeError('parent must be an Object or None')
self._parent = parent
self.parent_event(ParentEvent(old_parent, parent))
if old_parent is not None:
old_kids = old_parent._children
idx = old_kids.index(self)
with ChildrenEventContext(old_parent):
old_parent._children = old_kids[:idx] + old_kids[idx + 1:]
if parent is not None:
with ChildrenEventContext(parent):
parent._children += (self,)
[docs] def insert_children(self, before, insert):
""" Insert children into this object at the given location.
The children will be automatically parented and inserted into
the object's children. If any children are already children of
this object, then they will be moved appropriately.
Parameters
----------
before : Object or None
A child object to use as the marker for inserting the new
children. The new children will be inserted directly before
this marker. If the Object is None or not a child, then the
new children will be added to the end of the children.
insert : iterable
An iterable of Object children to insert into this object.
Notes
-----
It is the responsibility of the caller to intialize and activate
the object if it is reparented dynamically at runtime and should
be involved with a session.
"""
insert_tup = tuple(insert)
insert_set = set(insert_tup)
if self in insert_set:
raise ValueError('cannot use `self` as Object child')
if len(insert_tup) != len(insert_set):
raise ValueError('cannot insert duplicate children')
if not all(isinstance(child, Object) for child in insert_tup):
raise TypeError('children must be an Object instances')
new = []
added = False
for child in self._children:
if child in insert_set:
continue
if child is before:
new.extend(insert_tup)
added = True
new.append(child)
if not added:
new.extend(insert_tup)
for child in insert_tup:
old_parent = child._parent
if old_parent is not self:
child._parent = self
child.parent_event(ParentEvent(old_parent, self))
if old_parent is not None:
old_kids = old_parent._children
idx = old_kids.index(child)
old_kids = old_kids[:idx] + old_kids[idx + 1:]
with ChildrenEventContext(old_parent):
old_parent._children = old_kids
with ChildrenEventContext(self):
self._children = tuple(new)
[docs] def parent_event(self, event):
""" Handle a `ParentEvent` posted to this object.
This event handler is called when the parent on the object has
changed, but before the children of the new parent have been
updated. Sublasses may reimplement this method as required. The
default implementation emits the trait change notification, so
subclasses which rely on that notification must be sure to call
super(...) when reimplmenting this method.
Parameters
----------
event : ParentEvent
The event for the parent change of this object.
"""
self.trait_property_changed('parent', event.old, event.new)
[docs] def children_event(self, event):
""" Handle a `ChildrenEvent` posted to this object.
This event handler is called by a `ChildrenEventContext` when
the last nested context is exited. The default implementation
emits the trait change notification, so subclasses which rely
on that notification must be sure to call super(...) when
reimplmenting this method.
Parameters
----------
event : ChildrenEvent
The event for the children change of this object.
"""
self.trait_property_changed('children', event.old, event.new)
#--------------------------------------------------------------------------
# Messaging API
#--------------------------------------------------------------------------
[docs] def send_action(self, action, content):
""" Send an action to the client of this object.
The action will only be sent if the current state of the object
is `active`. Subclasses may reimplement this method if more
control is needed.
Parameters
----------
action : str
The name of the action which the client should perform.
content : dict
The content data for the action.
"""
if self.is_active:
self._session.send(self.object_id, action, content)
[docs] def receive_action(self, action, content):
""" Receive an action from the client of this object.
The default implementation will dynamically dispatch the action
to specially named handlers if the current state of the object
is 'active'. Subclasses may reimplement this method if more
control is needed.
Parameters
----------
action : str
The name of the action to perform.
content : dict
The content data for the action.
"""
if self.is_active:
dispatch_action(self, action, content)
#--------------------------------------------------------------------------
# Object Tree API
#--------------------------------------------------------------------------
[docs] def traverse(self, depth_first=False):
""" Yield all of the objects in the tree, from this object down.
Parameters
----------
depth_first : bool, optional
If True, yield the nodes in depth first order. If False,
yield the nodes in breadth first order. Defaults to False.
"""
if depth_first:
stack = [self]
stack_pop = stack.pop
stack_extend = stack.extend
else:
stack = deque([self])
stack_pop = stack.popleft
stack_extend = stack.extend
while stack:
obj = stack_pop()
yield obj
stack_extend(obj._children)
[docs] def traverse_ancestors(self, root=None):
""" Yield all of the objects in the tree, from this object up.
Parameters
----------
root : Object, optional
The object at which to stop traversal. Defaults to None.
"""
parent = self._parent
while parent is not root and parent is not None:
yield parent
parent = parent._parent
[docs] def find(self, name, regex=False):
""" Find the first object in the subtree with the given name.
This method will traverse the tree of objects, breadth first,
from this object downward, looking for an object with the given
name. The first object with the given name is returned, or None
if no object is found with the given name.
Parameters
----------
name : string
The name of the object for which to search.
regex : bool, optional
Whether the given name is a regex string which should be
matched against the names of children instead of tested
for equality. Defaults to False.
Returns
-------
result : Object or None
The first object found with the given name, or None if no
object is found with the given name.
"""
if regex:
rgx = re.compile(name)
match = lambda n: bool(rgx.match(n))
else:
match = lambda n: n == name
for obj in self.traverse():
if match(obj.name):
return obj
[docs] def find_all(self, name, regex=False):
""" Find all objects in the subtree with the given name.
This method will traverse the tree of objects, breadth first,
from this object downward, looking for a objects with the given
name. All of the objects with the given name are returned as a
list.
Parameters
----------
name : string
The name of the objects for which to search.
regex : bool, optional
Whether the given name is a regex string which should be
matched against the names of objects instead of testing
for equality. Defaults to False.
Returns
-------
result : list of Object
The list of objects found with the given name, or an empty
list if no objects are found with the given name.
"""
if regex:
rgx = re.compile(name)
match = lambda n: bool(rgx.match(n))
else:
match = lambda n: n == name
res = []
push = res.append
for obj in self.traverse():
if match(obj.name):
push(obj)
return res
#--------------------------------------------------------------------------
# HasTraits Fixes
#--------------------------------------------------------------------------
#: The HasTraits class defines a class attribute 'set' which is a
#: deprecated alias for the 'trait_set' method. The problem is that
#: having that as an attribute interferes with the ability of Enaml
#: expressions to resolve the builtin 'set', since dynamic scoping
#: takes precedence over builtins. This resets those ill-effects.
set = Disallow
[docs] def add_notifier(self, name, notifier):
""" Add a notifier to a trait on the object.
This is different from `on_trait_change` in that it allows the
developer to provide the notifier object directly. This allows
the possibility of more efficient notifier patterns.
"""
self._trait(name, 2)._notifiers(1).append(notifier)