Version 19 (modified by 13 years ago) ( diff ) | ,
---|
Trac Component Architecture
As the heart of Trac, trac.core implements a minimal component kernel that allows components to easily extend each others' functionality. It provides a "meta-plugin-API": every component can easily offer its own plugin API by declaring "extension points".
What is a Component?
For our purposes, a component is an object that provides a certain type of service within the context of the application. There is at most one instance of any component: components are singletons. That implies that a component does not map to an entity of the application's object model; instead, components represent functional subsystems.
Components can declare "extension points" that other components can “plug in” to. This allows one component to enhance the functionality of the component it extends, without the extended component even knowing that the extending component exists. All that is needed is that the original component exposes – and uses – one or more extension points.
A component can extend any number of other components and still offer its own extension points. This allows a plugin to itself offer a plugin API (i.e. extension point). This feature is the basis for a plugin-based architecture.
The actual functionality and APIs are defined by individual components. The component kernel provides the “magic glue” to hook the different subsystems together – without them necessarily knowing about each other.
Public classes
trac.core.ComponentManager
Manages component life cycle, instantiating registered components on demand
trac.core.Component
Abstract base class for components.
trac.core.ExtensionPoint
Declares an extension point on a component that other components can plug in to.
trac.core.Interface
Every extension point specifies the contract that extenders must conform to via an
Interface
subclass.
Declaring a component
The simplest possible component is an empty class derived from trac.core.Component
:
from trac.core import * class MyComponent(Component): pass
In the context of a component manager, this component can already be used:
comp_mgr = ComponentManager() my_comp = MyComponent(comp_mgr)
Remember that components follow the singleton pattern, but component managers are not. There is only one active instance of any component per component manager. The component constructor is “magic” in that it checks with the component manager whether there´s already an active instance before allocating a new instance. If the component was already instantiated, the existing instance is returned:
my_comp1 = MyComponent(comp_mgr) my_comp2 = MyComponent(comp_mgr) assert id(my_comp1) == id(my_comp2)
If a component needs to initialize data members, it can override the __init__
method. But because the component manager needs to be able to instantiate the component on demand, __init__
must not require any extra parameters, including the reference to the component manager passed into the constructor:
from trac.core import * class MyComponent(Component): def __init__(self): self.data = {} comp_mgr = ComponentManager() my_comp = MyComponent(comp_mgr)
Direct Component
sub-classes also do not need to worry about invoking the base class' (i.e. Component
's) __init__
method, as it's empty.
Components instantiating other Components
If one Component
instantiates another, it typically will use the same ComponentManager
, instead of creating a new ComponentManager
.
class MyComponent(Component): def callOtherComponent(self): MyOtherComponent(self.compmgr).someFunction()
Note that within trac, the component manager is more commonly referenced as self.env
.
Declaring an extension point
The component above doesn't actually do anything. Making an object a component only makes it act as a singleton in the scope of a component manager, which isn't that exciting in itself.
The real value of components becomes clearer when the facilities for extensions are used. As a simple example, the following component provides an extension point that lets other components listen to changes to the data it manages (in this case a list of to-do items) – following the widely known observable pattern:
from trac.core import * class ITodoObserver(Interface): def todo_added(name, description): """Called when a to-do item is added.""" class TodoList(Component): observers = ExtensionPoint(ITodoObserver) def __init__(self): self.todos = {} def add(self, name, description): assert not name in self.todos, 'To-do already in list' self.todos[name] = description for observer in self.observers: observer.todo_added(name, description)
Here, the TodoList
class declares an extension point called observers
with the interface ITodoObserver
. The interface defines the contract that extending components need to conform to.
The TodoList
component notifies the observers inside the add()
method by iterating over self.observers
and calling the todo_added()
method for each. This works because the observers
attribute is a descriptor: When it is accessed, it finds all enabled components that declare to extend the extension point. For each of those components, it gets the instance from the component manager, potentially activating it if it is getting accessed for the first time.
Note: Only component classes that are enabled (in the [components]
section of trac.ini) will be available when iterating over an extension point (like in the code above). Thus it's not enough to only instantiate a component (e.g. by calling the component constructor as ComponentName(self.compmgr)
) but one needs to enable it as well.
Plugging in to an extension point
Now that we have an extendable component, let's add another component that extends it:
class TodoPrinter(Component): implements(ITodoObserver) def todo_added(self, name, description): print 'TODO:', name print ' ', description
This class implements
the ITodoObserver
interface declared above, and simply prints every new to-do item to the console. By declaring to implement the interface, it transparently registers itself as an extension of the TodoList
class.
Note that you don't actually derive the component from the interface it implements. That is because conformance to an interface is orthogonal to inheritance; and because Python doesn't have static typing, there's no need to explicitly mark the component as implementing an interface.
You can specify multiple extension point interfaces to extend with the implements
method by simply passing them as additional arguments.
Putting it together
Now that we've declared both a component exposing an extension point, and another component extending that extension point, let's use the to-do list example to see what happens:
comp_mgr = ComponentManager() todo_list = TodoList(comp_mgr) todo_list.add('Make coffee', 'Really need to make some coffee') todo_list.add('Bug triage', 'Double-check that all known issues were addressed')
Running this script will produce the following output:
TODO: Make coffee Really need to make some coffee TODO: Bug triage Double-check that all known issues were addressed
This output obviously comes from the TodoPrinter
. Note however that the code snippet above doesn't even mention that class. All that is needed to have it participating in the action is to declare the class. (That implies that an extending class needs to be imported by a python script to be registered. The aspect of loading components is however separate from the extension mechanism itself.)
How components are used in Trac's code
The typical use of components in Trac starts with a top-level “service provider” that we'll pick up and use directly. Take, for example, trac.perm.PermissionSystem
:
permission_system = trac.perm.PermissionSystem(env) actions = permission_system.get_permission_actions()
Note that
trac.env.Environment
inheritstrac.core.ComponentManager
, so you'll typically see components initialized with an environment.
These are the first few lines of PermissionSystem
as of r5790 (or in context):
class PermissionSystem(Component): """Sub-system that manages user permissions.""" implements(IPermissionRequestor) requestors = ExtensionPoint(IPermissionRequestor)
Note that this Component
:
- implements the
IPermissionRequestor
interface - has an extension point for registering all the Components implementing
IPermissionRequestor
(in context):class IPermissionRequestor(Interface): """Extension point interface for components that define actions.""" def get_permission_actions(): """Return a list of actions defined by this component."""
Note that interface authors have not always been consistent about declaring the “
self
” parameter in signatures.
When we use PermissionSystem
, the plugin system will have automatically gathered up all implementations of IPermissionRequestor
and placed them in PermissionSystem
's list of requestors
.
In this specific case PermissionSystem
will be part of that list as well, because it implements the IPermissionRequestor
interface. In no way a Component is bound to implement the interfaces it declares an extension point for, the two operations being entirely independent. But when that make sense, it's entirely possible to do so.
Note: it's certainly debatable whether it makes sense in this particular case—but if you do decide to do it, watch out for infinite recursion as
PermissionStore
does here.
Next in PermissionSystem
there is a declaration of an ExtensionOption
called store
:
store = ExtensionOption('trac', 'permission_store', IPermissionStore, 'DefaultPermissionStore', """Name of the component implementing `IPermissionStore`, which is used for managing user and group permissions.""")
The above adds an option called permission_store
to trac.ini
, declares that the component named by the option implements IPermissionStore
, and sets its default to DefaultPermissionStore
. See trac.config for ExtensionOption
and friends. Methods of service providers such as PermissionSystem
are commonly a thin forwarding layer over such an ExtensionOption
. For example:
def get_all_permissions(self): """Return all permissions for all users. The permissions are returned as a list of (subject, action) formatted tuples.""" return self.store.get_all_permissions()
Thus, service providers are directly manipulated from Python, and are customized through the automatic aggregation of components implementing ExtensionPoint
s and through configuration of ExtensionOption
s by Trac administrators.
See also: TracDev
Attachments (2)
-
comparch.png
(16.5 KB
) - added by 19 years ago.
Package diagram
-
xtnpt.png
(17.5 KB
) - added by 19 years ago.
Extension point graph
Download all attachments as: .zip