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