|
1 """ |
|
2 * the vregistry handle various type of objects interacting |
|
3 together. The vregistry handle registration of dynamically loaded |
|
4 objects and provide a convenient api access to those objects |
|
5 according to a context |
|
6 |
|
7 * to interact with the vregistry, object should inherit from the |
|
8 VObject abstract class |
|
9 |
|
10 * the registration procedure is delegated to a registerer. Each |
|
11 registerable vobject must defines its registerer class using the |
|
12 __registerer__ attribute. A registerer is instantianted at |
|
13 registration time after what the instance is lost |
|
14 |
|
15 * the selection procedure has been generalized by delegating to a |
|
16 selector, which is responsible to score the vobject according to the |
|
17 current state (req, rset, row, col). At the end of the selection, if |
|
18 a vobject class has been found, an instance of this class is |
|
19 returned. The selector is instantiated at vobject registration |
|
20 |
|
21 |
|
22 :organization: Logilab |
|
23 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
24 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
25 """ |
|
26 __docformat__ = "restructuredtext en" |
|
27 |
|
28 import sys |
|
29 from os import listdir, stat |
|
30 from os.path import dirname, join, realpath, split, isdir |
|
31 from logging import getLogger |
|
32 |
|
33 from cubicweb import CW_SOFTWARE_ROOT, set_log_methods |
|
34 from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject |
|
35 |
|
36 |
|
37 class vobject_helper(object): |
|
38 """object instantiated at registration time to help a wrapped |
|
39 VObject subclass |
|
40 """ |
|
41 |
|
42 def __init__(self, registry, vobject): |
|
43 self.registry = registry |
|
44 self.vobject = vobject |
|
45 self.config = registry.config |
|
46 self.schema = registry.schema |
|
47 |
|
48 |
|
49 class registerer(vobject_helper): |
|
50 """do whatever is needed at registration time for the wrapped |
|
51 class, according to current application schema and already |
|
52 registered objects of the same kind (i.e. same registry name and |
|
53 same id). |
|
54 |
|
55 The wrapped class may be skipped, some previously selected object |
|
56 may be kicked out... After whatever works needed, if the object or |
|
57 a transformed object is returned, it will be added to previously |
|
58 registered objects. |
|
59 """ |
|
60 |
|
61 def __init__(self, registry, vobject): |
|
62 super(registerer, self).__init__(registry, vobject) |
|
63 self.kicked = set() |
|
64 |
|
65 def do_it_yourself(self, registered): |
|
66 raise NotImplementedError(str(self.vobject)) |
|
67 |
|
68 def kick(self, registered, kicked): |
|
69 self.debug('kicking vobject %s', kicked) |
|
70 registered.remove(kicked) |
|
71 self.kicked.add(kicked.classid()) |
|
72 |
|
73 def skip(self): |
|
74 self.debug('no schema compat, skipping %s', self.vobject) |
|
75 |
|
76 |
|
77 def selector(cls, *args, **kwargs): |
|
78 """selector is called to help choosing the correct object for a |
|
79 particular request and result set by returning a score. |
|
80 |
|
81 it must implement a .score_method taking a request, a result set and |
|
82 optionaly row and col arguments which return an int telling how well |
|
83 the wrapped class apply to the given request and result set. 0 score |
|
84 means that it doesn't apply. |
|
85 |
|
86 rset may be None. If not, row and col arguments may be optionally |
|
87 given if the registry is scoring a given row or a given cell of |
|
88 the result set (both row and col are int if provided). |
|
89 """ |
|
90 raise NotImplementedError(cls) |
|
91 |
|
92 |
|
93 class autoselectors(type): |
|
94 """implements __selectors__ / __select__ compatibility layer so that: |
|
95 |
|
96 __select__ = chainall(classmethod(A, B, C)) |
|
97 |
|
98 can be replaced by something like: |
|
99 |
|
100 __selectors__ = (A, B, C) |
|
101 """ |
|
102 def __new__(mcs, name, bases, classdict): |
|
103 if '__select__' in classdict and '__selectors__' in classdict: |
|
104 raise TypeError("__select__ and __selectors__ " |
|
105 "can't be used together") |
|
106 if '__select__' not in classdict and '__selectors__' in classdict: |
|
107 selectors = classdict['__selectors__'] |
|
108 classdict['__select__'] = classmethod(chainall(*selectors)) |
|
109 return super(autoselectors, mcs).__new__(mcs, name, bases, classdict) |
|
110 |
|
111 def __setattr__(self, attr, value): |
|
112 if attr == '__selectors__': |
|
113 self.__select__ = classmethod(chainall(*value)) |
|
114 super(autoselectors, self).__setattr__(attr, value) |
|
115 |
|
116 |
|
117 class VObject(object): |
|
118 """visual object, use to be handled somehow by the visual components |
|
119 registry. |
|
120 |
|
121 The following attributes should be set on concret vobject subclasses: |
|
122 |
|
123 :__registry__: |
|
124 name of the registry for this object (string like 'views', |
|
125 'templates'...) |
|
126 :id: |
|
127 object's identifier in the registry (string like 'main', |
|
128 'primary', 'folder_box') |
|
129 :__registerer__: |
|
130 registration helper class |
|
131 :__select__: |
|
132 selection helper function |
|
133 :__selectors__: |
|
134 tuple of selectors to be chained |
|
135 (__select__ and __selectors__ are mutually exclusive) |
|
136 |
|
137 Moreover, the `__abstract__` attribute may be set to True to indicate |
|
138 that a vobject is abstract and should not be registered |
|
139 """ |
|
140 __metaclass__ = autoselectors |
|
141 # necessary attributes to interact with the registry |
|
142 id = None |
|
143 __registry__ = None |
|
144 __registerer__ = None |
|
145 __select__ = None |
|
146 |
|
147 @classmethod |
|
148 def registered(cls, registry): |
|
149 """called by the registry when the vobject has been registered. |
|
150 |
|
151 It must return the object that will be actually registered (this |
|
152 may be the right hook to create an instance for example). By |
|
153 default the vobject is returned without any transformation. |
|
154 """ |
|
155 return cls |
|
156 |
|
157 @classmethod |
|
158 def selected(cls, *args, **kwargs): |
|
159 """called by the registry when the vobject has been selected. |
|
160 |
|
161 It must return the object that will be actually returned by the |
|
162 .select method (this may be the right hook to create an |
|
163 instance for example). By default the selected object is |
|
164 returned without any transformation. |
|
165 """ |
|
166 return cls |
|
167 |
|
168 @classmethod |
|
169 def classid(cls): |
|
170 """returns a unique identifier for the vobject""" |
|
171 return '%s.%s' % (cls.__module__, cls.__name__) |
|
172 |
|
173 |
|
174 class VRegistry(object): |
|
175 """class responsible to register, propose and select the various |
|
176 elements used to build the web interface. Currently, we have templates, |
|
177 views, actions and components. |
|
178 """ |
|
179 |
|
180 def __init__(self, config):#, cache_size=1000): |
|
181 self.config = config |
|
182 # dictionnary of registry (themself dictionnary) by name |
|
183 self._registries = {} |
|
184 self._lastmodifs = {} |
|
185 |
|
186 def reset(self): |
|
187 self._registries = {} |
|
188 self._lastmodifs = {} |
|
189 |
|
190 def __getitem__(self, key): |
|
191 return self._registries[key] |
|
192 |
|
193 def get(self, key, default=None): |
|
194 return self._registries.get(key, default) |
|
195 |
|
196 def items(self): |
|
197 return self._registries.items() |
|
198 |
|
199 def values(self): |
|
200 return self._registries.values() |
|
201 |
|
202 def __contains__(self, key): |
|
203 return key in self._registries |
|
204 |
|
205 def register_vobject_class(self, cls, _kicked=set()): |
|
206 """handle vobject class registration |
|
207 |
|
208 vobject class with __abstract__ == True in their local dictionnary or |
|
209 with a name starting starting by an underscore are not registered. |
|
210 Also a vobject class needs to have __registry__ and id attributes set |
|
211 to a non empty string to be registered. |
|
212 |
|
213 Registration is actually handled by vobject's registerer. |
|
214 """ |
|
215 if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_' |
|
216 or not cls.__registry__ or not cls.id): |
|
217 return |
|
218 # while reloading a module : |
|
219 # if cls was previously kicked, it means that there is a more specific |
|
220 # vobject defined elsewhere re-registering cls would kick it out |
|
221 if cls.classid() in _kicked: |
|
222 self.debug('not re-registering %s because it was previously kicked', |
|
223 cls.classid()) |
|
224 else: |
|
225 regname = cls.__registry__ |
|
226 if cls.id in self.config['disable-%s' % regname]: |
|
227 return |
|
228 registry = self._registries.setdefault(regname, {}) |
|
229 vobjects = registry.setdefault(cls.id, []) |
|
230 registerer = cls.__registerer__(self, cls) |
|
231 cls = registerer.do_it_yourself(vobjects) |
|
232 #_kicked |= registerer.kicked |
|
233 if cls: |
|
234 vobject = cls.registered(self) |
|
235 try: |
|
236 vname = vobject.__name__ |
|
237 except AttributeError: |
|
238 vname = vobject.__class__.__name__ |
|
239 self.debug('registered vobject %s in registry %s with id %s', |
|
240 vname, cls.__registry__, cls.id) |
|
241 vobjects.append(vobject) |
|
242 |
|
243 def unregister_module_vobjects(self, modname): |
|
244 """removes registered objects coming from a given module |
|
245 |
|
246 returns a dictionnary classid/class of all classes that will need |
|
247 to be updated after reload (i.e. vobjects referencing classes defined |
|
248 in the <modname> module) |
|
249 """ |
|
250 unregistered = {} |
|
251 # browse each registered object |
|
252 for registry, objdict in self.items(): |
|
253 for oid, objects in objdict.items(): |
|
254 for obj in objects[:]: |
|
255 objname = obj.classid() |
|
256 # if the vobject is defined in this module, remove it |
|
257 if objname.startswith(modname): |
|
258 unregistered[objname] = obj |
|
259 objects.remove(obj) |
|
260 self.debug('unregistering %s in %s registry', |
|
261 objname, registry) |
|
262 # if not, check if the vobject can be found in baseclasses |
|
263 # (because we also want subclasses to be updated) |
|
264 else: |
|
265 if not isinstance(obj, type): |
|
266 obj = obj.__class__ |
|
267 for baseclass in obj.__bases__: |
|
268 if hasattr(baseclass, 'classid'): |
|
269 baseclassid = baseclass.classid() |
|
270 if baseclassid.startswith(modname): |
|
271 unregistered[baseclassid] = baseclass |
|
272 # update oid entry |
|
273 if objects: |
|
274 objdict[oid] = objects |
|
275 else: |
|
276 del objdict[oid] |
|
277 return unregistered |
|
278 |
|
279 |
|
280 def update_registered_subclasses(self, oldnew_mapping): |
|
281 """updates subclasses of re-registered vobjects |
|
282 |
|
283 if baseviews.PrimaryView is changed, baseviews.py will be reloaded |
|
284 automatically and the new version of PrimaryView will be registered. |
|
285 But all existing subclasses must also be notified of this change, and |
|
286 that's what this method does |
|
287 |
|
288 :param oldnew_mapping: a dict mapping old version of a class to |
|
289 the new version |
|
290 """ |
|
291 # browse each registered object |
|
292 for objdict in self.values(): |
|
293 for objects in objdict.values(): |
|
294 for obj in objects: |
|
295 if not isinstance(obj, type): |
|
296 obj = obj.__class__ |
|
297 # build new baseclasses tuple |
|
298 newbases = tuple(oldnew_mapping.get(baseclass, baseclass) |
|
299 for baseclass in obj.__bases__) |
|
300 # update obj's baseclasses tuple (__bases__) if needed |
|
301 if newbases != obj.__bases__: |
|
302 self.debug('updating %s.%s base classes', |
|
303 obj.__module__, obj.__name__) |
|
304 obj.__bases__ = newbases |
|
305 |
|
306 def registry(self, name): |
|
307 """return the registry (dictionary of class objects) associated to |
|
308 this name |
|
309 """ |
|
310 try: |
|
311 return self._registries[name] |
|
312 except KeyError: |
|
313 raise RegistryNotFound(name), None, sys.exc_info()[-1] |
|
314 |
|
315 def registry_objects(self, name, oid=None): |
|
316 """returns objects registered with the given oid in the given registry. |
|
317 If no oid is given, return all objects in this registry |
|
318 """ |
|
319 registry = self.registry(name) |
|
320 if oid: |
|
321 try: |
|
322 return registry[oid] |
|
323 except KeyError: |
|
324 raise ObjectNotFound(oid), None, sys.exc_info()[-1] |
|
325 else: |
|
326 result = [] |
|
327 for objs in registry.values(): |
|
328 result += objs |
|
329 return result |
|
330 |
|
331 def select(self, vobjects, *args, **kwargs): |
|
332 """return an instance of the most specific object according |
|
333 to parameters |
|
334 |
|
335 raise NoSelectableObject if not object apply |
|
336 """ |
|
337 score, winner = 0, None |
|
338 for vobject in vobjects: |
|
339 vobjectscore = vobject.__select__(*args, **kwargs) |
|
340 if vobjectscore > score: |
|
341 score, winner = vobjectscore, vobject |
|
342 if winner is None: |
|
343 raise NoSelectableObject('args: %s\nkwargs: %s %s' |
|
344 % (args, kwargs.keys(), [repr(v) for v in vobjects])) |
|
345 # return the result of the .selected method of the vobject |
|
346 return winner.selected(*args, **kwargs) |
|
347 |
|
348 def possible_objects(self, registry, *args, **kwargs): |
|
349 """return an iterator on possible objects in a registry for this result set |
|
350 |
|
351 actions returned are classes, not instances |
|
352 """ |
|
353 for vobjects in self.registry(registry).values(): |
|
354 try: |
|
355 yield self.select(vobjects, *args, **kwargs) |
|
356 except NoSelectableObject: |
|
357 continue |
|
358 |
|
359 def select_object(self, registry, cid, *args, **kwargs): |
|
360 """return the most specific component according to the resultset""" |
|
361 return self.select(self.registry_objects(registry, cid), *args, **kwargs) |
|
362 |
|
363 def object_by_id(self, registry, cid, *args, **kwargs): |
|
364 """return the most specific component according to the resultset""" |
|
365 objects = self[registry][cid] |
|
366 assert len(objects) == 1, objects |
|
367 return objects[0].selected(*args, **kwargs) |
|
368 |
|
369 # intialization methods ################################################### |
|
370 |
|
371 |
|
372 def register_objects(self, path, force_reload=None): |
|
373 if force_reload is None: |
|
374 force_reload = self.config.mode == 'dev' |
|
375 elif not force_reload: |
|
376 # force_reload == False usually mean modules have been reloaded |
|
377 # by another connection, so we want to update the registry |
|
378 # content even if there has been no module content modification |
|
379 self.reset() |
|
380 # need to clean sys.path this to avoid import confusion pb (i.e. |
|
381 # having the same module loaded as 'cubicweb.web.views' subpackage and |
|
382 # as views' or 'web.views' subpackage |
|
383 # this is mainly for testing purpose, we should'nt need this in |
|
384 # production environment |
|
385 for webdir in (join(dirname(realpath(__file__)), 'web'), |
|
386 join(dirname(__file__), 'web')): |
|
387 if webdir in sys.path: |
|
388 sys.path.remove(webdir) |
|
389 if CW_SOFTWARE_ROOT in sys.path: |
|
390 sys.path.remove(CW_SOFTWARE_ROOT) |
|
391 # load views from each directory in the application's path |
|
392 change = False |
|
393 for fileordirectory in path: |
|
394 if isdir(fileordirectory): |
|
395 if self.read_directory(fileordirectory, force_reload): |
|
396 change = True |
|
397 else: |
|
398 directory, filename = split(fileordirectory) |
|
399 if self.load_file(directory, filename, force_reload): |
|
400 change = True |
|
401 if change: |
|
402 for registry, objects in self.items(): |
|
403 self.debug('available in registry %s: %s', registry, |
|
404 sorted(objects)) |
|
405 return change |
|
406 |
|
407 def read_directory(self, directory, force_reload=False): |
|
408 """read a directory and register available views""" |
|
409 modified_on = stat(realpath(directory))[-2] |
|
410 # only read directory if it was modified |
|
411 _lastmodifs = self._lastmodifs |
|
412 if directory in _lastmodifs and modified_on <= _lastmodifs[directory]: |
|
413 return False |
|
414 self.info('loading directory %s', directory) |
|
415 for filename in listdir(directory): |
|
416 if filename[-3:] == '.py': |
|
417 try: |
|
418 self.load_file(directory, filename, force_reload) |
|
419 except OSError: |
|
420 # this typically happens on emacs backup files (.#foo.py) |
|
421 self.warning('Unable to load file %s. It is likely to be a backup file', |
|
422 filename) |
|
423 except Exception, ex: |
|
424 if self.config.mode in ('dev', 'test'): |
|
425 raise |
|
426 self.exception('%r while loading file %s', ex, filename) |
|
427 _lastmodifs[directory] = modified_on |
|
428 return True |
|
429 |
|
430 def load_file(self, directory, filename, force_reload=False): |
|
431 """load visual objects from a python file""" |
|
432 from logilab.common.modutils import load_module_from_modpath, modpath_from_file |
|
433 filepath = join(directory, filename) |
|
434 modified_on = stat(filepath)[-2] |
|
435 modpath = modpath_from_file(join(directory, filename)) |
|
436 modname = '.'.join(modpath) |
|
437 unregistered = {} |
|
438 _lastmodifs = self._lastmodifs |
|
439 if filepath in _lastmodifs: |
|
440 # only load file if it was modified |
|
441 if modified_on <= _lastmodifs[filepath]: |
|
442 return |
|
443 else: |
|
444 # if it was modified, unregister all exisiting objects |
|
445 # from this module, and keep track of what was unregistered |
|
446 unregistered = self.unregister_module_vobjects(modname) |
|
447 # load the module |
|
448 module = load_module_from_modpath(modpath, use_sys=not force_reload) |
|
449 registered = self.load_module(module) |
|
450 # if something was unregistered, we need to update places where it was |
|
451 # referenced |
|
452 if unregistered: |
|
453 # oldnew_mapping = {} |
|
454 oldnew_mapping = dict((unregistered[name], registered[name]) |
|
455 for name in unregistered if name in registered) |
|
456 self.update_registered_subclasses(oldnew_mapping) |
|
457 _lastmodifs[filepath] = modified_on |
|
458 return True |
|
459 |
|
460 def load_module(self, module): |
|
461 registered = {} |
|
462 self.info('loading %s', module) |
|
463 for objname, obj in vars(module).items(): |
|
464 if objname.startswith('_'): |
|
465 continue |
|
466 self.load_ancestors_then_object(module.__name__, registered, obj) |
|
467 return registered |
|
468 |
|
469 def load_ancestors_then_object(self, modname, registered, obj): |
|
470 # skip imported classes |
|
471 if getattr(obj, '__module__', None) != modname: |
|
472 return |
|
473 # skip non registerable object |
|
474 try: |
|
475 if not issubclass(obj, VObject): |
|
476 return |
|
477 except TypeError: |
|
478 return |
|
479 objname = '%s.%s' % (modname, obj.__name__) |
|
480 if objname in registered: |
|
481 return |
|
482 registered[objname] = obj |
|
483 for parent in obj.__bases__: |
|
484 self.load_ancestors_then_object(modname, registered, parent) |
|
485 self.load_object(obj) |
|
486 |
|
487 def load_object(self, obj): |
|
488 try: |
|
489 self.register_vobject_class(obj) |
|
490 except Exception, ex: |
|
491 if self.config.mode in ('test', 'dev'): |
|
492 raise |
|
493 self.exception('vobject %s registration failed: %s', obj, ex) |
|
494 |
|
495 # init logging |
|
496 set_log_methods(VObject, getLogger('cubicweb')) |
|
497 set_log_methods(VRegistry, getLogger('cubicweb.registry')) |
|
498 set_log_methods(registerer, getLogger('cubicweb.registration')) |
|
499 |
|
500 |
|
501 # advanced selector building functions ######################################## |
|
502 |
|
503 def chainall(*selectors): |
|
504 """return a selector chaining given selectors. If one of |
|
505 the selectors fail, selection will fail, else the returned score |
|
506 will be the sum of each selector'score |
|
507 """ |
|
508 assert selectors |
|
509 def selector(cls, *args, **kwargs): |
|
510 score = 0 |
|
511 for selector in selectors: |
|
512 partscore = selector(cls, *args, **kwargs) |
|
513 if not partscore: |
|
514 return 0 |
|
515 score += partscore |
|
516 return score |
|
517 return selector |
|
518 |
|
519 def chainfirst(*selectors): |
|
520 """return a selector chaining given selectors. If all |
|
521 the selectors fail, selection will fail, else the returned score |
|
522 will be the first non-zero selector score |
|
523 """ |
|
524 assert selectors |
|
525 def selector(cls, *args, **kwargs): |
|
526 for selector in selectors: |
|
527 partscore = selector(cls, *args, **kwargs) |
|
528 if partscore: |
|
529 return partscore |
|
530 return 0 |
|
531 return selector |
|
532 |