No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

context.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import importlib
  2. import importlib.machinery
  3. import importlib.abc
  4. import sys
  5. import types
  6. import os
  7. import re
  8. import warnings #For the moment no way to use the logger in this file (I guess)
  9. #A try to avoid circular dependencies problems
  10. if 'lodel' not in sys.modules:
  11. import lodel
  12. else:
  13. globals()['lodel'] = sys.modules['lodel']
  14. if 'lodelsites' not in sys.modules:
  15. import lodelsites
  16. else:
  17. globals()['lodelsites'] = sys.modules['lodelsites']
  18. ##@brief Name of the package that will contains all the virtual lodel
  19. #packages
  20. CTX_PKG = "lodelsites"
  21. #
  22. # Following exception classes are written here to avoid circular dependencies
  23. # problems.
  24. #
  25. ##@brief Designed to be raised by the context manager
  26. class ContextError(Exception):
  27. pass
  28. ##@brief Raised when an error concerning context modules occurs
  29. class ContextModuleError(ContextError):
  30. pass
  31. ##@brief Designed to permit dynamic packages creation from the lodel package
  32. #
  33. #The class is added in first position in the sys.metapath variable. Doing this
  34. #we override the earlier steps of the import mechanism.
  35. #
  36. #When called the find_spec method determine wether the imported module is
  37. #a part of a virtual lodel package, else it returns None and the standart
  38. #import mechanism go further.
  39. #If it's a submodule of a virtual lodel package we create a symlink
  40. #to represent the lodel package os the FS and then we make python import
  41. #files from the symlink.
  42. #
  43. #@note Current implementation is far from perfection. In fact no deletion
  44. #mechanisms is written and the virtual package cannot be a subpackage of
  45. #the lodel package for the moment...
  46. class LodelMetaPathFinder(importlib.abc.MetaPathFinder):
  47. def find_spec(fullname, path, target = None):
  48. print("find_spec called : fullname=%s path=%s target=%s" % (
  49. fullname, path, target))
  50. if fullname.startswith(CTX_PKG):
  51. spl = fullname.split('.')
  52. site_identifier = spl[1]
  53. #creating a symlink to represent the lodel site package
  54. mod_path = os.path.join(lodelsites.__path__[0], site_identifier)
  55. if not os.path.exists(mod_path):
  56. os.symlink(lodel.__path__[0], mod_path, True)
  57. #Cache invalidation after we "created" the new package
  58. #importlib.invalidate_caches()
  59. return None
  60. ##@brief Class designed to handle context switching and virtual module
  61. #exposure
  62. class LodelContext(object):
  63. ##@brief FLag telling that the context handler is in single context mode
  64. MONOSITE = 1
  65. ##@brief Flag telling that the context manager is in multi context mode
  66. MULTISITE = 2
  67. ##@brief Static property storing current context name
  68. _current = None
  69. ##@brief Stores the context type (single or multiple)
  70. _type = None
  71. ##@brief Stores the contexts
  72. _contexts = None
  73. ##@brief Create a new context
  74. #@see LodelContext.new()
  75. def __init__(self, site_id):
  76. if site_id is None:
  77. #Monosite instanciation
  78. if self.__class__._type != self.__class__.MONOSITE:
  79. raise ContextError("Cannot instanciate a context with \
  80. site_id set to None when we are in MULTISITE beahavior")
  81. else:
  82. #More verification can be done here (singleton specs ? )
  83. self.__class__._current = self.__class__._contexts = self
  84. self.__pkg_name = 'lodel'
  85. self.__package = lodel
  86. return
  87. else:
  88. #Multisite instanciation
  89. if self.__class__._type != self.__class__.MULTISITE:
  90. raise ContextError("Cannot instanciate a context with a \
  91. site_id when we are in MONOSITE beahvior")
  92. if not self.validate_identifier(site_id):
  93. raise ContextError("Given context name is not a valide identifier \
  94. : '%s'" % site_id)
  95. if site_id in self.__class__._contexts:
  96. raise ContextError(
  97. "A context named '%s' allready exists." % site_id)
  98. self.__id = site_id
  99. self.__pkg_name = '%s.%s' % (CTX_PKG, site_id)
  100. #Importing the site package to trigger its creation
  101. self.__package = importlib.import_module(self.__pkg_name)
  102. self.__class__._contexts[site_id] = self
  103. ##@brief Expose a module from the context
  104. #@param globs globals : globals where we have to expose the module
  105. #@param spec tuple : first item is module name, second is the alias
  106. def expose(self, globs, spec):
  107. if len(spec) != 2:
  108. raise ContextError("Invalid argument given. Expected a tuple of \
  109. length == 2 but got : %s" % spec)
  110. module_fullname, exposure_spec = spec
  111. module_fullname = self._translate(module_fullname)
  112. if isinstance(exposure_spec, str):
  113. self._expose_module(globs, module_fullname, exposure_spec)
  114. else:
  115. self._expose_objects(globs, module_fullname, exposure_spec)
  116. ##@brief Utility method to expose a module with an alias name in globals
  117. #@param globs globals() : concerned globals dict
  118. #@param fullname str : module fullname
  119. #@param alias str : alias name
  120. @classmethod
  121. def _expose_module(cls, globs, fullname, alias):
  122. module = importlib.import_module(fullname)
  123. cls.safe_exposure(globs, module, alias)
  124. ##@brief Utility mehod to expose objects like in a from x import y,z
  125. #form
  126. #@param globs globals() : dict of globals
  127. #@param fullename str : module fullname
  128. #@param objects list : list of object names to expose
  129. @classmethod
  130. def _expose_objects(cls, globs, fullname, objects):
  131. errors = []
  132. module = importlib.import_module(fullname)
  133. for o_name in objects:
  134. if isinstance(o_name, str):
  135. alias = o_name
  136. else:
  137. o_name, alias = o_name
  138. if not hasattr(module, o_name):
  139. errors.append(o_name)
  140. else:
  141. cls.safe_exposure(globs, getattr(module, o_name), alias)
  142. if len(errors) > 0:
  143. msg = "Module %s does not have any of [%s] as attribute" % (
  144. fullname, ','.join(errors))
  145. raise ImportError(msg)
  146. ##@brief Translate a module fullname to the context equivalent
  147. #@param module_fullname str : a module fullname
  148. #@return The module name in the current context
  149. def _translate(self, module_fullname):
  150. if not module_fullname.startswith('lodel'):
  151. raise ContextModuleError("Given module is not lodel or any \
  152. submodule : '%s'" % module_fullname)
  153. return module_fullname.replace('lodel', self.__pkg_name)
  154. ##@brief Set a context as active
  155. #@param site_id str : site identifier (identify a context)
  156. @classmethod
  157. def set(cls, site_id):
  158. if cls._type == cls.MONOSITE:
  159. raise ContextError("Context cannot be set in MONOSITE beahvior")
  160. if not cls.validate_identifier(site_id):
  161. raise ContextError("Given context name is not a valide identifier \
  162. : '%s'" % site_id)
  163. if site_id not in cls._contexts:
  164. raise ContextError("No context named '%s' found." % site_id)
  165. cls._current = cls._contexts[site_id]
  166. ##@brief Helper method that returns the current context
  167. @classmethod
  168. def get(cls):
  169. if cls._current is None:
  170. raise ContextError("No context loaded")
  171. return cls._current
  172. ##@brief Create a new context given a context name
  173. #
  174. #@note It's just an alias to the LodelContext.__init__ method
  175. #@param site_id str : context name
  176. #@return the context instance
  177. @classmethod
  178. def new(cls, site_id):
  179. return cls(site_id)
  180. ##@brief Helper function that import and expose specified modules
  181. #
  182. #The specs given is a dict. Each element is indexed by a module
  183. #fullname. Items can be of two types :
  184. #@par Simple import with alias
  185. #In this case items of specs is a string representing the alias name
  186. #for the module we are exposing
  187. #@par from x import i,j,k equivalent
  188. #In this case items are lists of object name to expose as it in globals.
  189. #You can specify an alias by giving a tuple instead of a string as
  190. #list element. In this case the first element of the tuple is the object
  191. #name and the second it's alias in the globals
  192. #
  193. #@todo make the specs format more consitant
  194. #@param cls : bultin params
  195. #@param globs dict : the globals dict of the caller module
  196. #@param specs dict : specs of exposure (see comments of this method)
  197. #@todo implements relative module imports. (maybe by looking for
  198. #"calling" package in globs dict)
  199. @classmethod
  200. def expose_modules(cls, globs, specs):
  201. ctx = cls.get()
  202. for spec in specs.items():
  203. ctx.expose(globs, spec)
  204. ##@brief Initialize the context manager
  205. #
  206. #@note Add the LodelMetaPathFinder class to sys.metapath if type is
  207. #LodelContext.MULTISITE
  208. #@param type FLAG : takes value in LodelContext.MONOSITE or
  209. #LodelContext.MULTISITE
  210. @classmethod
  211. def init(cls, type=MONOSITE):
  212. if cls._current is not None:
  213. raise ContextError("Context allready started and used. Enable to \
  214. initialize it anymore")
  215. if type not in ( cls.MONOSITE, cls.MULTISITE):
  216. raise ContextError("Invalid flag given : %s" % type)
  217. cls._type = type
  218. if cls._type == cls.MULTISITE:
  219. cls._contexts = dict()
  220. #Add custom MetaPathFinder allowing implementing custom imports
  221. sys.meta_path = [LodelMetaPathFinder] + sys.meta_path
  222. else:
  223. #Add a single context with no site_id
  224. cls._contexts = cls._current = cls(None)
  225. ##@brief Validate a context identifier
  226. #@param identifier str : the identifier to validate
  227. #@return true if the name is valide else false
  228. @staticmethod
  229. def validate_identifier(identifier):
  230. return identifier is None or \
  231. re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_]', identifier)
  232. ##@brief Safely expose a module in globals using an alias name
  233. #
  234. #@note designed to implements warning messages or stuff like that
  235. #when doing nasty stuff
  236. #
  237. #@todo try to use the logger module instead of warnings
  238. #@param globs globals : the globals where we want to expose our
  239. #module alias
  240. #@param obj object : the object we want to expose
  241. #@param alias str : the alias name for our module
  242. @staticmethod
  243. def safe_exposure(globs, obj, alias):
  244. if alias in globs:
  245. warnings.warn("A module exposure leads in globals overwriting for \
  246. key '%s'" % alias)
  247. globs[alias] = obj
  248. ##@brief Actives a context from a path
  249. #@param path str : the path from which we extract a sitename
  250. def from_path(cls, path):
  251. site_id = path.split('/')[-1]
  252. if cls._type == cls.MULTISITE:
  253. if site_id in cls._contexts:
  254. cls.set(site_id)
  255. else:
  256. cls._contexts[site_id] = cls.new(site_id)
  257. else:
  258. if cls._current is None:
  259. cls._current = cls.new()
  260. ##@brief Delete a site's context
  261. #@param site_id str : the site's name to remove the context
  262. def remove(cls, site_id):
  263. if site_id is None:
  264. if cls._type == cls.MULTISITE:
  265. raise ContextError("Cannot have a context with \
  266. site_id set to None when we are in MULTISITE beahavior")
  267. del cls._contexts
  268. else:
  269. if cls._type == cls.MULTISITE:
  270. if site_id in cls._contexts:
  271. del cls._contexts[site_id]
  272. else:
  273. raise ContextError("No site %s exist" % site_id)
  274. else:
  275. raise ContextError("Cannot have a context with \
  276. site_id set when we are in MONOSITE beahavior")