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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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 Flag indicating if the classe is initialized
  84. __initialized = False
  85. ##@brief Create a new context
  86. #@see LodelContext.new()
  87. def __init__(self, site_id):
  88. print("Registering new context for '%s'" % site_id)
  89. if site_id is None:
  90. #Monosite instanciation
  91. if self.__class__._type != self.__class__.MONOSITE:
  92. raise ContextError("Cannot instanciate a context with \
  93. site_id set to None when we are in MULTISITE beahavior")
  94. else:
  95. #More verification can be done here (singleton specs ? )
  96. self.__class__._current = self.__class__._contexts = self
  97. self.__pkg_name = 'lodel'
  98. self.__package = lodel
  99. return
  100. else:
  101. #Multisite instanciation
  102. if self.__class__._type != self.__class__.MULTISITE:
  103. raise ContextError("Cannot instanciate a context with a \
  104. site_id when we are in MONOSITE beahvior")
  105. if not self.validate_identifier(site_id):
  106. raise ContextError("Given context name is not a valide identifier \
  107. : '%s'" % site_id)
  108. if site_id in self.__class__._contexts:
  109. raise ContextError(
  110. "A context named '%s' allready exists." % site_id)
  111. self.__id = site_id
  112. self.__pkg_name = '%s.%s' % (CTX_PKG, site_id)
  113. if self.__id == LOAD_CTX:
  114. self.__pkg_name = 'lodel'
  115. #Importing the site package to trigger its creation
  116. self.__package = importlib.import_module(self.__pkg_name)
  117. self.__class__._contexts[site_id] = self
  118. ##@brief Expose a module from the context
  119. #@param globs globals : globals where we have to expose the module
  120. #@param spec tuple : first item is module name, second is the alias
  121. def expose(self, globs, spec):
  122. if len(spec) != 2:
  123. raise ContextError("Invalid argument given. Expected a tuple of \
  124. length == 2 but got : %s" % spec)
  125. module_fullname, exposure_spec = spec
  126. module_fullname = self._translate(module_fullname)
  127. if isinstance(exposure_spec, str):
  128. self._expose_module(globs, module_fullname, exposure_spec)
  129. else:
  130. self._expose_objects(globs, module_fullname, exposure_spec)
  131. ##@brief Utility method to expose a module with an alias name in globals
  132. #@param globs globals() : concerned globals dict
  133. #@param fullname str : module fullname
  134. #@param alias str : alias name
  135. @classmethod
  136. def _expose_module(cls, globs, fullname, alias):
  137. module = importlib.import_module(fullname)
  138. cls.safe_exposure(globs, module, alias)
  139. ##@brief Utility mehod to expose objects like in a from x import y,z
  140. #form
  141. #@param globs globals() : dict of globals
  142. #@param fullename str : module fullname
  143. #@param objects list : list of object names to expose
  144. @classmethod
  145. def _expose_objects(cls, globs, fullname, objects):
  146. errors = []
  147. module = importlib.import_module(fullname)
  148. for o_name in objects:
  149. if isinstance(o_name, str):
  150. alias = o_name
  151. else:
  152. o_name, alias = o_name
  153. if not hasattr(module, o_name):
  154. errors.append(o_name)
  155. else:
  156. cls.safe_exposure(globs, getattr(module, o_name), alias)
  157. if len(errors) > 0:
  158. msg = "Module %s does not have any of [%s] as attribute" % (
  159. fullname, ','.join(errors))
  160. raise ImportError(msg)
  161. ##@brief Translate a module fullname to the context equivalent
  162. #@param module_fullname str : a module fullname
  163. #@return The module name in the current context
  164. def _translate(self, module_fullname):
  165. if not module_fullname.startswith('lodel'):
  166. raise ContextModuleError("Given module is not lodel or any \
  167. submodule : '%s'" % module_fullname)
  168. return module_fullname.replace('lodel', self.__pkg_name)
  169. ##@brief Set a context as active
  170. #@param site_id str : site identifier (identify a context)
  171. @classmethod
  172. def set(cls, site_id):
  173. if cls._type == cls.MONOSITE:
  174. raise ContextError("Context cannot be set in MONOSITE beahvior")
  175. site_id = LOAD_CTX if site_id is None else site_id
  176. if not cls.validate_identifier(site_id):
  177. raise ContextError("Given context name is not a valide identifier \
  178. : '%s'" % site_id)
  179. if site_id not in cls._contexts:
  180. raise ContextError("No context named '%s' found." % site_id)
  181. cls._current = cls._contexts[site_id]
  182. ##@brief Helper method that returns the current context
  183. @classmethod
  184. def get(cls):
  185. if cls._current is None:
  186. raise ContextError("No context loaded")
  187. return cls._current
  188. ##@brief Create a new context given a context name
  189. #
  190. #@note It's just an alias to the LodelContext.__init__ method
  191. #@param site_id str : context name
  192. #@return the context instance
  193. @classmethod
  194. def new(cls, site_id):
  195. return cls(site_id)
  196. ##@brief Helper function that import and expose specified modules
  197. #
  198. #The specs given is a dict. Each element is indexed by a module
  199. #fullname. Items can be of two types :
  200. #@par Simple import with alias
  201. #In this case items of specs is a string representing the alias name
  202. #for the module we are exposing
  203. #@par from x import i,j,k equivalent
  204. #In this case items are lists of object name to expose as it in globals.
  205. #You can specify an alias by giving a tuple instead of a string as
  206. #list element. In this case the first element of the tuple is the object
  207. #name and the second it's alias in the globals
  208. #
  209. #@todo make the specs format more consitant
  210. #@param cls : bultin params
  211. #@param globs dict : the globals dict of the caller module
  212. #@param specs dict : specs of exposure (see comments of this method)
  213. #@todo implements relative module imports. (maybe by looking for
  214. #"calling" package in globs dict)
  215. @classmethod
  216. def expose_modules(cls, globs, specs):
  217. ctx = cls.get()
  218. for spec in specs.items():
  219. ctx.expose(globs, spec)
  220. ##@brief Initialize the context manager
  221. #
  222. #@note Add the LodelMetaPathFinder class to sys.metapath if type is
  223. #LodelContext.MULTISITE
  224. #@param type FLAG : takes value in LodelContext.MONOSITE or
  225. #LodelContext.MULTISITE
  226. @classmethod
  227. def init(cls, type=MONOSITE):
  228. if cls._current is not None:
  229. raise ContextError("Context allready started and used. Enable to \
  230. initialize it anymore")
  231. if type not in ( cls.MONOSITE, cls.MULTISITE):
  232. raise ContextError("Invalid flag given : %s" % type)
  233. cls._type = type
  234. if cls._type == cls.MULTISITE:
  235. cls._contexts = dict()
  236. #Add custom MetaPathFinder allowing implementing custom imports
  237. sys.meta_path = [LodelMetaPathFinder] + sys.meta_path
  238. #Create and set __loader__ context
  239. cls.new(LOAD_CTX)
  240. cls.set(LOAD_CTX)
  241. else:
  242. #Add a single context with no site_id
  243. cls._contexts = cls._current = cls(None)
  244. cls.__initialized = True
  245. @classmethod
  246. def is_initialized(cls):
  247. return cls.__initialized
  248. ##@brief Return the directory of the package of the current loaded context
  249. @classmethod
  250. def context_dir(cls):
  251. if cls._type == cls.MONOSITE:
  252. return './'
  253. return dir_for_context(cls._current.__id)
  254. ##@brief Validate a context identifier
  255. #@param identifier str : the identifier to validate
  256. #@return true if the name is valide else false
  257. @staticmethod
  258. def validate_identifier(identifier):
  259. if identifier == LOAD_CTX:
  260. return True
  261. return identifier is None or \
  262. re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_]', identifier)
  263. ##@brief Safely expose a module in globals using an alias name
  264. #
  265. #@note designed to implements warning messages or stuff like that
  266. #when doing nasty stuff
  267. #
  268. #@todo try to use the logger module instead of warnings
  269. #@param globs globals : the globals where we want to expose our
  270. #module alias
  271. #@param obj object : the object we want to expose
  272. #@param alias str : the alias name for our module
  273. @staticmethod
  274. def safe_exposure(globs, obj, alias):
  275. if alias in globs:
  276. warnings.warn("A module exposure leads in globals overwriting for \
  277. key '%s'" % alias)
  278. globs[alias] = obj
  279. ##@brief Create a context from a path and returns the context name
  280. #@param path str : the path from which we extract a sitename
  281. #@return the site identifier
  282. @classmethod
  283. def from_path(cls, path):
  284. if cls._type != cls.MULTISITE:
  285. raise ContextError("Cannot create a context from a path in \
  286. MONOSITE mode")
  287. site_id = os.path.basename(path.strip('/'))
  288. if not cls.validate_identifier(site_id):
  289. raise ContextError(
  290. "Unable to create a context named '%s'" % site_id)
  291. cls.new(site_id)
  292. return site_id
  293. ##@brief Delete a site's context
  294. #@param site_id str : the site's name to remove the context
  295. def remove(cls, site_id):
  296. if site_id is None:
  297. if cls._type == cls.MULTISITE:
  298. raise ContextError("Cannot have a context with \
  299. site_id set to None when we are in MULTISITE beahavior")
  300. del cls._contexts
  301. else:
  302. if cls._type == cls.MULTISITE:
  303. if site_id in cls._contexts:
  304. del cls._contexts[site_id]
  305. else:
  306. raise ContextError("No site %s exist" % site_id)
  307. else:
  308. raise ContextError("Cannot have a context with \
  309. site_id set when we are in MONOSITE beahavior")