Edgewall Software

Changes between Version 10 and Version 11 of TracPluggableModules


Ignore:
Timestamp:
Jan 7, 2005, 4:32:44 PM (17 years ago)
Author:
Christopher Lenz
Comment:

Update of the proposal

Legend:

Unmodified
Added
Removed
Modified
  • TracPluggableModules

    v10 v11  
    3131So assuming this basic model of operation, let's move on to discussing ideas for how this might be implemented.
    3232
    33 == The Module Manager ==
     33== The Plug-in Manager ==
    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 {{{ModuleManager}}} for now.
     35There 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.
    3636
    37 The module manager should probably be part of the environment, or in other words, for any environment their should be one module registry. Module discovery would presumably be based on a given paths (list of directories) that contain python modules. The registry class would find all classes that somehow identify themselves as Trac modules (for example by sub-classing a central {{{Module}}} class, or by defining some module-scope variable). There could also be a 'plugins' folder in Trac environments that would be added the path by default.
     37The plug-in manager should probably be part of the environment, or in other words, for any environment their 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.
    3838
    39 ''An alternative and possibly more efficient but also more cumbersome approach would be to require the user to specify the "fully qualified" name of every additional module, for example in TracIni. Modules such as the timeline would be in the list by default, but could be disabled by removing them.''
     39== The Plug-in Interface ==
    4040
    41 Modules would get loaded as any other python module, but should also be able to hook into the initialization of the system via an "exported" function. This would for example be used by the Wiki module to build the list of names of existing wiki pages.
    42 
    43 == The Module Interface ==
    44 
    45 In the current Trac architecture, modules are basically HTTP request processors that live for exactly one request. In the envisioned system, the modules should be able to outlive a request, and also participate in the request processing of other modules, so as to be able to contribute to their functionality.
     41In 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.
    4642
    4743So instead of getting the request object as argument to their constructor, they'd have an interface like:
     
    4945{{{
    5046#!python
    51 class XxxModule(Module):
     47class XxxPlugin(Plugin):
    5248
    5349    def __init__(self, env):
     
    5652        pass
    5753
    58     def process(self, req):
     54    def processRequest(self, req):
    5955        # do whatever needs to be done
    6056        pass
    6157}}}
    6258
    63 In an environment such as [wiki:TracStandalone tracd] or [wiki:TracModPython mod_python], the {{{process}}} method might be called concurrently on the same module object, so module implementations should not store request-specific data as instance variables, but rather pass such data through as parameters to other methods.
     59In 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.
    6460
    65 ''Renaming the current "{{{render}}}" to "{{{process}}}" here simply because the latter sounds more appropriate.'''
    66 
    67 == Module Cooperation ==
     61== Plug-in Cooperation ==
    6862
    6963As discussed above, a basic requirement is that the modules should be able to:
     
    7165 * provide hooks so that other modules can contribute to its functionality
    7266
    73 === By using Function Hooks ===
    74 
    75 The supporting model for such cooperation between modules should be functions defined by the contributing plug-in, and called by the plug-in to which we contribute. For example, let's assume the timeline module documents the following hook functions (or ''extension points'' in Eclipse jargon):
     67For 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:
    7668
    7769{{{
    7870#!python
    79     def get_timeline_events(self, start, to, filters):
    80         # return a list of TimedEvent objects in the time span delimited
    81         # by the start and stop parameters, only containing events of the
    82         # types included in the filters list
     71from protocols import *
     72from trac.plugin import *
    8373
    84     def get_timeline_filters(self):
    85         # return a list of (name,label) tuples that define the filters available
    86         # for this module. The ticket module might export ('ticket','Ticket changes'),
    87         # for example.
     74import time
     75
     76class TimelinePlugin(Plugin):
     77
     78    _name = 'timeline'
     79    eventProviders = ExtensionPoint(IEventProvider)
     80
     81    def processRequest(self, req):
     82        if not filters:
     83            filters = []
     84            for provider in self.eventProviders:
     85                filters += provider.getTimelineFilters()
     86        events = []
     87        for provider in self.eventProviders:
     88            events += provider.getTimelineEvents(start, stop, filters)
     89
     90        # render the page etc.
     91
     92
     93class ChangesetPlugin(Plugin):
     94
     95    _name = 'changeset'
     96    _extends = ['timeline.eventProviders']
     97    advise(instancesProvide=[timeline.IEventProvider])
     98
     99    def getTimelineEvents(self, start, stop, filters):
     100        if 'changesets' in filters:
     101            return [timeline.Event(time.time(), 'Changeset [1]'),
     102                    timeline.Event(time.time(), 'Changeset [2]')]
     103
     104    def getTimelineFilters(self):
     105        return ['changesets']
    88106}}}
    89107
    90 The wiki module would contribute to the timeline simply by appropriately implementing these functions. Same for any other module that wants to put events in the timeline.
     108In this example, the {{{TimelinePlugin}}} declares the extension point
     109{{{eventProviders}}}. Extensions to that extension point must support
     110the interface {{{IEventProvider}}} (not shown here). This code currently uses
     111[http://peak.telecommunity.com/PyProtocols.html PyProtocols] to provide
     112a flexible mechanism for interfaces and adaptation. The
     113[http://zope.org/Products/ZopeInterface zope.interface] package has been
     114brought up as an alternative that we're looking into.
    91115
    92 The timeline module would use a convenience function in the {{{Module}}} base class to collect the events from all available plug-ins. That might look like the following:
     116The {{{ChangesetPlugin}}} implements the {{{IEventProvider}}} interface
     117with the methods {{{getTimelineEvents()}}} and {{{getTimelineFilters()}}}.
    93118
    94 {{{
    95 #!python
    96     def process(self, req):
    97         # ...
    98         events = self.extensions.get_timeline_events(start, stop, filters)
    99 }}}
    100 
    101 Here, I'd like the 'extensions' object to be a somewhat magic ''proxy'' that would call a given function on all modules defining that function, and returning the individual results as a list.
     119The interesting piece is in {{{TimelinePlugin.processRequest()}}}: Here
     120we can access a "magic" attribute with the same name as the extension
     121point we declared, and iterate over it to access the individual plugins
     122that 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.''
    102123
    103124A couple of drawbacks become visible here:
     
    106127
    107128This is probably just the price to pay for a cleaner architecture, but optimizations may be possible.
    108 
    109 See the attachment {{{moduly.py}}} (at the bottom of this page) for an example how this might work. Note that the example uses new-style Python classes and meta-classes, and as such requires at least Python 2.2. I've only tested it on Python 2.3 myself.
    110 
    111 === By using ExtensionPoint Interfaces ===
    112 
    113 Alternatively, a Module could define one or
    114 several client interfaces (all ExtensionPoint
    115 subclasses), and the other modules could provide zero,
    116 one or more implementations for this Client interface.
    117 
    118 This approach would provide both clarity and flexibility
    119 (if not even a simpler programming model) than the
    120 ''hook functions'' approach.
    121 
    122 See a more detailed description below
    123 '''Your Ideas Wanted / Modules and Configurability'''
    124 
    125129
    126130== Other Aspects of Plug-Ins ==
     
    148152--ChristopherLenz
    149153
    150 === Modules and Configurability ===
    151 
    152 In some cases, one would like to have a fine control
    153 on how a module works. I'll take an example.
    154 The Timeline module interacts with the Ticket module.
    155 The latter provide several timeline entries to be
    156 displayed by the former, as well as specific filters
    157 for composing the filter panel.
    158 
    159 But let's say I would be interested in having
    160 the Ticket Contributions displayed (See #187 and my
    161 patch at #890), whereas others wouldn't want this
    162 option to be available. How to solve this?
    163  * Subclassing the Ticket module?
    164    (For the ''hook function approach'': not very
    165    flexible, what if there are multiple optional
    166    features)
    167  * A configuration flag for the Ticket module?
    168    (This would be more flexible, but, well, would be
    169    spaghetti code in the end!)
    170  * Have the Ticket module provide a (configurable) list
    171    of !TimelineEventProvider objects (the, or one of the
    172    ExtensionPoint interface that the Timeline module
    173    would offer)?
    174 
    175 The last idea seems the most interesting.
    176 The Timeline module expects that the
    177 other modules will provide zero or more
    178 !TimelineEventProvider object.
    179 
    180 The Ticket module would implement all of the following subclasses of !TimelineEventProvider class:
    181  * !NewAndCloseTicketsInTimeline
    182  * !ReopenedTicketsInTimeline
    183  * !TicketContributionsInTimeline
    184 
    185 After being configured (either from the {{{trac.ini}}}
    186 file, or any other mean, even dynamically!),
    187 the Ticket module could decide which instances of
    188 these subclasses will be given to the Timeline instance.
    189 
    190 The !TimelineEventProvider and ExtensionPoint could be
    191 defined as:
    192 {{{
    193 #!python
    194 class ExtensionPoint:
    195     def module(self):
    196         """The module providing this client for the timeline"""
    197         pass
    198     ...
    199 
    200 class TimelineEventProvider(ExtensionPoint):
    201     def query_string(self):
    202         """The SQL fragment which provides:
    203  * time: the timestamp of the event
    204  * idata: the unique identifier for this event
    205  * ... and so on
    206 """
    207         pass
    208 
    209     def render_item(self,item):
    210         """Populate the item dictionary with hdf data"""
    211         pass
    212     ...
    213 }}}
    214 
    215 --ChristianBoos
    216 
    217 I don't see the original problem, to be honest. If you wanted to add a plug-in that adds e.g. ticket comments to the timeline, just do that. No need to subclass the Ticket module, or add a configuration flag. Just add a new plug-in:
    218 
    219 {{{
    220 #!python
    221 class TicketCommentsInTimeline(Module):
    222 
    223     def get_timeline_events(self, start, to, filters):
    224         if 'comments' in filters:
    225             # get all comments from the ticket_change table and return them
    226         return []
    227 
    228     def get_timeline_filters(self):
    229         return [ ('comments', 'Ticket comments') ]
    230 }}}
    231 
    232 This is, in my humble opinion, a lot simpler than adding typed extension points and what not.
    233 
    234154== Identity Crisis? ==
    235155
    236156Eclipse 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).
    237157
    238 ''Eclipes 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
     158''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
    239159
    240160Trac, 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.