Source code for qt_binder.binding

#------------------------------------------------------------------------------
#
#  Copyright (c) 2014-2015, Enthought, Inc.
#  All rights reserved.
#
#  This software is provided without warranty under the terms of the BSD
#  license included in LICENSE.txt and may be redistributed only
#  under the conditions described in the aforementioned license.  The license
#  is also available online at http://www.enthought.com/licenses/BSD.txt
#
#  Thanks for using Enthought open source!
#
#------------------------------------------------------------------------------

from abc import ABCMeta, abstractmethod
from collections import deque
import parser
import re
import symbol
import token

import six

from traits.trait_base import xgetattr, xsetattr

from .binder import Binder


class AnyString(object):
    """ Compare equal to any string type.
    """
    def __eq__(self, other):
        return isinstance(other, six.string_types)

    def __ne__(self, other):
        return not (self == other)


def yield_subtrees(expr):
    """ Preorder traversal of all subtrees in an expression.
    """
    root = parser.expr(expr).totuple()
    stack = deque([root])
    while stack:
        node = stack.popleft()
        children = [child for child in node[1:] if isinstance(child, tuple)]
        stack.extend(children)
        yield node


def find_ext_attrs(expr):
    """ Find all dotted references in the expression.
    """
    ext_attrs = []
    for subtree in yield_subtrees(expr):
        if subtree[1] == (symbol.atom, (token.NAME, AnyString())):
            parts = [subtree[1][1][1]]
            for trailer in subtree[2:]:
                if trailer == (symbol.trailer,
                               (token.DOT, '.'),
                               (token.NAME, AnyString())):
                    parts.append(trailer[2][1])
                else:
                    break
            if len(parts) > 1:
                ext_attrs.append('.'.join(parts))
    return ext_attrs


class _TraitModified(object):
    """ Expose a well-formed trait change handler function with extra data.

    This is a workaround to avoid some problems with defining change handler
    functions inside of other functions. The ``handler()`` method is a proper
    trait change handler.
    """
    def __init__(self, obj, xattr):
        self.obj = obj
        self.xattr = xattr
        self._in_handler = False

    def handler(self, new):
        if not self._in_handler:
            self._in_handler = True
            try:
                xsetattr(self.obj, self.xattr, new)
            finally:
                self._in_handler = False


class _EvaluateExpression(_TraitModified):

    def __init__(self, obj, xattr, context, expression):
        super(_EvaluateExpression, self).__init__(obj, xattr)
        self.context = context
        self.expression = expression

    def handler(self):
        if not self._in_handler:
            self._in_handler = True
            try:
                value = eval(self.expression, self.context)
                xsetattr(self.obj, self.xattr, value)
            finally:
                self._in_handler = False


[docs]class Binding(object): """ Interface for a single binding pair. """ __metaclass__ = ABCMeta _op_regex = re.compile(r'\s*(=|>>|<<|:=)\s*') def __init__(self, left, right): self.left = left self.right = right def __repr__(self): return '{0.__name__}({1.left!r}, {1.right!r})'.format(type(self), self) def __eq__(self, other): if type(other) is not type(self): return False return (self.left, self.right) == (other.left, other.right) def __ne__(self, other): return not (self == other) def __hash__(self): return hash((type(self), self.left, self.right)) @classmethod
[docs] def parse(cls, obj): """ Parse a binding expression into the right :class:`~.Binding` subclass. """ if isinstance(obj, six.string_types): left, op, right = cls._op_regex.split(obj, 1) return { '=': SetOnceTo, ':=': SyncedWith, '>>': PushedTo, '<<': PulledFrom, }[op](left, right) elif isinstance(obj, Binding): return obj else: raise TypeError("Expected Binding, or binding string; " "got {0!r}".format(obj))
@abstractmethod
[docs] def bind(self, binder, context): """ Perform the binding and store the information needed to undo it. """ raise NotImplementedError()
@abstractmethod
[docs] def unbind(self): """ Undo the binding. """ raise NotImplementedError()
def _normalize_binder_trait(self, binder, binder_trait, context): """ Normalize the Binder and binder trait. """ if '.' in binder_trait: head, tail = binder_trait.split('.', 1) if isinstance(context.get(head, None), Binder): binder = context[head] binder_trait = tail return binder, binder_trait
[docs]class Factory(Binding): """ Call the factory to initialize a value. The right item of the pair is a callable that will be called once on initialization to provide a value for the destination trait. """ def bind(self, binder, context): the_binder, binder_trait = self._normalize_binder_trait( binder, self.left, context) value = self.right() xsetattr(the_binder, binder_trait, value) def unbind(self): pass
[docs]class SetOnceTo(Binding): """ Evaluate values once. The right item of the pair is a string that will be evaluated in the Traits UI context once on initialization. Mnemonic: ``binder_trait is set once to expression`` """ def bind(self, binder, context): expression = self.right the_binder, binder_trait = self._normalize_binder_trait( binder, self.left, context) value = eval(expression, context) xsetattr(the_binder, binder_trait, value) def unbind(self): pass def __str__(self): return '{0.left} = {0.right}'.format(self)
[docs]class PulledFrom(Binding): """ Listen to traits in the context. The right item of each pair is a string representing the extended trait to listen to. The first part of this string should be a key into the Traits UI context; e.g. to listen to the ``foo`` trait on the model object, use ``'object.foo'``. When the ``foo`` trait on the model object fires a trait change notification, the ``Binder`` trait will be assigned. The reverse is not true: see :class:`~.PushedTo` and :class:`~.SyncedWith` for that functionality. Mnemonic: ``binder_trait is pulled from context_trait`` """ # FIXME: Allow users to explicitly specify a `depends_on` list. This would # let users avoid problems with too many dots. def bind(self, binder, context): the_binder, binder_trait = self._normalize_binder_trait( binder, self.left, context) rhs = self.right.strip() ext_traits = find_ext_attrs(rhs) if ext_traits == [rhs]: # Simple case of one attribute. context_name, xattr = rhs.split('.', 1) context_obj = context[context_name] handler = _TraitModified(the_binder, binder_trait).handler # FIXME: Only check as far down as are HasTraits objects available. # We would like to be able to include references to methods on # attributes of HasTraits classes. # Unfortunately, a valid use case is where a leading object in # a true trait chain is None. context_obj.on_trait_change(handler, xattr) self.pull_handler_data = [(context_obj, handler, xattr)] # FIXME: do a better check for an event trait try: xsetattr(the_binder, binder_trait, xgetattr(context_obj, xattr)) except AttributeError as e: if 'event' not in str(e): raise elif ext_traits == []: msg = "No traits found in expression: {0!r}".format(rhs) raise ValueError(msg) else: # Expression. self.pull_handler_data = [] handler = _EvaluateExpression(the_binder, binder_trait, context, rhs).handler for ext_trait in ext_traits: context_name, xattr = ext_trait.split('.', 1) if context_name not in context: # Assume it's a builtin. continue context_obj = context[context_name] context_obj.on_trait_change(handler, xattr) self.pull_handler_data.append((context_obj, handler, xattr)) # Call the handler once to evaluate and set the value initially. handler() def unbind(self): for context_obj, handler, xattr in self.pull_handler_data: context_obj.on_trait_change(handler, xattr, remove=True) def __str__(self): return '{0.left} << {0.right}'.format(self)
[docs]class PushedTo(Binding): """ Send trait updates from the ``Binder`` to the model. The right item of each pair is a string representing the extended trait to assign the value to. The first part of this string should be a key into the Traits UI context; e.g. to send to the ``foo`` trait on the model object, use ``'object.foo'``. When a change notification for ``binder_trait`` is fired, ``object.foo`` will be assigned the sent object. The reverse is not true: see :class:`~.PulledFrom` and :class:`~.SyncedWith` for that functionality. Mnemonic: ``binder_trait is sent to context_trait`` """ def bind(self, binder, context): ext_trait = self.right the_binder, binder_trait = self._normalize_binder_trait( binder, self.left, context) context_name, xattr = ext_trait.split('.', 1) context_obj = context[context_name] handler = _TraitModified(context_obj, xattr).handler the_binder.on_trait_change(handler, binder_trait) self.pushed_handler_data = (the_binder, handler, binder_trait) def unbind(self): the_binder, handler, binder_trait = self.pushed_handler_data the_binder.on_trait_change(handler, binder_trait, remove=True) def __str__(self): return '{0.left} >> {0.right}'.format(self)
[docs]class SyncedWith(PulledFrom, PushedTo): """ Bidirectionally synchronize a ``Binder`` trait and a model trait. The right item of each pair is a string representing the extended trait to synchronize the binder trait with. The first part of this string should be a key into the Traits UI context; e.g. to synchronize with the ``foo`` trait on the model object, use ``'object.foo'``. When a change notification for either trait is sent, the value will be assigned to the other. See :class:`~.PulledFrom` and :class:`~.PushedTo` for unidirectional synchronization. Mnemonic: ``binder_trait is synced with context_trait`` """ def bind(self, binder, context): PulledFrom.bind(self, binder, context) PushedTo.bind(self, binder, context) def unbind(self): PushedTo.unbind(self) PulledFrom.unbind(self) def __str__(self): return '{0.left} := {0.right}'.format(self)