Browse Source

POC of context manager to handle virtual lodel packages

Yann Weber 8 years ago
parent
commit
572c408a79

+ 266
- 0
lodel/context.py View File

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

+ 9
- 2
lodel/settings/settings.py View File

@@ -8,8 +8,15 @@ import warnings
8 8
 import types # for dynamic bindings
9 9
 from collections import namedtuple
10 10
 
11
-from lodel import logger
12
-from lodel.settings.utils import SettingsError, SettingsErrors
11
+from lodel.context import LodelContext
12
+
13
+LodelContext.expose_modules(globals(),{
14
+    'lodel.logger': 'logger',
15
+    'lodel.settings.utils': ['SettingsError', 'SettingsErrors']})
16
+    
17
+
18
+#from lodel import logger
19
+#from lodel.settings.utils import SettingsError, SettingsErrors
13 20
 from lodel.settings.validator import SettingValidator, LODEL2_CONF_SPECS, \
14 21
     confspec_append
15 22
 from lodel.settings.settings_loader import SettingsLoader

+ 0
- 0
lodelsites/__init__.py View File


+ 4
- 0
progs/slim/install_model/loader.py View File

@@ -24,6 +24,10 @@ except ImportError as e:
24 24
     print(e)
25 25
     exit(1)
26 26
 
27
+#Set context to MONOSITE
28
+from lodel.context import LodelContext
29
+LodelContext.init()
30
+
27 31
 if 'LODEL2_NO_SETTINGS_LOAD' not in os.environ:
28 32
     #
29 33
     # Loading settings

+ 0
- 0
progs/slim/install_model/lodel_admin.py View File


Loading…
Cancel
Save