diff options
-rw-r--r-- | becommands/show.py | 26 | ||||
-rw-r--r-- | libbe/bug.py | 36 | ||||
-rw-r--r-- | libbe/comment.py | 51 | ||||
-rw-r--r-- | libbe/properties.py | 10 | ||||
-rw-r--r-- | libbe/settings_object.py | 102 |
5 files changed, 206 insertions, 19 deletions
diff --git a/becommands/show.py b/becommands/show.py index 87b890f..7c48257 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -35,6 +35,19 @@ def execute(args, test=False): Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) Bug A <BLANKLINE> + >>> execute (["--xml", "a"], test=True) + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <assigned><class 'libbe.settings_object.EMPTY'></assigned> + <target><class 'libbe.settings_object.EMPTY'></target> + <reporter><class 'libbe.settings_object.EMPTY'></reporter> + <creator>John Doe <jdoe@example.com></creator> + <created>Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)</created> + <summary>Bug A</summary> + </bug> """ parser = get_parser() options, args = parser.parse_args(args) @@ -45,12 +58,17 @@ def execute(args, test=False): bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) for bugid in args: bug = bd.bug_from_shortname(bugid) - print bug.string(show_comments=True) - if bugid != args[-1]: - print "" # add a blank line between bugs + if options.dumpXML: + print bug.xml(show_comments=True) + else: + print bug.string(show_comments=True) + if bugid != args[-1]: + print "" # add a blank line between bugs def get_parser(): - parser = cmdutil.CmdOptionParser("be show BUG-ID [BUG-ID ...]") + parser = cmdutil.CmdOptionParser("be show [options] BUG-ID [BUG-ID ...]") + parser.add_option("-x", "--xml", action="store_true", + dest='dumpXML', help="Dump as XML") return parser longhelp=""" diff --git a/libbe/bug.py b/libbe/bug.py index f871c7a..fe059fa 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -235,6 +235,42 @@ class Bug(settings_object.SavedSettingsObject): else: return str(value) + def xml(self, show_comments=False): + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) + + if self.time == None: + timestring = "" + else: + htime = utility.handy_time(self.time) + ftime = utility.time_to_str(self.time) + timestring = "%s (%s)" % (htime, ftime) + + info = [("uuid", self.uuid), + ("short-name", shortname), + ("severity", self.severity), + ("status", self.status), + ("assigned", self.assigned), + ("target", self.target), + ("reporter", self.reporter), + ("creator", self.creator), + ("created", timestring), + ("summary", self.summary)] + ret = '<bug>\n' + for (k,v) in info: + if v is not None: + ret += ' <%s>%s</%s>\n' % (k,v,k) + + if show_comments == True: + comout = self.comment_root.xml_thread(auto_name_map=True, + bug_shortname=shortname) + ret += comout + + ret += '</bug>' + return ret + def string(self, shortlist=False, show_comments=False): if self.bugdir == None: shortname = self.uuid diff --git a/libbe/comment.py b/libbe/comment.py index cb5ea59..e5c86c7 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -216,6 +216,38 @@ class Comment(Tree, settings_object.SavedSettingsObject): else: return str(value) + def xml(self, indent=0, shortname=None): + """ + >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.uuid = "0123" + >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> print comm.xml(indent=2, shortname="com-1") + <comment> + <name>com-1</name> + <uuid>0123</uuid> + <from></from> + <date>Thu, 01 Jan 1970 00:00:00 +0000</date> + <body>Some + insightful + remarks</body> + </comment> + """ + if shortname == None: + shortname = self.uuid + lines = ["<comment>", + " <name>%s</name>" % (shortname,), + " <uuid>%s</uuid>" % self.uuid,] + if self.in_reply_to != None: + lines.append(" <in_reply_to>%s</in_reply_to>" % self.in_reply_to) + lines.extend([ + " <from>%s</from>" % self._setting_attr_string("From"), + " <date>%s</date>" % self.time_string, + " <body>%s</body>" % (self.body or "").rstrip('\n'), + "</comment>\n"]) + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") @@ -313,12 +345,18 @@ class Comment(Tree, settings_object.SavedSettingsObject): #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to) return reply - def string_thread(self, name_map={}, indent=0, flatten=True, + def string_thread(self, string_method_name="string", name_map={}, + indent=0, flatten=True, auto_name_map=False, bug_shortname=None): """ - Return a sting displaying a thread of comments. + Return a string displaying a thread of comments. bug_shortname is only used if auto_name_map == True. + string_method_name (defaults to "string") is the name of the + Comment method used to generate the output string for each + Comment in the thread. The method must take the arguments + indent and shortname. + SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() which will sort the tree by comment.time. Avoid by calling name_map = {} @@ -401,9 +439,16 @@ class Comment(Tree, settings_object.SavedSettingsObject): sname = name_map[comment.uuid] else: sname = None - stringlist.append(comment.string(indent=ind, shortname=sname)) + string_fn = getattr(comment, string_method_name) + stringlist.append(string_fn(indent=ind, shortname=sname)) return '\n'.join(stringlist) + def xml_thread(self, name_map={}, indent=0, + auto_name_map=False, bug_shortname=None): + return self.string_thread(string_method_name="xml", name_map=name_map, + indent=indent, auto_name_map=auto_name_map, + bug_shortname=bug_shortname) + def comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. diff --git a/libbe/properties.py b/libbe/properties.py index 176e898..a8e89fb 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -28,6 +28,7 @@ for more information on decorators. import unittest + class ValueCheckError (ValueError): def __init__(self, name, value, allowed): msg = "%s not in %s for %s" % (value, allowed, name) @@ -65,10 +66,11 @@ def doc_property(doc=None): return funcs return decorator -def local_property(name): +def local_property(name, null=None): """ Define get/set access to per-parent-instance local storage. Uses ._<name>_value to store the value for a particular owner instance. + If the ._<name>_value attribute does not exist, returns null. """ def decorator(funcs): if hasattr(funcs, "__call__"): @@ -78,7 +80,7 @@ def local_property(name): def _fget(self): if fget is not None: fget(self) - value = getattr(self, "_%s_value" % name, None) + value = getattr(self, "_%s_value" % name, null) return value def _fset(self, value): setattr(self, "_%s_value" % name, value) @@ -90,7 +92,7 @@ def local_property(name): return funcs return decorator -def settings_property(name): +def settings_property(name, null=None): """ Similar to local_property, except where local_property stores the value in instance._<name>_value, settings_property stores the @@ -104,7 +106,7 @@ def settings_property(name): def _fget(self): if fget is not None: fget(self) - value = self.settings.get(name, None) + value = self.settings.get(name, null) return value def _fset(self, value): self.settings[name] = value diff --git a/libbe/settings_object.py b/libbe/settings_object.py index 8b0ff47..1df3e6b 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -29,20 +29,43 @@ from properties import Property, doc_property, local_property, \ cached_property, primed_property, change_hook_property, \ settings_property -# Define an invalid value for our properties, distinct from None, -# which shows that a property has been initialized but has no value. -EMPTY = -1 + +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.sync_with_disk==True: self.save_settings() + def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ if self.sync_with_disk==True 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 @@ -60,6 +83,7 @@ def attr_name_to_setting_name(self, name): """ return name.capitalize().replace('_', '-') + def versioned_property(name, doc, default=None, generator=None, change_hook=prop_save_settings, @@ -75,7 +99,9 @@ def versioned_property(name, doc, 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. + 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 @@ -112,8 +138,8 @@ def versioned_property(name, doc, fulldoc += "\n\nThe allowed values for this property are: %s." \ % (', '.join(allowed)) hooked = change_hook_property(hook=change_hook) - primed = primed_property(primer=primer) - settings = settings_property(name=name) + 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: @@ -154,11 +180,14 @@ class SavedSettingsObject(object): self._setup_saved_settings() def _setup_saved_settings(self, flag_as_loaded=True): - """To be run after setting self.settings up from disk.""" + """ + 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] == None: + elif self.settings[property] == UNPRIMED: self.settings[property] = EMPTY if flag_as_loaded == True: self._settings_loaded = True @@ -189,6 +218,63 @@ class SavedSettingsObject(object): class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + 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 == EMPTY, 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 == EMPTY, 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 = None + self.failUnless(t.settings["Content-type"] == None, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.settings["Content-type"] == None, + 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 == EMPTY, 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): class Test(SavedSettingsObject): settings_properties = [] |