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

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