Edgewall Software

source: trunk/trac/core.py

Last change on this file was 10612, checked in by cboos, 15 months ago

core: make ExtensionPoint automatic documentation more useful when processed by Sphinx.

  • Property svn:eol-style set to native
File size: 8.0 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2011 Edgewall Software
4# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
5# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution. The terms
10# are also available at http://trac.edgewall.org/wiki/TracLicense.
11#
12# This software consists of voluntary contributions made by many
13# individuals. For the exact contribution history, see the revision
14# history and logs, available at http://trac.edgewall.org/log/.
15#
16# Author: Jonas Borgström <jonas@edgewall.com>
17#         Christopher Lenz <cmlenz@gmx.de>
18
19__all__ = ['Component', 'ExtensionPoint', 'implements', 'Interface',
20           'TracError']
21
22
23def N_(string):
24    """No-op translation marker, inlined here to avoid importing from
25    `trac.util`.
26    """
27    return string
28
29
30class TracError(Exception):
31    """Exception base class for errors in Trac."""
32
33    title = N_('Trac Error')
34   
35    def __init__(self, message, title=None, show_traceback=False):
36        """If message is a genshi.builder.tag object, everything up to
37        the first <p> will be displayed in the red box, and everything
38        after will be displayed below the red box.  If title is given,
39        it will be displayed as the large header above the error
40        message.
41        """
42        from trac.util.translation import gettext
43        Exception.__init__(self, message)
44        self._message = message
45        self.title = title or gettext(self.title)
46        self.show_traceback = show_traceback
47
48    message = property(lambda self: self._message, 
49                       lambda self, v: setattr(self, '_message', v))
50
51    def __unicode__(self):
52        return unicode(self.message)
53
54
55class Interface(object):
56    """Marker base class for extension point interfaces."""
57
58
59class ExtensionPoint(property):
60    """Marker class for extension points in components."""
61
62    def __init__(self, interface):
63        """Create the extension point.
64       
65        :param interface: the `Interface` subclass that defines the
66                          protocol for the extension point
67        """
68        property.__init__(self, self.extensions)
69        self.interface = interface
70        self.__doc__ = ("List of components that implement `~%s.%s`" %
71                        (self.interface.__module__, self.interface.__name__))
72
73    def extensions(self, component):
74        """Return a list of components that declare to implement the
75        extension point interface.
76        """
77        classes = ComponentMeta._registry.get(self.interface, ())
78        components = [component.compmgr[cls] for cls in classes]
79        return [c for c in components if c]
80
81    def __repr__(self):
82        """Return a textual representation of the extension point."""
83        return '<ExtensionPoint %s>' % self.interface.__name__
84
85
86class ComponentMeta(type):
87    """Meta class for components.
88   
89    Takes care of component and extension point registration.
90    """
91    _components = []
92    _registry = {}
93
94    def __new__(mcs, name, bases, d):
95        """Create the component class."""
96
97        new_class = type.__new__(mcs, name, bases, d)
98        if name == 'Component':
99            # Don't put the Component base class in the registry
100            return new_class
101
102        if d.get('abstract'):
103            # Don't put abstract component classes in the registry
104            return new_class
105
106        ComponentMeta._components.append(new_class)
107        registry = ComponentMeta._registry
108        for cls in new_class.__mro__:
109            for interface in cls.__dict__.get('_implements', ()):
110                classes = registry.setdefault(interface, [])
111                if new_class not in classes:
112                    classes.append(new_class)
113
114        return new_class
115
116    def __call__(cls, *args, **kwargs):
117        """Return an existing instance of the component if it has
118        already been activated, otherwise create a new instance.
119        """
120        # If this component is also the component manager, just invoke that
121        if issubclass(cls, ComponentManager):
122            self = cls.__new__(cls)
123            self.compmgr = self
124            self.__init__(*args, **kwargs)
125            return self
126
127        # The normal case where the component is not also the component manager
128        compmgr = args[0]
129        self = compmgr.components.get(cls)
130        # Note that this check is racy, we intentionally don't use a
131        # lock in order to keep things simple and avoid the risk of
132        # deadlocks, as the impact of having temporarily two (or more)
133        # instances for a given `cls` is negligible.
134        if self is None:
135            self = cls.__new__(cls)
136            self.compmgr = compmgr
137            compmgr.component_activated(self)
138            self.__init__()
139            # Only register the instance once it is fully initialized (#9418)
140            compmgr.components[cls] = self
141        return self
142
143
144class Component(object):
145    """Base class for components.
146
147    Every component can declare what extension points it provides, as
148    well as what extension points of other components it extends.
149    """
150    __metaclass__ = ComponentMeta
151
152    @staticmethod
153    def implements(*interfaces):
154        """Can be used in the class definition of `Component`
155        subclasses to declare the extension points that are extended.
156        """
157        import sys
158
159        frame = sys._getframe(1)
160        locals_ = frame.f_locals
161
162        # Some sanity checks
163        assert locals_ is not frame.f_globals and '__module__' in locals_, \
164               'implements() can only be used in a class definition'
165
166        locals_.setdefault('_implements', []).extend(interfaces)
167
168
169implements = Component.implements
170
171
172class ComponentManager(object):
173    """The component manager keeps a pool of active components."""
174
175    def __init__(self):
176        """Initialize the component manager."""
177        self.components = {}
178        self.enabled = {}
179        if isinstance(self, Component):
180            self.components[self.__class__] = self
181
182    def __contains__(self, cls):
183        """Return wether the given class is in the list of active
184        components."""
185        return cls in self.components
186
187    def __getitem__(self, cls):
188        """Activate the component instance for the given class, or
189        return the existing instance if the component has already been
190        activated.
191        """
192        if not self.is_enabled(cls):
193            return None
194        component = self.components.get(cls)
195        if not component:
196            if cls not in ComponentMeta._components:
197                raise TracError('Component "%s" not registered' % cls.__name__)
198            try:
199                component = cls(self)
200            except TypeError, e:
201                raise TracError('Unable to instantiate component %r (%s)' %
202                                (cls, e))
203        return component
204
205    def is_enabled(self, cls):
206        """Return whether the given component class is enabled."""
207        if cls not in self.enabled:
208            self.enabled[cls] = self.is_component_enabled(cls)
209        return self.enabled[cls]
210   
211    def disable_component(self, component):
212        """Force a component to be disabled.
213       
214        :param component: can be a class or an instance.
215        """
216        if not isinstance(component, type):
217            component = component.__class__
218        self.enabled[component] = False
219        self.components[component] = None
220
221    def component_activated(self, component):
222        """Can be overridden by sub-classes so that special
223        initialization for components can be provided.
224        """
225
226    def is_component_enabled(self, cls):
227        """Can be overridden by sub-classes to veto the activation of
228        a component.
229
230        If this method returns `False`, the component was disabled
231        explicitly.  If it returns `None`, the component was neither
232        enabled nor disabled explicitly. In both cases, the component
233        with the given class will not be available.
234        """
235        return True
Note: See TracBrowser for help on using the repository browser.