Changes between Version 10 and Version 11 of TracPluggableModules
- Timestamp:
- Jan 7, 2005, 4:32:44 PM (19 years ago)
Legend:
- Unmodified
- Added
- Removed
- Modified
-
TracPluggableModules
v10 v11 31 31 So assuming this basic model of operation, let's move on to discussing ideas for how this might be implemented. 32 32 33 == The ModuleManager ==33 == The Plug-in Manager == 34 34 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.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. 36 36 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.37 The 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. 38 38 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 == 40 40 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. 41 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. 46 42 47 43 So instead of getting the request object as argument to their constructor, they'd have an interface like: … … 49 45 {{{ 50 46 #!python 51 class Xxx Module(Module):47 class XxxPlugin(Plugin): 52 48 53 49 def __init__(self, env): … … 56 52 pass 57 53 58 def process (self, req):54 def processRequest(self, req): 59 55 # do whatever needs to be done 60 56 pass 61 57 }}} 62 58 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 moduleimplementations should not store request-specific data as instance variables, but rather pass such data through as parameters to other methods.59 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. 64 60 65 ''Renaming the current "{{{render}}}" to "{{{process}}}" here simply because the latter sounds more appropriate.''' 66 67 == Module Cooperation == 61 == Plug-in Cooperation == 68 62 69 63 As discussed above, a basic requirement is that the modules should be able to: … … 71 65 * provide hooks so that other modules can contribute to its functionality 72 66 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): 67 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: 76 68 77 69 {{{ 78 70 #!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 71 from protocols import * 72 from trac.plugin import * 83 73 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. 74 import time 75 76 class 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 93 class 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'] 88 106 }}} 89 107 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. 108 In this example, the {{{TimelinePlugin}}} declares the extension point 109 {{{eventProviders}}}. Extensions to that extension point must support 110 the interface {{{IEventProvider}}} (not shown here). This code currently uses 111 [http://peak.telecommunity.com/PyProtocols.html PyProtocols] to provide 112 a flexible mechanism for interfaces and adaptation. The 113 [http://zope.org/Products/ZopeInterface zope.interface] package has been 114 brought up as an alternative that we're looking into. 91 115 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: 116 The {{{ChangesetPlugin}}} implements the {{{IEventProvider}}} interface 117 with the methods {{{getTimelineEvents()}}} and {{{getTimelineFilters()}}}. 93 118 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. 119 The interesting piece is in {{{TimelinePlugin.processRequest()}}}: Here 120 we can access a "magic" attribute with the same name as the extension 121 point we declared, and iterate over it to access the individual plugins 122 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.'' 102 123 103 124 A couple of drawbacks become visible here: … … 106 127 107 128 This 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 or114 several client interfaces (all ExtensionPoint115 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 flexibility119 (if not even a simpler programming model) than the120 ''hook functions'' approach.121 122 See a more detailed description below123 '''Your Ideas Wanted / Modules and Configurability'''124 125 129 126 130 == Other Aspects of Plug-Ins == … … 148 152 --ChristopherLenz 149 153 150 === Modules and Configurability ===151 152 In some cases, one would like to have a fine control153 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 be156 displayed by the former, as well as specific filters157 for composing the filter panel.158 159 But let's say I would be interested in having160 the Ticket Contributions displayed (See #187 and my161 patch at #890), whereas others wouldn't want this162 option to be available. How to solve this?163 * Subclassing the Ticket module?164 (For the ''hook function approach'': not very165 flexible, what if there are multiple optional166 features)167 * A configuration flag for the Ticket module?168 (This would be more flexible, but, well, would be169 spaghetti code in the end!)170 * Have the Ticket module provide a (configurable) list171 of !TimelineEventProvider objects (the, or one of the172 ExtensionPoint interface that the Timeline module173 would offer)?174 175 The last idea seems the most interesting.176 The Timeline module expects that the177 other modules will provide zero or more178 !TimelineEventProvider object.179 180 The Ticket module would implement all of the following subclasses of !TimelineEventProvider class:181 * !NewAndCloseTicketsInTimeline182 * !ReopenedTicketsInTimeline183 * !TicketContributionsInTimeline184 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 of188 these subclasses will be given to the Timeline instance.189 190 The !TimelineEventProvider and ExtensionPoint could be191 defined as:192 {{{193 #!python194 class ExtensionPoint:195 def module(self):196 """The module providing this client for the timeline"""197 pass198 ...199 200 class TimelineEventProvider(ExtensionPoint):201 def query_string(self):202 """The SQL fragment which provides:203 * time: the timestamp of the event204 * idata: the unique identifier for this event205 * ... and so on206 """207 pass208 209 def render_item(self,item):210 """Populate the item dictionary with hdf data"""211 pass212 ...213 }}}214 215 --ChristianBoos216 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 #!python221 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 them226 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 234 154 == Identity Crisis? == 235 155 236 156 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). 237 157 238 ''Eclip esis 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.'' --ChristopherLenz158 ''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 239 159 240 160 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.