1# This is a copy of the with_metaclass function from 'six' from the
2# development branch.  This fixes a bug in six 1.6.1.
3#
4# Copyright (c) 2010-2014 Benjamin Peterson
5#
6# Permission is hereby granted, free of charge, to any person obtaining a copy
7# of this software and associated documentation files (the "Software"), to deal
8# in the Software without restriction, including without limitation the rights
9# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10# copies of the Software, and to permit persons to whom the Software is
11# furnished to do so, subject to the following conditions:
12#
13# The above copyright notice and this permission notice shall be included in
14# all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22# SOFTWARE.
23
24import re
25import sys
26import weakref
27from six import itervalues, string_types
28import logging
29logger = logging.getLogger('pyutilib.component.core')
30
31__all__ = ['Plugin', 'implements', 'Interface', 'CreatePluginFactory',
32           'PluginMeta', 'alias', 'ExtensionPoint', 'SingletonPlugin',
33           'PluginFactory', 'PluginError', 'PluginGlobals', 'with_metaclass',
34           'IPluginLoader', 'IPluginLoadPath', 'IIgnorePluginWhenLoading',
35           'IOptionDataProvider', 'PluginEnvironment']
36
37# print "ZZZ - IMPORTING CORE"
38
39
40def with_metaclass(meta, *bases):
41    """Create a base class with a metaclass."""
42
43    # This requires a bit of explanation: the basic idea is to make a
44    # dummy metaclass for one level of class instantiation that replaces
45    # itself with the actual metaclass.  Because of internal type checks
46    # we also need to make sure that we downgrade the custom metaclass
47    # for one level to something closer to type (that's why __call__ and
48    # __init__ comes back from type etc.).
49    class metaclass(meta):
50        __call__ = type.__call__
51        __init__ = type.__init__
52
53        def __new__(cls, name, this_bases, d):
54            if this_bases is None:
55                return type.__new__(cls, name, (), d)
56            return meta(name, bases, d)
57
58    return metaclass('temporary_class', None, {})
59
60#
61# Plugins define within Pyomo
62#
63
64
65#
66# Define the default logging behavior for a given namespace, which is to
67# ignore the log messages.
68#
69def logger_factory(namespace):
70    log = logging.getLogger('pyutilib.component.core.' + namespace)
71
72    class NullHandler(logging.Handler):
73
74        def emit(self, record):  # pragma:nocover
75            """Do not generate logging record"""
76
77    log.addHandler(NullHandler())
78    return log
79
80
81class PluginError(Exception):
82    """Exception base class for plugin errors."""
83
84    def __init__(self, value):
85        """Constructor, whose argument is the error message"""
86        self.value = value
87
88    def __str__(self):
89        """Return a string value for this message"""
90        return str(self.value)
91
92
93class PluginEnvironment(object):
94
95    def __init__(self, name=None, bootstrap=False):
96        if bootstrap:
97            self.env_id = 1
98        else:
99            PluginGlobals.env_counter += 1
100            self.env_id = PluginGlobals.env_counter
101            if name is None:
102                name = "env%d" % PluginGlobals.env_counter
103            if name in PluginGlobals.env:
104                raise PluginError("Environment %s is already defined" % name)
105        #
106        self.loaders = None
107        self.loader_paths = None
108        #
109        self.name = name
110        self.log = logger_factory(self.name)
111        if __debug__ and self.log.isEnabledFor(logging.DEBUG):
112            self.log.debug("Creating PluginEnvironment %r" % self.name)
113        # A dictionary of plugin classes
114        #   name -> plugin cls
115        self.plugin_registry = {}
116        # The interfaces that have been defined
117        #   name -> interface cls
118        self.interfaces = {}
119        # A dictionary of singleton plugin class instance ids
120        #   plugin cls -> id
121        self.singleton_services = {}
122        # A set of nonsingleton plugin instances
123        self.nonsingleton_plugins = set()
124
125    def cleanup(self, singleton=True):
126        # ZZZ
127        # return
128        if PluginGlobals is None or PluginGlobals.plugin_instances is None:
129            return
130        if singleton:
131            for id_ in itervalues(self.singleton_services):
132                if (id_ in PluginGlobals.plugin_instances and
133                        PluginGlobals.plugin_instances[id_] is not None):
134                    del PluginGlobals.plugin_instances[id_]
135            self.singleton_services = {}
136        #
137        for id_ in self.nonsingleton_plugins:
138            del PluginGlobals.plugin_instances[id_]
139        self.nonsingleton_plugins = set()
140
141    def plugins(self):
142        for id_ in itervalues(self.singleton_services):
143            if (id_ in PluginGlobals.plugin_instances and
144                    PluginGlobals.plugin_instances[id_] is not None):
145                yield PluginGlobals.plugin_instances[id_]
146        for id_ in sorted(self.nonsingleton_plugins):
147            yield PluginGlobals.plugin_instances[id_]
148
149    def load_services(self, path=None, auto_disable=False, name_re=True):
150        """Load services from IPluginLoader extension points"""
151
152        if self.loaders is None:
153            self.loaders = ExtensionPoint(IPluginLoader)
154            self.loader_paths = ExtensionPoint(IPluginLoadPath)
155        #
156        # Construct the search path
157        #
158        search_path = []
159        if path is not None:
160            if isinstance(path, string_types):
161                search_path.append(path)
162            elif type(path) is list:
163                search_path += path
164            else:
165                raise PluginError("Unknown type of path argument: " + str(
166                    type(path)))
167        for item in self.loader_paths:
168            search_path += item.get_load_path()
169        self.log.info("Loading services to environment "
170                      "%s from search path %s" % (self.name, search_path))
171        #
172        # Compile the enable expression
173        #
174        if type(auto_disable) is bool:
175            if auto_disable:
176                disable_p = re.compile("")
177            else:
178                disable_p = re.compile("^$")
179        else:
180            disable_p = re.compile(auto_disable)
181        #
182        # Compile the name expression
183        #
184        if type(name_re) is bool:
185            if name_re:
186                name_p = re.compile("")
187            else:  # pragma:nocover
188                raise PluginError(
189                    "It doesn't make sense to specify name_re=False")
190        else:
191            name_p = re.compile(name_re)
192        #
193        for loader in self.loaders:
194            loader.load(self, search_path, disable_p, name_p)
195        # self.clear_cache()
196
197    def Xclear_cache(self):
198        """Clear the cache of active services"""
199        self._cache = {}
200
201
202class ExtensionPoint(object):
203    """Marker class for extension points in services."""
204
205    def __init__(self, *args):
206        """Create the extension point.
207
208        @param interface: the `Interface` subclass that defines the protocol
209            for the extension point
210        """
211        #
212        # Construct the interface, passing in this extension
213        #
214        nargs = len(args)
215        if nargs == 0:
216            raise PluginError(
217                "Must specify interface class used in the ExtensionPoint")
218        self.interface = args[0]
219        self.__doc__ = 'List of services that implement `%s`' % \
220            self.interface.__name__
221
222    def __iter__(self):
223        """Return an iterator to a set of services that match the interface of
224        this extension point.
225        """
226        return self.extensions().__iter__()
227
228    def __call__(self, key=None, all=False):
229        """Return a set of services that match the interface of this
230        extension point.
231        """
232        if type(key) in (int, int):
233            raise PluginError("Access of the n-th extension point is "
234                              "disallowed.  This is not well-defined, since "
235                              "ExtensionPoints are stored as unordered sets.")
236        return self.extensions(all=all, key=key)
237
238    def service(self, key=None, all=False):
239        """Return the unique service that matches the interface of this
240        extension point.  An exception occurs if no service matches the
241        specified key, or if multiple services match.
242        """
243        ans = ExtensionPoint.__call__(self, key=key, all=all)
244        if len(ans) == 1:
245            #
246            # There is a single service, so return it.
247            #
248            return ans.pop()
249        elif len(ans) == 0:
250            return None
251        else:
252            raise PluginError("The ExtensionPoint does not have a unique "
253                              "service!  %d services are defined for interface"
254                              " %s.  (key=%s)" %
255                              (len(ans), self.interface.__name__, str(key)))
256
257    def __len__(self):
258        """Return the number of services that match the interface of this
259        extension point.
260        """
261        return len(self.extensions())
262
263    def extensions(self, all=False, key=None):
264        """Return a set of services that match the interface of this
265        extension point.  This tacitly filters out disabled extension
266        points.
267
268        TODO - Can this support caching?
269        How would that relate to the weakref test?
270        """
271        strkey = str(key)
272        ans = set()
273        remove = set()
274        if self.interface in PluginGlobals.interface_services:
275            for id_ in PluginGlobals.interface_services[self.interface]:
276                if id_ not in PluginGlobals.plugin_instances:
277                    remove.add(id_)
278                    continue
279                if id_ < 0:
280                    plugin = PluginGlobals.plugin_instances[id_]
281                else:
282                    plugin = PluginGlobals.plugin_instances[id_]()
283                if plugin is None:
284                    remove.add(id_)
285                elif ((all or plugin.enabled()) and
286                      (key is None or strkey == plugin.name)):
287                    ans.add(plugin)
288            # Remove weakrefs that were empty
289            # ZZ
290            for id_ in remove:
291                PluginGlobals.interface_services[self.interface].remove(id_)
292        return sorted(ans, key=lambda x: x._id)
293
294    def __repr__(self, simple=False):
295        """Return a textual representation of the extension point.
296
297        TODO: use the 'simple' argument
298        """
299        env_str = ""
300        for env_ in itervalues(PluginGlobals.env):
301            if self.interface in set(itervalues(env_.interfaces)):
302                env_str += " env=%s" % env_.name
303        return '<ExtensionPoint %s%s>' % (self.interface.__name__, env_str)
304
305
306class PluginGlobals(object):
307    """Global data for plugins. The main role of this class is to manage
308    the stack of PluginEnvironment instances.
309
310    Note: a single ID counter is used for tagging both environment and
311    plugins registrations.  This enables the  user to track the relative
312    order of construction of these objects.
313    """
314
315    def __init__(self):  # pragma:nocover
316        """Disable construction."""
317        raise PluginError("The PluginGlobals class should not be created.")
318
319    # A dictionary of interface classes mapped to sets of plugin class
320    # instance ids
321    #   interface cls -> set(ids)
322    interface_services = {}
323
324    # A dictionary of plugin instances
325    #   id -> weakref(instance)
326    plugin_instances = {}
327
328    # Environments
329    env = {'pca': PluginEnvironment('pca', bootstrap=True)}
330    env_map = {1: 'pca'}
331    env_stack = ['pca']
332    """A unique id used to name plugin objects"""
333    plugin_counter = 0
334    """A unique id used to name environment objects"""
335    env_counter = 1
336    """A list of executables"""
337    _executables = []
338    """TODO"""
339    _default_OptionData = None
340
341    @staticmethod
342    def get_env(arg=None):
343        """Return the current environment."""
344        if arg is None:
345            return PluginGlobals.env[PluginGlobals.env_stack[-1]]
346        else:
347            return PluginGlobals.env.get(arg, None)
348
349    @staticmethod
350    def add_env(name=None, validate=False):
351        if name is not None and not isinstance(name, string_types):
352            if validate and name.name in PluginGlobals.env:
353                raise PluginError("Environment %s is already defined" % name)
354            # We assume we have a PluginEnvironment object here
355            PluginGlobals.env[name.name] = name
356            PluginGlobals.env_map[name.env_id] = name.name
357            PluginGlobals.env_stack.append(name.name)
358            if __debug__ and name.log.isEnabledFor(logging.DEBUG):
359                name.log.debug("Pushing environment %r on the "
360                               "PluginGlobals stack" % name.name)
361            return name
362        else:
363            env_ = PluginGlobals.env.get(name, None)
364            if validate and env_ is not None:
365                raise PluginError("Environment %s is already defined" % name)
366            if env_ is None:
367                env_ = PluginEnvironment(name)
368                PluginGlobals.env[env_.name] = env_
369            PluginGlobals.env_map[env_.env_id] = env_.name
370            PluginGlobals.env_stack.append(env_.name)
371            if __debug__ and env_.log.isEnabledFor(logging.DEBUG):
372                env_.log.debug("Pushing environment %r on the "
373                               "PluginGlobals stack" % env_.name)
374            return env_
375
376    @staticmethod
377    def pop_env():
378        if len(PluginGlobals.env_stack) > 1:
379            name = PluginGlobals.env_stack.pop()
380            env_ = PluginGlobals.env[name]
381            if __debug__ and env_.log.isEnabledFor(logging.DEBUG):
382                env_.log.debug("Popping environment %r from the "
383                               "PluginGlobals stack" % env_.name)
384            return env_
385        else:
386            return PluginGlobals.env[PluginGlobals.env_stack[0]]
387
388    @staticmethod
389    def remove_env(name, cleanup=False, singleton=True):
390        tmp = PluginGlobals.env.get(name, None)
391        if tmp is None:
392            raise PluginError("No environment %s is defined" % name)
393        # print "HERE - remove", name, tmp.env_id
394        del PluginGlobals.env_map[tmp.env_id]
395        del PluginGlobals.env[name]
396        if cleanup:
397            tmp.cleanup(singleton=singleton)
398        PluginGlobals.env_stack = [name_ for name_ in PluginGlobals.env_stack
399                                   if name_ in PluginGlobals.env]
400        return tmp
401
402    @staticmethod
403    def clear():
404        # ZZ
405        # return
406        for env_ in itervalues(PluginGlobals.env):
407            env_.cleanup()
408        PluginGlobals.interface_services = {}
409        PluginGlobals.plugin_instances = {}
410        PluginGlobals.env = {'pca': PluginEnvironment('pca', bootstrap=True)}
411        PluginGlobals.env_map = {1: 'pca'}
412        PluginGlobals.env_stack = ['pca']
413        PluginGlobals.plugin_counter = 0
414        PluginGlobals.env_counter = 1
415        PluginGlobals._executables = []
416
417    @staticmethod
418    def clear_global_data(keys=None):
419        # ZZ
420        # return
421        ep = ExtensionPoint(IOptionDataProvider)
422        for ep_ in ep:
423            ep_.clear(keys=keys)
424
425    @staticmethod
426    def services(name=None):
427        """A convenience function that returns the services in the
428        current environment.
429
430        TODO:  env-specific services?
431        """
432        ans = set()
433        for ids in itervalues(PluginGlobals.interface_services):
434            for id_ in ids:
435                if id_ not in PluginGlobals.plugin_instances:
436                    # TODO: discard the id from the set?
437                    continue
438                if id_ < 0:
439                    ans.add(PluginGlobals.plugin_instances[id_])
440                else:
441                    ans.add(PluginGlobals.plugin_instances[id_]())
442        return ans
443
444    @staticmethod
445    def load_services(**kwds):
446        """Load services from IPluginLoader extension points"""
447        PluginGlobals.get_env().load_services(**kwds)
448
449    @staticmethod
450    def pprint(**kwds):
451        """A pretty-print function"""
452
453        ans = {}
454        s = "#------------------------------"\
455            "--------------------------------\n"
456        i = 1
457        ans['Environment Stack'] = {}
458        s += "Environment Stack:\n"
459        for env in PluginGlobals.env_stack:
460            ans['Environment Stack'][i] = env
461            s += "  '" + str(i) + "': " + env + "\n"
462            i += 1
463        s += "#------------------------------"\
464             "--------------------------------\n"
465        ans['Interfaces Declared'] = {}
466        s += "Interfaces Declared:\n"
467        keys = []
468        for env_ in itervalues(PluginGlobals.env):
469            keys.extend(interface_ for interface_ in env_.interfaces)
470        keys.sort()
471        for key in keys:
472            ans['Interfaces Declared'][key] = None
473            s += "  " + key + ":\n"
474        s += "#------------------------------"\
475             "--------------------------------\n"
476        ans['Interfaces Declared by Environment'] = {}
477        s += "Interfaces Declared by Environment:\n"
478        for env_name in sorted(PluginGlobals.env.keys()):
479            env_ = PluginGlobals.env[env_name]
480            if len(env_.interfaces) == 0:
481                continue
482            ans['Interfaces Declared by Environment'][env_.name] = {}
483            s += "  " + env_.name + ":\n"
484            for interface_ in sorted(env_.interfaces.keys()):
485                ans['Interfaces Declared by Environment'][env_.name][
486                    interface_] = None
487                s += "    " + interface_ + ":\n"
488        #
489        # Coverage is disabled here because different platforms give different
490        # results.
491        #
492        if kwds.get('plugins', True):  # pragma:nocover
493            s += "#---------------------------"\
494                 "-----------------------------------\n"
495            ans['Plugins by Environment'] = {}
496            s += "Plugins by Environment:\n"
497            for env_name in sorted(PluginGlobals.env.keys()):
498                env_ = PluginGlobals.env[env_name]
499                ans['Plugins by Environment'][env_.name] = {}
500                s += "  " + env_.name + ":\n"
501                flag = True
502                for service_ in env_.plugins():
503                    flag = False
504                    try:
505                        service_.name
506                    except:
507                        service_ = service_()
508                    service_active = False
509                    for interface in service_.__interfaces__:
510                        if interface not in PluginGlobals.interface_services:
511                            continue
512                        service_active = service_._id in \
513                            PluginGlobals.interface_services[interface]
514                        break
515                    service_s = service_.__repr__(
516                        simple=not kwds.get('show_ids', True))
517                    ans['Plugins by Environment'][env_.name][service_s] = {}
518                    s += "    " + service_s + ":\n"
519                    if kwds.get('show_ids', True):
520                        ans['Plugins by Environment'][env_.name][service_s][
521                            'name'] = service_.name
522                        s += "       name:      " + service_.name + "\n"
523                    ans['Plugins by Environment'][env_.name][service_s][
524                        'id'] = str(service_._id)
525                    s += "       id:        " + str(service_._id) + "\n"
526                    ans['Plugins by Environment'][env_.name][service_s][
527                        'singleton'] = str(service_.__singleton__)
528                    s += "       singleton: " + \
529                        str(service_.__singleton__) + "\n"
530                    ans['Plugins by Environment'][env_.name][service_s][
531                        'service'] = str(service_active)
532                    s += "       service:   " + str(service_active) + "\n"
533                    ans['Plugins by Environment'][env_.name][service_s][
534                        'disabled'] = str(not service_.enabled())
535                    s += "       disabled:  " + \
536                        str(not service_.enabled()) + "\n"
537                if flag:
538                    s += "       None:\n"
539        s += "#------------------------------"\
540             "--------------------------------\n"
541        ans['Plugins by Interface'] = {}
542        s += "Plugins by Interface:\n"
543        tmp = {}
544        for env_ in itervalues(PluginGlobals.env):
545            for interface_ in itervalues(env_.interfaces):
546                tmp[interface_] = []
547        for env_ in itervalues(PluginGlobals.env):
548            for key in env_.plugin_registry:
549                for item in env_.plugin_registry[key].__interfaces__:
550                    if item in tmp:
551                        tmp[item].append(key)
552        keys = list(tmp.keys())
553        for key in sorted(keys, key=lambda v: v.__name__.upper()):
554            ans['Plugins by Interface'][str(key.__name__)] = {}
555            if key.__name__ == "":  # pragma:nocover
556                s += "  `" + str(key.__name__) + "`:\n"
557            else:
558                s += "  " + str(key.__name__) + ":\n"
559            ttmp = tmp[key]
560            ttmp.sort()
561            if len(ttmp) == 0:
562                s += "    None:\n"
563            else:
564                for item in ttmp:
565                    ans['Plugins by Interface'][str(key.__name__)][item] = None
566                    s += "    " + item + ":\n"
567        s += "#-------------------------------"\
568             "-------------------------------\n"
569        ans['Plugins by Python Module'] = {}
570        s += "Plugins by Python Module:\n"
571        tmp = {}
572        for env_ in itervalues(PluginGlobals.env):
573            for name_ in env_.plugin_registry:
574                tmp.setdefault(env_.plugin_registry[name_].__module__,
575                               []).append(name_)
576        if '__main__' in tmp:
577            # This is a hack to ensure consistency in the tests
578            tmp['pyutilib.component.core.tests.test_core'] = tmp['__main__']
579            del tmp['__main__']
580        keys = list(tmp.keys())
581        keys.sort()
582        for key in keys:
583            ans['Plugins by Python Module'][str(key)] = {}
584            if key == "":  # pragma:nocover
585                s += "  `" + str(key) + "`:\n"
586            else:
587                s += "  " + str(key) + ":\n"
588            ttmp = tmp[key]
589            ttmp.sort()
590            for item in ttmp:
591                ans['Plugins by Python Module'][str(key)][item] = None
592                s += "    " + item + ":\n"
593        if kwds.get('json', False):
594            import json
595            print(
596                json.dumps(
597                    ans, sort_keys=True, indent=4, separators=(',', ': ')))
598        else:
599            print(s)
600
601    @staticmethod
602    def display(interface=None, verbose=False):
603        print("Plugin Instances:", len(PluginGlobals.plugin_instances))
604        if interface is not None:
605            print("Interface:", interface.name)
606            print("Count:",
607                  len(PluginGlobals.interface_services.get(interface, [])))
608            if verbose:
609                for service in interface.services.get(interface, []):
610                    print(service)
611        else:
612            print("Interfaces", len(PluginGlobals.interface_services))
613            for interface in PluginGlobals.interface_services:
614                print("  Interface:", interface)
615                print("  Count:",
616                      len(PluginGlobals.interface_services.get(interface, [])))
617                if verbose:
618                    for service in PluginGlobals.interface_services.get(
619                            interface, []):
620                        print("     ", PluginGlobals.plugin_instances[service])
621        #
622        print("")
623        for env_ in itervalues(PluginGlobals.env):
624            print("Plugin Declarations:", env_.name)
625            for interface in sorted(
626                    env_.interfaces.keys(), key=lambda v: v.upper()):
627                print("Interface:", interface)
628                # print "Aliases:"
629                # for alias in sorted(interface._factory_cls.keys(),
630                #                     key=lambda v: v.upper()):
631                #     print "   ",alias,interface._factory_cls[alias]
632
633
634class InterfaceMeta(type):
635    """Meta class that registered the declaration of an interface"""
636
637    def __new__(cls, name, bases, d):
638        """Register this interface"""
639        new_class = type.__new__(cls, name, bases, d)
640        if name != "Interface":
641            if name in PluginGlobals.get_env().interfaces:
642                raise PluginError("Interface %s has already been defined" %
643                                  name)
644            PluginGlobals.get_env().interfaces[name] = new_class
645        return new_class
646
647
648class Interface(with_metaclass(InterfaceMeta, object)):
649    """Marker base class for extension point interfaces.  This class
650    is not intended to be instantiated.  Instead, the declaration
651    of subclasses of Interface are recorded, and these
652    classes are used to define extension points.
653    """
654    pass
655
656
657class IPluginLoader(Interface):
658    """An interface for loading plugins."""
659
660    def load(self, env, path, disable_re, name_re):
661        """Load plugins found on the specified path.  If disable_re is
662        not none, then it is interpreted as a regular expression.  If
663        this expression matches the path of a plugin, then that plugin
664        is disabled. Otherwise, the plugin is enabled by default.
665        """
666
667
668class IPluginLoadPath(Interface):
669
670    def get_load_path(self):
671        """Returns a list of paths that are searched for plugins"""
672
673
674class IIgnorePluginWhenLoading(Interface):
675    """Interface used by Plugin loaders to identify Plugins that should
676    be ignored"""
677
678    def ignore(self, name):
679        """Returns true if a loader should ignore a plugin during loading"""
680
681
682class IOptionDataProvider(Interface):
683    """An interface that supports the management of common data between
684    Options.  This interface is also used to share this data with
685    the Configuration class.
686    """
687
688    def get_data(self):
689        """Returns a dictionary of dictionaries that represents the
690        options data."""
691
692    def set(self, section, name, value):
693        """Sets the value of a given (section,name) pair"""
694
695    def get(self, section, name):
696        """Returns the value of a given (section,name) pair"""
697
698    def clear(self):
699        """Clears the data"""
700
701
702class PluginMeta(type):
703    """Meta class for the Plugin class.  This meta class
704    takes care of service and extension point registration.  This class
705    also instantiates singleton plugins.
706    """
707
708    def __new__(cls, name, bases, d):
709        """Find all interfaces that need to be registered."""
710        #
711        # Avoid cycling in the Python logic by hard-coding the behavior
712        # for the Plugin and SingletonPlugin classes.
713        #
714        if name == "Plugin":
715            d['__singleton__'] = False
716            return type.__new__(cls, name, bases, d)
717        if name == "SingletonPlugin":
718            d['__singleton__'] = True
719            return type.__new__(cls, name, bases, d)
720        if name == "ManagedSingletonPlugin":
721            #
722            # This is a derived class of SingletonPlugin for which
723            # we do not need to build an instance
724            #
725            d['__singleton__'] = True
726            return type.__new__(cls, name, bases, d)
727        #
728        # Check if plugin has already been registered
729        #
730        if len(d.get('_implements', [])) == 0 and (
731                name in PluginGlobals.get_env().plugin_registry):
732            return PluginGlobals.get_env().plugin_registry[name]
733        #
734        # Find all interfaces that this plugin will support
735        #
736        __interfaces__ = {}
737        for interface in d.get('_implements', {}):
738            __interfaces__.setdefault(interface,
739                                      []).extend(d['_implements'][interface])
740        for base in [base for base in bases if hasattr(base, '__interfaces__')]:
741            for interface in base.__interfaces__:
742                __interfaces__.setdefault(
743                    interface, []).extend(base.__interfaces__[interface])
744        d['__interfaces__'] = __interfaces__
745        #
746        # Create a boolean, which indicates whether this is
747        # a singleton class.
748        #
749        if True in [issubclass(x, SingletonPlugin) for x in bases]:
750            d['__singleton__'] = True
751        else:
752            d['__singleton__'] = False
753        #
754        # Add interfaces to the list of base classes if they are
755        # declared inherited.
756        #
757        flag = False
758        bases = list(bases)
759        for interface in d.get('_inherited_interfaces', set()):
760            if interface not in bases:
761                bases.append(interface)
762                flag = True
763        if flag:
764            cls = MergedPluginMeta
765        #
766        # Create new class
767        #
768        try:
769            new_class = type.__new__(cls, name, tuple(bases), d)
770        except:
771            raise
772        setattr(new_class, '__name__', name)
773        #
774        for _interface in __interfaces__:
775            if getattr(_interface, '_factory_active', None) is None:
776                continue
777            for _name, _doc, _subclass in getattr(new_class, "_factory_aliases",
778                                                  []):
779                if _name in _interface._factory_active:
780                    if _subclass:
781                        continue
782                    else:
783                        raise PluginError("Alias '%s' has already been "
784                                          "defined for interface '%s'" %
785                                          (_name, str(_interface)))
786                _interface._factory_active[_name] = name
787                _interface._factory_doc[_name] = _doc
788                _interface._factory_cls[_name] = new_class
789        #
790        if d['__singleton__']:
791            #
792            # Here, we create an instance of a singleton class, which
793            # registers itself in singleton_services
794            #
795            PluginGlobals.get_env().singleton_services[new_class] = True
796            __instance__ = new_class()
797            PluginGlobals.plugin_instances[__instance__._id] = __instance__
798            PluginGlobals.get_env().singleton_services[new_class] = \
799                __instance__._id
800        else:
801            __instance__ = None
802        #
803        # Register this plugin
804        #
805        PluginGlobals.get_env().plugin_registry[name] = new_class
806        return new_class
807
808
809class MergedPluginMeta(PluginMeta, InterfaceMeta):
810
811    def __new__(cls, name, bases, d):
812        return PluginMeta.__new__(cls, name, bases, d)
813
814
815class Plugin(with_metaclass(PluginMeta, object)):
816    """Base class for plugins.  A 'service' is an instance of a Plugin.
817
818    Every Plugin class can declare what extension points it provides, as
819    well as what extension points of other Plugins it extends.
820    """
821
822    def __del__(self):
823        # print "HERE - plugin __del__", self._id, self.name,
824        #       self.__class__.__name__
825        # ZZZ
826        # return
827        self.deactivate()
828        if (PluginGlobals is not None and
829                PluginGlobals.plugin_instances is not None and
830                self._id in PluginGlobals.plugin_instances and
831                PluginGlobals.plugin_instances[self._id] is not None):
832            # print "HERE - plugin __del__", self._id
833            # print "interface_services", PluginGlobals.interface_services
834            # print "HERE", self.name, self.__class__.__name__
835            del PluginGlobals.plugin_instances[self._id]
836        if (PluginGlobals is not None and PluginGlobals.env_map is not None and
837                self._id_env in PluginGlobals.env_map):
838            PluginGlobals.env[PluginGlobals.env_map[
839                self._id_env]].nonsingleton_plugins.discard(self._id)
840
841    def __init__(self, **kwargs):
842        if "name" in kwargs:
843            self.name = kwargs["name"]
844
845    def __new__(cls, *args, **kwargs):
846        """Plugin constructor"""
847        #
848        # If this service is a singleton, then allocate and configure
849        # it differently.
850        #
851        if cls in PluginGlobals.get_env().singleton_services:  # pragma:nocover
852            id = PluginGlobals.get_env().singleton_services[cls]
853            if id is True:
854                self = super(Plugin, cls).__new__(cls)
855                PluginGlobals.plugin_counter += 1
856                self._id = -PluginGlobals.plugin_counter
857                self._id_env = PluginGlobals.get_env().env_id
858                self.name = self.__class__.__name__
859                self._enable = True
860                self.activate()
861            else:
862                self = PluginGlobals.plugin_instances[id]
863            # print "HERE - Plugin singleton:",
864            #       self._id, self.name, self._id_env
865            return self
866        #
867        # Else we generate a normal plugin
868        #
869        self = super(Plugin, cls).__new__(cls)
870        PluginGlobals.plugin_counter += 1
871        self._id = PluginGlobals.plugin_counter
872        self._id_env = PluginGlobals.get_env().env_id
873        self.name = "Plugin." + str(self._id)
874        PluginGlobals.get_env().nonsingleton_plugins.add(self._id)
875        # print "HERE - Normal Plugin:", self._id, self.name,
876        #   self.__class__.__name__, self._id_env
877        self._enable = True
878        PluginGlobals.plugin_instances[self._id] = weakref.ref(self)
879        if getattr(cls, '_service', True):
880            # self._HERE_ = self._id
881            self.activate()
882        return self
883
884    def activate(self):
885        """Register this plugin with all interfaces that it implements."""
886        for interface in self.__interfaces__:
887            PluginGlobals.interface_services.setdefault(interface,
888                                                        set()).add(self._id)
889
890    def deactivate(self):
891        """Unregister this plugin with all interfaces that it implements."""
892        # ZZ
893        # return
894        if PluginGlobals is None or PluginGlobals.interface_services is None:
895            # This could happen when python quits
896            return
897        # for interface in self.__interfaces__:
898        # if interface in PluginGlobals.interface_services:
899        for interface in PluginGlobals.interface_services:
900            # Remove an element if it exists
901            PluginGlobals.interface_services[interface].discard(self._id)
902
903    #
904    # Support "with" statements. Forgetting to call deactivate
905    # on Plugins is a common source of memory leaks
906    #
907    def __enter__(self):
908        return self
909
910    def __exit__(self, type, value, traceback):
911        self.deactivate()
912
913    @staticmethod
914    def alias(name, doc=None, subclass=False):
915        """This function is used to declare aliases that can be used by a
916        factory for constructing plugin instances.
917
918        When the subclass option is True, then subsequent calls to
919        alias() with this class name are ignored, because they are
920        assumed to be due to subclasses of the original class declaration.
921        """
922        frame = sys._getframe(1)
923        locals_ = frame.f_locals
924        assert locals_ is not frame.f_globals and '__module__' in locals_, \
925            'alias() can only be used in a class definition'
926        locals_.setdefault('_factory_aliases', set()).add((name, doc, subclass))
927
928    @staticmethod
929    def implements(interface, inherit=None, namespace=None, service=False):
930        """Can be used in the class definition of `Plugin` subclasses to
931        declare the extension points that are implemented by this
932        interface class.
933        """
934        frame = sys._getframe(1)
935        locals_ = frame.f_locals
936        #
937        # Some sanity checks
938        #
939        assert namespace is None or isinstance(namespace, str), \
940            'second implements() argument must be a string'
941        assert locals_ is not frame.f_globals and '__module__' in locals_, \
942            'implements() can only be used in a class definition'
943        #
944        locals_.setdefault('_implements', {}).setdefault(interface,
945                                                         []).append(namespace)
946        if inherit:
947            locals_.setdefault('_inherited_interfaces', set()).add(interface)
948        locals_['_service'] = service
949
950    def disable(self):
951        """Disable this plugin"""
952        self._enable = False
953
954    def enable(self):
955        """Enable this plugin"""
956        self._enable = True
957
958    def enabled(self):
959        """Return value indicating if this plugin is enabled"""
960        enable = self._enable
961        if hasattr(enable, 'get_value'):
962            try:
963                enable = enable.get_value()
964            except:
965                enable = None
966        return enable
967
968    def __repr__(self, simple=False):
969        """Return a textual representation of the plugin."""
970        if simple or self.__class__.__name__ == self.name:
971            return '<Plugin %s>' % (self.__class__.__name__)
972        else:
973            return '<Plugin %s %r>' % (self.__class__.__name__, self.name)
974
975
976alias = Plugin.alias
977implements = Plugin.implements
978
979
980class SingletonPlugin(Plugin):
981    """The base class for singleton plugins.  The PluginMeta class
982    instantiates a SingletonPlugin class when it is declared.  Note that
983    only one instance of a SingletonPlugin class is created in
984    any environment.
985    """
986    pass
987
988
989def CreatePluginFactory(_interface):
990    if getattr(_interface, '_factory_active', None) is None:
991        setattr(_interface, '_factory_active', {})
992        setattr(_interface, '_factory_doc', {})
993        setattr(_interface, '_factory_cls', {})
994        setattr(_interface, '_factory_deactivated', {})
995
996    class PluginFactoryFunctor(object):
997
998        def __call__(self, _name=None, args=[], **kwds):
999            if _name is None:
1000                return self
1001            _name = str(_name)
1002            if _name not in _interface._factory_active:
1003                return None
1004            return PluginFactory(_interface._factory_cls[_name], args, **kwds)
1005
1006        def services(self):
1007            return list(_interface._factory_active.keys())
1008
1009        def get_class(self, name):
1010            return _interface._factory_cls[name]
1011
1012        def doc(self, name):
1013            tmp = _interface._factory_doc[name]
1014            if tmp is None:
1015                return ""
1016            return tmp
1017
1018        def deactivate(self, name):
1019            if name in _interface._factory_active:
1020                _interface._factory_deactivated[
1021                    name] = _interface._factory_active[name]
1022                del _interface._factory_active[name]
1023
1024        def activate(self, name):
1025            if name in _interface._factory_deactivated:
1026                _interface._factory_active[
1027                    name] = _interface._factory_deactivated[name]
1028                del _interface._factory_deactivated[name]
1029
1030    return PluginFactoryFunctor()
1031
1032
1033def PluginFactory(classname, args=[], **kwds):
1034    """Construct a Plugin instance, and optionally assign it a name"""
1035
1036    if isinstance(classname, str):
1037        try:
1038            cls = PluginGlobals.get_env(kwds.get('env', None)).plugin_registry[
1039                classname]
1040        except KeyError:
1041            raise PluginError("Unknown class %r" % str(classname))
1042    else:
1043        cls = classname
1044    obj = cls(*args, **kwds)
1045    if 'name' in kwds:
1046        obj.name = kwds['name']
1047    if __debug__ and logger.isEnabledFor(logging.DEBUG):
1048        if obj is None:
1049            logger.debug("Failed to create plugin %s" % (classname))
1050        else:
1051            logger.debug("Creating plugin %s with name %s" % (classname,
1052                                                              obj.name))
1053    return obj
1054