45 | | So instead of getting the request object as argument to their constructor, they'd have an interface like: |
| 45 | 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. |
| 46 | |
| 47 | 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. |
| 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 | |
| 63 | http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/comparch.png?format=raw |
| 64 | |
| 65 | === Declaring a component === |
| 66 | |
| 67 | The simplest possible component is an empty class derived from {{{trac.core.Component}}}: |
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) |
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 | |
| 104 | Direct {{{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 | |
| 108 | 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. |
| 109 | |
| 110 | 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: |
| 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 | |
| 132 | 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. |
| 133 | |
| 134 | The {{{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 | |
| 138 | Now 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 | |
| 149 | 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. |
| 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 | |
| 153 | You 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 | |
| 157 | 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: |
| 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 | |
| 169 | Running 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 | |
| 178 | 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.)'' |
| 179 | |
| 180 | == Current Status == |
| 181 | |
| 182 | The 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 | |
| 188 | The branch also provides an overview of installed plugins, and what extension points are provided and extended: |
| 189 | |
| 190 | http://projects.edgewall.com/trac/attachment/wiki/TracPluggableModules/about_plugins.png?format=raw |
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 | | |