= Trac Component Architecture As the heart of Trac, [source:/trunk/trac/core.py#latest 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? 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. [[Image(xtnpt.png)]] 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, ie 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 glue to hook the different subsystems together, without them necessarily knowing about each other. == Public classes `trac.core.ComponentManager` [[BR]] Manages component life cycle, instantiating registered components on demand. `trac.core.Component` [[BR]] Abstract base class for components. `trac.core.ExtensionPoint` [[BR]] Declares an extension point on a component that other components can plug in to. `trac.core.Interface` [[BR]] Every extension point specifies the contract that extenders must conform to via an `Interface` subclass. [[Image(comparch.png)]] == Declaring a component The simplest possible component is an empty class derived from `trac.core.Component`: {{{ #!python from trac.core import * class MyComponent(Component): pass }}} In the context of a component manager, this component can already be used: {{{ #!python comp_mgr = ComponentManager() my_comp = MyComponent(comp_mgr) }}} Remember that components follow the singleton pattern, but component managers do not. There is only one active instance of any component per component manager. The component constructor checks with the component manager whether there is already an active instance before allocating a new instance. If the component was already instantiated, the existing instance is returned: {{{ #!python 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: {{{ #!python 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 is empty. '''Note''': You can't pass data to the constructor of a component. === Components instantiating other Components If one `Component` instantiates another, it typically will use the same `ComponentManager`, instead of creating a new `ComponentManager`. {{{ #!python 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: {{{ #!python 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 [http://users.rcn.com/python/download/Descriptor.htm descriptor]: When it is accessed, it finds all '''enabled''' components (see [#component_lifecycle below]) 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 that there are multiple ways to define an extension point: * `trac.core.ExtensionPoint`: This is an ''unordered list of all'' enabled components implementing a specific extension point interface. * `trac.config.ExtensionOption`: An option for [wiki:TracIni trac.ini] that describes ''exactly one'' enabled component implementing a specific extension point interface. * `trac.config.OrderedExtensionsOption`: An option for [wiki:TracIni trac.ini] that describes an ''ordered list'' of enabled components implementing a specific extension point interface. Components that also implement the same interface but are not listed in the option can automatically be appended to the list. == Plugging in to an extension point Now that we have an extendable component, let's add another component that extends it: {{{ #!python 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: {{{ #!python 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. == Component Lifecycle and State == #component_lifecycle This section will shed some light on how components come to be. It describes some inner workings and terminology that you may come across when trying to understand components, and provides information about where the specific parts are implemented. First of all, let's repeat the basic rules for components: ''There is only '''one active instance''' of any component per component manager. They should be '''stateless'''.'' This means that within the same component manager instance (usually `trac.env.Environment()`) components can be considered singletons. Components need to be implemented to be reused for multiple - possibly concurrent - web request. So they can't (read: should not) store information from one request in their class members and reuse this information in the next request. Now, the following list describes the stages a component goes through until it's ready to be used: 1. ''Registration:'' The first step is to register a component so that Trac knows about it. This happens automatically when the Python file contain the component gets imported for the first time.[[BR]] A component can be registered using one of the following methods: * with an `import` statement in a file that has already been imported * by listing the file in `entry_points` section of a plugin (see [wiki:TracDev/PluginDevelopment#Packagingplugins Packaging Plugins]) The registration is handled by `trac.core.ComponentMeta` using [http://www.ibm.com/developerworks/linux/library/l-pymeta3/index.html metaclass programming]. 2. ''Activation:'' A component gets activated when it's first used. So, "activation" is basically another word for "instantiation", though since a component is a singleton it's only activated/instantiated once. When a component gets activated, Trac's component manager adds some useful fields to the component, specifically: `env`, `log`, and `config`.[[BR]] A component can be activated using one of the following methods: * when it's manually constructed for the first time, using `Component(compmngr)` * when one of the extension points the component implements is used for the first time. Note that the component must be enabled for this way to work (see below). The activation and making sure that only one instance of a component exists is handled by `trac.core.Component.__new__()`. This method then calls `trac.core.ComponentManager.component_activated()`. In Trac the component manager is usually `trac.env.Environment`. As stated above components can be either "enabled" or "disabled". The main (and only) difference between these states is: ''Extension points will '''only''' use enabled components.'' This means that the extension point methods (like `todo_added` in the example above) of a ''disabled'' component (that implements a certain extension point interface) ''won't be called''. Note also that even disabled components can be activated (instantiated), but only by constructing them manually (as mentioned before). Enabling a component is done in the `[components]` section of [wiki:TracIni trac.ini]. This is implemented in `trac.env.Environment.is_component_enabled()`. Whether a component is enabled or disabled is checked ''only once'' when an extension point that component implements is first used. '''Miscellaneous notes:''' * Components can be marked "abstract". This is done simply by adding a member field `abstract = True` to the component class. {{{ #!python class MyAbstractComponent(Component): abstract = True # implementation stuff here }}} Abstract components can't be enabled and therefore don't appear in the plugin panel of Trac's web interface. * Not all components require to be enabled to work properly. ''Only'' components implementing an extension point interface (using `implements`) need to be enabled and therefore listed in the `entry_points` section of a plugin. If you just want to have the utility class (like a database manager) that takes the benefits of a component (like being a singleton and/or having access to Trac's database or configuration) that doesn't implement any extension point interfaces, it doesn't need to be enabled (or even listed in the `entry_points` section). Such a component should then be marked "abstract". * Components should be listed in the `entry_points` section, if they define any options (from `trac.config`). This way `trac.ini` editors can find this option even if it still has its default value. Options are registered when the component is registered. The component that defines the option doesn't need to be enabled for the option to be registered and can even be abstract. == 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`: {{{ #!python permission_system = trac.perm.PermissionSystem(env) actions = permission_system.get_permission_actions() }}} ''Note that `trac.env.Environment` inherits `trac.core.ComponentManager`, so you'll typically see components initialized with an environment.'' These are the first few lines of `PermissionSystem` as of r5790 (or [browser:trunk/trac/perm.py@5790#L230 in context]): {{{ #!python class PermissionSystem(Component): """Sub-system that manages user permissions.""" implements(IPermissionRequestor) requestors = ExtensionPoint(IPermissionRequestor) }}} Note that this `Component`: 1. implements the `IPermissionRequestor` interface 1. has an extension point for registering all the Components implementing `IPermissionRequestor` ([browser:trunk/trac/perm.py@5790#L40 in context]): {{{ #!python 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 [browser:trunk/trac/perm.py@5790#L380 here]. Next in `PermissionSystem` there is a declaration of an `ExtensionOption` called `store`: {{{ #!python 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 [browser:trunk/trac/config.py 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: {{{ #!python 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, TracPluggableModules