[sugar] review: Guillaume's pubsub-notif branch of Gadget

Guillaume Desmottes guillaume.desmottes at collabora.co.uk
Mon May 26 10:53:43 EDT 2008


Le lundi 26 mai 2008 à 14:08 +0100, Dafydd Harries a écrit :
> > diff --git a/gadget/component.py b/gadget/component.py
> > @@ -421,3 +435,24 @@ class GadgetService(component.Service):
> >                  if jid.resource in room.members:
> >                      del room.members[jid.resource]
> >  
> > +    def message_pubsub_notification(self, stanza, event):
> > +        items = event.firstChildElement()
> > +        if items['node'] == ns.BUDDY_PROP:
> > +            self.message_buddy_properties_notification(stanza, items)
> > +
> > +    def message_buddy_properties_notification(self, stanza, items):
> > +        from_ = stanza.getAttribute('from')
> > +        for item in items.elements():
> > +            if item.name != 'item':
> > +                continue
> > +            for properties in item.elements():
> > +                if properties.name != 'properties':
> > +                    continue
> 
> You should check the URIs in addition to the node names.
> 
> This is a common pattern in the code; perhaps we could use a utility function
> like this:
> 
>   def filterElements(elem, uri, name):
>       for child in elem.elements():
>           if child.uri == uri and child.name == name:
>               yield child
> 
> Then we could rewrite the above code as:
> 
>   for item in filterElements(item, ns.FOO, 'item'):
>       for properties in filterElements(item, ns.BUDDY_PROP, 'properties'):
>           props = parse_properties(properties)
>           ...
> 
> Alternatively, we could use twisted.words.xish.xpath:
> 
>   from twisted.words.xish import xpath
> 
>   query = '{%s}item/{%s}properties' % (ns.FOO, ns.BUDDY_PROP)
> 
>   for properties in xpath.queryForNodes(elem, query):
>       pass
> 
> That's if I remember the syntax correctly. Not sure which is better.
> 

I don't think we need to invent our own function to do that. I modified
the code to use xpath.

> > +
> > +                props = parse_properties(properties)
> > +                try:
> > +                    self.model.buddy_update(from_, props)
> > +                except KeyError:
> > +                    self.model.buddy_add(from_, props)
> > +
> > +                # FIXME: send notification messages to interested buddies
> 
> I would prefer:
> 
>   if self.model.has_buddy(from_):
>       self.model.buddy_update(from_, props)
>   else:
>       self.model.buddy_add(from_, props)
> 

done.

> > diff --git a/gadget/ns.py b/gadget/ns.py
> > index 293d4dd..8700f04 100644
> > --- a/gadget/ns.py
> > +++ b/gadget/ns.py
> > @@ -8,4 +8,6 @@ DISCO_INFO = 'http://jabber.org/protocol/disco#info'
> >  MUC = 'http://jabber.org/protocol/muc'
> >  MUC_USER = 'http://jabber.org/protocol/muc#user'
> >  XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'
> > -
> > +PUBSUB = 'http://jabber.org/protocol/pubsub'
> > +PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
> > +CAPS = 'http://jabber.org/protocol/caps'
> 
> Can we keep this file in ASCII order?
> 

sure.

> > diff --git a/gadget/test_component.py b/gadget/test_component.py
> > index 178424d..5379870 100644
> > --- a/gadget/test_component.py
> > +++ b/gadget/test_component.py
> > @@ -948,3 +948,100 @@ class PropertiesToXmlTest(unittest.TestCase):
> >          assert property['name'] == 'some_bytes'
> >          child = property.children[0]
> >          assert child == 'ABCDEF'
> > +
> > +class CapsTest(TestCase):
> > +    def runTest(self):
> > +        self.done = defer.Deferred()
> > +        self.server.addOnetimeObserver('//presence', self.presence)
> > +
> > +        presence = domish.Element((ns.CLIENT, 'presence'))
> > +        presence['from'] = 'bob at foo.org'
> > +        presence['to'] = 'gadget.foor.org'
> 
> Typo?
> 

fixed.

> > +        presence['type'] = 'probe'
> > +        self.server.send(presence)
> > +        return self.done
> > +
> > +    def presence(self, stanza):
> > +        assert stanza.name == 'presence'
> > +        c = stanza.firstChildElement()
> > +        assert c.name == 'c'
> > +        assert c.uri == ns.CAPS
> > +
> > +        self.server.addOnetimeObserver('//iq', self.iq)
> > +        iq = IQ(self.server, "get")
> > +        iq['from'] = 'bob at foo.org'
> > +        iq['to'] = 'gadget.bar.org'
> > +        query = iq.addElement((ns.DISCO_INFO, 'query'))
> > +        query['node'] = c['node']
> > +        self.server.send(iq)
> > +
> > +    def iq(self, stanza):
> > +        query = stanza.firstChildElement()
> > +        assert query.name == 'query'
> > +        identities = set()
> > +        features = set()
> > +        for elt in query.children:
> > +            assert elt.name in ('feature', 'identity')
> > +            if elt.name == 'identity':
> > +                identities.add((elt['category'], elt['type'], elt['name']))
> > +            elif elt.name == 'feature':
> > +                features.add(elt['var'])
> > +        assert identities == set([('collaboration', 'gadget', 'OLPC Gadget')])
> > +        assert features == set([ns.ACTIVITY, ns.BUDDY, "%s+notify" % ns.BUDDY_PROP])
> > +
> > +        self.done.callback(None)
> 
> I don't see the connection between the presence probe and the disco query
> here. Don't we already have a test for the latter?
> 

Because this test check if the presence contains the right capa and
gadget send it when the buddy turns online.


> > +class BuddyPropertiesChangeTest(TestCase):
> > +    def runTest(self):
> > +        self.done = defer.Deferred()
> > +        self.service.model.buddy_add('bob at foo.org', {})
> > +
> > +        message = domish.Element((None, 'message'))
> > +        message['from'] = 'bob at foo.org'
> > +        message['to'] = 'gadget.foo.org'
> > +        event = message.addElement((ns.PUBSUB_EVENT, 'event'))
> > +        items = event.addElement((None, 'items'))
> > +        items['node'] = ns.BUDDY_PROP
> > +        item = items.addElement((None, 'item'))
> > +        properties = item.addElement((ns.BUDDY_PROP, 'properties'))
> > +        property = properties.addElement((None, 'property'))
> > +        property['name'] = 'color'
> > +        property['type'] = 'str'
> > +        property.addContent('red')
> > +
> > +        self.server.send(message)
> 
> I think this should be synchronous, so we shouldn't need to iterate the
> reactor, but in some cases the loopback transport doesn't behave as I expect
> it to. I guess it behaved oddly for you here too?
> 

Yeah, that's the only way I found to be sure that the message have been
processed by Gadget.

> > +        reactor.callLater(0, self.later1)
> > +
> > +        return self.done
> > +
> > +    def later1(self):
> > +        buddy = self.service.model.buddy_by_id('bob at foo.org')
> > +        assert buddy.properties == {'color': ('str', 'red')}
> > +
> > +        message = domish.Element((None, 'message'))
> > +        message['from'] = 'bob at foo.org'
> > +        message['to'] = 'gadget.foo.org'
> > +        event = message.addElement((ns.PUBSUB_EVENT, 'event'))
> > +        items = event.addElement((None, 'items'))
> > +        items['node'] = ns.BUDDY_PROP
> > +        item = items.addElement((None, 'item'))
> > +        properties = item.addElement((ns.BUDDY_PROP, 'properties'))
> > +        property = properties.addElement((None, 'property'))
> > +        property['type'] = 'str'
> > +        property['name'] = 'key'
> > +        property.addContent('my key')
> > +        property = properties.addElement((None, 'property'))
> > +        property['type'] = 'str'
> > +        property['name'] = 'color'
> > +        property.addContent('blue')
> > +
> > +        self.server.send(message)
> > +        reactor.callLater(0, self.later2)
> > +        return self.done
> 
> I don't think you need to return anything here.
> 

right.

> > +
> > +    def later2(self):
> > +        buddy = self.service.model.buddy_by_id('bob at foo.org')
> > +        assert buddy.properties == {'color': ('str', 'blue'),
> > +            'key': ('str', 'my key')}
> > +
> > +        self.done.callback(None)
> > diff --git a/gadget/util.py b/gadget/util.py
> > index ac8638d..f342085 100644
> > --- a/gadget/util.py
> > +++ b/gadget/util.py
> > @@ -1,3 +1,9 @@
> > +import base64
> > +try:
> > +    from hashlib import sha1
> > +except ImportError:
> > +    # Python < 2.5
> > +    from sha import new as sha1
> >  
> >  from twisted.words.xish import domish
> >  
> > @@ -51,6 +57,28 @@ def xml_pformat(e, indent=0, width=80):
> >  
> >      return ret
> >  
> > +def capa_hash(S):
> 
> I think we can give these functions more descriptive names; perhaps we can
> rename capa_hash to something like _hash_capabilities and gen_capa_ver to
> hash_capabilities. Although the result of gen_capa_ver is used in the 'ver'
> field, what it does is perform a hash.
> 

good idea. I wasn't very happy with these names too.

> > +    r"""
> > +    >>> capa_hash('client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<')
> > +    'QgayPKawpkPSDYmwT/WM94uAlu0=\n'
> > +    """
> > +    return base64.encodestring(sha1(S).digest())
> > +
> > +def gen_capa_ver(identities, features):
> > +    r"""
> > +    >>> gen_capa_ver([('client', 'pc', 'Exodus 0.9.1')], ["http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/disco#items", "http://jabber.org/protocol/muc", "http://jabber.org/protocol/caps"])
> > +    'QgayPKawpkPSDYmwT/WM94uAlu0=\n'
> > +    """
> > +    S = ""
> > +    identities.sort()
> > +    for identity in identities:
> > +        S += "%s/%s//%s<" % identity
> 
> You can write:
> 
>   for identity in sorted(identities):
> 
> This is shorter and has the advantage of not modifying data that the caller
> passes you.
> 

done.

> > +
> > +    features.sort()
> > +    for feature in features:
> 
> You can simplify this too.
> 

done.

> > +        S += "%s<" % feature
> > +    return capa_hash(S)
> > +
> >  if __name__ == '__main__':
> >      import doctest
> >      doctest.testmod()
> 
> Otherwise, fine.
> 

I pushed my changes. Thanks for the review.


	G.



More information about the Sugar mailing list