OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.web.test.test_woven -*- | |
2 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 __version__ = "$Revision: 1.53 $"[11:-2] | |
7 | |
8 import types | |
9 import weakref | |
10 import warnings | |
11 | |
12 from zope.interface import implements | |
13 | |
14 from twisted.python import components, reflect | |
15 from twisted.internet import defer | |
16 | |
17 from twisted.web.woven import interfaces | |
18 | |
19 class _Nothing: pass | |
20 | |
21 def adaptToIModel(m, parent=None, submodel=None): | |
22 adapted = interfaces.IModel(m, None) | |
23 if adapted is None: | |
24 adapted = Wrapper(m) | |
25 adapted.parent = parent | |
26 adapted.name = submodel | |
27 return adapted | |
28 | |
29 | |
30 class Model: | |
31 """ | |
32 A Model which keeps track of views which are looking at it in order | |
33 to notify them when the model changes. | |
34 """ | |
35 implements(interfaces.IModel) | |
36 | |
37 def __init__(self, *args, **kwargs): | |
38 if len(args): | |
39 self.original = args[0] | |
40 else: | |
41 self.original = self | |
42 self.name = '' | |
43 self.parent = None | |
44 self.views = [] | |
45 self.subviews = {} | |
46 self.submodels = {} | |
47 self._getter = kwargs.get('getter') | |
48 self._setter = kwargs.get('setter') | |
49 self.cachedFor = None | |
50 self.initialize(*args, **kwargs) | |
51 | |
52 def __getstate__(self): | |
53 self.views = [] | |
54 self.subviews = {} | |
55 self.submodels = {} | |
56 return self.__dict__ | |
57 | |
58 def invalidateCache(self): | |
59 """Invalidate the cache for this object, so the next time | |
60 getData is called, it's getter method is called again. | |
61 """ | |
62 self.cachedFor = None | |
63 | |
64 def initialize(self, *args, **kwargs): | |
65 """ | |
66 Hook for subclasses to initialize themselves without having to | |
67 mess with the __init__ chain. | |
68 """ | |
69 pass | |
70 | |
71 def addView(self, view): | |
72 """ | |
73 Add a view for the model to keep track of. | |
74 """ | |
75 if view not in [ref() for ref in self.views]: | |
76 self.views.append(weakref.ref(view)) | |
77 | |
78 def addSubview(self, name, subview): | |
79 subviewList = self.subviews.get(name, []) | |
80 subviewList.append(weakref.ref(subview)) | |
81 self.subviews[name] = subviewList | |
82 | |
83 def removeView(self, view): | |
84 """ | |
85 Remove a view that the model no longer should keep track of. | |
86 """ | |
87 # AM: loop on a _copy_ of the list, since we're changing it!!! | |
88 for weakref in list(self.views): | |
89 ref = weakref() | |
90 if ref is view or ref is None: | |
91 self.views.remove(weakref) | |
92 | |
93 def setGetter(self, getter): | |
94 self._getter = getter | |
95 | |
96 def setSetter(self, setter): | |
97 self._setter = setter | |
98 | |
99 def notify(self, changed=None): | |
100 """ | |
101 Notify all views that something was changed on me. | |
102 Passing a dictionary of {'attribute': 'new value'} in changed | |
103 will pass this dictionary to the view for increased performance. | |
104 If you don't want to do this, don't, and just use the traditional | |
105 MVC paradigm of querying the model for things you're interested | |
106 in. | |
107 """ | |
108 self.cachedFor = None | |
109 if changed is None: changed = {} | |
110 retVal = [] | |
111 # AM: loop on a _copy_ of the list, since we're changing it!!! | |
112 for view in list(self.views): | |
113 ref = view() | |
114 if ref is not None: | |
115 retVal.append((ref, ref.modelChanged(changed))) | |
116 else: | |
117 self.views.remove(view) | |
118 for key, value in self.subviews.items(): | |
119 if value.wantsAllNotifications or changed.has_key(key): | |
120 for item in list(value): | |
121 ref = item() | |
122 if ref is not None: | |
123 retVal.append((ref, ref.modelChanged(changed))) | |
124 else: | |
125 value.remove(item) | |
126 return retVal | |
127 | |
128 protected_names = ['initialize', 'addView', 'addSubview', 'removeView', 'not
ify', 'getSubmodel', 'setSubmodel', 'getData', 'setData'] | |
129 allowed_names = [] | |
130 | |
131 def lookupSubmodel(self, request, submodelName): | |
132 """ | |
133 Look up a full submodel name. I will split on `/' and call | |
134 L{getSubmodel} on each element in the 'path'. | |
135 | |
136 Override me if you don't want 'traversing'-style lookup, but | |
137 would rather like to look up a model based on the entire model | |
138 name specified. | |
139 | |
140 If you override me to return Deferreds, make sure I look up | |
141 values in a cache (created by L{setSubmodel}) before doing a | |
142 regular Deferred lookup. | |
143 | |
144 XXX: Move bits of this docstring to interfaces.py | |
145 """ | |
146 if not submodelName: | |
147 return None | |
148 | |
149 # Special case: If the first character is / | |
150 # Start at the bottom of the model stack | |
151 currentModel = self | |
152 if submodelName[0] == '/': | |
153 while currentModel.parent is not None: | |
154 currentModel = currentModel.parent | |
155 submodelName = submodelName[1:] | |
156 | |
157 submodelList = submodelName.split('/') #[:-1] | |
158 # print "submodelList", submodelList | |
159 for element in submodelList: | |
160 if element == '.' or element == '': | |
161 continue | |
162 elif element == '..': | |
163 currentModel = currentModel.parent | |
164 else: | |
165 currentModel = currentModel.getSubmodel(request, element) | |
166 if currentModel is None: | |
167 return None | |
168 return currentModel | |
169 | |
170 def submodelCheck(self, request, name): | |
171 """Check if a submodel name is allowed. Subclass me to implement a | |
172 name security policy. | |
173 """ | |
174 if self.allowed_names: | |
175 return (name in self.allowed_names) | |
176 else: | |
177 return (name and name[0] != '_' and name not in self.protected_names
) | |
178 | |
179 | |
180 def submodelFactory(self, request, name): | |
181 warnings.warn("Warning: default Model lookup strategy is changing:" | |
182 "use either AttributeModel or MethodModel for now.", | |
183 DeprecationWarning) | |
184 if hasattr(self, name): | |
185 return getattr(self, name) | |
186 else: | |
187 return None | |
188 | |
189 def getSubmodel(self, request, name): | |
190 """ | |
191 Get the submodel `name' of this model. If I ever return a | |
192 Deferred, then I ought to check for cached values (created by | |
193 L{setSubmodel}) before doing a regular Deferred lookup. | |
194 """ | |
195 if self.submodels.has_key(name): | |
196 return self.submodels[name] | |
197 if not self.submodelCheck(request, name): | |
198 return None | |
199 m = self.submodelFactory(request, name) | |
200 if m is None: | |
201 return None | |
202 sm = adaptToIModel(m, self, name) | |
203 self.submodels[name] = sm | |
204 return sm | |
205 | |
206 def setSubmodel(self, request=None, name=None, value=None): | |
207 """ | |
208 Set a submodel on this model. If getSubmodel or lookupSubmodel | |
209 ever return a Deferred, I ought to set this in a place that | |
210 lookupSubmodel/getSubmodel know about, so they can use it as a | |
211 cache. | |
212 """ | |
213 if self.submodelCheck(request, name): | |
214 if self.submodels.has_key(name): | |
215 del self.submodels[name] | |
216 setattr(self, name, value) | |
217 | |
218 def dataWillChange(self): | |
219 pass | |
220 | |
221 def getData(self, request): | |
222 if self.cachedFor != id(request) and self._getter is not None: | |
223 self.cachedFor = id(request) | |
224 self.dataWillChange() | |
225 self.orig = self.original = self._getter(request) | |
226 return self.original | |
227 | |
228 def setData(self, request, data): | |
229 if self._setter is not None: | |
230 self.cachedFor = None | |
231 return self._setter(request, data) | |
232 else: | |
233 if hasattr(self, 'parent') and self.parent: | |
234 self.parent.setSubmodel(request, self.name, data) | |
235 self.orig = self.original = data | |
236 | |
237 | |
238 class MethodModel(Model): | |
239 """Look up submodels with wmfactory_* methods. | |
240 """ | |
241 | |
242 def submodelCheck(self, request, name): | |
243 """Allow any submodel for which I have a submodel. | |
244 """ | |
245 return hasattr(self, "wmfactory_"+name) | |
246 | |
247 def submodelFactory(self, request, name): | |
248 """Call a wmfactory_name method on this model. | |
249 """ | |
250 meth = getattr(self, "wmfactory_"+name) | |
251 return meth(request) | |
252 | |
253 def getSubmodel(self, request=None, name=None): | |
254 if name is None: | |
255 warnings.warn("Warning! getSubmodel should now take the request as t
he first argument") | |
256 name = request | |
257 request = None | |
258 | |
259 cached = self.submodels.has_key(name) | |
260 sm = Model.getSubmodel(self, request, name) | |
261 if sm is not None: | |
262 if not cached: | |
263 sm.cachedFor = id(request) | |
264 sm._getter = getattr(self, "wmfactory_"+name) | |
265 return sm | |
266 | |
267 | |
268 class AttributeModel(Model): | |
269 """Look up submodels as attributes with hosts.allow/deny-style security. | |
270 """ | |
271 def submodelFactory(self, request, name): | |
272 if hasattr(self, name): | |
273 return getattr(self, name) | |
274 else: | |
275 return None | |
276 | |
277 | |
278 #backwards compatibility | |
279 WModel = Model | |
280 | |
281 | |
282 class Wrapper(Model): | |
283 """ | |
284 I'm a generic wrapper to provide limited interaction with the | |
285 Woven models and submodels. | |
286 """ | |
287 parent = None | |
288 name = None | |
289 def __init__(self, orig): | |
290 Model.__init__(self) | |
291 self.orig = self.original = orig | |
292 | |
293 def dataWillChange(self): | |
294 pass | |
295 | |
296 def __repr__(self): | |
297 myLongName = reflect.qual(self.__class__) | |
298 return "<%s instance at 0x%x: wrapped data: %s>" % (myLongName, | |
299 id(self), self.origi
nal) | |
300 | |
301 | |
302 class ListModel(Wrapper): | |
303 """ | |
304 I wrap a Python list and allow it to interact with the Woven | |
305 models and submodels. | |
306 """ | |
307 def dataWillChange(self): | |
308 self.submodels = {} | |
309 | |
310 def getSubmodel(self, request=None, name=None): | |
311 if name is None and type(request) is type(""): | |
312 warnings.warn("Warning!") | |
313 name = request | |
314 request = None | |
315 if self.submodels.has_key(name): | |
316 return self.submodels[name] | |
317 orig = self.original | |
318 try: | |
319 i = int(name) | |
320 except: | |
321 return None | |
322 if i > len(orig): | |
323 return None | |
324 sm = adaptToIModel(orig[i], self, name) | |
325 self.submodels[name] = sm | |
326 return sm | |
327 | |
328 def setSubmodel(self, request=None, name=None, value=None): | |
329 if value is None: | |
330 warnings.warn("Warning!") | |
331 value = name | |
332 name = request | |
333 request = None | |
334 self.original[int(name)] = value | |
335 | |
336 def __len__(self): | |
337 return len(self.original) | |
338 | |
339 def __getitem__(self, name): | |
340 return self.getSubmodel(None, str(name)) | |
341 | |
342 def __setitem__(self, name, value): | |
343 self.setSubmodel(None, str(name), value) | |
344 | |
345 def __repr__(self): | |
346 myLongName = reflect.qual(self.__class__) | |
347 return "<%s instance at 0x%x: wrapped data: %s>" % (myLongName, | |
348 id(self), self.origi
nal) | |
349 | |
350 | |
351 class StringModel(ListModel): | |
352 | |
353 """ I wrap a Python string and allow it to interact with the Woven models | |
354 and submodels. """ | |
355 | |
356 def setSubmodel(self, request=None, name=None, value=None): | |
357 raise ValueError("Strings are immutable.") | |
358 | |
359 | |
360 # pyPgSQL returns "PgResultSet" instances instead of lists, which look, act | |
361 # and breathe just like lists. pyPgSQL really shouldn't do this, but this works | |
362 try: | |
363 from pyPgSQL import PgSQL | |
364 components.registerAdapter(ListModel, PgSQL.PgResultSet, interfaces.IModel) | |
365 except: | |
366 pass | |
367 | |
368 class DictionaryModel(Wrapper): | |
369 """ | |
370 I wrap a Python dictionary and allow it to interact with the Woven | |
371 models and submodels. | |
372 """ | |
373 def dataWillChange(self): | |
374 self.submodels = {} | |
375 | |
376 def getSubmodel(self, request=None, name=None): | |
377 if name is None and type(request) is type(""): | |
378 warnings.warn("getSubmodel must get a request argument now") | |
379 name = request | |
380 request = None | |
381 if self.submodels.has_key(name): | |
382 return self.submodels[name] | |
383 orig = self.original | |
384 if name not in orig: | |
385 return None | |
386 sm = adaptToIModel(orig[name], self, name) | |
387 self.submodels[name] = sm | |
388 return sm | |
389 | |
390 def setSubmodel(self, request=None, name=None, value=None): | |
391 if value is None: | |
392 warnings.warn("Warning!") | |
393 value = name | |
394 name = request | |
395 request = None | |
396 self.original[name] = value | |
397 | |
398 | |
399 class AttributeWrapper(Wrapper): | |
400 """ | |
401 I wrap an attribute named "name" of the given parent object. | |
402 """ | |
403 def __init__(self, parent, name): | |
404 self.original = None | |
405 parent = ObjectWrapper(parent) | |
406 Wrapper.__init__(self, parent.getSubmodel(None, name)) | |
407 self.parent = parent | |
408 self.name = name | |
409 | |
410 | |
411 class ObjectWrapper(Wrapper): | |
412 """ | |
413 I may wrap an object and allow it to interact with the Woven models | |
414 and submodels. By default, I am not registered for use with anything. | |
415 """ | |
416 def getSubmodel(self, request=None, name=None): | |
417 if name is None and type(request) is type(""): | |
418 warnings.warn("Warning!") | |
419 name = request | |
420 request = None | |
421 if self.submodels.has_key(name): | |
422 return self.submodels[name] | |
423 sm = adaptToIModel(getattr(self.original, name), self, name) | |
424 self.submodels[name] = sm | |
425 return sm | |
426 | |
427 def setSubmodel(self, request=None, name=None, value=None): | |
428 if value is None: | |
429 warnings.warn("Warning!") | |
430 value = name | |
431 name = request | |
432 request = None | |
433 setattr(self.original, name, value) | |
434 | |
435 class UnsafeObjectWrapper(ObjectWrapper): | |
436 """ | |
437 I may wrap an object and allow it to interact with the Woven models | |
438 and submodels. By default, I am not registered for use with anything. | |
439 I am unsafe because I allow methods to be called. In fact, I am | |
440 dangerously unsafe. Be wary or I will kill your security model! | |
441 """ | |
442 def getSubmodel(self, request=None, name=None): | |
443 if name is None and type(request) is type(""): | |
444 warnings.warn("Warning!") | |
445 name = request | |
446 request = None | |
447 if self.submodels.has_key(name): | |
448 return self.submodels[name] | |
449 value = getattr(self.original, name) | |
450 if callable(value): | |
451 return value() | |
452 sm = adaptToIModel(value, self, name) | |
453 self.submodels = sm | |
454 return sm | |
455 | |
456 | |
457 class DeferredWrapper(Wrapper): | |
458 def setData(self, request=None, data=_Nothing): | |
459 if data is _Nothing: | |
460 warnings.warn("setData should be called with request as first arg") | |
461 data = request | |
462 request = None | |
463 if isinstance(data, defer.Deferred): | |
464 self.original = data | |
465 else: | |
466 views, subviews = self.views, self.subviews | |
467 new = adaptToIModel(data, self.parent, self.name) | |
468 self.__class__ = new.__class__ | |
469 self.__dict__ = new.__dict__ | |
470 self.views, self.subviews = views, subviews | |
471 | |
472 class Link(AttributeModel): | |
473 def __init__(self, href, text): | |
474 AttributeModel.__init__(self) | |
475 self.href = href | |
476 self.text = text | |
477 | |
478 try: | |
479 components.registerAdapter(StringModel, types.StringType, interfaces.IModel) | |
480 components.registerAdapter(ListModel, types.ListType, interfaces.IModel) | |
481 components.registerAdapter(ListModel, types.TupleType, interfaces.IModel) | |
482 components.registerAdapter(DictionaryModel, types.DictionaryType, interfaces
.IModel) | |
483 components.registerAdapter(DeferredWrapper, defer.Deferred, interfaces.IMode
l) | |
484 components.registerAdapter(DeferredWrapper, defer.DeferredList, interfaces.I
Model) | |
485 except ValueError: | |
486 # The adapters were already registered | |
487 pass | |
OLD | NEW |