1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008-2011 Chris Ball <cjb@laptop.org>
3 # Gianluca Montecchi <gian@grys.it>
4 # W. Trevor King <wking@drexel.edu>
6 # This file is part of Bugs Everywhere.
8 # Bugs Everywhere is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation, either version 2 of the License, or (at your
11 # option) any later version.
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
21 """Provides :class:`SavedSettingsObject` implementing settings-dict
22 based property storage.
26 :mod:`libbe.storage.util.properties` : underlying property definitions
30 from properties import Property, doc_property, local_property, \
31 defaulting_property, checked_property, fn_checked_property, \
32 cached_property, primed_property, change_hook_property, \
34 if libbe.TESTING == True:
38 class _Token (object):
39 """`Control' value class for properties.
41 We want values that only mean something to the `settings_object`
46 class UNPRIMED (_Token):
47 "Property has not been primed (loaded)."
51 """Property has been primed but has no user-set value, so use
52 default/generator value.
57 def prop_save_settings(self, old, new):
58 """The default action undertaken when a property changes.
60 if self.storage != None and self.storage.is_writeable():
63 def prop_load_settings(self):
64 """The default action undertaken when an UNPRIMED property is
67 Attempt to run `.load_settings()`, which calls
68 `._setup_saved_settings()` internally. If `.storage` is
69 inaccessible, don't do anything.
71 if self.storage != None and self.storage.is_readable():
74 # Some name-mangling routines for pretty printing setting names
75 def setting_name_to_attr_name(self, name):
76 """Convert keys to the `.settings` dict into their associated
77 SavedSettingsObject attribute names.
82 >>> print setting_name_to_attr_name(None,"User-id")
87 attr_name_to_setting_name : inverse
89 return name.lower().replace('-', '_')
91 def attr_name_to_setting_name(self, name):
92 """Convert SavedSettingsObject attribute names to `.settings` dict
97 >>> print attr_name_to_setting_name(None, "user_id")
102 setting_name_to_attr_name : inverse
104 return name.capitalize().replace('_', '-')
107 def versioned_property(name, doc,
108 default=None, generator=None,
109 change_hook=prop_save_settings,
111 primer=prop_load_settings,
112 allowed=None, check_fn=None,
113 settings_properties=[],
114 required_saved_properties=[],
116 """Combine the common decorators in a single function.
118 Use zero or one (but not both) of default or generator, since a
119 working default will keep the generator from functioning. Use the
120 default if you know what you want the default value to be at
121 'coding time'. Use the generator if you can write a function to
122 determine a valid default at run time. If both default and
123 generator are None, then the property will be a defaulting
124 property which defaults to None.
126 allowed and check_fn have a similar relationship, although you can
127 use both of these if you want. allowed compares the proposed
128 value against a list determined at 'coding time' and check_fn
129 allows more flexible comparisons to take place at run time.
131 Set require_save to True if you want to save the default/generated
132 value for a property, to protect against future changes. E.g., we
133 currently expect all comments to be 'text/plain' but in the future
134 we may want to default to 'text/html'. If we don't want the old
135 comments to be interpreted as 'text/html', we would require that
136 the content type be saved.
138 change_hook, primer, settings_properties, and
139 required_saved_properties are only options to get their defaults
140 into our local scope. Don't mess with them.
144 * default is a mutable
145 * your generator function may return mutables
146 * you set change_hook and might have mutable property values
148 See the docstrings in `libbe.properties` for details on how each of
149 these cases are handled.
151 The value stored in `.settings[name]` will be
153 * no value (or UNPRIMED) if the property has been neither set,
155 * EMPTY if the value has been loaded as blank.
156 * some value if the property has been either loaded or set.
158 settings_properties.append(name)
159 if require_save == True:
160 required_saved_properties.append(name)
161 def decorator(funcs):
163 if default != None or generator == None:
164 defaulting = defaulting_property(default=default, null=EMPTY,
165 mutable_default=mutable)
166 fulldoc += "\n\nThis property defaults to %s." % default
167 if generator != None:
168 cached = cached_property(generator=generator, initVal=EMPTY,
170 fulldoc += "\n\nThis property is generated with %s." % generator
172 fn_checked = fn_checked_property(value_allowed_fn=check_fn)
173 fulldoc += "\n\nThis property is checked with %s." % check_fn
175 checked = checked_property(allowed=allowed)
176 fulldoc += "\n\nThe allowed values for this property are: %s." \
177 % (', '.join(allowed))
178 hooked = change_hook_property(hook=change_hook, mutable=mutable,
180 primed = primed_property(primer=primer, initVal=UNPRIMED,
181 unprimeableVal=EMPTY)
182 settings = settings_property(name=name, null=UNPRIMED)
183 docp = doc_property(doc=fulldoc)
184 deco = hooked(primed(settings(docp(funcs))))
185 if default != None or generator == None:
186 deco = defaulting(deco)
187 if generator != None:
190 deco = fn_checked(deco)
193 return Property(deco)
196 class SavedSettingsObject(object):
197 """Setup a framework for lazy saving and loading of `.settings`
200 This is useful for BE objects with saved properties
201 (e.g. :class:`~libbe.bugdir.BugDir`, :class:`~libbe.bug.Bug`,
202 :class:`~libbe.comment.Comment`). For example usage, consider the
203 unittests at the end of the module.
207 versioned_property, prop_save_settings, prop_load_settings
208 setting_name_to_attr_name, attr_name_to_setting_name
210 # Keep a list of properties that may be stored in the .settings dict.
211 #settings_properties = []
213 # A list of properties that we save to disk, even if they were
214 # never set (in which case we save the default value). This
215 # protects against future changes in default values.
216 #required_saved_properties = []
218 _setting_name_to_attr_name = setting_name_to_attr_name
219 _attr_name_to_setting_name = attr_name_to_setting_name
225 def load_settings(self):
226 """Load the settings from disk."""
227 # Override. Must call ._setup_saved_settings({}) with
228 # from-storage settings.
229 self._setup_saved_settings({})
231 def _setup_saved_settings(self, settings=None):
233 Sets up a settings dict loaded from storage. Fills in
234 all missing settings entries with EMPTY.
238 for property in self.settings_properties:
239 if property not in self.settings \
240 or self.settings[property] == UNPRIMED:
241 if property in settings:
242 self.settings[property] = settings[property]
244 self.settings[property] = EMPTY
246 def save_settings(self):
247 """Save the settings to disk."""
248 # Override. Should save the dict output of ._get_saved_settings()
249 settings = self._get_saved_settings()
250 pass # write settings to disk....
252 def _get_saved_settings(self):
254 In order to avoid overwriting unread on-disk data, make sure
255 we've loaded anything sitting on the disk. In the current
256 implementation, all the settings are stored in a single file,
257 so we need to load _all_ the saved settings. Another approach
258 would be per-setting saves, in which case you could skip this
259 step, since any setting changes would have forced that setting
263 for k in self.settings_properties: # force full load
264 if not k in self.settings or self.settings[k] == UNPRIMED:
266 self, self._setting_name_to_attr_name(k))
267 for k in self.settings_properties:
268 if k in self.settings and self.settings[k] != EMPTY:
269 settings[k] = self.settings[k]
270 elif k in self.required_saved_properties:
271 settings[k] = getattr(
272 self, self._setting_name_to_attr_name(k))
275 def clear_cached_setting(self, setting=None):
276 "If setting=None, clear *all* cached settings"
278 if hasattr(self, "_%s_cached_value" % setting):
279 delattr(self, "_%s_cached_value" % setting)
281 for setting in settings_properties:
282 self.clear_cached_setting(setting)
285 if libbe.TESTING == True:
288 class TestStorage (list):
292 self.writeable = True
293 def is_readable(self):
295 def is_writeable(self):
296 return self.writeable
298 class TestObject (SavedSettingsObject):
299 def load_settings(self):
301 if len(self.storage) == 0:
304 settings = copy.deepcopy(self.storage[-1])
305 self._setup_saved_settings(settings)
306 def save_settings(self):
307 settings = self._get_saved_settings()
308 self.storage.append(copy.deepcopy(settings))
310 SavedSettingsObject.__init__(self)
312 self.storage = TestStorage()
314 class SavedSettingsObjectTests(unittest.TestCase):
315 def testSimplePropertyDoc(self):
316 """Testing a minimal versioned property docstring"""
317 class Test (TestObject):
318 settings_properties = []
319 required_saved_properties = []
322 doc="A test property",
323 settings_properties=settings_properties,
324 required_saved_properties=required_saved_properties)
325 def content_type(): return {}
326 expected = "A test property\n\nThis property defaults to None."
327 self.failUnless(Test.content_type.__doc__ == expected,
328 Test.content_type.__doc__)
329 def testSimplePropertyFromMemory(self):
330 """Testing a minimal versioned property from memory"""
331 class Test (TestObject):
332 settings_properties = []
333 required_saved_properties = []
336 doc="A test property",
337 settings_properties=settings_properties,
338 required_saved_properties=required_saved_properties)
339 def content_type(): return {}
341 self.failUnless(len(t.settings) == 0, len(t.settings))
342 # accessing t.content_type triggers the priming, but
343 # t.storage.is_readable() == False, so nothing happens.
344 t.storage.readable = False
345 self.failUnless(t.content_type == None, t.content_type)
346 self.failUnless(t.settings == {}, t.settings)
347 self.failUnless(len(t.settings) == 0, len(t.settings))
348 self.failUnless(t.content_type == None, t.content_type)
349 # accessing t.content_type triggers the priming again, and
350 # now that t.storage.is_readable() == True, this fills out
351 # t.settings with EMPTY data. At this point there should
352 # be one load and no saves.
353 t.storage.readable = True
354 self.failUnless(t.content_type == None, t.content_type)
355 self.failUnless(len(t.settings) == 1, len(t.settings))
356 self.failUnless(t.settings["Content-type"] == EMPTY,
357 t.settings["Content-type"])
358 self.failUnless(t.content_type == None, t.content_type)
359 self.failUnless(t.load_count == 1, t.load_count)
360 self.failUnless(len(t.storage) == 0, len(t.storage))
361 # an explicit call to load settings forces a reload,
362 # but nothing else changes.
364 self.failUnless(len(t.settings) == 1, len(t.settings))
365 self.failUnless(t.settings["Content-type"] == EMPTY,
366 t.settings["Content-type"])
367 self.failUnless(t.content_type == None, t.content_type)
368 self.failUnless(t.load_count == 2, t.load_count)
369 self.failUnless(len(t.storage) == 0, len(t.storage))
372 self.failUnless(t.settings["Content-type"] == 5,
373 t.settings["Content-type"])
374 self.failUnless(t.load_count == 2, t.load_count)
375 self.failUnless(len(t.storage) == 1, len(t.storage))
376 self.failUnless(t.storage == [{'Content-type':5}], t.storage)
377 # getting its value changes nothing
378 self.failUnless(t.content_type == 5, t.content_type)
379 self.failUnless(t.settings["Content-type"] == 5,
380 t.settings["Content-type"])
381 self.failUnless(t.load_count == 2, t.load_count)
382 self.failUnless(len(t.storage) == 1, len(t.storage))
383 self.failUnless(t.storage == [{'Content-type':5}], t.storage)
384 # now we set another value
385 t.content_type = "text/plain"
386 self.failUnless(t.content_type == "text/plain", t.content_type)
387 self.failUnless(t.settings["Content-type"] == "text/plain",
388 t.settings["Content-type"])
389 self.failUnless(t.load_count == 2, t.load_count)
390 self.failUnless(len(t.storage) == 2, len(t.storage))
391 self.failUnless(t.storage == [{'Content-type':5},
392 {'Content-type':'text/plain'}],
394 # t._get_saved_settings() returns a dict of required or
395 # non-default values.
396 self.failUnless(t._get_saved_settings() == \
397 {"Content-type":"text/plain"},
398 t._get_saved_settings())
399 # now we clear to the post-primed value
400 t.content_type = EMPTY
401 self.failUnless(t.settings["Content-type"] == EMPTY,
402 t.settings["Content-type"])
403 self.failUnless(t.content_type == None, t.content_type)
404 self.failUnless(len(t.settings) == 1, len(t.settings))
405 self.failUnless(t.settings["Content-type"] == EMPTY,
406 t.settings["Content-type"])
407 self.failUnless(t._get_saved_settings() == {},
408 t._get_saved_settings())
409 self.failUnless(t.storage == [{'Content-type':5},
410 {'Content-type':'text/plain'},
413 def testSimplePropertyFromStorage(self):
414 """Testing a minimal versioned property from storage"""
415 class Test (TestObject):
416 settings_properties = []
417 required_saved_properties = []
420 doc="A test property",
421 settings_properties=settings_properties,
422 required_saved_properties=required_saved_properties)
423 def prop_a(): return {}
426 doc="Another test property",
427 settings_properties=settings_properties,
428 required_saved_properties=required_saved_properties)
429 def prop_b(): return {}
431 t.storage.append({'prop-a':'saved'})
432 # setting prop-b forces a load (to check for changes),
433 # which also pulls in prop-a.
435 settings = {'prop-b':'new-b', 'prop-a':'saved'}
436 self.failUnless(t.settings == settings, t.settings)
437 self.failUnless(t._get_saved_settings() == settings,
438 t._get_saved_settings())
439 # test that _get_saved_settings() works even when settings
440 # were _not_ loaded beforehand
442 t.storage.append({'prop-a':'saved'})
443 settings ={'prop-a':'saved'}
444 self.failUnless(t.settings == {}, t.settings)
445 self.failUnless(t._get_saved_settings() == settings,
446 t._get_saved_settings())
447 def testSimplePropertySetStorageSave(self):
448 """Set a property, then attach storage and save"""
449 class Test (TestObject):
450 settings_properties = []
451 required_saved_properties = []
454 doc="A test property",
455 settings_properties=settings_properties,
456 required_saved_properties=required_saved_properties)
457 def prop_a(): return {}
460 doc="Another test property",
461 settings_properties=settings_properties,
462 required_saved_properties=required_saved_properties)
463 def prop_b(): return {}
467 t.prop_a = 'text/html'
470 self.failUnless(t.prop_a == 'text/html', t.prop_a)
471 self.failUnless(t.settings == {'prop-a':'text/html',
474 self.failUnless(t.load_count == 1, t.load_count)
475 self.failUnless(len(t.storage) == 1, len(t.storage))
476 self.failUnless(t.storage == [{'prop-a':'text/html'}],
478 def testDefaultingProperty(self):
479 """Testing a defaulting versioned property"""
480 class Test (TestObject):
481 settings_properties = []
482 required_saved_properties = []
485 doc="A test property",
486 default="text/plain",
487 settings_properties=settings_properties,
488 required_saved_properties=required_saved_properties)
489 def content_type(): return {}
491 self.failUnless(t.settings == {}, t.settings)
492 self.failUnless(t.content_type == "text/plain", t.content_type)
493 self.failUnless(t.settings == {"Content-type":EMPTY},
495 self.failUnless(t.load_count == 1, t.load_count)
496 self.failUnless(len(t.storage) == 0, len(t.storage))
497 self.failUnless(t._get_saved_settings() == {},
498 t._get_saved_settings())
499 t.content_type = "text/html"
500 self.failUnless(t.content_type == "text/html",
502 self.failUnless(t.settings == {"Content-type":"text/html"},
504 self.failUnless(t.load_count == 1, t.load_count)
505 self.failUnless(len(t.storage) == 1, len(t.storage))
506 self.failUnless(t.storage == [{'Content-type':'text/html'}],
508 self.failUnless(t._get_saved_settings() == \
509 {"Content-type":"text/html"},
510 t._get_saved_settings())
511 def testRequiredDefaultingProperty(self):
512 """Testing a required defaulting versioned property"""
513 class Test (TestObject):
514 settings_properties = []
515 required_saved_properties = []
518 doc="A test property",
519 default="text/plain",
520 settings_properties=settings_properties,
521 required_saved_properties=required_saved_properties,
523 def content_type(): return {}
525 self.failUnless(t.settings == {}, t.settings)
526 self.failUnless(t.content_type == "text/plain", t.content_type)
527 self.failUnless(t.settings == {"Content-type":EMPTY},
529 self.failUnless(t.load_count == 1, t.load_count)
530 self.failUnless(len(t.storage) == 0, len(t.storage))
531 self.failUnless(t._get_saved_settings() == \
532 {"Content-type":"text/plain"},
533 t._get_saved_settings())
534 t.content_type = "text/html"
535 self.failUnless(t.content_type == "text/html",
537 self.failUnless(t.settings == {"Content-type":"text/html"},
539 self.failUnless(t.load_count == 1, t.load_count)
540 self.failUnless(len(t.storage) == 1, len(t.storage))
541 self.failUnless(t.storage == [{'Content-type':'text/html'}],
543 self.failUnless(t._get_saved_settings() == \
544 {"Content-type":"text/html"},
545 t._get_saved_settings())
546 def testClassVersionedPropertyDefinition(self):
547 """Testing a class-specific _versioned property decorator"""
548 class Test (TestObject):
549 settings_properties = []
550 required_saved_properties = []
551 def _versioned_property(
552 settings_properties=settings_properties,
553 required_saved_properties=required_saved_properties,
555 if "settings_properties" not in kwargs:
556 kwargs["settings_properties"] = settings_properties
557 if "required_saved_properties" not in kwargs:
558 kwargs["required_saved_properties"] = \
559 required_saved_properties
560 return versioned_property(**kwargs)
561 @_versioned_property(name="Content-type",
562 doc="A test property",
563 default="text/plain",
565 def content_type(): return {}
567 self.failUnless(t._get_saved_settings() == \
568 {"Content-type":"text/plain"},
569 t._get_saved_settings())
570 self.failUnless(t.load_count == 1, t.load_count)
571 self.failUnless(len(t.storage) == 0, len(t.storage))
572 t.content_type = "text/html"
573 self.failUnless(t._get_saved_settings() == \
574 {"Content-type":"text/html"},
575 t._get_saved_settings())
576 self.failUnless(t.load_count == 1, t.load_count)
577 self.failUnless(len(t.storage) == 1, len(t.storage))
578 self.failUnless(t.storage == [{'Content-type':'text/html'}],
580 def testMutableChangeHookedProperty(self):
581 """Testing a mutable change-hooked property"""
582 class Test (TestObject):
583 settings_properties = []
584 required_saved_properties = []
587 doc="A test property",
589 change_hook=prop_save_settings,
590 settings_properties=settings_properties,
591 required_saved_properties=required_saved_properties)
592 def list_type(): return {}
594 self.failUnless(len(t.storage) == 0, len(t.storage))
595 self.failUnless(t.list_type == None, t.list_type)
596 self.failUnless(len(t.storage) == 0, len(t.storage))
597 self.failUnless(t.settings["List-type"]==EMPTY,
598 t.settings["List-type"])
600 self.failUnless(t.settings["List-type"] == [],
601 t.settings["List-type"])
602 self.failUnless(len(t.storage) == 1, len(t.storage))
603 self.failUnless(t.storage == [{'List-type':[]}],
605 t.list_type.append(5) # external modification not detected yet
606 self.failUnless(len(t.storage) == 1, len(t.storage))
607 self.failUnless(t.storage == [{'List-type':[]}],
609 self.failUnless(t.settings["List-type"] == [5],
610 t.settings["List-type"])
611 self.failUnless(t.list_type == [5], t.list_type)# get triggers save
612 self.failUnless(len(t.storage) == 2, len(t.storage))
613 self.failUnless(t.storage == [{'List-type':[]},
617 unitsuite = unittest.TestLoader().loadTestsFromTestCase( \
618 SavedSettingsObjectTests)
619 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])