Edgewall Software

Changes between Version 19 and Version 20 of TracPluggableModules


Ignore:
Timestamp:
May 3, 2005, 11:37:08 PM (19 years ago)
Author:
Christopher Lenz
Comment:

Major update to reflect the current status

Legend:

Unmodified
Added
Removed
Modified
  • TracPluggableModules

    v19 v20  
    3131So assuming this basic model of operation, let's move on to discussing ideas for how this might be implemented.
    3232
    33 == The Plug-in Manager ==
     33== Introducing the new trac.core ==
    3434
    35 There needs to be a central facility that manages the discovery and loading of plug-ins as well as the communication between them. We'll call this the {{{PluginManager}}} for now.
     35The first step towards a plugin-based architecture is a revised {{{trac.core}}} module: as the heart of the new architecture it 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"''.
    3636
    37 The plug-in manager should probably be part of the environment, or in other words, for any environment there should be one plug-in manager. Plug-in ''discovery'' would presumably be based on a given path (list of directories) that contain python modules. The {{{PluginManager}}} class would collect all classes that somehow identify themselves as Trac plug-ins (for example by sub-classing a central {{{Plugin}}} class). There could also be a 'plugins' folder in Trac environments that would be added the path by default.
     37=== What is a Component? ===
    3838
    39  ''Does it make sense to have the notion of namespaces on plugins?  If all of the plugins that come with Trac and are the vanilla ones that we all know and love currently are marked as com.edgewall.ticket and com.edgewall.wiki, others can make more.  Chris Lenz's super-duper Ticket plugin could be de.gmx.cmlenz.ticket, for instance.  -- brad''
     39For 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.
    4040
    41 == The Plug-in Interface ==
     41Components 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.
    4242
    43 In the current Trac architecture, modules are basically HTTP request processors that live for exactly one request. In the envisioned system, plug-ins should be able to outlive a request, and also participate in the request processing of other plug-ins, so as to be able to contribute to their functionality.
     43http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/xtnpt.png?format=raw
    4444
    45 So instead of getting the request object as argument to their constructor, they'd have an interface like:
     45A 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.
     46
     47The 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.
     48
     49=== Public classes ===
     50
     51{{{trac.core.ComponentManager}}} [[BR]]
     52  Manages component life cycle, instantiating registered components on demand
     53
     54{{{trac.core.Component}}} [[BR]]
     55  Abstract base class for components.
     56
     57{{{trac.core.ExtensionPoint}}} [[BR]]
     58  Declares an extension point on a component that other components can plug in to.
     59
     60{{{trac.core.Interface}}} [[BR]]
     61  Every extension point specifies the contract that extenders must conform to via an {{{Interface}}} subclass.
     62
     63http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/comparch.png?format=raw
     64
     65=== Declaring a component ===
     66
     67The simplest possible component is an empty class derived from {{{trac.core.Component}}}:
    4668
    4769{{{
    48 #!python
    49 class XxxPlugin(Plugin):
     70from trac.core import *</strong>
    5071
    51     def __init__(self, env):
    52         Module.__init__(self, env)
    53         # do whatever other initialization may be needed
    54         pass
    55 
    56     def processRequest(self, req):
    57         # do whatever needs to be done
    58         pass
     72class MyComponent(Component):
     73    pass
    5974}}}
    6075
    61 In an environment such as [wiki:TracStandalone tracd] or [wiki:TracModPython mod_python], the {{{processRequest}}} method might be called concurrently on the same module object, so plug-in implementations should not store request-specific data as instance variables, but rather pass such data through as parameters to other methods.
    62 
    63 == Plug-in Cooperation ==
    64 
    65 As discussed above, a basic requirement is that the modules should be able to:
    66  * contribute functionality to other modules
    67  * provide hooks so that other modules can contribute to its functionality
    68 
    69 For this to work, plug-ins can declare their own ''extension points'' and also declare themselves as extending one or more extension points of other plug-ins:
     76In the context of a component manager, this component can already be used:
    7077
    7178{{{
    72 #!python
    73 from protocols import *
    74 from trac.plugin import *
    75 
    76 import time
    77 
    78 class TimelinePlugin(Plugin):
    79 
    80     _name = 'timeline'
    81     eventProviders = ExtensionPoint(IEventProvider)
    82 
    83     def processRequest(self, req):
    84         if not filters:
    85             filters = []
    86             for provider in self.eventProviders:
    87                 filters += provider.getTimelineFilters()
    88         events = []
    89         for provider in self.eventProviders:
    90             events += provider.getTimelineEvents(start, stop, filters)
    91 
    92         # render the page etc.
    93 
    94 
    95 class ChangesetPlugin(Plugin):
    96 
    97     _name = 'changeset'
    98     _extends = ['timeline.eventProviders']
    99     advise(instancesProvide=[timeline.IEventProvider])
    100 
    101     def getTimelineEvents(self, start, stop, filters):
    102         if 'changesets' in filters:
    103             return [timeline.Event(time.time(), 'Changeset [1]'),
    104                     timeline.Event(time.time(), 'Changeset [2]')]
    105 
    106     def getTimelineFilters(self):
    107         return ['changesets']
     79    comp_mgr = ComponentManager()
     80    my_comp = MyComponent(comp_mgr)
    10881}}}
    10982
    110 In this example, the {{{TimelinePlugin}}} declares the extension point
    111 {{{eventProviders}}}. Extensions to that extension point must support
    112 the interface {{{IEventProvider}}} (not shown here). This code currently uses
    113 [http://peak.telecommunity.com/PyProtocols.html PyProtocols] to provide
    114 a flexible mechanism for interfaces and adaptation. The
    115 [http://zope.org/Products/ZopeInterface zope.interface] package has been
    116 brought up as an alternative that we're looking into.
     83Remember that components follow the singleton pattern. 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:
    11784
    118 The {{{ChangesetPlugin}}} implements the {{{IEventProvider}}} interface
    119 with the methods {{{getTimelineEvents()}}} and {{{getTimelineFilters()}}}.
     85{{{
     86    my_comp1 = MyComponent(comp_mgr)
     87    my_comp2 = MyComponent(comp_mgr)
     88    assert id(my_comp1) == id(my_comp2)
     89}}}
    12090
    121 The interesting piece is in {{{TimelinePlugin.processRequest()}}}: Here
    122 we can access a "magic" attribute with the same name as the extension
    123 point we declared, and iterate over it to access the individual plugins
    124 that extend it. ''Note: the plugins are automatically adapted to the required interface before being returned, so the plugin class does not have to implement the interface itself, but could also register an apprioprate adapter.''
     91If 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:
    12592
    126 A couple of drawbacks become visible here:
    127  * The current timeline code is very efficient by basically consisting of a single SQL query over all the related tables. Under the plug-in model, we'd have at least one query per contributing module. ''Not necessarily: each contributing module could contribute the appropriate SQL fragment for the query, as well as a post-processing routine. The current timeline code could be easily refactored  that way, without losing its efficiency.''
    128  * Similarly, ordering of the events will need to be done in Python in the timeline module after it has collected all events. ''See above.''
     93{{{
     94    from trac.core import *
    12995
    130 This is probably just the price to pay for a cleaner architecture, but optimizations may be possible.
     96    class MyComponent(Component):
     97        def __init__(self):
     98            self.data = {}
     99
     100    comp_mgr = ComponentManager()
     101    my_comp = MyComponent(comp_mgr)
     102}}}
     103
     104Direct {{{Component}}} sub-classes also do not need to worry about invoking the base classes {{{__init__}}} method (which is empty).
     105
     106=== Declaring an extension point ===
     107
     108The 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.
     109
     110The 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:
     111
     112{{{
     113    from trac.core import *
     114   
     115    class ITodoObserver(Interface):
     116        def todo_added(name, description):
     117            """Called when a to-do item is added."""
     118
     119    class TodoList(Component):
     120        observers = ExtensionPoint(ITodoObserver)
     121
     122        def __init__(self):
     123            self.todos = {}
     124
     125        def add(self, name, description):
     126            assert not name in self.todos, 'To-do already in list'
     127            self.todos[name] = description
     128            for observer in self.observers:
     129                observer.todo_added(name, description)
     130}}}
     131
     132Here, 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.
     133
     134The {{{TodoList}}} component notifies the observers inside the {{{add()}}} method by iterating over {{{self.observers}}} and calling the {{{todo_added()}}} method for each. The {{{Component}}} class performs some magic to enable that: Conceptually, it intercepts access to the extension point attribute and finds all registered 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.
     135
     136=== Plugging in to an extension point ===
     137
     138Now that we have an extendable component, let's add another component that extends it:
     139
     140{{{
     141    class TodoPrinter(Component):
     142        implements(ITodoObserver)
     143
     144        def todo_added(self, name, description):
     145            print 'TODO:', name
     146            print '     ', description
     147}}}
     148
     149This 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.
     150
     151''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.''
     152
     153You can specify multiple extension point interfaces to extend with the {{{implements}}} method by simply passing them as additional arguments.
     154
     155=== Putting it together ===
     156
     157Now 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:
     158
     159{{{
     160    comp_mgr = ComponentManager()
     161    todo_list = TodoList(comp_mgr)
     162
     163    todo_list.add('Make coffee',
     164                  'Really need to make some coffee')
     165    todo_list.add('Bug triage',
     166                  'Double-check that all known issues were addressed')
     167}}}
     168
     169Running this script will produce the following output:
     170
     171{{{
     172    TODO: Make coffee
     173          Really need to make some coffee
     174    TODO: Bug triage
     175          Double-check that all known issues were addressed
     176}}}
     177
     178This 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.)''
     179
     180== Current Status ==
     181
     182The architecture explained above is implemented for Trac in the [source:/branches/cmlenz-dev/rearch rearch branch]. There are currently three extension points exposed:
     183
     184 * '''IRequestHandler''': allows plugins to process HTTP requests
     185 * '''INavigationContributor''': allows plugins to contribute links to the Trac navigation menus
     186 * '''ITimelineEventProvider''': allows plugins to add events to the timeline
     187
     188The branch also provides an overview of installed plugins, and what extension points are provided and extended:
     189
     190http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/about_plugins.png?format=raw
    131191
    132192== Other Aspects of Plug-Ins ==
     
    137197
    138198Plug-ins should also be able to not only hook into the web-interface, but also into TracAdmin. The approach for this would be similar to the one discussed above, meaning that modules should be able to contribute commands.
    139 
    140 == Status ==
    141 
    142 Previous snapshots of this code have been added as attachments to this page, but the "real" work has now started on the new [source:/branches/cmlenz-dev/rearch rearch branch]. That code is fully functional with the following exceptions:
    143 
    144  * The mod_python handler and tracd are most likely broken, only way to use the branch is CGI.
    145  * No "pretty" error pages.
    146  * Sessions cannot be loaded/recovered.
    147 
    148 What we '''do''' have:
    149 
    150  * A working plug-in system as described above, minus the configuration of which plug-ins are to be loaded.
    151  * A new (and as of yet incomplete) abstraction layer between the Trac web-application and the web-server. Unlike the current {{{trac.core.Request}}} class, it provides separate request and response objects. It is based on [http://www.python.org/peps/pep-0333.html WSGI] with a simple adapter for CGI. ''This doesn't really have much to do with the plug-in system, it's just another refactoring that I'm playing with in the same sandbox.''
    152  * A new abstraction layer on top of the ClearSilver templating engine. It allows populating the HDF using native Python data structures such as lists and dicts.
    153  * ClearSilver templating is a plug-in ({{{trac/web/clearsilver.py}}}), request dispatching is a plug-in ({{{trac/web/dispatcher.py}}}) and building the web-site chrome (e.g. the navigation) is another plug-in ({{{trac/web/chrome.py}}}). Persistent session support is implemented by the plug-in {{{trac/web/session.py}}}. Hooking into HTTP authentication is also a plug-in ({{{trac/web/ext_auth.py}}}). The idea with the latter is that we could provide an alternative plug-in that would do form-based authentication.
    154  * A plug-in that runs the current set of modules in a compatability layer ({{{trac/plugins/compat.py}}}), providing them the environment they expect.
    155  * A plug-in that replaces the ''Settings'' module (at least partially).
    156 
    157 None of the actual modules have been migrated to plug-ins yet, they all run under the compatibility layer. They will be migrated one after another in the next couple of days/weeks.
    158 
    159 To use the CGI, configure your web-server the same way as for a regular Trac instance. The CGI script is in {{{cgi-bin/trac.cgi}}} just like before. You will however need to add the following snippet to your [wiki:TracIni trac.ini] file:
    160 
    161 {{{
    162 [web]
    163 default = compat
    164 filters = external_auth, clearsilver, session, compat, chrome
    165 }}}
    166 
    167 Here, ''default'' tells the request dispatcher which plug-in to use on a request to the root URL (i.e. {{{SCRIPT_NAME}}} without further {{{PATH_INFO}}}). It basically designates the welcome page. ''filters'' determines the order in which the request filters will be run (so yes, order does matter -- a lot).
    168 
    169 You will also need to have [http://peak.telecommunity.com/PyProtocols.html PyProtocols] installed.
    170 
    171 == Your Ideas Wanted ==
    172 
    173 Comments, alternative approaches, constructive criticism, etc. Bring it on!
    174 --ChristopherLenz
    175 
    176 === Trac Modules and Trac Objects ===
    177 
    178 The approach of Pluggable Module described here
    179 would be complementary to the Trac Objects idea
    180 described here: TracObjectModelProposal.
    181 
    182 --ChristianBoos
    183 
    184 ''I don't see how the two ideas are complementary, other than both are refactoring discussions.''
    185 --ChristopherLenz
    186 
    187 == Identity Crisis? ==
    188 
    189 Eclipse is a fantastic tool and an excellent example of a very impressive plug-in architecture...  but, Eclipse is a "Rich Client Platform" - a universal tool.  It wants to be everything to everyone.  I think a better model for Trac to follow would be jEdit which has a wealth of plug-ins, but jEdit remains a text editor, not a plug-in manager with a text editing plug-in.  (I believe my example is correct, I've never looked under the hood of jEdit).
    190 
    191 ''Eclipse is first and foremost an IDE featuring an extremely modular architecture (the whole RCP thing was slapped on much later). Whether you call the pieces of the software "components", "modules" or "plug-ins" doesn't really matter. Anyway, I might have taken the analogy with Eclipse too far, please don't let that distract you from the rest of the message.'' --ChristopherLenz
    192 
    193 Trac, as defined in the logo at the top of this page, is an SCM Issue Tracker/Project Manager.  It makes good design sense to have a modular architecture (as it is now) and a plug-in interface for non-core extensions, but to make everything a plug-in??  What would Trac become?  It would be an empty shell that loads Python modules - but we already have Python to load Python modules.
    194 
    195 ''Not an empty shell that loads Python modules, but a shell designed to contain SCM-related modules, combined with a collection of useful modules focused around project management.''
    196 
    197 ''The way I see it, Trac would be the sum of the modules that make up the functionality we have now. The fact that the actual core would just consist of a plug-in manager and request dispatcher is an implementation detail that serves to make the architecture more modular. It should have no immediate effect on the user or administrator apart from allowing more flexible deployments.'' --ChristopherLenz
    198 
    199 I would say that the features available in Trac 0.8 are pretty close to what I would consider core functionality - but that is because I want the total package, one-stop shopping: SCM Issue Tracker/Project Management.  I can spare the 50 or 100k of disk space per module, even if I don't use them.
    200 
    201 If a user doesn't want to use the TracRoadmap or the TracBrowser, etc - then why not have some boolean settings in TracIni to disable their appearance?  Its a poor-man's way to eliminate "bloat", but it would keep the current "core" intact.
    202 
    203 
    204 ''What benefit is there to maintaining a high level of coupling between the core modules?''
    205 
    206 I'm all for a plug-in interface for TracReleaselist and whatever anyone else can dream up (Anthill or CruiseControl plug-in, anyone?)  Personally, I do not think that refactoring the core into individual plug-ins provides any real benefit.
    207 
    208 ''Refactoring the core would make it easier to abstract the frontend away from the backend, so that other frontends (an XML-RPC interface, say) can be constructed more easily.''
    209 
    210 Developing a good plug-in extension interface for the existing core, however, would be very worthwhile.  And to develop a good interface is going to be tricky enough.
    211 
    212 --JamesMoger
    213 
    214 == Re-read of proposal ==
    215 
    216 What you've got so far sounds like a good start - although I still think the Module Manager should not be the top-level component, but a peer of timeline, roadmap, etc. and at that point perhaps a more appropriate name would be Plugin Manager.  :)
    217 
    218 --James Moger
    219 
    220 ''James, the idea I was trying to get across with this proposal is that modularizing the current core of Trac, while allowing the mechanism to be used for plugging in new or custom functionality, would probably be easier and than developing a well designed plug-in interface, '''and''' come with more benefits.'' --ChristopherLenz
    221