From 79154201c1c012063aa3fe1881ff06a3f239fdc5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 09:01:26 -0500 Subject: Moved properties.py and settings_object.py to libbe/storage/util/ --- libbe/storage/util/settings_object.py | 433 ++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 libbe/storage/util/settings_object.py (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py new file mode 100644 index 0000000..be119dd --- /dev/null +++ b/libbe/storage/util/settings_object.py @@ -0,0 +1,433 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +This module provides a base class implementing settings-dict based +property storage useful for BE objects with saved properties +(e.g. BugDir, Bug, Comment). For example usage, consider the +unittests at the end of the module. +""" + +import libbe +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property +if libbe.TESTING == True: + import doctest + import unittest + +class _Token (object): + """ + `Control' value class for properties. We want values that only + mean something to the settings_object module. + """ + pass + +class UNPRIMED (_Token): + "Property has not been primed." + pass + +class EMPTY (_Token): + """ + Property has been primed but has no user-set value, so use + default/generator value. + """ + pass + + +def prop_save_settings(self, old, new): + """ + The default action undertaken when a property changes. + """ + if self.storage != None and self.storage.is_writeable(): + self.save_settings() + +def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ + if self.storage != None and self.storage.is_readable() \ + and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + +# Some name-mangling routines for pretty printing setting names +def setting_name_to_attr_name(self, name): + """ + Convert keys to the .settings dict into their associated + SavedSettingsObject attribute names. + >>> print setting_name_to_attr_name(None,"User-id") + user_id + """ + return name.lower().replace('-', '_') + +def attr_name_to_setting_name(self, name): + """ + The inverse of setting_name_to_attr_name. + >>> print attr_name_to_setting_name(None, "user_id") + User-id + """ + return name.capitalize().replace('_', '-') + + +def versioned_property(name, doc, + default=None, generator=None, + change_hook=prop_save_settings, + mutable=False, + primer=prop_load_settings, + allowed=None, check_fn=None, + settings_properties=[], + required_saved_properties=[], + require_save=False): + """ + Combine the common decorators in a single function. + + Use zero or one (but not both) of default or generator, since a + working default will keep the generator from functioning. Use the + default if you know what you want the default value to be at + 'coding time'. Use the generator if you can write a function to + determine a valid default at run time. If both default and + generator are None, then the property will be a defaulting + property which defaults to None. + + allowed and check_fn have a similar relationship, although you can + use both of these if you want. allowed compares the proposed + value against a list determined at 'coding time' and check_fn + allows more flexible comparisons to take place at run time. + + Set require_save to True if you want to save the default/generated + value for a property, to protect against future changes. E.g., we + currently expect all comments to be 'text/plain' but in the future + we may want to default to 'text/html'. If we don't want the old + comments to be interpreted as 'text/html', we would require that + the content type be saved. + + change_hook, primer, settings_properties, and + required_saved_properties are only options to get their defaults + into our local scope. Don't mess with them. + + Set mutable=True if: + * default is a mutable + * your generator function may return mutables + * you set change_hook and might have mutable property values + See the docstrings in libbe.properties for details on how each of + these cases are handled. + """ + settings_properties.append(name) + if require_save == True: + required_saved_properties.append(name) + def decorator(funcs): + fulldoc = doc + if default != None or generator == None: + defaulting = defaulting_property(default=default, null=EMPTY, + mutable_default=mutable) + fulldoc += "\n\nThis property defaults to %s." % default + if generator != None: + cached = cached_property(generator=generator, initVal=EMPTY, + mutable=mutable) + fulldoc += "\n\nThis property is generated with %s." % generator + if check_fn != None: + fn_checked = fn_checked_property(value_allowed_fn=check_fn) + fulldoc += "\n\nThis property is checked with %s." % check_fn + if allowed != None: + checked = checked_property(allowed=allowed) + fulldoc += "\n\nThe allowed values for this property are: %s." \ + % (', '.join(allowed)) + hooked = change_hook_property(hook=change_hook, mutable=mutable, + default=EMPTY) + primed = primed_property(primer=primer, initVal=UNPRIMED) + settings = settings_property(name=name, null=UNPRIMED) + docp = doc_property(doc=fulldoc) + deco = hooked(primed(settings(docp(funcs)))) + if default != None or generator == None: + deco = defaulting(deco) + if generator != None: + deco = cached(deco) + if check_fn != None: + deco = fn_checked(deco) + if allowed != None: + deco = checked(deco) + return Property(deco) + return decorator + +class SavedSettingsObject(object): + + # Keep a list of properties that may be stored in the .settings dict. + #settings_properties = [] + + # A list of properties that we save to disk, even if they were + # never set (in which case we save the default value). This + # protects against future changes in default values. + #required_saved_properties = [] + + _setting_name_to_attr_name = setting_name_to_attr_name + _attr_name_to_setting_name = attr_name_to_setting_name + + def __init__(self): + self._settings_loaded = False + self.storage = None + self.settings = {} + + def load_settings(self): + """Load the settings from disk.""" + # Override. Must call ._setup_saved_settings() after loading. + self.settings = {} + self._setup_saved_settings() + + def _setup_saved_settings(self, flag_as_loaded=True): + """ + To be run after setting self.settings up from disk. Marks all + settings as primed. + """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY + elif self.settings[property] == UNPRIMED: + self.settings[property] = EMPTY + if flag_as_loaded == True: + self._settings_loaded = True + + def save_settings(self): + """Load the settings from disk.""" + # Override. Should save the dict output of ._get_saved_settings() + settings = self._get_saved_settings() + pass # write settings to disk.... + + def _get_saved_settings(self): + settings = {} + for k,v in self.settings.items(): + if v != None and v != EMPTY: + settings[k] = v + for k in self.required_saved_properties: + settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + return settings + + def clear_cached_setting(self, setting=None): + "If setting=None, clear *all* cached settings" + if setting != None: + if hasattr(self, "_%s_cached_value" % setting): + delattr(self, "_%s_cached_value" % setting) + else: + for setting in settings_properties: + self.clear_cached_setting(setting) + + +if libbe.TESTING == True: + class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + """Testing a minimal versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = 5 + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + self.failUnless(t.content_type == 5, t.content_type) + self.failUnless(t.settings["Content-type"] == 5, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + """Testing a defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + """Testing a required defaulting versioned property""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + """Testing a class-specific _versioned property decorator""" + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties= \ + settings_properties, + required_saved_properties= \ + required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"] = \ + required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings() == \ + {"Content-type":"text/html"}, + t._get_saved_settings()) + def testMutableChangeHookedProperty(self): + """Testing a mutable change-hooked property""" + SAVES = [] + def prop_log_save_settings(self, old, new, saves=SAVES): + saves.append("'%s' -> '%s'" % (str(old), str(new))) + prop_save_settings(self, old, new) + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_log_save_settings, + settings_properties=settings_properties, + required_saved_properties= \ + required_saved_properties) + def list_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(SAVES == [], SAVES) + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.list_type == None, t.list_type) + self.failUnless(SAVES == [], SAVES) + self.failUnless(t.settings["List-type"]==EMPTY, + t.settings["List-type"]) + t.list_type = [] + self.failUnless(t.settings["List-type"] == [], + t.settings["List-type"]) + self.failUnless(SAVES == [ + "'' -> '[]'" + ], SAVES) + t.list_type.append(5) + self.failUnless(SAVES == [ + "'' -> '[]'", + ], SAVES) + self.failUnless(t.settings["List-type"] == [5], + t.settings["List-type"]) + self.failUnless(SAVES == [ # the append(5) has not yet been saved + "'' -> '[]'", + ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)#get triggers saved + + self.failUnless(SAVES == [ # now the append(5) has been saved. + "'' -> '[]'", + "'[]' -> '[5]'" + ], SAVES) + + unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ + SavedSettingsObjectTests) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From dff6bd9bf89ca80e2265696a478e540476718c9c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 12 Dec 2009 20:57:59 -0500 Subject: Moved be to libbe.ui.command_line and transitioned to Command format. --- libbe/storage/util/settings_object.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index be119dd..760df03 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -410,21 +410,21 @@ if libbe.TESTING == True: self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) self.failUnless(SAVES == [ - "'' -> '[]'" + "'' -> '[]'" ], SAVES) t.list_type.append(5) self.failUnless(SAVES == [ - "'' -> '[]'", + "'' -> '[]'", ], SAVES) self.failUnless(t.settings["List-type"] == [5], t.settings["List-type"]) self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", + "'' -> '[]'", ], SAVES) self.failUnless(t.list_type == [5], t.list_type)#get triggers saved self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", + "'' -> '[]'", "'[]' -> '[5]'" ], SAVES) -- cgit From 89b7a1411e4658e831f5d635534b24355dbb941d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 06:44:20 -0500 Subject: Fixed libbe.command.diff + ugly BugDir.duplicate_bugdir implementation duplicate_bugdir() works, but for the vcs backends, it could require shelling out for _every_ file read. This could, and probably will, be horribly slow. Still it works ;). I'm not sure what a better implementation would be. The old implementation checked out the entire earlier state into a temporary directory pros: single shell out, simple upgrade implementation cons: wouldn't work well for HTTP backens I think a good solution would run along the lines of the currently commented out code in duplicate_bugdir(), where a VersionedStorage.changed_since(revision) call would give you a list of changed files. diff could work off of that directly, without the need to generate a whole duplicate bugdir. I'm stuck on how to handle upgrades though... Also removed trailing whitespace from all python files. --- libbe/storage/util/settings_object.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 760df03..8b86829 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -197,9 +197,8 @@ class SavedSettingsObject(object): settings as primed. """ for property in self.settings_properties: - if property not in self.settings: - self.settings[property] = EMPTY - elif self.settings[property] == UNPRIMED: + if property not in self.settings \ + or self.settings[property] == UNPRIMED: self.settings[property] = EMPTY if flag_as_loaded == True: self._settings_loaded = True -- cgit From 4d4283ecd654f1efb058cd7f7dba6be88b70ee92 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 1 Jan 2010 08:11:08 -0500 Subject: Updated copyright information --- libbe/storage/util/settings_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 8b86829..9f2b7af 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -1,5 +1,5 @@ # Bugs Everywhere - a distributed bugtracker -# Copyright (C) 2008-2009 Gianluca Montecchi +# Copyright (C) 2008-2010 Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify -- cgit From be2de86f947b7bf2bb44f415df0a6f685633a64b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 20 Jan 2010 19:15:02 -0500 Subject: Fix not-yet-loaded bug in SavedSettingsObject._get_saved_settings() The earlier implementation only copied in the currently loaded properties and the required ones. The new implementation copies in _all_ the non-default properties. --- libbe/storage/util/settings_object.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 9f2b7af..181d4ab 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -204,18 +204,31 @@ class SavedSettingsObject(object): self._settings_loaded = True def save_settings(self): - """Load the settings from disk.""" + """Save the settings to disk.""" # Override. Should save the dict output of ._get_saved_settings() settings = self._get_saved_settings() pass # write settings to disk.... def _get_saved_settings(self): + """ + In order to avoid overwriting unread on-disk data, make sure + we've loaded anything sitting on the disk. In the current + implementation, all the settings are stored in a single file, + so we need to load _all_ the saved settings. Another approach + would be per-setting saves, in which case you could skip this + step, since any setting changes would have forced that setting + load already. + """ settings = {} - for k,v in self.settings.items(): - if v != None and v != EMPTY: - settings[k] = v - for k in self.required_saved_properties: - settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + for k in self.settings_properties: + if k in self.settings and \ + not self.settings[k] in [None, EMPTY]: + settings[k] = self.settings[k] + else: + value = getattr( + self, self._setting_name_to_attr_name(k)) + if value not in [None, EMPTY, []]: + settings[k] = value return settings def clear_cached_setting(self, setting=None): -- cgit From 7fae8599ba74ecf3d36c9466feec25b3c3f36529 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 21 Jan 2010 12:47:38 -0500 Subject: Update libbe.storage.util.settings_object tests for new ._get_saved_settings --- libbe/storage/util/settings_object.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 181d4ab..ca94f23 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -326,7 +326,8 @@ if libbe.TESTING == True: self.failUnless(t.content_type == "text/plain", t.content_type) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == {}, + self.failUnless(t._get_saved_settings() == + {"Content-type":"text/plain"}, t._get_saved_settings()) t.content_type = "text/html" self.failUnless(t.content_type == "text/html", -- cgit From bda68bb5d93f4b608fb1dd17c5a0cf1bb406daf9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 22 Jan 2010 11:30:26 -0500 Subject: Reworked settings_object module, but command.init tests still fail: $ python test.py libbe.command.init Doctest: libbe.command.init.Init ... FAIL ... ----------------------- File ".../libbe/command/init.py", line 47, in libbe.command.init.Init Failed example: ui.run(cmd) Exception raised: Traceback (most recent call last): ... File "/tmp/be.wtk/libbe/command/init.py", line 97, in _run bd = libbe.bugdir.BugDir(storage, from_storage=False) File "/tmp/be.wtk/libbe/bugdir.py", line 185, in __init__ self.save() File "/tmp/be.wtk/libbe/bugdir.py", line 228, in save self.save_settings() File "/tmp/be.wtk/libbe/bugdir.py", line 204, in save_settings mf = mapfile.generate(self._get_saved_settings()) File "/tmp/be.wtk/libbe/storage/util/settings_object.py", line 230, in _get_saved_settings self, self._setting_name_to_attr_name(k)) File "/tmp/be.wtk/libbe/storage/util/properties.py", line 194, in _fget value = fget(self) File "/tmp/be.wtk/libbe/storage/util/properties.py", line 329, in _fget primer(self) File "/tmp/be.wtk/libbe/storage/util/settings_object.py", line 69, in prop_load_settings self.load_settings() File "/tmp/be.wtk/libbe/bugdir.py", line 194, in load_settings self.settings = mapfile.parse(settings_mapfile) File "/tmp/be.wtk/libbe/storage/util/mapfile.py", line 123, in parse c = yaml.load(contents) ... File "/usr/lib/python2.6/site-packages/yaml/reader.py", line 213, in update_raw data = self.stream.read(size) AttributeError: 'NoneType' object has no attribute 'read' ... --- libbe/storage/util/settings_object.py | 345 ++++++++++++++++++++++------------ 1 file changed, 226 insertions(+), 119 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index ca94f23..655e0ed 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -40,7 +40,7 @@ class _Token (object): pass class UNPRIMED (_Token): - "Property has not been primed." + "Property has not been primed (loaded)." pass class EMPTY (_Token): @@ -60,13 +60,13 @@ def prop_save_settings(self, old, new): def prop_load_settings(self): """ - The default action undertaken when an UNPRIMED property is accessed. + The default action undertaken when an UNPRIMED property is + accessed. Attempt to run .load_settings(), which calls + ._setup_saved_settings() internally. If .storage is inaccessible, + don't do anything. """ - if self.storage != None and self.storage.is_readable() \ - and self._settings_loaded==False: + if self.storage != None and self.storage.is_readable(): self.load_settings() - else: - self._setup_saved_settings(flag_as_loaded=False) # Some name-mangling routines for pretty printing setting names def setting_name_to_attr_name(self, name): @@ -129,6 +129,12 @@ def versioned_property(name, doc, * you set change_hook and might have mutable property values See the docstrings in libbe.properties for details on how each of these cases are handled. + + The value stored in .settings[name] will be + * no value (or UNPRIMED) if the property has been neither set, + nor loaded as blank. + * EMPTY if the value has been loaded as blank. + * some value if the property has been either loaded or set. """ settings_properties.append(name) if require_save == True: @@ -152,7 +158,8 @@ def versioned_property(name, doc, % (', '.join(allowed)) hooked = change_hook_property(hook=change_hook, mutable=mutable, default=EMPTY) - primed = primed_property(primer=primer, initVal=UNPRIMED) + primed = primed_property(primer=primer, initVal=UNPRIMED, + unprimeableVal=EMPTY) settings = settings_property(name=name, null=UNPRIMED) docp = doc_property(doc=fulldoc) deco = hooked(primed(settings(docp(funcs)))) @@ -181,7 +188,6 @@ class SavedSettingsObject(object): _attr_name_to_setting_name = attr_name_to_setting_name def __init__(self): - self._settings_loaded = False self.storage = None self.settings = {} @@ -191,17 +197,15 @@ class SavedSettingsObject(object): self.settings = {} self._setup_saved_settings() - def _setup_saved_settings(self, flag_as_loaded=True): + def _setup_saved_settings(self): """ - To be run after setting self.settings up from disk. Marks all - settings as primed. + To be run after setting self.settings up from disk. Fills in + all missing settings entries with EMPTY. """ for property in self.settings_properties: if property not in self.settings \ or self.settings[property] == UNPRIMED: self.settings[property] = EMPTY - if flag_as_loaded == True: - self._settings_loaded = True def save_settings(self): """Save the settings to disk.""" @@ -220,15 +224,16 @@ class SavedSettingsObject(object): load already. """ settings = {} + for k in self.settings_properties: # force full load + if not k in self.settings or self.settings[k] == UNPRIMED: + value = getattr( + self, self._setting_name_to_attr_name(k)) for k in self.settings_properties: - if k in self.settings and \ - not self.settings[k] in [None, EMPTY]: + if k in self.settings and self.settings[k] != EMPTY: settings[k] = self.settings[k] - else: - value = getattr( + elif k in self.required_saved_properties: + settings[k] = getattr( self, self._setting_name_to_attr_name(k)) - if value not in [None, EMPTY, []]: - settings[k] = value return settings def clear_cached_setting(self, setting=None): @@ -242,134 +247,245 @@ class SavedSettingsObject(object): if libbe.TESTING == True: + import copy + + class TestStorage (list): + def __init__(self): + list.__init__(self) + self.readable = True + self.writeable = True + def is_readable(self): + return self.readable + def is_writeable(self): + return self.writeable + + class TestObject (SavedSettingsObject): + def load_settings(self): + self.load_count += 1 + if len(self.storage) == 0: + self.settings = {} + else: + self.settings = copy.deepcopy(self.storage[-1]) + self._setup_saved_settings() + def save_settings(self): + settings = self._get_saved_settings() + self.storage.append(copy.deepcopy(settings)) + def __init__(self): + SavedSettingsObject.__init__(self) + self.load_count = 0 + self.storage = TestStorage() + class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): + def testSimplePropertyDoc(self): + """Testing a minimal versioned property docstring""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + expected = "A test property\n\nThis property defaults to None." + self.failUnless(Test.content_type.__doc__ == expected, + Test.content_type.__doc__) + def testSimplePropertyFromMemory(self): + """Testing a minimal versioned property from memory""" + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) self.failUnless(len(t.settings) == 0, len(t.settings)) + # accessing t.content_type triggers the priming, but + # t.storage.is_readable() == False, so nothing happens. + t.storage.readable = False + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.settings == {}, t.settings) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming again, and + # now that t.storage.is_readable() == True, this fills out + # t.settings with EMPTY data. At this point there should + # be one load and no saves. + t.storage.readable = True self.failUnless(t.content_type == None, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. self.failUnless(len(t.settings) == 1, len(t.settings)) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) + # an explicit call to load settings forces a reload, + # but nothing else changes. + t.load_settings() self.failUnless(len(t.settings) == 1, len(t.settings)) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) # now we set a value t.content_type = 5 self.failUnless(t.settings["Content-type"] == 5, t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}], t.storage) + # getting its value changes nothing self.failUnless(t.content_type == 5, t.content_type) self.failUnless(t.settings["Content-type"] == 5, t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}], t.storage) # now we set another value t.content_type = "text/plain" self.failUnless(t.content_type == "text/plain", t.content_type) self.failUnless(t.settings["Content-type"] == "text/plain", t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 2, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}, + {'Content-type':'text/plain'}], + t.storage) + # t._get_saved_settings() returns a dict of required or + # non-default values. self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) # now we clear to the post-primed value t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) self.failUnless(t.content_type == None, t.content_type) self.failUnless(len(t.settings) == 1, len(t.settings)) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + self.failUnless(t.storage == [{'Content-type':5}, + {'Content-type':'text/plain'}, + {}], + t.storage) + def testSimplePropertyFromStorage(self): + """Testing a minimal versioned property from storage""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="prop-a", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_a(): return {} + @versioned_property( + name="prop-b", + doc="Another test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_b(): return {} + t = Test() + t.storage.append({'prop-a':'saved'}) + # setting prop-b forces a load (to check for changes), + # which also pulls in prop-a. + t.prop_b = 'new-b' + settings = {'prop-b':'new-b', 'prop-a':'saved'} + self.failUnless(t.settings == settings, t.settings) + self.failUnless(t._get_saved_settings() == settings, + t._get_saved_settings()) + # test that _get_saved_settings() works even when settings + # were _not_ loaded beforehand + t = Test() + t.storage.append({'prop-a':'saved'}) + settings ={'prop-a':'saved'} + self.failUnless(t.settings == {}, t.settings) + self.failUnless(t._get_saved_settings() == settings, + t._get_saved_settings()) + def testDefaultingProperty(self): """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.settings == {}, t.settings) self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == - {"Content-type":"text/plain"}, + self.failUnless(t.settings == {"Content-type":EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) + self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) t.content_type = "text/html" self.failUnless(t.content_type == "text/html", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) + self.failUnless(t.settings == {"Content-type":"text/html"}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) def testRequiredDefaultingProperty(self): """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties, - require_save=True) + @versioned_property( + name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + require_save=True) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() + self.failUnless(t.settings == {}, t.settings) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings == {"Content-type":EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings == {"Content-type":"text/html"}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) def testClassVersionedPropertyDefinition(self): """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - def _versioned_property(settings_properties= \ - settings_properties, - required_saved_properties= \ - required_saved_properties, - **kwargs): + def _versioned_property( + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): if "settings_properties" not in kwargs: kwargs["settings_properties"] = settings_properties if "required_saved_properties" not in kwargs: @@ -377,69 +493,60 @@ if libbe.TESTING == True: required_saved_properties return versioned_property(**kwargs) @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) + doc="A test property", + default="text/plain", + require_save=True) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) t.content_type = "text/html" self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) def testMutableChangeHookedProperty(self): """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_save_settings, + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t.list_type == None, t.list_type) - self.failUnless(SAVES == [], SAVES) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t.settings["List-type"]==EMPTY, t.settings["List-type"]) t.list_type = [] self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) - self.failUnless(SAVES == [ - "'' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'' -> '[]'", - ], SAVES) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}], + t.storage) + t.list_type.append(5) # external modification not detected yet + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}], + t.storage) self.failUnless(t.settings["List-type"] == [5], t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type)#get triggers saved - - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)# get triggers save + self.failUnless(len(t.storage) == 2, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}, + {'List-type':[5]}], + t.storage) unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ SavedSettingsObjectTests) -- cgit From 4d925a688394b7840ec81165bbf2cdd43536f01b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 22 Jan 2010 20:35:23 -0500 Subject: Added testSimplePropertySetStorageSave and relavant rewrites to settings_object It hadn't been handling the "attach storage after initializing" technique that BugDir, Bug, and Comment use when from_memory==True. Now it does, by refusing to overwrite self.settings with the newly-loaded settings. --- libbe/storage/util/settings_object.py | 55 ++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 655e0ed..8434952 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -193,19 +193,24 @@ class SavedSettingsObject(object): def load_settings(self): """Load the settings from disk.""" - # Override. Must call ._setup_saved_settings() after loading. - self.settings = {} - self._setup_saved_settings() + # Override. Must call ._setup_saved_settings({}) with + # from-storage settings. + self._setup_saved_settings({}) - def _setup_saved_settings(self): + def _setup_saved_settings(self, settings=None): """ - To be run after setting self.settings up from disk. Fills in + Sets up a settings dict loaded from storage. Fills in all missing settings entries with EMPTY. """ + if settings == None: + settings = {} for property in self.settings_properties: if property not in self.settings \ or self.settings[property] == UNPRIMED: - self.settings[property] = EMPTY + if property in settings: + self.settings[property] = settings[property] + else: + self.settings[property] = EMPTY def save_settings(self): """Save the settings to disk.""" @@ -263,10 +268,10 @@ if libbe.TESTING == True: def load_settings(self): self.load_count += 1 if len(self.storage) == 0: - self.settings = {} + settings = {} else: - self.settings = copy.deepcopy(self.storage[-1]) - self._setup_saved_settings() + settings = copy.deepcopy(self.storage[-1]) + self._setup_saved_settings(settings) def save_settings(self): settings = self._get_saved_settings() self.storage.append(copy.deepcopy(settings)) @@ -408,7 +413,37 @@ if libbe.TESTING == True: self.failUnless(t.settings == {}, t.settings) self.failUnless(t._get_saved_settings() == settings, t._get_saved_settings()) - + def testSimplePropertySetStorageSave(self): + """Set a property, then attach storage and save""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="prop-a", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_a(): return {} + @versioned_property( + name="prop-b", + doc="Another test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_b(): return {} + t = Test() + storage = t.storage + t.storage = None + t.prop_a = 'text/html' + t.storage = storage + t.save_settings() + self.failUnless(t.prop_a == 'text/html', t.prop_a) + self.failUnless(t.settings == {'prop-a':'text/html', + 'prop-b':EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'prop-a':'text/html'}], + t.storage) def testDefaultingProperty(self): """Testing a defaulting versioned property""" class Test (TestObject): -- cgit From 977eff5af10b50ba6e6edb6abc4f40804c418b12 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 7 Feb 2010 17:53:53 -0500 Subject: Fixed docstrings so only Sphinx errors are "autosummary" and "missing attribute" --- libbe/storage/util/settings_object.py | 95 +++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 33 deletions(-) (limited to 'libbe/storage/util/settings_object.py') diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index 8434952..6e4da55 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -16,11 +16,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" -This module provides a base class implementing settings-dict based -property storage useful for BE objects with saved properties -(e.g. BugDir, Bug, Comment). For example usage, consider the -unittests at the end of the module. +"""Provides :class:`SavedSettingsObject` implementing settings-dict +based property storage. + +See Also +-------- +:mod:`libbe.storage.util.properties` : underlying property definitions """ import libbe @@ -33,9 +34,10 @@ if libbe.TESTING == True: import unittest class _Token (object): - """ - `Control' value class for properties. We want values that only - mean something to the settings_object module. + """`Control' value class for properties. + + We want values that only mean something to the `settings_object` + module. """ pass @@ -44,45 +46,58 @@ class UNPRIMED (_Token): pass class EMPTY (_Token): - """ - Property has been primed but has no user-set value, so use + """Property has been primed but has no user-set value, so use default/generator value. """ pass def prop_save_settings(self, old, new): - """ - The default action undertaken when a property changes. + """The default action undertaken when a property changes. """ if self.storage != None and self.storage.is_writeable(): self.save_settings() def prop_load_settings(self): - """ - The default action undertaken when an UNPRIMED property is - accessed. Attempt to run .load_settings(), which calls - ._setup_saved_settings() internally. If .storage is inaccessible, - don't do anything. + """The default action undertaken when an UNPRIMED property is + accessed. + + Attempt to run `.load_settings()`, which calls + `._setup_saved_settings()` internally. If `.storage` is + inaccessible, don't do anything. """ if self.storage != None and self.storage.is_readable(): self.load_settings() # Some name-mangling routines for pretty printing setting names def setting_name_to_attr_name(self, name): - """ - Convert keys to the .settings dict into their associated + """Convert keys to the `.settings` dict into their associated SavedSettingsObject attribute names. + + Examples + -------- + >>> print setting_name_to_attr_name(None,"User-id") user_id + + See Also + -------- + attr_name_to_setting_name : inverse """ return name.lower().replace('-', '_') def attr_name_to_setting_name(self, name): - """ - The inverse of setting_name_to_attr_name. + """Convert SavedSettingsObject attribute names to `.settings` dict + keys. + + Examples: + >>> print attr_name_to_setting_name(None, "user_id") User-id + + See Also + -------- + setting_name_to_attr_name : inverse """ return name.capitalize().replace('_', '-') @@ -96,8 +111,7 @@ def versioned_property(name, doc, settings_properties=[], required_saved_properties=[], require_save=False): - """ - Combine the common decorators in a single function. + """Combine the common decorators in a single function. Use zero or one (but not both) of default or generator, since a working default will keep the generator from functioning. Use the @@ -124,17 +138,20 @@ def versioned_property(name, doc, into our local scope. Don't mess with them. Set mutable=True if: - * default is a mutable - * your generator function may return mutables - * you set change_hook and might have mutable property values - See the docstrings in libbe.properties for details on how each of + + * default is a mutable + * your generator function may return mutables + * you set change_hook and might have mutable property values + + See the docstrings in `libbe.properties` for details on how each of these cases are handled. - The value stored in .settings[name] will be - * no value (or UNPRIMED) if the property has been neither set, - nor loaded as blank. - * EMPTY if the value has been loaded as blank. - * some value if the property has been either loaded or set. + The value stored in `.settings[name]` will be + + * no value (or UNPRIMED) if the property has been neither set, + nor loaded as blank. + * EMPTY if the value has been loaded as blank. + * some value if the property has been either loaded or set. """ settings_properties.append(name) if require_save == True: @@ -175,7 +192,19 @@ def versioned_property(name, doc, return decorator class SavedSettingsObject(object): - + """Setup a framework for lazy saving and loading of `.settings` + properties. + + This is useful for BE objects with saved properties + (e.g. :class:`~libbe.bugdir.BugDir`, :class:`~libbe.bug.Bug`, + :class:`~libbe.comment.Comment`). For example usage, consider the + unittests at the end of the module. + + See Also + -------- + versioned_property, prop_save_settings, prop_load_settings + setting_name_to_attr_name, attr_name_to_setting_name + """ # Keep a list of properties that may be stored in the .settings dict. #settings_properties = [] -- cgit