Sin descripción
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 13KB

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