13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """ |
|
19 * the vregistry handles various types of objects interacting |
|
20 together. The vregistry handles registration of dynamically loaded |
|
21 objects and provides a convenient api to access those objects |
|
22 according to a context |
|
23 |
|
24 * to interact with the vregistry, objects should inherit from the |
|
25 AppObject abstract class |
|
26 |
|
27 * the selection procedure has been generalized by delegating to a |
|
28 selector, which is responsible to score the appobject according to the |
|
29 current state (req, rset, row, col). At the end of the selection, if |
|
30 a appobject class has been found, an instance of this class is |
|
31 returned. The selector is instantiated at appobject registration |
|
32 """ |
|
33 |
|
34 __docformat__ = "restructuredtext en" |
|
35 |
|
36 import sys |
|
37 from os import listdir, stat |
|
38 from os.path import dirname, join, realpath, isdir, exists |
|
39 from logging import getLogger |
|
40 from warnings import warn |
18 from warnings import warn |
41 |
19 warn('[3.15] moved to logilab.common.registry', DeprecationWarning, stacklevel=2) |
42 from logilab.common.deprecation import deprecated, class_moved |
20 from logilab.common.registry import * |
43 from logilab.common.logging_ext import set_log_methods |
|
44 |
|
45 from cubicweb import CW_SOFTWARE_ROOT |
|
46 from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject |
|
47 from cubicweb.appobject import AppObject, class_regid |
|
48 |
|
49 |
|
50 def _toload_info(path, extrapath, _toload=None): |
|
51 """return a dictionary of <modname>: <modpath> and an ordered list of |
|
52 (file, module name) to load |
|
53 """ |
|
54 from logilab.common.modutils import modpath_from_file |
|
55 if _toload is None: |
|
56 assert isinstance(path, list) |
|
57 _toload = {}, [] |
|
58 for fileordir in path: |
|
59 if isdir(fileordir) and exists(join(fileordir, '__init__.py')): |
|
60 subfiles = [join(fileordir, fname) for fname in listdir(fileordir)] |
|
61 _toload_info(subfiles, extrapath, _toload) |
|
62 elif fileordir[-3:] == '.py': |
|
63 modpath = modpath_from_file(fileordir, extrapath) |
|
64 # omit '__init__' from package's name to avoid loading that module |
|
65 # once for each name when it is imported by some other appobject |
|
66 # module. This supposes import in modules are done as:: |
|
67 # |
|
68 # from package import something |
|
69 # |
|
70 # not:: |
|
71 # |
|
72 # from package.__init__ import something |
|
73 # |
|
74 # which seems quite correct. |
|
75 if modpath[-1] == '__init__': |
|
76 modpath.pop() |
|
77 modname = '.'.join(modpath) |
|
78 _toload[0][modname] = fileordir |
|
79 _toload[1].append((fileordir, modname)) |
|
80 return _toload |
|
81 |
|
82 |
|
83 def classid(cls): |
|
84 """returns a unique identifier for an appobject class""" |
|
85 return '%s.%s' % (cls.__module__, cls.__name__) |
|
86 |
|
87 def class_registries(cls, registryname): |
|
88 if registryname: |
|
89 return (registryname,) |
|
90 return cls.__registries__ |
|
91 |
|
92 |
|
93 class Registry(dict): |
|
94 |
|
95 def __init__(self, config): |
|
96 super(Registry, self).__init__() |
|
97 self.config = config |
|
98 |
|
99 def __getitem__(self, name): |
|
100 """return the registry (dictionary of class objects) associated to |
|
101 this name |
|
102 """ |
|
103 try: |
|
104 return super(Registry, self).__getitem__(name) |
|
105 except KeyError: |
|
106 raise ObjectNotFound(name), None, sys.exc_info()[-1] |
|
107 |
|
108 def initialization_completed(self): |
|
109 for appobjects in self.itervalues(): |
|
110 for appobjectcls in appobjects: |
|
111 appobjectcls.__registered__(self) |
|
112 |
|
113 def register(self, obj, oid=None, clear=False): |
|
114 """base method to add an object in the registry""" |
|
115 assert not '__abstract__' in obj.__dict__ |
|
116 oid = oid or class_regid(obj) |
|
117 assert oid |
|
118 if clear: |
|
119 appobjects = self[oid] = [] |
|
120 else: |
|
121 appobjects = self.setdefault(oid, []) |
|
122 assert not obj in appobjects, \ |
|
123 'object %s is already registered' % obj |
|
124 appobjects.append(obj) |
|
125 |
|
126 def register_and_replace(self, obj, replaced): |
|
127 # XXXFIXME this is a duplication of unregister() |
|
128 # remove register_and_replace in favor of unregister + register |
|
129 # or simplify by calling unregister then register here |
|
130 if not isinstance(replaced, basestring): |
|
131 replaced = classid(replaced) |
|
132 # prevent from misspelling |
|
133 assert obj is not replaced, 'replacing an object by itself: %s' % obj |
|
134 registered_objs = self.get(class_regid(obj), ()) |
|
135 for index, registered in enumerate(registered_objs): |
|
136 if classid(registered) == replaced: |
|
137 del registered_objs[index] |
|
138 break |
|
139 else: |
|
140 self.warning('trying to replace an unregistered view %s by %s', |
|
141 replaced, obj) |
|
142 self.register(obj) |
|
143 |
|
144 def unregister(self, obj): |
|
145 clsid = classid(obj) |
|
146 oid = class_regid(obj) |
|
147 for registered in self.get(oid, ()): |
|
148 # use classid() to compare classes because vreg will probably |
|
149 # have its own version of the class, loaded through execfile |
|
150 if classid(registered) == clsid: |
|
151 self[oid].remove(registered) |
|
152 break |
|
153 else: |
|
154 self.warning('can\'t remove %s, no id %s in the registry', |
|
155 clsid, oid) |
|
156 |
|
157 def all_objects(self): |
|
158 """return a list containing all objects in this registry. |
|
159 """ |
|
160 result = [] |
|
161 for objs in self.values(): |
|
162 result += objs |
|
163 return result |
|
164 |
|
165 # dynamic selection methods ################################################ |
|
166 |
|
167 def object_by_id(self, oid, *args, **kwargs): |
|
168 """return object with the `oid` identifier. Only one object is expected |
|
169 to be found. |
|
170 |
|
171 raise :exc:`ObjectNotFound` if not object with id <oid> in <registry> |
|
172 |
|
173 raise :exc:`AssertionError` if there is more than one object there |
|
174 """ |
|
175 objects = self[oid] |
|
176 assert len(objects) == 1, objects |
|
177 return objects[0](*args, **kwargs) |
|
178 |
|
179 def select(self, __oid, *args, **kwargs): |
|
180 """return the most specific object among those with the given oid |
|
181 according to the given context. |
|
182 |
|
183 raise :exc:`ObjectNotFound` if not object with id <oid> in <registry> |
|
184 |
|
185 raise :exc:`NoSelectableObject` if not object apply |
|
186 """ |
|
187 obj = self._select_best(self[__oid], *args, **kwargs) |
|
188 if obj is None: |
|
189 raise NoSelectableObject(args, kwargs, self[__oid] ) |
|
190 return obj |
|
191 |
|
192 def select_or_none(self, __oid, *args, **kwargs): |
|
193 """return the most specific object among those with the given oid |
|
194 according to the given context, or None if no object applies. |
|
195 """ |
|
196 try: |
|
197 return self.select(__oid, *args, **kwargs) |
|
198 except (NoSelectableObject, ObjectNotFound): |
|
199 return None |
|
200 |
|
201 def possible_objects(self, *args, **kwargs): |
|
202 """return an iterator on possible objects in this registry for the given |
|
203 context |
|
204 """ |
|
205 for appobjects in self.itervalues(): |
|
206 obj = self._select_best(appobjects, *args, **kwargs) |
|
207 if obj is None: |
|
208 continue |
|
209 yield obj |
|
210 |
|
211 def _select_best(self, appobjects, *args, **kwargs): |
|
212 """return an instance of the most specific object according |
|
213 to parameters |
|
214 |
|
215 return None if not object apply (don't raise `NoSelectableObject` since |
|
216 it's costly when searching appobjects using `possible_objects` |
|
217 (e.g. searching for hooks). |
|
218 """ |
|
219 score, winners = 0, None |
|
220 for appobject in appobjects: |
|
221 appobjectscore = appobject.__select__(appobject, *args, **kwargs) |
|
222 if appobjectscore > score: |
|
223 score, winners = appobjectscore, [appobject] |
|
224 elif appobjectscore > 0 and appobjectscore == score: |
|
225 winners.append(appobject) |
|
226 if winners is None: |
|
227 return None |
|
228 if len(winners) > 1: |
|
229 # log in production environement / test, error while debugging |
|
230 msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)' |
|
231 if self.config.debugmode or self.config.mode == 'test': |
|
232 # raise bare exception in debug mode |
|
233 raise Exception(msg % (winners, args, kwargs.keys())) |
|
234 self.error(msg, winners, args, kwargs.keys()) |
|
235 # return the result of calling the appobject |
|
236 return winners[0](*args, **kwargs) |
|
237 |
|
238 # these are overridden by set_log_methods below |
|
239 # only defining here to prevent pylint from complaining |
|
240 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
|
241 |
|
242 |
|
243 class VRegistry(dict): |
|
244 """class responsible to register, propose and select the various |
|
245 elements used to build the web interface. Currently, we have templates, |
|
246 views, actions and components. |
|
247 """ |
|
248 |
|
249 def __init__(self, config): |
|
250 super(VRegistry, self).__init__() |
|
251 self.config = config |
|
252 # need to clean sys.path this to avoid import confusion pb (i.e. having |
|
253 # the same module loaded as 'cubicweb.web.views' subpackage and as |
|
254 # views' or 'web.views' subpackage. This is mainly for testing purpose, |
|
255 # we should'nt need this in production environment |
|
256 for webdir in (join(dirname(realpath(__file__)), 'web'), |
|
257 join(dirname(__file__), 'web')): |
|
258 if webdir in sys.path: |
|
259 sys.path.remove(webdir) |
|
260 if CW_SOFTWARE_ROOT in sys.path: |
|
261 sys.path.remove(CW_SOFTWARE_ROOT) |
|
262 |
|
263 def reset(self): |
|
264 # don't use self.clear, we want to keep existing subdictionaries |
|
265 for subdict in self.itervalues(): |
|
266 subdict.clear() |
|
267 self._lastmodifs = {} |
|
268 |
|
269 def __getitem__(self, name): |
|
270 """return the registry (dictionary of class objects) associated to |
|
271 this name |
|
272 """ |
|
273 try: |
|
274 return super(VRegistry, self).__getitem__(name) |
|
275 except KeyError: |
|
276 raise RegistryNotFound(name), None, sys.exc_info()[-1] |
|
277 |
|
278 # methods for explicit (un)registration ################################### |
|
279 |
|
280 # default class, when no specific class set |
|
281 REGISTRY_FACTORY = {None: Registry} |
|
282 |
|
283 def registry_class(self, regid): |
|
284 try: |
|
285 return self.REGISTRY_FACTORY[regid] |
|
286 except KeyError: |
|
287 return self.REGISTRY_FACTORY[None] |
|
288 |
|
289 def setdefault(self, regid): |
|
290 try: |
|
291 return self[regid] |
|
292 except KeyError: |
|
293 self[regid] = self.registry_class(regid)(self.config) |
|
294 return self[regid] |
|
295 |
|
296 # def clear(self, key): |
|
297 # regname, oid = key.split('.') |
|
298 # self[regname].pop(oid, None) |
|
299 |
|
300 def register_all(self, objects, modname, butclasses=()): |
|
301 """register all `objects` given. Objects which are not from the module |
|
302 `modname` or which are in `butclasses` won't be registered. |
|
303 |
|
304 Typical usage is: |
|
305 |
|
306 .. sourcecode:: python |
|
307 |
|
308 vreg.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,)) |
|
309 |
|
310 So you get partially automatic registration, keeping manual registration |
|
311 for some object (to use |
|
312 :meth:`~cubicweb.cwvreg.CubicWebRegistry.register_and_replace` for |
|
313 instance) |
|
314 """ |
|
315 for obj in objects: |
|
316 try: |
|
317 if obj.__module__ != modname or obj in butclasses: |
|
318 continue |
|
319 oid = class_regid(obj) |
|
320 except AttributeError: |
|
321 continue |
|
322 if oid and not '__abstract__' in obj.__dict__: |
|
323 self.register(obj, oid=oid) |
|
324 |
|
325 def register(self, obj, registryname=None, oid=None, clear=False): |
|
326 """register `obj` application object into `registryname` or |
|
327 `obj.__registry__` if not specified, with identifier `oid` or |
|
328 `obj.__regid__` if not specified. |
|
329 |
|
330 If `clear` is true, all objects with the same identifier will be |
|
331 previously unregistered. |
|
332 """ |
|
333 assert not '__abstract__' in obj.__dict__ |
|
334 try: |
|
335 vname = obj.__name__ |
|
336 except AttributeError: |
|
337 # XXX may occurs? |
|
338 vname = obj.__class__.__name__ |
|
339 for registryname in class_registries(obj, registryname): |
|
340 registry = self.setdefault(registryname) |
|
341 registry.register(obj, oid=oid, clear=clear) |
|
342 self.debug('register %s in %s[\'%s\']', |
|
343 vname, registryname, oid or class_regid(obj)) |
|
344 self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj |
|
345 |
|
346 def unregister(self, obj, registryname=None): |
|
347 """unregister `obj` application object from the registry `registryname` or |
|
348 `obj.__registry__` if not specified. |
|
349 """ |
|
350 for registryname in class_registries(obj, registryname): |
|
351 self[registryname].unregister(obj) |
|
352 |
|
353 def register_and_replace(self, obj, replaced, registryname=None): |
|
354 """register `obj` application object into `registryname` or |
|
355 `obj.__registry__` if not specified. If found, the `replaced` object |
|
356 will be unregistered first (else a warning will be issued as it's |
|
357 generally unexpected). |
|
358 """ |
|
359 for registryname in class_registries(obj, registryname): |
|
360 self[registryname].register_and_replace(obj, replaced) |
|
361 |
|
362 # initialization methods ################################################### |
|
363 |
|
364 def init_registration(self, path, extrapath=None): |
|
365 self.reset() |
|
366 # compute list of all modules that have to be loaded |
|
367 self._toloadmods, filemods = _toload_info(path, extrapath) |
|
368 # XXX is _loadedmods still necessary ? It seems like it's useful |
|
369 # to avoid loading same module twice, especially with the |
|
370 # _load_ancestors_then_object logic but this needs to be checked |
|
371 self._loadedmods = {} |
|
372 return filemods |
|
373 |
|
374 def register_objects(self, path, extrapath=None): |
|
375 # load views from each directory in the instance's path |
|
376 filemods = self.init_registration(path, extrapath) |
|
377 for filepath, modname in filemods: |
|
378 self.load_file(filepath, modname) |
|
379 self.initialization_completed() |
|
380 |
|
381 def initialization_completed(self): |
|
382 for regname, reg in self.iteritems(): |
|
383 reg.initialization_completed() |
|
384 |
|
385 def _mdate(self, filepath): |
|
386 try: |
|
387 return stat(filepath)[-2] |
|
388 except OSError: |
|
389 # this typically happens on emacs backup files (.#foo.py) |
|
390 self.warning('Unable to load %s. It is likely to be a backup file', |
|
391 filepath) |
|
392 return None |
|
393 |
|
394 def is_reload_needed(self, path): |
|
395 """return True if something module changed and the registry should be |
|
396 reloaded |
|
397 """ |
|
398 lastmodifs = self._lastmodifs |
|
399 for fileordir in path: |
|
400 if isdir(fileordir) and exists(join(fileordir, '__init__.py')): |
|
401 if self.is_reload_needed([join(fileordir, fname) |
|
402 for fname in listdir(fileordir)]): |
|
403 return True |
|
404 elif fileordir[-3:] == '.py': |
|
405 mdate = self._mdate(fileordir) |
|
406 if mdate is None: |
|
407 continue # backup file, see _mdate implementation |
|
408 elif "flymake" in fileordir: |
|
409 # flymake + pylint in use, don't consider these they will corrupt the registry |
|
410 continue |
|
411 if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate: |
|
412 self.info('File %s changed since last visit', fileordir) |
|
413 return True |
|
414 return False |
|
415 |
|
416 def load_file(self, filepath, modname): |
|
417 """load app objects from a python file""" |
|
418 from logilab.common.modutils import load_module_from_name |
|
419 if modname in self._loadedmods: |
|
420 return |
|
421 self._loadedmods[modname] = {} |
|
422 mdate = self._mdate(filepath) |
|
423 if mdate is None: |
|
424 return # backup file, see _mdate implementation |
|
425 elif "flymake" in filepath: |
|
426 # flymake + pylint in use, don't consider these they will corrupt the registry |
|
427 return |
|
428 # set update time before module loading, else we get some reloading |
|
429 # weirdness in case of syntax error or other error while importing the |
|
430 # module |
|
431 self._lastmodifs[filepath] = mdate |
|
432 # load the module |
|
433 module = load_module_from_name(modname) |
|
434 self.load_module(module) |
|
435 |
|
436 def load_module(self, module): |
|
437 self.info('loading %s from %s', module.__name__, module.__file__) |
|
438 if hasattr(module, 'registration_callback'): |
|
439 module.registration_callback(self) |
|
440 else: |
|
441 for objname, obj in vars(module).items(): |
|
442 if objname.startswith('_'): |
|
443 continue |
|
444 self._load_ancestors_then_object(module.__name__, obj) |
|
445 |
|
446 def _load_ancestors_then_object(self, modname, appobjectcls): |
|
447 """handle automatic appobject class registration: |
|
448 |
|
449 - first ensure parent classes are already registered |
|
450 |
|
451 - class with __abstract__ == True in their local dictionnary or |
|
452 with a name starting with an underscore are not registered |
|
453 |
|
454 - appobject class needs to have __registry__ and __regid__ attributes |
|
455 set to a non empty string to be registered. |
|
456 """ |
|
457 # imported classes |
|
458 objmodname = getattr(appobjectcls, '__module__', None) |
|
459 if objmodname != modname: |
|
460 if objmodname in self._toloadmods: |
|
461 self.load_file(self._toloadmods[objmodname], objmodname) |
|
462 return |
|
463 # skip non registerable object |
|
464 try: |
|
465 if not issubclass(appobjectcls, AppObject): |
|
466 return |
|
467 except TypeError: |
|
468 return |
|
469 clsid = classid(appobjectcls) |
|
470 if clsid in self._loadedmods[modname]: |
|
471 return |
|
472 self._loadedmods[modname][clsid] = appobjectcls |
|
473 for parent in appobjectcls.__bases__: |
|
474 self._load_ancestors_then_object(modname, parent) |
|
475 if (appobjectcls.__dict__.get('__abstract__') |
|
476 or appobjectcls.__name__[0] == '_' |
|
477 or not appobjectcls.__registries__ |
|
478 or not class_regid(appobjectcls)): |
|
479 return |
|
480 try: |
|
481 self.register(appobjectcls) |
|
482 except Exception, ex: |
|
483 if self.config.mode in ('test', 'dev'): |
|
484 raise |
|
485 self.exception('appobject %s registration failed: %s', |
|
486 appobjectcls, ex) |
|
487 # these are overridden by set_log_methods below |
|
488 # only defining here to prevent pylint from complaining |
|
489 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
|
490 |
|
491 |
|
492 # init logging |
|
493 set_log_methods(VRegistry, getLogger('cubicweb.vreg')) |
|
494 set_log_methods(Registry, getLogger('cubicweb.registry')) |
|
495 |
|
496 |
|
497 # XXX bw compat functions ##################################################### |
|
498 |
|
499 from cubicweb.appobject import objectify_selector, AndSelector, OrSelector, Selector |
|
500 |
|
501 Selector = class_moved(Selector) |
|