1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2003-2021 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 286