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