Source code for envisage.extension_point_binding
# (C) Copyright 2007-2024 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!
""" A binding between a trait on an object and an extension point. """
# Enthought library imports.
from traits.api import Any, HasTraits, Instance, Str, Undefined
# Local imports.
from .i_extension_registry import IExtensionRegistry
[docs]class ExtensionPointBinding(HasTraits):
"""A binding between a trait on an object and an extension point."""
#### 'ExtensionPointBinding' *CLASS* interface ############################
# Global dictionary used to keep ExtensionPointBinding objects alive.
_bindings = {}
#### 'ExtensionPointBinding' interface ####################################
# The object that we are binding the extension point to.
obj = Any
# The Id of the extension point.
extension_point_id = Str
# The extension registry used by the binding. If this trait is not set then
# the class-scope extension registry set on the 'ExtensionPoint' class is
# used (and if that is not set then the binding won't work ;^)
extension_registry = Instance(IExtensionRegistry)
# The name of the trait that we are binding the extension point to.
trait_name = Str
#### Private interface ####################################################
# A flag that prevents us from setting a trait twice.
_event_handled = False
###########################################################################
# 'object' interface.
###########################################################################
def __init__(self, **traits):
"""Constructor."""
super().__init__(**traits)
# Initialize the object's trait from the extension point.
self._set_trait(notify=False)
# Wire-up the trait change and extension point handlers.
self._bind()
###########################################################################
# Private interface.
###########################################################################
#### Trait change handlers ################################################
def _on_trait_changed(self, obj, trait_name, old, new):
"""Dynamic trait change handler."""
if not self._event_handled:
self._set_extensions(new)
def _on_trait_items_changed(self, obj, trait_name, old, event):
"""Dynamic trait change handler."""
if not self._event_handled:
self._set_extensions(getattr(obj, self.trait_name))
#### Other observer pattern listeners #####################################
def _extension_point_listener(self, extension_registry, event):
"""Listener called when an extension point is changed."""
self._event_handled = True
if event.index is not None:
self._update_trait(event)
else:
self._set_trait(notify=True)
self._event_handled = False
#### Methods ##############################################################
def _bind(self):
"""Wire-up trait change handlers etc."""
# Listen for the object's trait being changed.
self.obj.on_trait_change(self._on_trait_changed, self.trait_name)
self.obj.on_trait_change(
self._on_trait_items_changed, self.trait_name + "_items"
)
# Listen for the extension point being changed.
self.extension_registry.add_extension_point_listener(
self._extension_point_listener, self.extension_point_id
)
def _unbind(self):
"""Undo the effects of _bind"""
self.extension_registry.remove_extension_point_listener(
self._extension_point_listener, self.extension_point_id
)
self.obj.on_trait_change(
self._on_trait_items_changed,
self.trait_name + "_items",
remove=True,
)
self.obj.on_trait_change(
self._on_trait_changed, self.trait_name, remove=True
)
def _set_trait(self, notify):
"""Set the object's trait to the value of the extension point."""
value = self.extension_registry.get_extensions(self.extension_point_id)
traits = {self.trait_name: value}
self.obj.trait_set(trait_change_notify=notify, **traits)
def _update_trait(self, event):
"""Update the object's trait to the value of the extension point."""
self._set_trait(notify=False)
self.obj.trait_property_changed(
self.trait_name + "_items", Undefined, event
)
def _set_extensions(self, extensions):
"""Set the extensions to an extension point."""
self.extension_registry.set_extensions(
self.extension_point_id, extensions
)
# Factory function for creating bindings.
[docs]def bind_extension_point(
obj, trait_name, extension_point_id, extension_registry
):
"""Create a binding to an extension point.
The returned ExtensionPointBinding object is also stored in a (private)
global dictionary, so that users aren't required to keep a reference to it.
That global dictionary entry can be removed with a matching call to
unbind_extension_point.
Parameters
----------
obj : HasTraits
The HasTraits object that we're binding to
trait_name : str
The name of the trait on obj to bind to.
extension_point_id : str
The id of the extension point.
extension_registry : IExtensionRegistry
The extension registry that the extension point is registered to.
Returns
-------
ExtensionPointBinding
An object that manages the binding.
"""
binding = ExtensionPointBinding(
obj=obj,
trait_name=trait_name,
extension_point_id=extension_point_id,
extension_registry=extension_registry,
)
# Keep a reference to each binding in the ExtensionPointBinding._bindings
# global dictionary. Without this, the binding would become inactive
# if the caller didn't keep a reference.
bindings = ExtensionPointBinding._bindings.setdefault(obj, [])
bindings.append(binding)
return binding
[docs]def unbind_extension_point(
obj, trait_name, extension_point_id, extension_registry
):
"""
Remove an extension point binding.
Changes to extension point contributions will no longer affect the target
trait. Also removes the matching ExtensionPointBinding object from the
global dictionary.
Parameters
----------
obj : HasTraits
The HasTraits object that we're binding to
trait_name : str
The name of the trait on obj to bind to.
extension_point_id : str
The id of the extension point.
extension_registry : IExtensionRegistry
The extension registry that the extension point is registered to.
"""
# Find the corresponding binding in the global dict.
bindings = ExtensionPointBinding._bindings
# Find the matching ExtensionPointBinding object. Search in reverse order,
# since that's most likely to give the right binding quickly under the
# normal first-in-last-out pattern for nested setup and teardown.
index, binding = next(
(index, binding)
for index, binding in reversed(list(enumerate(bindings[obj])))
if binding.trait_name == trait_name
if binding.extension_point_id == extension_point_id
if binding.extension_registry == extension_registry
)
# Remove the binding from the global dict, and remove the dict entry
# altogether if it's now empty.
bindings[obj].pop(index)
if not bindings[obj]:
bindings.pop(obj)
# Undo the binding
binding._unbind()