1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 This module provides a base class implementing settings-dict based
20 property storage useful for BE objects with saved properties
21 (e.g. BugDir, Bug, Comment). For example usage, consider the
22 unittests at the end of the module.
28 from properties import Property, doc_property, local_property, \
29 defaulting_property, checked_property, fn_checked_property, \
30 cached_property, primed_property, change_hook_property, \
34 class _Token (object):
36 `Control' value class for properties. We want values that only
37 mean something to the settings_object module.
41 class UNPRIMED (_Token):
42 "Property has not been primed."
47 Property has been primed but has no user-set value, so use
48 default/generator value.
53 def prop_save_settings(self, old, new):
55 The default action undertaken when a property changes.
57 if self.sync_with_disk==True:
60 def prop_load_settings(self):
62 The default action undertaken when an UNPRIMED property is accessed.
64 if self.sync_with_disk==True and self._settings_loaded==False:
67 self._setup_saved_settings(flag_as_loaded=False)
69 # Some name-mangling routines for pretty printing setting names
70 def setting_name_to_attr_name(self, name):
72 Convert keys to the .settings dict into their associated
73 SavedSettingsObject attribute names.
74 >>> print setting_name_to_attr_name(None,"User-id")
77 return name.lower().replace('-', '_')
79 def attr_name_to_setting_name(self, name):
81 The inverse of setting_name_to_attr_name.
82 >>> print attr_name_to_setting_name(None, "user_id")
85 return name.capitalize().replace('_', '-')
88 def versioned_property(name, doc,
89 default=None, generator=None,
90 change_hook=prop_save_settings,
92 primer=prop_load_settings,
93 allowed=None, check_fn=None,
94 settings_properties=[],
95 required_saved_properties=[],
98 Combine the common decorators in a single function.
100 Use zero or one (but not both) of default or generator, since a
101 working default will keep the generator from functioning. Use the
102 default if you know what you want the default value to be at
103 'coding time'. Use the generator if you can write a function to
104 determine a valid default at run time. If both default and
105 generator are None, then the property will be a defaulting
106 property which defaults to None.
108 allowed and check_fn have a similar relationship, although you can
109 use both of these if you want. allowed compares the proposed
110 value against a list determined at 'coding time' and check_fn
111 allows more flexible comparisons to take place at run time.
113 Set require_save to True if you want to save the default/generated
114 value for a property, to protect against future changes. E.g., we
115 currently expect all comments to be 'text/plain' but in the future
116 we may want to default to 'text/html'. If we don't want the old
117 comments to be interpreted as 'text/html', we would require that
118 the content type be saved.
120 change_hook, primer, settings_properties, and
121 required_saved_properties are only options to get their defaults
122 into our local scope. Don't mess with them.
125 * default is a mutable
126 * your generator function may return mutables
127 * you set change_hook and might have mutable property values
128 See the docstrings in libbe.properties for details on how each of
129 these cases are handled.
131 settings_properties.append(name)
132 if require_save == True:
133 required_saved_properties.append(name)
134 def decorator(funcs):
136 if default != None or generator == None:
137 defaulting = defaulting_property(default=default, null=EMPTY,
138 mutable_default=mutable)
139 fulldoc += "\n\nThis property defaults to %s." % default
140 if generator != None:
141 cached = cached_property(generator=generator, initVal=EMPTY,
143 fulldoc += "\n\nThis property is generated with %s." % generator
145 fn_checked = fn_checked_property(value_allowed_fn=check_fn)
146 fulldoc += "\n\nThis property is checked with %s." % check_fn
148 checked = checked_property(allowed=allowed)
149 fulldoc += "\n\nThe allowed values for this property are: %s." \
150 % (', '.join(allowed))
151 hooked = change_hook_property(hook=change_hook, mutable=mutable,
153 primed = primed_property(primer=primer, initVal=UNPRIMED)
154 settings = settings_property(name=name, null=UNPRIMED)
155 docp = doc_property(doc=fulldoc)
156 deco = hooked(primed(settings(docp(funcs))))
157 if default != None or generator == None:
158 deco = defaulting(deco)
159 if generator != None:
162 deco = fn_checked(deco)
165 return Property(deco)
168 class SavedSettingsObject(object):
170 # Keep a list of properties that may be stored in the .settings dict.
171 #settings_properties = []
173 # A list of properties that we save to disk, even if they were
174 # never set (in which case we save the default value). This
175 # protects against future changes in default values.
176 #required_saved_properties = []
178 _setting_name_to_attr_name = setting_name_to_attr_name
179 _attr_name_to_setting_name = attr_name_to_setting_name
182 self._settings_loaded = False
183 self.sync_with_disk = False
186 def load_settings(self):
187 """Load the settings from disk."""
188 # Override. Must call ._setup_saved_settings() after loading.
190 self._setup_saved_settings()
192 def _setup_saved_settings(self, flag_as_loaded=True):
194 To be run after setting self.settings up from disk. Marks all
197 for property in self.settings_properties:
198 if property not in self.settings:
199 self.settings[property] = EMPTY
200 elif self.settings[property] == UNPRIMED:
201 self.settings[property] = EMPTY
202 if flag_as_loaded == True:
203 self._settings_loaded = True
205 def save_settings(self):
206 """Load the settings from disk."""
207 # Override. Should save the dict output of ._get_saved_settings()
208 settings = self._get_saved_settings()
209 pass # write settings to disk....
211 def _get_saved_settings(self):
213 for k,v in self.settings.items():
214 if v != None and v != EMPTY:
216 for k in self.required_saved_properties:
217 settings[k] = getattr(self, self._setting_name_to_attr_name(k))
220 def clear_cached_setting(self, setting=None):
221 "If setting=None, clear *all* cached settings"
223 if hasattr(self, "_%s_cached_value" % setting):
224 delattr(self, "_%s_cached_value" % setting)
226 for setting in settings_properties:
227 self.clear_cached_setting(setting)
230 class SavedSettingsObjectTests(unittest.TestCase):
231 def testSimpleProperty(self):
232 """Testing a minimal versioned property"""
233 class Test(SavedSettingsObject):
234 settings_properties = []
235 required_saved_properties = []
236 @versioned_property(name="Content-type",
237 doc="A test property",
238 settings_properties=settings_properties,
239 required_saved_properties=required_saved_properties)
240 def content_type(): return {}
242 SavedSettingsObject.__init__(self)
244 # access missing setting
245 self.failUnless(t._settings_loaded == False, t._settings_loaded)
246 self.failUnless(len(t.settings) == 0, len(t.settings))
247 self.failUnless(t.content_type == None, t.content_type)
248 # accessing t.content_type triggers the priming, which runs
249 # t._setup_saved_settings, which fills out t.settings with
250 # EMPTY data. t._settings_loaded is still false though, since
251 # the default priming does not do any of the `official' loading
252 # that occurs in t.load_settings.
253 self.failUnless(len(t.settings) == 1, len(t.settings))
254 self.failUnless(t.settings["Content-type"] == EMPTY,
255 t.settings["Content-type"])
256 self.failUnless(t._settings_loaded == False, t._settings_loaded)
257 # load settings creates an EMPTY value in the settings array
259 self.failUnless(t._settings_loaded == True, t._settings_loaded)
260 self.failUnless(t.settings["Content-type"] == EMPTY,
261 t.settings["Content-type"])
262 self.failUnless(t.content_type == None, t.content_type)
263 self.failUnless(len(t.settings) == 1, len(t.settings))
264 self.failUnless(t.settings["Content-type"] == EMPTY,
265 t.settings["Content-type"])
268 self.failUnless(t.settings["Content-type"] == 5,
269 t.settings["Content-type"])
270 self.failUnless(t.content_type == 5, t.content_type)
271 self.failUnless(t.settings["Content-type"] == 5,
272 t.settings["Content-type"])
273 # now we set another value
274 t.content_type = "text/plain"
275 self.failUnless(t.content_type == "text/plain", t.content_type)
276 self.failUnless(t.settings["Content-type"] == "text/plain",
277 t.settings["Content-type"])
278 self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
279 t._get_saved_settings())
280 # now we clear to the post-primed value
281 t.content_type = EMPTY
282 self.failUnless(t._settings_loaded == True, t._settings_loaded)
283 self.failUnless(t.settings["Content-type"] == EMPTY,
284 t.settings["Content-type"])
285 self.failUnless(t.content_type == None, t.content_type)
286 self.failUnless(len(t.settings) == 1, len(t.settings))
287 self.failUnless(t.settings["Content-type"] == EMPTY,
288 t.settings["Content-type"])
289 def testDefaultingProperty(self):
290 """Testing a defaulting versioned property"""
291 class Test(SavedSettingsObject):
292 settings_properties = []
293 required_saved_properties = []
294 @versioned_property(name="Content-type",
295 doc="A test property",
296 default="text/plain",
297 settings_properties=settings_properties,
298 required_saved_properties=required_saved_properties)
299 def content_type(): return {}
301 SavedSettingsObject.__init__(self)
303 self.failUnless(t._settings_loaded == False, t._settings_loaded)
304 self.failUnless(t.content_type == "text/plain", t.content_type)
305 self.failUnless(t._settings_loaded == False, t._settings_loaded)
307 self.failUnless(t._settings_loaded == True, t._settings_loaded)
308 self.failUnless(t.content_type == "text/plain", t.content_type)
309 self.failUnless(t.settings["Content-type"] == EMPTY,
310 t.settings["Content-type"])
311 self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings())
312 t.content_type = "text/html"
313 self.failUnless(t.content_type == "text/html",
315 self.failUnless(t.settings["Content-type"] == "text/html",
316 t.settings["Content-type"])
317 self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
318 t._get_saved_settings())
319 def testRequiredDefaultingProperty(self):
320 """Testing a required defaulting versioned property"""
321 class Test(SavedSettingsObject):
322 settings_properties = []
323 required_saved_properties = []
324 @versioned_property(name="Content-type",
325 doc="A test property",
326 default="text/plain",
327 settings_properties=settings_properties,
328 required_saved_properties=required_saved_properties,
330 def content_type(): return {}
332 SavedSettingsObject.__init__(self)
334 self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
335 t._get_saved_settings())
336 t.content_type = "text/html"
337 self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
338 t._get_saved_settings())
339 def testClassVersionedPropertyDefinition(self):
340 """Testing a class-specific _versioned property decorator"""
341 class Test(SavedSettingsObject):
342 settings_properties = []
343 required_saved_properties = []
344 def _versioned_property(settings_properties=settings_properties,
345 required_saved_properties=required_saved_properties,
347 if "settings_properties" not in kwargs:
348 kwargs["settings_properties"] = settings_properties
349 if "required_saved_properties" not in kwargs:
350 kwargs["required_saved_properties"]=required_saved_properties
351 return versioned_property(**kwargs)
352 @_versioned_property(name="Content-type",
353 doc="A test property",
354 default="text/plain",
356 def content_type(): return {}
358 SavedSettingsObject.__init__(self)
360 self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
361 t._get_saved_settings())
362 t.content_type = "text/html"
363 self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
364 t._get_saved_settings())
365 def testMutableChangeHookedProperty(self):
366 """Testing a mutable change-hooked property"""
368 def prop_log_save_settings(self, old, new, saves=SAVES):
369 saves.append("'%s' -> '%s'" % (str(old), str(new)))
370 prop_save_settings(self, old, new)
371 class Test(SavedSettingsObject):
372 settings_properties = []
373 required_saved_properties = []
374 @versioned_property(name="List-type",
375 doc="A test property",
377 change_hook=prop_log_save_settings,
378 settings_properties=settings_properties,
379 required_saved_properties=required_saved_properties)
380 def list_type(): return {}
382 SavedSettingsObject.__init__(self)
384 self.failUnless(t._settings_loaded == False, t._settings_loaded)
386 self.failUnless(SAVES == [], SAVES)
387 self.failUnless(t._settings_loaded == True, t._settings_loaded)
388 self.failUnless(t.list_type == None, t.list_type)
389 self.failUnless(SAVES == [], SAVES)
390 self.failUnless(t.settings["List-type"]==EMPTY,t.settings["List-type"])
392 self.failUnless(t.settings["List-type"] == [], t.settings["List-type"])
393 self.failUnless(SAVES == [
394 "'<class 'libbe.settings_object.EMPTY'>' -> '[]'"
396 t.list_type.append(5)
397 self.failUnless(SAVES == [
398 "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
400 self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"])
401 self.failUnless(SAVES == [ # the append(5) has not yet been saved
402 "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
404 self.failUnless(t.list_type == [5], t.list_type) # <-get triggers saved
406 self.failUnless(SAVES == [ # now the append(5) has been saved.
407 "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
411 unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests)
412 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])