Edgewall Software

source: trunk/trac/core.py

Last change on this file was 17657, checked in by Jun Omae, 15 months ago

1.5.4dev: update copyright year to 2023 (refs #13402)

[skip ci]

  • Property svn:eol-style set to native
File size: 9.7 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2023 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 https://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 https://trac.edgewall.org/log/.
15#
16# Author: Jonas Borgström <jonas@edgewall.com>
17# Christopher Lenz <cmlenz@gmx.de>
18
19import sys
20
21__all__ = ['Component', 'ExtensionPoint', 'implements', 'Interface',
22 'TracBaseError', 'TracError', 'TracValueError']
23
24
25def N_(string):
26 """No-op translation marker, inlined here to avoid importing from
27 `trac.util`.
28 """
29 return string
30
31
32class TracBaseError(Exception):
33 """Base class for all exceptions defined in Trac."""
34
35 title = N_("Trac Error")
36
37
38class TracError(TracBaseError):
39 """Standard exception for errors in Trac."""
40
41 def __init__(self, message, title=None, show_traceback=False):
42 """If the `message` contains a `p` or `div` element it will be
43 rendered directly. Use the `message` class on the `p` or `div`
44 element to style as a red box. Otherwise, the message should be
45 plain text or contain only inline elements and will be wrapped
46 in a `p` element and rendered in a red box.
47
48 If title is given, it will be displayed as the large header
49 above the error message.
50 """
51 from trac.util.translation import gettext
52 super().__init__(message)
53 self._message = message
54 self.title = title or gettext(self.title)
55 self.show_traceback = show_traceback
56
57 message = property(lambda self: self._message,
58 lambda self, v: setattr(self, '_message', v))
59
60 def __str__(self):
61 from trac.util.text import to_unicode
62 return to_unicode(self.message)
63
64
65class TracValueError(TracError, ValueError):
66 """Raised when a function or operator receives an argument that is
67 the correct type, but inappropriate value.
68
69 :since: 1.2.1
70 """
71
72
73class Interface(object):
74 """Marker base class for extension point interfaces."""
75
76
77class ExtensionPoint(property):
78 """Marker class for extension points in components."""
79
80 def __init__(self, interface):
81 """Create the extension point.
82
83 :param interface: the `Interface` subclass that defines the
84 protocol for the extension point
85 """
86 property.__init__(self, self.extensions)
87 self.interface = interface
88 self.__doc__ = ("List of components that implement `~%s.%s`" %
89 (self.interface.__module__, self.interface.__name__))
90
91 def extensions(self, component):
92 """Return a list of components that declare to implement the
93 extension point interface.
94 """
95 classes = ComponentMeta._registry.get(self.interface, ())
96 components = [component.compmgr[cls] for cls in classes]
97 return [c for c in components if c]
98
99 def __repr__(self):
100 """Return a textual representation of the extension point."""
101 return '<ExtensionPoint %s>' % self.interface.__name__
102
103
104class ComponentMeta(type):
105 """Meta class for components.
106
107 Takes care of component and extension point registration.
108 """
109 _components = []
110 _registry = {}
111
112 def __new__(mcs, name, bases, d):
113 """Create the component class."""
114
115 new_class = type.__new__(mcs, name, bases, d)
116 if name == 'Component':
117 # Don't put the Component base class in the registry
118 return new_class
119
120 if d.get('abstract'):
121 # Don't put abstract component classes in the registry
122 return new_class
123
124 ComponentMeta._components.append(new_class)
125 registry = ComponentMeta._registry
126 for cls in new_class.__mro__:
127 for interface in cls.__dict__.get('_implements', ()):
128 classes = registry.setdefault(interface, [])
129 if new_class not in classes:
130 classes.append(new_class)
131
132 return new_class
133
134 def __call__(cls, *args, **kwargs):
135 """Return an existing instance of the component if it has
136 already been activated, otherwise create a new instance.
137 """
138 # If this component is also the component manager, just invoke that
139 if issubclass(cls, ComponentManager):
140 self = cls.__new__(cls)
141 self.compmgr = self
142 self.__init__(*args, **kwargs)
143 return self
144
145 # The normal case where the component is not also the component manager
146 assert len(args) >= 1 and isinstance(args[0], ComponentManager), \
147 "First argument must be a ComponentManager instance"
148 compmgr = args[0]
149 self = compmgr.components.get(cls)
150 # Note that this check is racy, we intentionally don't use a
151 # lock in order to keep things simple and avoid the risk of
152 # deadlocks, as the impact of having temporarily two (or more)
153 # instances for a given `cls` is negligible.
154 if self is None:
155 self = cls.__new__(cls)
156 self.compmgr = compmgr
157 compmgr.component_activated(self)
158 self.__init__()
159 # Only register the instance once it is fully initialized (#9418)
160 compmgr.components[cls] = self
161 return self
162
163 @classmethod
164 def deregister(cls, component):
165 """Remove a component from the registry."""
166 try:
167 cls._components.remove(component)
168 except ValueError:
169 pass
170 for class_ in component.__mro__:
171 for interface in class_.__dict__.get('_implements', ()):
172 implementers = cls._registry.get(interface)
173 try:
174 implementers.remove(component)
175 except ValueError:
176 pass
177
178
179class Component(object, metaclass=ComponentMeta):
180 """Base class for components.
181
182 Every component can declare what extension points it provides, as
183 well as what extension points of other components it extends.
184 """
185
186 @staticmethod
187 def implements(*interfaces):
188 """Can be used in the class definition of `Component`
189 subclasses to declare the extension points that are extended.
190 """
191 frame = sys._getframe(1)
192 locals_ = frame.f_locals
193
194 # Some sanity checks
195 assert locals_ is not frame.f_globals and '__module__' in locals_, \
196 'implements() can only be used in a class definition'
197
198 locals_.setdefault('_implements', []).extend(interfaces)
199
200 def __repr__(self):
201 """Return a textual representation of the component."""
202 return '<Component %s.%s>' % (self.__class__.__module__,
203 self.__class__.__name__)
204
205implements = Component.implements
206
207
208class ComponentManager(object):
209 """The component manager keeps a pool of active components."""
210
211 def __init__(self):
212 """Initialize the component manager."""
213 self.components = {}
214 self.enabled = {}
215 if isinstance(self, Component):
216 self.components[self.__class__] = self
217
218 def __contains__(self, cls):
219 """Return whether the given class is in the list of active
220 components."""
221 return cls in self.components
222
223 def __getitem__(self, cls):
224 """Activate the component instance for the given class, or
225 return the existing instance if the component has already been
226 activated.
227
228 Note that `ComponentManager` components can't be activated
229 that way.
230 """
231 if not self.is_enabled(cls):
232 return None
233 component = self.components.get(cls)
234 if not component and not issubclass(cls, ComponentManager):
235 if cls not in ComponentMeta._components:
236 raise TracError('Component "%s" not registered' % cls.__name__)
237 try:
238 component = cls(self)
239 except TypeError as e:
240 raise TracError("Unable to instantiate component %r (%s)" %
241 (cls, e))
242 return component
243
244 def is_enabled(self, cls):
245 """Return whether the given component class is enabled."""
246 if cls not in self.enabled:
247 self.enabled[cls] = self.is_component_enabled(cls)
248 return self.enabled[cls]
249
250 def disable_component(self, component):
251 """Force a component to be disabled.
252
253 :param component: can be a class or an instance.
254 """
255 if not isinstance(component, type):
256 component = component.__class__
257 self.enabled[component] = False
258 self.components[component] = None
259
260 def enable_component(self, component):
261 """Force a component to be enabled.
262
263 :param component: can be a class or an instance.
264
265 :since: 1.0.13
266 """
267 if not isinstance(component, type):
268 component = component.__class__
269 self.enabled[component] = True
270
271 def component_activated(self, component):
272 """Can be overridden by sub-classes so that special
273 initialization for components can be provided.
274 """
275
276 def is_component_enabled(self, cls):
277 """Can be overridden by sub-classes to veto the activation of
278 a component.
279
280 If this method returns `False`, the component was disabled
281 explicitly. If it returns `None`, the component was neither
282 enabled nor disabled explicitly. In both cases, the component
283 with the given class will not be available.
284 """
285 return True
Note: See TracBrowser for help on using the repository browser.