# Path: src/circular/template/observer.py
# def observe(obj, observer=None, ignore_errors=False):
#     """
#         Returns an observer object monitoring changes to ``obj``.
# 
#         One can specify any instance of EventMixin as the ``observer``
#         parameter. In this case any change events will be emitted
#         (forwarded) by the provided observer.
# 
#         If ``ignore_errors`` is False, the method may throw, e.g. if
#         the user tries to observe a built in type. Otherwise it will
#         just return None in case of error.
# 
#         WARNING: Only instances of user-defined classes can be
#         observed. If you need to observe a ``list`` or a ``dict``
#         wrap them in a ``ListProxy`` or ``DictProxy``. Or, alternatively,
#         consider using the Context class which automatically and
#         recursively wraps any ``list`` and ``Dict`` variables which
#         are assigned to it.
#     """
#     if not hasattr(obj, '_obs____'):
#         if type(obj) in [str, int, dict, list, tuple, set, type(None), bool]:
#             if not ignore_errors:
#                 raise Exception("Cannot observe primitive types:")
#             else:
#                 return None
#         try:
#             if hasattr(obj, '__setitem__'):
#                 extend_instance(obj, ArrayMixin)
#             else:
#                 extend_instance(obj, ObjMixin)
#             obj._create_observer()
#         except Exception as exc:
#             # pylint: disable-broad-except; not sure what possible exceptions can happen here, must catch them though
#             if not ignore_errors:
#                 raise Exception("Cannot observe (is obj a primitive type?):"+str(exc))
#             else:
#                 return None
#     if observer is not None:
#         obj._obs____.bind('change', observer, 'change')
#     return obj._obs____


from circular.utils.events import EventMixin
from .observer import observe
                ret += ':' + repr(step)
            return ret
        else:
            return repr(start)


class AttrAccessNode(ExpNode):
    """ Node representing attribute access, e.g. obj.prop """

    def __init__(self, obj, attribute):
        super().__init__()
        self._obj = obj
        self._attr = attribute
        self._observer = None
        self._obj.bind('change', self._change_handler)

    def clone(self):
        return AttrAccessNode(self._obj.clone(), self._attr.clone())

    def eval(self, force_cache_refresh=False):
        """
           Note that this function expects the AST of the attr access to
           be rooted at the rightmost element of the attr access chain !!
        """
        if self._dirty or force_cache_refresh:
            self.defined = False
            if self._observer:
                self._observer.unbind()
            obj_val = self._obj.eval(force_cache_refresh=force_cache_refresh)
            self._cached_val = getattr(obj_val, self._attr.name())

# Path: src/circular/template/observer.py
# def observe(obj, observer=None, ignore_errors=False):
#     """
#         Returns an observer object monitoring changes to ``obj``.
# 
#         One can specify any instance of EventMixin as the ``observer``
#         parameter. In this case any change events will be emitted
#         (forwarded) by the provided observer.
# 
#         If ``ignore_errors`` is False, the method may throw, e.g. if
#         the user tries to observe a built in type. Otherwise it will
#         just return None in case of error.
# 
#         WARNING: Only instances of user-defined classes can be
#         observed. If you need to observe a ``list`` or a ``dict``
#         wrap them in a ``ListProxy`` or ``DictProxy``. Or, alternatively,
#         consider using the Context class which automatically and
#         recursively wraps any ``list`` and ``Dict`` variables which
#         are assigned to it.
#     """
#     if not hasattr(obj, '_obs____'):
#         if type(obj) in [str, int, dict, list, tuple, set, type(None), bool]:
#             if not ignore_errors:
#                 raise Exception("Cannot observe primitive types:")
#             else:
#                 return None
#         try:
#             if hasattr(obj, '__setitem__'):
#                 extend_instance(obj, ArrayMixin)
#             else:
#                 extend_instance(obj, ObjMixin)
#             obj._create_observer()
#         except Exception as exc:
#             # pylint: disable-broad-except; not sure what possible exceptions can happen here, must catch them though
#             if not ignore_errors:
#                 raise Exception("Cannot observe (is obj a primitive type?):"+str(exc))
#             else:
#                 return None
#     if observer is not None:
#         obj._obs____.bind('change', observer, 'change')
#     return obj._obs____


# Path: src/circular/template/expression.py
from circular.utils.events import EventMixin
from .observer import observe

                ret += ':' + repr(step)
            return ret
        else:
            return repr(start)


class AttrAccessNode(ExpNode):
    """ Node representing attribute access, e.g. obj.prop """

    def __init__(self, obj, attribute):
        super().__init__()
        self._obj = obj
        self._attr = attribute
        self._observer = None
        self._obj.bind('change', self._change_handler)

    def clone(self):
        return AttrAccessNode(self._obj.clone(), self._attr.clone())

    def eval(self, force_cache_refresh=False):
        """
           Note that this function expects the AST of the attr access to
           be rooted at the rightmost element of the attr access chain !!
        """
        if self._dirty or force_cache_refresh:
            self.defined = False
            if self._observer:
                self._observer.unbind()
            obj_val = self._obj.eval(force_cache_refresh=force_cache_refresh)
            self._cached_val = getattr(obj_val, self._attr.name())

            self._observer = observe(self._cached_val, self._change_attr_handler, ignore_errors=True)