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.

module_wrapper.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import types
  2. import sys
  3. import imp
  4. import importlib.machinery, importlib.abc
  5. import re
  6. import warnings
  7. from .exceptions import MultiSiteIdentifierError
  8. ##@brief Constant name of the package containing all sites module
  9. SITE_PACKAGE = 'lodelsites'
  10. ##@brief Stores all the lodel sites modules indexed by site identifier
  11. lodel_site_packages = dict()
  12. ##@brief Will stores the module object of the SITE_PACKAGE
  13. _lodel_site_root_package = None
  14. ##@brief Stores the site identifier validation re
  15. _site_identifier_re = None
  16. SITE_PACKAGE_STRUCT = {
  17. 'modules': {
  18. 'auth': {
  19. },
  20. 'settings': {
  21. },
  22. 'logger': {
  23. },
  24. 'plugin': {
  25. 'modules': {
  26. 'hooks': {
  27. 'classes': [ 'DecoratedWrapper', 'LodelHook' ]
  28. },
  29. 'plugins': {
  30. 'classes': [
  31. 'PluginVersion', 'MetaPlugType', 'Plugin',
  32. 'CustomMethod']
  33. },
  34. 'interface': {
  35. 'classes': [ 'InterfacePlugin']
  36. },
  37. 'extensions': {
  38. 'classes': [ 'Extension']
  39. }
  40. },
  41. }
  42. },
  43. 'classes': {}
  44. }
  45. class LodelSiteModuleSpec(importlib.machinery.ModuleSpec):
  46. ##@brief Add a site_id attribute to ModuleSpec object
  47. #
  48. #@param name str : fully qualified module name
  49. #@param loader : module loader. Child of importlib.abc.Loader and in our
  50. #case mostly CustomModuleLoader instances
  51. #@param parent str : parent absolute package name. Will be used to set
  52. #new module __package___ attribute
  53. #@param origin None : None because no source code
  54. #@param loader_state ? <- leave None
  55. #@param is_package bool : <- useless, can be deleted
  56. #
  57. #@see https://docs.python.org/3.4/library/importlib.html#importlib.machinery.ModuleSpec
  58. def __init__(self, name, loader, *,
  59. parent=None, origin=None, loader_state=None, is_package=None,
  60. site_id = None):
  61. super().__init__(name=name, loader=loader, origin=origin,
  62. loader_state = loader_state)
  63. ##@brief Stores the parent package fullname
  64. self.parent_package = parent
  65. ##@brief Stores the module site_id
  66. self.site_id = None
  67. self._is_package = False if is_package is None else True
  68. if not site_id is None:
  69. self.site_id = site_id
  70. def __str__(self):
  71. res = super().__str__()
  72. res = res[:-1] +", site_id = %s, parent = %s)" % (
  73. self.site_id, repr(self.parent))
  74. return res
  75. ##@brief Custom class of module Loader
  76. #
  77. #Designed to handle dynamic module creation for lodel2 sites
  78. #@see https://docs.python.org/3.4/library/importlib.html#importlib.abc.Loader
  79. #@see https://docs.python.org/3.4/library/types.html#types.ModuleType
  80. class LodelSiteModuleLoader(importlib.abc.Loader):
  81. ##@brief Handles module creation
  82. #@param spec ModuleSpec instance
  83. #@return The newly created module
  84. def create_module(self, spec):
  85. #Here we do not want to import but get the module object of the parent
  86. #to store it's reference and set the new module as attribute
  87. print("CREATE_MODULE debug : ", spec)
  88. if spec.parent_package is not None:
  89. print("PARENT NAMe ;", spec.parent_package)
  90. parent_module = importlib.import_module(spec.parent_package)
  91. if hasattr(parent_module, spec.name):
  92. warnings.warn("Overloading an existing module attribute will \
  93. creating %s module" % spec.name)
  94. else:
  95. parent_module = None
  96. res = types.ModuleType(spec.name)
  97. root_pkg_name = globals()['SITE_PACKAGE']
  98. res.__spec__ = spec
  99. res.__name__ = spec.name
  100. res.__loader__ = self
  101. res.__package__ = spec.parent_package
  102. res.__path__ = [] if spec._is_package else None
  103. if spec.site_id is not None:
  104. res.__site_id__ = spec.site_id
  105. if parent_module is not None:
  106. rel_name = spec.name.split('.')[-1]
  107. setattr(parent_module, rel_name, res)
  108. #sys.modules[fullname] = res
  109. sys.modules[spec.name] = res
  110. print("INFO : module %s loaded" % spec.name)
  111. print("INFO current state on sys.modules : ", [ mname for mname in sys.modules.keys() if mname .startswith('lodelsites.')])
  112. self.__dyn_module__ = res
  113. return res
  114. def load_module(self, fullname):
  115. if self.__dyn_module__.__name__ != fullname:
  116. raise MultiSiteError("The name given to load_module do not match \
  117. the name of the handled module...")
  118. sys.modules[fullname] = self.__dyn_module__
  119. print("INFO : module %s loaded" % fullname)
  120. ##@brief Custom metapath finder that is able to handle our
  121. #dynamically created modules
  122. class LodelSiteMetaPathFinder(importlib.abc.MetaPathFinder):
  123. def find_spec(fullname, path, target = None):
  124. print("FINDSPEC CALLEd : ", fullname, path)
  125. if not fullname.startswith(globals()['SITE_PACKAGE']):
  126. return None
  127. n_spl = fullname.split('.')
  128. site_id = n_spl[1]
  129. res_mod = get_site_module(site_id)
  130. print("Begin to walk in submodules. Starting from ", res_mod.__name__)
  131. print("DEBUG RESMOD : ", res_mod.__name__, dir(res_mod))
  132. for nxt in n_spl[2:]:
  133. res_mod = getattr(res_mod, nxt)
  134. print("Real result : ", res_mod.__name__, res_mod)
  135. return res_mod.__spec__
  136. ##@brief Simple getter using site identifier
  137. def get_site_module(identifier):
  138. glob_pkg = globals()['lodel_site_packages']
  139. if identifier not in glob_pkg:
  140. raise MultiSiteIdentifierError(
  141. "No site identified by '%s'" % identifier)
  142. return glob_pkg[identifier]
  143. ##@brief Create a new site module with given site identifier
  144. #@param identifier str : site identifier
  145. def new_site_module(identifier):
  146. new_module_fullname = globals()['SITE_PACKAGE'] + '.' + identifier
  147. new_modules = _new_site_module(
  148. identifier, new_module_fullname, globals()['SITE_PACKAGE_STRUCT'],
  149. parent = globals()['_lodel_site_root_package'])
  150. new_mod = new_modules[0] #fetching root module
  151. globals()['lodel_site_packages'][identifier] = new_mod
  152. setattr(globals()['_lodel_site_root_package'], identifier, new_mod)
  153. for mod in new_modules:
  154. print("Call reload on : ", mod)
  155. imp.reload(mod)
  156. for n in dir(mod):
  157. v = getattr(mod, n)
  158. if isinstance(v, types.ModuleType):
  159. print("\t%s : %s" % (n, getattr(mod, n)))
  160. return new_mod
  161. ##@brief Create a new site module (part of a site package)
  162. #
  163. #@note module informations are expected to be part of SITE_PACKAGE_STRUCT
  164. #@note reccursiv function
  165. #
  166. #@param identifier str
  167. #@param module_name str
  168. #@param module_infos dict
  169. #@param parent : modul object
  170. #@param all_mods list : in/out accumulator for reccursiv calls allowing to
  171. #return the list of all created modules
  172. #
  173. #@return the created module
  174. def _new_site_module(identifier, module_name, module_infos, parent,
  175. mod_acc = None):
  176. mod_acc = list() if mod_acc is None else mod_acc
  177. print("Rec debug : ", identifier, module_name, module_infos, parent)
  178. identifier = identifier_validation(identifier)
  179. if parent is None:
  180. parent_name = None
  181. else:
  182. parent_name = parent.__name__
  183. res = _module_from_spec(name = module_name, parent = parent_name,
  184. site_id = identifier)
  185. orig_modname = _original_name_from_module(res)
  186. if len(mod_acc) == 0:
  187. #we just created a site root package. Because we will reimport
  188. #parent modules when creating submodules we have to insert the
  189. #site root package NOW to the site_root_packages
  190. print("WARNING : inserting module as site root package : ", res)
  191. globals()['lodel_site_packages'][identifier] = res
  192. mod_acc.append(res) #Add created module to accumulator asap
  193. orig_mod = importlib.import_module(orig_modname)
  194. print("ORIG MOD = ", orig_mod)
  195. if 'classes' in module_infos:
  196. for cname in module_infos['classes']:
  197. orig_cls = getattr(orig_mod, cname)
  198. res_cls = onthefly_child_class(cname, orig_cls, parent)
  199. setattr(res, cname, res_cls)
  200. #child modules creation
  201. if 'modules' in module_infos:
  202. for mname in module_infos['modules']:
  203. new_mname = module_name + '.' + mname
  204. print("DEBUG NAME ON REC CALL : ", new_mname)
  205. submod = _new_site_module(
  206. identifier, new_mname, module_infos['modules'][mname],
  207. parent = res, mod_acc = mod_acc)
  208. submod = submod[-1]
  209. #setattr(res, mname, submod) #done in create_module
  210. return mod_acc
  211. ##@brief Validate a site identifier
  212. #@param identifier str
  213. #@return the identifier
  214. #@throw MultiSiteIdentifierError on invalid id
  215. def identifier_validation(identifier):
  216. re_str = r'^[a-zA-Z][a-zA-Z0-9-]*$'
  217. _site_identifier_re = globals()['_site_identifier_re']
  218. if _site_identifier_re is None:
  219. _site_identifier_re = re.compile(re_str)
  220. globals()['_site_identifier_re'] = _site_identifier_re
  221. if not _site_identifier_re.match(identifier):
  222. raise MultiSiteIdentifierError("Excpected an identifier that matches \
  223. r'%s', but got '%s'" % (re_str, identifier))
  224. return identifier
  225. ##@brief Create new root package for a lodel site
  226. #@param identifer str : the site identifier
  227. def new_site_root_package(identifier):
  228. identifier_validation(identifier)
  229. if identifier in _lodel_site_root_package:
  230. raise NameError("A site identified by '%s' allready exists")
  231. module_name = identifier
  232. res = _module_from_spec(
  233. name = identifier, parent = globals()['SITE_PACKAGE'])
  234. _lodel_site_root_packages[identifier] = res
  235. return res
  236. ##@brief Create a new child class on the fly
  237. #@param identifier str : site identifier
  238. #@param original_cls class : the original class (will be the single base class
  239. #of our dynamically created class)
  240. #@param parent_module module object : the module designed to contains the class
  241. #@return the created class
  242. def onthefly_child_class(identifier, original_cls, parent_module):
  243. def ns_callback(ns):
  244. ns['__module__'] = parent_module.__name__
  245. ns['__site_id__'] = identifier
  246. res = types.new_class(original_cls.__name__, (original_cls,), None,
  247. ns_callback)
  248. setattr(parent_module, original_cls.__name__, res)
  249. return res
  250. ##@brief Module initialisation function
  251. #
  252. #Takes care to create the lodel_site_package module object
  253. def init_module():
  254. #Creating the package that contains all site packages
  255. site_pkg_name = globals()['SITE_PACKAGE']
  256. res = _module_from_spec(name = site_pkg_name)
  257. globals()['_lodel_site_root_package'] = res
  258. sys.modules[site_pkg_name] = res
  259. #Add our custom metapathfinder
  260. sys.meta_path = [LodelSiteMetaPathFinder] + sys.meta_path
  261. return res
  262. ##@brief Utility function that takes LodelSiteModuleSpec __init__ arguments
  263. #as parameter and handles the module creation
  264. #@return A newly created module according to given arguments
  265. def _module_from_spec(name, parent = None, origin = None, loader_state = None,
  266. is_package = None, site_id = None):
  267. loader = LodelSiteModuleLoader()
  268. spec = LodelSiteModuleSpec(name = name, parent = parent, origin = origin,
  269. loader_state = None, loader = loader, is_package = is_package,
  270. site_id = site_id)
  271. return loader.create_module(spec)
  272. ##@brief Replace all lodel modules references by references on dynamically
  273. #modules
  274. #@param mod ModuleType : the module to update
  275. #@return the modified module
  276. def _module_update_globals(mod):
  277. return None
  278. print("DEBUG : ", mod.__name__, dir(mod), mod.__dir__())
  279. site_id = mod.__site_id__
  280. lodel_site_package = get_site_module(mod.__site_id__)
  281. for kname, val in mod.__globals__:
  282. if isinstance(val, types.ModuleType) and \
  283. val.__package__.startswith('lodel'):
  284. #we have to replace the module reference
  285. fullname = "%s.%s" % (val.__package__, val.__name__)
  286. walkthrough = fullname.split('.')[1:]
  287. repl = lodel_site_package
  288. for submod in walkthrough:
  289. repl = getattr(repl, submod)
  290. mod.__globals__[kname] = repl
  291. return mod
  292. ##@brief Build the original fully quilified module name given a module
  293. #@warning Behavior is totally hardcoded given the lodel2 architecture
  294. #@param mod
  295. #@return a fully qualified module name
  296. def _original_name_from_module(mod):
  297. print("DEBUG MODNAME : ", mod, mod.__name__, mod.__package__)
  298. spl = mod.__name__.split('.')
  299. if len(spl) <= 2:
  300. return "lodel"
  301. return "lodel.%s" % ('.'.join(spl[2:]))