# (C) Copyright 2005-2022 Enthought, Inc., Austin, TX
# 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!
""" Classes that implement and support the Traits change notification mechanism
"""
import contextlib
import logging
import threading
from threading import local as thread_local
from threading import Thread
import traceback
from types import MethodType
import weakref
import sys
from .constants import ComparisonMode, TraitKind
from .trait_base import Uninitialized
from .trait_errors import TraitNotificationError
# Global Data
# The thread ID for the user interface thread
ui_thread = -1
# The handler for notifications that must be run on the UI thread
ui_handler = None
[docs]def set_ui_handler(handler):
""" Sets up the user interface thread handler.
"""
global ui_handler, ui_thread
ui_handler = handler
ui_thread = threading.current_thread().ident
def ui_dispatch(handler, *args, **kw):
if threading.current_thread().ident == ui_thread:
handler(*args, **kw)
else:
ui_handler(handler, *args, **kw)
[docs]class NotificationExceptionHandlerState(object):
def __init__(self, handler, reraise_exceptions, locked):
self.handler = handler
self.reraise_exceptions = reraise_exceptions
self.locked = locked
[docs]class NotificationExceptionHandler(object):
def __init__(self):
self.traits_logger = None
self.main_thread = None
self.thread_local = thread_local()
# -- Private Methods ------------------------------------------------------
def _push_handler(
self, handler=None, reraise_exceptions=False, main=False, locked=False
):
""" Pushes a new traits notification exception handler onto the stack,
making it the new exception handler. Returns a
NotificationExceptionHandlerState object describing the previous
exception handler.
Parameters
----------
handler : handler
The new exception handler, which should be a callable or
None. If None (the default), then the default traits
notification exception handler is used. If *handler* is not
None, then it must be a callable which can accept four
arguments: object, trait_name, old_value, new_value.
reraise_exceptions : bool
Indicates whether exceptions should be reraised after the
exception handler has executed. If True, exceptions will be
re-raised after the specified handler has been executed.
The default value is False.
main : bool
Indicates whether the caller represents the main application
thread. If True, then the caller's exception handler is
made the default handler for any other threads that are
created. Note that a thread can explicitly set its own
exception handler if desired. The *main* flag is provided to
make it easier to set a global application policy without
having to explicitly set it for each thread. The default
value is False.
locked : bool
Indicates whether further changes to the Traits notification
exception handler state should be allowed. If True, then
any subsequent calls to _push_handler() or _pop_handler() for
that thread will raise a TraitNotificationError. The default
value is False.
"""
handlers = self._get_handlers()
self._check_lock(handlers)
if handler is None:
handler = self._log_exception
handlers.append(
NotificationExceptionHandlerState(
handler, reraise_exceptions, locked
)
)
if main:
self.main_thread = handlers
return handlers[-2]
def _pop_handler(self):
""" Pops the traits notification exception handler stack, restoring
the exception handler in effect prior to the most recent
_push_handler() call. If the stack is empty or locked, a
TraitNotificationError exception is raised.
Note that each thread has its own independent stack. See the
description of the _push_handler() method for more information on
this.
"""
handlers = self._get_handlers()
self._check_lock(handlers)
if len(handlers) > 1:
handlers.pop()
else:
raise TraitNotificationError(
"Attempted to pop an empty traits notification exception "
"handler stack."
)
def _handle_exception(self, object, trait_name, old, new):
""" Handles a traits notification exception using the handler defined
by the topmost stack entry for the corresponding thread.
"""
excp_class, excp = sys.exc_info()[:2]
handler_info = self._get_handlers()[-1]
handler_info.handler(object, trait_name, old, new)
if handler_info.reraise_exceptions or isinstance(
excp, TraitNotificationError
):
raise excp
def _get_handlers(self):
""" Returns the handler stack associated with the currently executing
thread.
"""
thread_local = self.thread_local
if isinstance(thread_local, dict):
id = threading.current_thread().ident
handlers = thread_local.get(id)
else:
handlers = getattr(thread_local, "handlers", None)
if handlers is None:
if self.main_thread is not None:
handler = self.main_thread[-1]
else:
handler = NotificationExceptionHandlerState(
self._log_exception, False, False
)
handlers = [handler]
if isinstance(thread_local, dict):
thread_local[id] = handlers
else:
thread_local.handlers = handlers
return handlers
def _check_lock(self, handlers):
""" Raises an exception if the specified handler stack is locked.
"""
if handlers[-1].locked:
raise TraitNotificationError(
"The traits notification exception handler is locked. "
"No changes are allowed."
)
def _log_exception(self, object, trait_name, old, new):
""" Logs any exceptions generated in a trait notification handler.
This method defines the default notification exception handling
behavior of traits. However, it can be completely overridden by pushing
a new handler using the '_push_handler' method.
"""
# When the stack depth is too great, the logger can't always log the
# message. Make sure that it goes to the console at a minimum:
excp_class, excp = sys.exc_info()[:2]
if (
(excp_class is RuntimeError)
and (len(excp.args) > 0)
and (excp.args[0] == "maximum recursion depth exceeded")
):
sys.__stderr__.write(
"Exception occurred in traits notification "
"handler for object: %s, trait: %s, old value: %s, "
"new value: %s.\n%s\n"
% (
object,
trait_name,
old,
new,
"".join(traceback.format_exception(*sys.exc_info())),
)
)
logger = self.traits_logger
if logger is None:
self.traits_logger = logger = logging.getLogger("traits")
try:
logger.exception(
"Exception occurred in traits notification handler for "
"object: %s, trait: %s, old value: %s, new value: %s"
% (object, trait_name, old, new)
)
except Exception:
# Ignore anything we can't log the above way:
pass
# Traits global notification exception handler
notification_exception_handler = NotificationExceptionHandler()
push_exception_handler = notification_exception_handler._push_handler
pop_exception_handler = notification_exception_handler._pop_handler
handle_exception = notification_exception_handler._handle_exception
# Traits global notification event tracer
_pre_change_event_tracer = None
_post_change_event_tracer = None
def set_change_event_tracers(pre_tracer=None, post_tracer=None):
""" Set the global trait change event tracers.
The global tracers are called whenever a trait change event is dispatched.
There are two tracers: `pre_tracer` is called before the notification is
sent; `post_tracer` is called after the notification is sent, even if the
notification failed with an exception (in which case the `post_tracer` is
called with a reference to the exception, then the exception is sent to
the `notification_exception_handler`).
The tracers should be a callable taking 5 arguments:
::
tracer(obj, trait_name, old, new, handler)
`obj` is the source object, on which trait `trait_name` was changed from
value `old` to value `new`. `handler` is the function or method that will
be notified of the change.
The post-notification tracer also has a keyword argument, `exception`,
that is `None` if no exception has been raised, and the a reference to the
raise exception otherwise.
::
post_tracer(obj, trait_name, old, new, handler, exception=None)
Note that for static trait change listeners, `handler` is not a method, but
rather the function before class creation, since this is the way Traits
works at the moment.
"""
global _pre_change_event_tracer
global _post_change_event_tracer
_pre_change_event_tracer = pre_tracer
_post_change_event_tracer = post_tracer
def get_change_event_tracers():
""" Get the currently active global trait change event tracers. """
return _pre_change_event_tracer, _post_change_event_tracer
def clear_change_event_tracers():
""" Clear the global trait change event tracer. """
global _pre_change_event_tracer
global _post_change_event_tracer
_pre_change_event_tracer = None
_post_change_event_tracer = None
@contextlib.contextmanager
def change_event_tracers(pre_tracer, post_tracer):
""" Context manager to temporarily change the global event tracers. """
old_pre_tracer, old_post_tracer = get_change_event_tracers()
set_change_event_tracers(pre_tracer, post_tracer)
try:
yield
finally:
set_change_event_tracers(old_pre_tracer, old_post_tracer)
class AbstractStaticChangeNotifyWrapper(object):
"""
Concrete implementation must define the 'argument_transforms' class
argument, a dictionary mapping the number of arguments in the event
handler to a function that takes the arguments (obj, trait_name, old, new)
and returns the arguments tuple for the actual handler.
"""
arguments_transforms = {}
def __init__(self, handler):
arg_count = handler.__code__.co_argcount
if arg_count > 4:
raise TraitNotificationError(
(
"Invalid number of arguments for the static anytrait "
"change notification handler: %s. A maximum of 4 "
"arguments is allowed, but %s were specified."
)
% (handler.__name__, arg_count)
)
self.argument_transform = self.argument_transforms[arg_count]
self.handler = handler
def __call__(self, object, trait_name, old, new):
""" Dispatch to the appropriate handler method. """
if _change_accepted(object, trait_name, old, new):
# Extract the arguments needed from the handler.
args = self.argument_transform(object, trait_name, old, new)
# Send a description of the change event to the event tracer.
if _pre_change_event_tracer is not None:
_pre_change_event_tracer(
object, trait_name, old, new, self.handler
)
try:
# Call the handler.
self.handler(*args)
except Exception as e:
if _post_change_event_tracer is not None:
_post_change_event_tracer(
object, trait_name, old, new, self.handler, exception=e
)
handle_exception(object, trait_name, old, new)
else:
if _post_change_event_tracer is not None:
_post_change_event_tracer(
object,
trait_name,
old,
new,
self.handler,
exception=None,
)
def equals(self, handler):
return False
[docs]class StaticAnytraitChangeNotifyWrapper(AbstractStaticChangeNotifyWrapper):
# The wrapper is called with the full set of argument, and we need to
# create a tuple with the arguments that need to be sent to the event
# handler, depending on the number of those.
argument_transforms = {
0: lambda obj, name, old, new: (),
1: lambda obj, name, old, new: (obj,),
2: lambda obj, name, old, new: (obj, name),
3: lambda obj, name, old, new: (obj, name, new),
4: lambda obj, name, old, new: (obj, name, old, new),
}
[docs]class StaticTraitChangeNotifyWrapper(AbstractStaticChangeNotifyWrapper):
# The wrapper is called with the full set of argument, and we need to
# create a tuple with the arguments that need to be sent to the event
# handler, depending on the number of those.
argument_transforms = {
0: lambda obj, name, old, new: (),
1: lambda obj, name, old, new: (obj,),
2: lambda obj, name, old, new: (obj, new),
3: lambda obj, name, old, new: (obj, old, new),
4: lambda obj, name, old, new: (obj, name, old, new),
}
[docs]class TraitChangeNotifyWrapper(object):
""" Dynamic change notify wrapper.
This class is in charge to dispatch trait change events to dynamic
listener, typically created using the `on_trait_change` method, or
the decorator with the same name.
"""
# The wrapper is called with the full set of argument, and we need to
# create a tuple with the arguments that need to be sent to the event
# handler, depending on the number of those.
argument_transforms = {
0: lambda obj, name, old, new: (),
1: lambda obj, name, old, new: (new,),
2: lambda obj, name, old, new: (name, new),
3: lambda obj, name, old, new: (obj, name, new),
4: lambda obj, name, old, new: (obj, name, old, new),
}
def __init__(self, handler, owner, target=None):
self.init(handler, owner, target)
def init(self, handler, owner, target=None):
# If target is not None and handler is a function then the handler
# will be removed when target is deleted.
if type(handler) is MethodType:
func = handler.__func__
object = handler.__self__
if object is not None:
self.object = weakref.ref(object, self.listener_deleted)
self.name = handler.__name__
self.owner = owner
arg_count = func.__code__.co_argcount - 1
if arg_count > 4:
raise TraitNotificationError(
(
"Invalid number of arguments for the dynamic "
"trait change notification handler: %s. A maximum "
"of 4 arguments is allowed, but %s were specified."
)
% (func.__name__, arg_count)
)
# We use the unbound method here to prevent cyclic garbage
# (issue #100).
self.notify_listener = type(self)._notify_method_listener
self.argument_transform = self.argument_transforms[arg_count]
return arg_count
elif target is not None:
# Set up so the handler will be removed when the target is deleted.
self.object = weakref.ref(target, self.listener_deleted)
self.owner = owner
arg_count = handler.__code__.co_argcount
if arg_count > 4:
raise TraitNotificationError(
(
"Invalid number of arguments for the dynamic trait change "
"notification handler: %s. A maximum of 4 arguments is "
"allowed, but %s were specified."
)
% (handler.__name__, arg_count)
)
self.name = None
self.handler = handler
# We use the unbound method here to prevent cyclic garbage
# (issue #100).
self.notify_listener = type(self)._notify_function_listener
self.argument_transform = self.argument_transforms[arg_count]
return arg_count
def __call__(self, object, trait_name, old, new):
""" Dispatch to the appropriate method.
We do explicit dispatch instead of assigning to the .__call__ instance
attribute to avoid reference cycles.
"""
# `notify_listener` is either the *unbound*
# `_notify_method_listener` or `_notify_function_listener` to
# prevent cyclic garbage (issue #100).
self.notify_listener(self, object, trait_name, old, new)
[docs] def dispatch(self, handler, *args):
""" Dispatch the event to the listener.
This method is normally the only one that needs to be overridden in
a subclass to implement the subclass's dispatch mechanism.
"""
handler(*args)
def equals(self, handler):
if handler is self:
return True
if (type(handler) is MethodType) and (handler.__self__ is not None):
return (handler.__name__ == self.name) and (
handler.__self__ is self.object()
)
return (self.name is None) and (handler == self.handler)
def listener_deleted(self, ref):
# In multithreaded situations, it's possible for this method to
# be called after, or concurrently with, the dispose method.
# Don't raise in that case.
try:
self.owner.remove(self)
except ValueError:
pass
self.object = self.owner = None
def dispose(self):
self.object = None
def _dispatch_change_event(self, object, trait_name, old, new, handler):
""" Prepare and dispatch a trait change event to a listener. """
# Extract the arguments needed from the handler.
args = self.argument_transform(object, trait_name, old, new)
# Send a description of the event to the change event tracer.
if _pre_change_event_tracer is not None:
_pre_change_event_tracer(object, trait_name, old, new, handler)
# Dispatch the event to the listener.
try:
self.dispatch(handler, *args)
except Exception as e:
if _post_change_event_tracer is not None:
_post_change_event_tracer(
object, trait_name, old, new, handler, exception=e
)
# This call needs to be made inside the `except` block in case
# the handler wants to re-raise the exception.
handle_exception(object, trait_name, old, new)
else:
if _post_change_event_tracer is not None:
_post_change_event_tracer(
object, trait_name, old, new, handler, exception=None
)
def _notify_method_listener(self, object, trait_name, old, new):
""" Dispatch a trait change event to a method listener. """
obj_weak_ref = self.object
if (obj_weak_ref is not None
and _change_accepted(object, trait_name, old, new)):
# We make sure to hold a reference to the object before invoking
# `getattr` so that the listener does not disappear in a
# multi-threaded case.
obj = obj_weak_ref()
if obj is not None:
# Dynamically resolve the listener by name.
listener = getattr(obj, self.name)
self._dispatch_change_event(
object, trait_name, old, new, listener
)
def _notify_function_listener(self, object, trait_name, old, new):
""" Dispatch a trait change event to a function listener. """
if _change_accepted(object, trait_name, old, new):
self._dispatch_change_event(
object, trait_name, old, new, self.handler
)
[docs]class ExtendedTraitChangeNotifyWrapper(TraitChangeNotifyWrapper):
""" Change notify wrapper for "extended" trait change events..
The "extended notifiers" are set up internally when using extended traits,
to add/remove traits listeners when one of the intermediate traits changes.
For example, in a listener for the extended trait `a.b`, we need to
add/remove listeners to `a:b` when `a` changes.
"""
def _dispatch_change_event(self, object, trait_name, old, new, handler):
""" Prepare and dispatch a trait change event to a listener. """
# Extract the arguments needed from the handler.
args = self.argument_transform(object, trait_name, old, new)
# Dispatch the event to the listener.
try:
self.dispatch(handler, *args)
except Exception:
handle_exception(object, trait_name, old, new)
def _notify_method_listener(self, object, trait_name, old, new):
""" Dispatch a trait change event to a method listener. """
obj_weak_ref = self.object
if obj_weak_ref is not None:
# We make sure to hold a reference to the object before invoking
# `getattr` so that the listener does not disappear in a
# multi-threaded case.
obj = obj_weak_ref()
if obj is not None:
# Dynamically resolve the listener by name.
listener = getattr(obj, self.name)
self._dispatch_change_event(
object, trait_name, old, new, listener
)
def _notify_function_listener(self, object, trait_name, old, new):
""" Dispatch a trait change event to a function listener. """
self._dispatch_change_event(object, trait_name, old, new, self.handler)
[docs]class FastUITraitChangeNotifyWrapper(TraitChangeNotifyWrapper):
""" Dynamic change notify wrapper, dispatching on the UI thread.
This class is in charge to dispatch trait change events to dynamic
listener, typically created using the `on_trait_change` method and the
`dispatch` parameter set to 'ui' or 'fast_ui'.
"""
[docs] def dispatch(self, handler, *args):
if threading.current_thread().ident == ui_thread:
handler(*args)
else:
ui_handler(handler, *args)
[docs]class NewTraitChangeNotifyWrapper(TraitChangeNotifyWrapper):
""" Dynamic change notify wrapper, dispatching on a new thread.
This class is in charge to dispatch trait change events to dynamic
listener, typically created using the `on_trait_change` method and the
`dispatch` parameter set to 'new'.
"""
[docs] def dispatch(self, handler, *args):
Thread(target=handler, args=args).start()
def _change_accepted(object, name, old, new):
""" Return true if notifications should be emitted for the change.
Parameters
----------
object : HasTraits
The object on which the trait is changed.
name : str
The name of the trait changed.
old : any
The old value
new : any
The new value
Returns
-------
accepted : bool
Whether the event should be emitted.
"""
if old is Uninitialized:
return False
trait = object._trait(name, 2)
if (trait.type == TraitKind.trait.name
and trait.comparison_mode == ComparisonMode.equality):
try:
return bool(old != new)
except Exception:
# Maybe do something else about the exception
# enthought/traits#1230
pass
return True