Package entropy :: Package core

Source Code for Package entropy.core

  1  # -*- coding: utf-8 -*- 
  2  """ 
  3   
  4      @author: Fabio Erculiani <[email protected]> 
  5      @contact: [email protected] 
  6      @copyright: Fabio Erculiani 
  7      @license: GPL-2 
  8   
  9      B{Entropy Framework core module}. 
 10   
 11      This module contains base classes used by entropy.client, 
 12      entropy.server and entropy.services. 
 13   
 14      "Singleton" is a class that is inherited from singleton objects. 
 15   
 16  """ 
 17  import codecs 
 18  import inspect 
 19  import os 
 20  import re 
 21  import sys 
 22   
 23  from entropy.const import etpConst 
24 25 26 -class Singleton(object):
27 28 """ 29 If your class wants to become a sexy Singleton, 30 subclass this and replace __init__ with init_singleton. 31 Your subclass can expose a method called "is_destroyed()" that 32 returns a bool stating if singleton instance has been destroyed. 33 """ 34
35 - def __new__(cls, *args, **kwds):
36 37 # Support for overridable singleton class used for loading 38 # instance. This is required when your Singleton class is subclassed 39 # and reloaded using different class stack, just push the preferred 40 # singleton class into Class.__singleton_class__ 41 cls = getattr(cls, '__singleton_class__', cls) 42 43 singleton = getattr(cls, '__singleton__', None) 44 if singleton is not None: 45 destroyed = getattr(singleton, 'is_destroyed', None) 46 if destroyed is not None: 47 if not destroyed(): 48 return singleton 49 cls.__singleton__ = None 50 else: 51 return singleton 52 53 # dict support 54 if issubclass(cls, dict): 55 singleton = dict.__new__(cls) 56 else: 57 singleton = object.__new__(cls) 58 singleton.init_singleton(*args, **kwds) 59 cls.__singleton__ = singleton 60 return singleton
61
62 - def __init__(self, *args, **kwargs):
63 """ 64 This is a fake method, necessary for Python 3. 65 """ 66 pass
67
68 -class EntropyPluginStore(object):
69 70 """ 71 This is a base class for handling a map of plugin objects by providing 72 generic add/remove functions. 73 """ 74
75 - def __init__(self):
76 object.__init__(self) 77 self.__plugins = {}
78
79 - def get_plugins(self):
80 """ 81 Return a copy of the internal dictionary that contains the currently 82 stored plugins map. 83 84 @return: stored plugins map 85 @rtype: dict 86 """ 87 return self.__plugins.copy()
88
89 - def drop_plugins(self):
90 """ 91 Drop all the currently stored plugins from storage. 92 """ 93 self.__plugins.clear()
94
95 - def add_plugin(self, plugin_id, plugin_object):
96 """ 97 This method lets you add a plugin to the store. 98 99 @param plugin_id: plugin identifier 100 @type plugin_id: string 101 @param plugin_object: valid SystemSettingsPlugin instance 102 @type plugin_object: any Python object 103 """ 104 self.__plugins[plugin_id] = plugin_object
105
106 - def remove_plugin(self, plugin_id):
107 """ 108 This method lets you remove previously added plugin objects via 109 identifiers. 110 111 @param plugin_id: plugin identifier 112 @type plugin_id: basestring 113 """ 114 del self.__plugins[plugin_id]
115
116 - def has_plugin(self, plugin_id):
117 """ 118 Return whether EntropyPluginStore instance contains the given 119 plugin id. 120 """ 121 return plugin_id in self.__plugins
122
123 124 -class EntropyPluginFactory:
125 126 """ 127 Generic Entropy Components Plugin Factory (loader). 128 """ 129 130 _PLUGIN_SUFFIX = "_plugin" 131 _PYTHON_EXTENSION = ".py" 132
133 - def __init__(self, base_plugin_class, plugin_package_module, 134 default_plugin_name = None, fallback_plugin_name = None, 135 egg_entry_point_group = None):
136 """ 137 Entropy Generic Plugin Factory constructor. 138 MANDATORY: every plugin module/package(name) must end with _plugin 139 suffix. 140 141 Base plugin classes must have the following class attributes set: 142 143 - BASE_PLUGIN_API_VERSION: integer describing API revision in use 144 in class 145 146 Subclasses of Base plugin class must have the following class 147 attributes set: 148 149 - PLUGIN_API_VERSION: integer describing the currently implemented 150 plugin API revision, must match with BASE_PLUGIN_API_VERSION 151 above otherwise plugin won't be loaded and a warning will be 152 printed. 153 154 Moreover, plugin classes must be "Python new-style classes", otherwise 155 parser won't be able to determine if classes have subclasses and thus 156 pick the proper object (one with no subclasses!!). 157 See: http://www.python.org/doc/newstyle -- in other words, you have 158 to inherit the built-in "object" class (yeah, it's called object). 159 So, even if using normal classes could work, if you start doing nasty 160 things (nested inherittance of plugin classes), behaviour cannot 161 be guaranteed. 162 If it's not clear, let me repeat once again, valid plugin classes 163 must not have subclasses around! Think about it, it's an obvious thing. 164 165 If plugin class features a "PLUGIN_DISABLED" class attribute with 166 a boolean value of True, such plugin will be ignored. 167 168 If egg_entry_point_group is specified, Python Egg support is enabled 169 and classes are loaded via this infrastructure. 170 NOTE: if egg_entry_point_group is set, you NEED the setuptools package. 171 172 @param base_plugin_class: Base class that valid plugin classes must 173 inherit from. 174 @type base_plugin_class: class 175 @param plugin_package_module: every plugin repository must work as 176 Python package, the value of this argument must be a valid 177 Python package module that can be scanned looking for valid 178 Entropy Plugin classes. 179 @type plugin_package_module: Python module 180 @keyword default_plugin_name: identifier of the default plugin to load 181 @type default_plugin_name: string 182 @keyword fallback_plugin_name: identifier of the fallback plugin to load 183 if default is not available 184 @type fallback_plugin_name: string 185 @keyword egg_entry_point_group: valid Python Egg entry point group, in 186 this case, Python Egg support is used 187 @type egg_entry_point_group: string 188 @raise AttributeError: when passed plugin_package_module is not a 189 valid Python package module 190 """ 191 self.__modfile = plugin_package_module.__file__ 192 self.__base_class = base_plugin_class 193 self.__plugin_package_module = plugin_package_module 194 self.__default_plugin_name = default_plugin_name 195 self.__fallback_plugin_name = fallback_plugin_name 196 self.__egg_entry_group = egg_entry_point_group 197 self.__cache = None 198 self.__inspect_cache = None
199
200 - def clear_cache(self):
201 """ 202 Clear available plugins cache. When calling get_available_plugins() 203 module object is parsed again. 204 """ 205 self.__cache = None 206 self.__inspect_cache = None
207
208 - def _inspect_object(self, obj):
209 """ 210 This method verifies if given object is a valid plugin. 211 212 @return: True, if valid 213 @rtype: bool 214 """ 215 216 if self.__inspect_cache is None: 217 self.__inspect_cache = {} 218 219 # use memory position 220 obj_memory_pos = id(obj) 221 # To avoid infinite recursion and improve 222 # objects inspection, check if obj has been already 223 # analyzed 224 inspected_rc = self.__inspect_cache.get(obj_memory_pos) 225 if inspected_rc is not None: 226 # avoid infinite recursion 227 return inspected_rc 228 229 base_api = self.__base_class.BASE_PLUGIN_API_VERSION 230 231 if not inspect.isclass(obj): 232 self.__inspect_cache[obj_memory_pos] = False 233 return False 234 235 if not issubclass(obj, self.__base_class): 236 self.__inspect_cache[obj_memory_pos] = False 237 return False 238 239 if hasattr(obj, '__subclasses__'): 240 # new style class 241 if obj.__subclasses__(): # only lower classes taken 242 self.__inspect_cache[obj_memory_pos] = False 243 return False 244 else: 245 sys.stderr.write("!!! Entropy Plugin warning: " \ 246 "%s is not a new style class !!!\n" % (obj,)) 247 248 if obj is self.__base_class: 249 # in this case, obj is our base class, 250 # so we are very sure that obj is not valid 251 self.__inspect_cache[obj_memory_pos] = False 252 return False 253 254 if not hasattr(obj, "PLUGIN_API_VERSION"): 255 sys.stderr.write("!!! Entropy Plugin warning: " \ 256 "no PLUGIN_API_VERSION in %s !!!\n" % (obj,)) 257 self.__inspect_cache[obj_memory_pos] = False 258 return False 259 260 if obj.PLUGIN_API_VERSION != base_api: 261 sys.stderr.write("!!! Entropy Plugin warning: " \ 262 "PLUGIN_API_VERSION mismatch in %s !!!\n" % (obj,)) 263 self.__inspect_cache[obj_memory_pos] = False 264 return False 265 266 if hasattr(obj, 'PLUGIN_DISABLED'): 267 if obj.PLUGIN_DISABLED: 268 # this plugin has been disabled 269 self.__inspect_cache[obj_memory_pos] = False 270 return False 271 272 self.__inspect_cache[obj_memory_pos] = True 273 return True
274
275 - def _scan_dir(self):
276 """ 277 Scan modules in given directory looking for a valid plugin class. 278 Directory is os.path.dirname(self.__modfile). 279 280 @return: module dictionary composed by module name as key and plugin 281 class as value 282 @rtype: dict 283 """ 284 available = {} 285 pkg_modname = self.__plugin_package_module.__name__ 286 mod_dir = os.path.dirname(self.__modfile) 287 288 for modname in os.listdir(mod_dir): 289 290 if modname.startswith("__"): 291 continue # python stuff 292 if not (modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION) \ 293 or "." not in modname): 294 continue # not something we want to load 295 296 if modname.endswith(EntropyPluginFactory._PYTHON_EXTENSION): 297 modname = modname[:-len(EntropyPluginFactory._PYTHON_EXTENSION)] 298 299 if not modname.endswith(EntropyPluginFactory._PLUGIN_SUFFIX): 300 continue 301 302 # remove suffix 303 modname_clean = modname[:-len(EntropyPluginFactory._PLUGIN_SUFFIX)] 304 305 modpath = "%s.%s" % (pkg_modname, modname,) 306 307 try: 308 __import__(modpath) 309 except ImportError as err: 310 sys.stderr.write("!!! Entropy Plugin warning, cannot " \ 311 "load module: %s | %s !!!\n" % (modpath, err,)) 312 continue 313 314 for obj in list(sys.modules[modpath].__dict__.values()): 315 316 valid = self._inspect_object(obj) 317 if not valid: 318 continue 319 320 available[modname_clean] = obj 321 322 return available
323
324 - def _scan_egg_group(self):
325 """ 326 Scan classes in given Python Egg group name looking for a valid plugin. 327 328 @return: module dictionary composed by module name as key and plugin 329 class as value 330 @rtype: dict 331 """ 332 # needs setuptools 333 import pkg_resources 334 available = {} 335 336 for entry in pkg_resources.iter_entry_points(self.__egg_entry_group): 337 338 obj = entry.load() 339 valid = self._inspect_object(obj) 340 if not valid: 341 continue 342 available[entry.name] = obj 343 344 return available
345
346 - def get_available_plugins(self):
347 """ 348 Return currently available plugin classes. 349 Note: Entropy plugins can either be Python packages or modules and 350 their name MUST end with PluginFactory._PLUGIN_SUFFIX ("_plugin"). 351 352 @return: dictionary composed by Entropy plugin id as key and Entropy 353 Python module as value 354 @rtype: dict 355 """ 356 if self.__cache is not None: 357 return self.__cache.copy() 358 359 if self.__egg_entry_group: 360 available = self._scan_egg_group() 361 else: 362 available = self._scan_dir() 363 364 self.__cache = available.copy() 365 return available
366
367 - def get_default_plugin(self):
368 """ 369 Return currently configured Entropy Plugin class. 370 371 @return: Entropy plugin class 372 @rtype: default plugin class given 373 @raise KeyError: if default plugin is not set or not found 374 """ 375 available = self.get_available_plugins() 376 plugin = self.__default_plugin_name 377 fallback = self.__fallback_plugin_name 378 klass = available.get(plugin) 379 380 if klass is None: 381 import warnings 382 warnings.warn("%s: %s" % ( 383 "selected Plugin not available, using fallback", plugin,)) 384 klass = available.get(fallback) 385 386 if klass is None: 387 raise KeyError 388 389 return klass
390
391 392 -class BaseConfigParser(dict):
393 """ 394 Entropy .ini-like configuration file parser. 395 396 This is a base class useful for implementing .ini-like 397 configuration files. 398 399 A possible syntax is something like: 400 401 [section] 402 param = value 403 param = value 404 405 [section] 406 param = value 407 param = value 408 """ 409 # Regular expression to match new configuration file sections. 410 _SECTION_RE = re.compile("^\[(.*)\]$") 411 412 # Starting character that denotes an ignored line. 413 _COMMENT_CHAR = "#" 414 415 # key <-> value statements separator. 416 _SEPARATOR = "=" 417 418 # Must be reimplemented by subclasses. 419 # Must be a list of supported statement keys. 420 _SUPPORTED_KEYS = None 421
422 - def __init__(self, encoding = None):
423 super(BaseConfigParser, self).__init__() 424 self._encoding = encoding 425 self._ordered_sections = []
426
427 - def read(self, files):
428 """ 429 Read the given list of configuration file paths and populate 430 the repository metadata. 431 432 @param files: configuration file paths 433 @type files: list 434 """ 435 for path in files: 436 self._parse(path)
437
438 - def _parse(self, path):
439 """ 440 Given a file path, parse the content and update the 441 dictionary. 442 443 @param path: a configuration file path 444 @type path: string 445 """ 446 if self._encoding is None: 447 with open(path, "r") as cfg_f: 448 content = cfg_f.readlines() 449 else: 450 with codecs.open(path, "r", encoding=self._encoding) as cfg_f: 451 content = cfg_f.readlines() 452 453 section_name = None 454 for line in content: 455 456 line = line.strip() 457 if not line: 458 continue 459 if line.startswith(self._COMMENT_CHAR): 460 continue 461 462 section = self._SECTION_RE.match(line) 463 if section: 464 candidate = self._validate_section(section) 465 if candidate: 466 section_name = candidate 467 if candidate not in self._ordered_sections: 468 self._ordered_sections.append(candidate) 469 continue 470 471 if section_name is None: 472 # skip lines, no section defined 473 continue 474 475 key_value = line.split(self._SEPARATOR, 1) 476 if len(key_value) != 2: 477 # not a well formed line, skip 478 continue 479 480 key, value = [x.strip() for x in key_value] 481 if key not in self._SUPPORTED_KEYS: 482 # key is invalid 483 continue 484 elif not value: 485 # value is invalid 486 continue 487 488 repo_data = self.setdefault(section_name, {}) 489 key_data = repo_data.setdefault(key, []) 490 key_data.append(value)
491 492 @classmethod
493 - def _validate_section(cls, match):
494 """ 495 Validate a matched section object and return the 496 extracted data (if valid) or None (if not valid). 497 498 @param match: a re.match object 499 @type match: re.match 500 @return: the valid section name, if any, or None 501 @rtype: string 502 """ 503 raise NotImplementedError()
504