diff --git a/sugar/clipboard/__init__.py b/sugar/clipboard/__init__.py index e69de29b..c42ffce4 100644 --- a/sugar/clipboard/__init__.py +++ b/sugar/clipboard/__init__.py @@ -0,0 +1,5 @@ +"""Client-code's interface to the ClipboardService + +Provides a simplified API for accessing the dbus service +which coordinates clipboard operations within Sugar. +""" diff --git a/sugar/graphics/__init__.py b/sugar/graphics/__init__.py index e69de29b..0a148b4c 100644 --- a/sugar/graphics/__init__.py +++ b/sugar/graphics/__init__.py @@ -0,0 +1 @@ +"""Hippo-based graphics/controls for use in Sugar""" diff --git a/sugar/presence/__init__.py b/sugar/presence/__init__.py index e69de29b..f09a5e3e 100644 --- a/sugar/presence/__init__.py +++ b/sugar/presence/__init__.py @@ -0,0 +1,7 @@ +"""Client-code's interface to the PresenceService + +Provides a simplified API for accessing the dbus service +which coordinates native network presence and sharing +information. This includes both "buddies" and "shared +activities". +""" diff --git a/sugar/presence/activity.py b/sugar/presence/activity.py index 16163784..f2903682 100644 --- a/sugar/presence/activity.py +++ b/sugar/presence/activity.py @@ -1,3 +1,4 @@ +"""UI interface to an activity in the presence service""" # Copyright (C) 2007, Red Hat, Inc. # # This library is free software; you can redistribute it and/or @@ -19,7 +20,19 @@ import gobject import dbus class Activity(gobject.GObject): - + """UI interface for an Activity in the presence service + + Activities in the presence service represent other user's + shared activities and your own activities (XXX shared or + otherwise?) + + Properties: + id + color + name + type + joined + """ __gsignals__ = { 'buddy-joined': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), @@ -33,6 +46,7 @@ class Activity(gobject.GObject): _ACTIVITY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Activity" def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialse the activity interface, connecting to service""" gobject.GObject.__init__(self) self._object_path = object_path self._ps_new_object = new_obj_cb @@ -50,9 +64,11 @@ class Activity(gobject.GObject): self._joined = False def object_path(self): + """Get our dbus object path""" return self._object_path def _emit_buddy_joined_signal(self, object_path): + """Generate buddy-joined GObject signal with presence Buddy object""" self.emit('buddy-joined', self._ps_new_object(object_path)) return False @@ -60,6 +76,10 @@ class Activity(gobject.GObject): gobject.idle_add(self._emit_buddy_joined_signal, object_path) def _emit_buddy_left_signal(self, object_path): + """Generate buddy-left GObject signal with presence Buddy object + + XXX note use of _ps_new_object instead of _ps_del_object here + """ self.emit('buddy-left', self._ps_new_object(object_path)) return False @@ -67,6 +87,10 @@ class Activity(gobject.GObject): gobject.idle_add(self._emit_buddy_left_signal, object_path) def _emit_new_channel_signal(self, object_path): + """Generate new-channel GObject signal with channel object path + + New telepathy-python communications channel has been opened + """ self.emit('new-channel', object_path) return False @@ -74,27 +98,35 @@ class Activity(gobject.GObject): gobject.idle_add(self._emit_new_channel_signal, object_path) def get_id(self): + """Retrieve the unique identifier for this activity instance""" # Cache activity ID, which should never change anyway if not self._id: self._id = self._activity.GetId() return self._id def get_color(self): + """Retrieve the activity icon colour for this activity instance""" if not self._color: self._color = self._activity.GetColor() return self._color def get_name(self): + """Retrieve the activity name for this activity instance""" if not self._name: self._name = self._activity.GetName() return self._name def get_type(self): + """Retrieve the activity/bundle type for this activity instance""" if not self._type: self._type = self._activity.GetType() return self._type def get_joined_buddies(self): + """Retrieve the set of Buddy objects attached to this activity + + returns list of presence Buddy objects + """ resp = self._activity.GetJoinedBuddies() buddies = [] for item in resp: @@ -102,15 +134,26 @@ class Activity(gobject.GObject): return buddies def join(self): + """Join this activity + + XXX if these are all activities, can I join my own activity? + """ if self._joined: return self._activity.Join() self._joined = True def get_channels(self): + """Retrieve communications channel descriptions for the activity + + Returns (bus name, connection, channels) for the activity + + XXX what are those values? + """ (bus_name, connection, channels) = self._activity.GetChannels() return bus_name, connection, channels def owner_has_joined(self): + """Retrieve whether the owner of the activity is active within it""" # FIXME return False diff --git a/sugar/presence/buddy.py b/sugar/presence/buddy.py index c6f51d57..048f3ea7 100644 --- a/sugar/presence/buddy.py +++ b/sugar/presence/buddy.py @@ -1,3 +1,4 @@ +"""UI interface to a buddy in the presence service""" # Copyright (C) 2007, Red Hat, Inc. # # This library is free software; you can redistribute it and/or @@ -20,13 +21,33 @@ import gtk import dbus def _bytes_to_string(bytes): + """Convertes an short-int (char) array to a string + + returns string or None for a null sequence + """ if len(bytes): + # if there's an internal buffer, we could use + # ctypes to pull it out without this... return ''.join([chr(item) for item in bytes]) return None class Buddy(gobject.GObject): - + """UI interface for a Buddy in the presence service + + Each buddy interface tracks a set of activities and properties + that can be queried to provide UI controls for manipulating + the presence interface. + + Properties Dictionary: + 'key': public key, + 'nick': nickname , + 'color': color (XXX what format), + 'current-activity': (XXX dbus path?), + 'owner': (XXX dbus path?), + 'icon': (XXX pixel data for an icon?) + See __gproperties__ + """ __gsignals__ = { 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), @@ -51,6 +72,13 @@ class Buddy(gobject.GObject): _BUDDY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Buddy" def __init__(self, bus, new_obj_cb, del_obj_cb, object_path): + """Initialise the reference to the buddy + + bus -- dbus bus object + new_obj_cb -- callback to call when this buddy joins an activity + del_obj_cb -- callback to call when this buddy leaves an activity + object_path -- path to the buddy object + """ gobject.GObject.__init__(self) self._object_path = object_path self._ps_new_object = new_obj_cb @@ -68,12 +96,19 @@ class Buddy(gobject.GObject): self._icon = None def _get_properties_helper(self): + """Retrieve the Buddy's property dictionary from the service object + """ props = self._buddy.GetProperties() if not props: return {} return props def do_get_property(self, pspec): + """Retrieve a particular property from our property dictionary + + pspec -- XXX some sort of GTK specifier object with attributes + including 'name', 'active' and 'icon-name' + """ if pspec.name == "key": return self._properties["key"] elif pspec.name == "nick": @@ -97,48 +132,79 @@ class Buddy(gobject.GObject): return self._icon def object_path(self): + """Retrieve our dbus object path""" return self._object_path def _emit_icon_changed_signal(self, bytes): + """Emit GObject signal when icon has changed""" self._icon = _bytes_to_string(bytes) self.emit('icon-changed') return False def _icon_changed_cb(self, icon_data): + """Handle dbus signal by emitting a GObject signal""" gobject.idle_add(self._emit_icon_changed_signal, icon_data) def _emit_joined_activity_signal(self, object_path): + """Emit activity joined signal with Activity object""" self.emit('joined-activity', self._ps_new_object(object_path)) return False def _joined_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Stores the activity in activities dictionary as well + """ if not self._activities.has_key(object_path): self._activities[object_path] = self._ps_new_object(object_path) gobject.idle_add(self._emit_joined_activity_signal, object_path) def _emit_left_activity_signal(self, object_path): + """Emit activity left signal with Activity object + + XXX this calls self._ps_new_object instead of self._ps_del_object, + which would seem to be the incorrect callback? + """ self.emit('left-activity', self._ps_new_object(object_path)) return False def _left_activity_cb(self, object_path): + """Handle dbus signal by emitting a GObject signal + + Also removes from the activities dictionary + """ if self._activities.has_key(object_path): del self._activities[object_path] gobject.idle_add(self._emit_left_activity_signal, object_path) def _handle_property_changed_signal(self, prop_list): + """Emit property-changed signal with property dictionary + + Generates a property-changed signal with the results of + _get_properties_helper() + """ self._properties = self._get_properties_helper() # FIXME: don't leak unexposed property names self.emit('property-changed', prop_list) return False def _property_changed_cb(self, prop_list): + """Handle dbus signal by emitting a GObject signal""" gobject.idle_add(self._handle_property_changed_signal, prop_list) def get_icon_pixbuf(self): + """Retrieve Buddy's icon as a GTK pixel buffer + + XXX Why aren't the icons coming in as SVG? + """ if self.props.icon and len(self.props.icon): pbl = gtk.gdk.PixbufLoader() icon_data = "" for item in self.props.icon: + # XXX this is a slow way to convert the data + # under Python 2.5 and below, collect in a + # list and then join with "", see + # _bytes_to_string in this module icon_data = icon_data + chr(item) pbl.write(icon_data) pbl.close() @@ -147,6 +213,14 @@ class Buddy(gobject.GObject): return None def get_joined_activities(self): + """Retrieve the set of all activities which this buddy has joined + + Uses the GetJoinedActivities method on the service + object to produce object paths, wraps each in an + Activity object. + + returns list of presence Activity objects + """ try: resp = self._buddy.GetJoinedActivities() except dbus.exceptions.DBusException: diff --git a/sugar/presence/presenceservice.py b/sugar/presence/presenceservice.py index 9b7bd3c3..78f51e6b 100644 --- a/sugar/presence/presenceservice.py +++ b/sugar/presence/presenceservice.py @@ -1,3 +1,4 @@ +"""UI class to access system-level presence object""" # Copyright (C) 2007, Red Hat, Inc. # # This library is free software; you can redistribute it and/or @@ -18,24 +19,78 @@ import dbus, dbus.glib, gobject import logging +# XXX use absolute imports +# from sugar.presence import buddy, activity +# this *kind* of relative import is deprecated +# with an explicit relative import slated to be +# introduced (available in Python 2.5 with a __future__ +# import), that would read as: +# from . import buddy, activity +# see PEP: http://docs.python.org/whatsnew/pep-328.html import buddy, activity class ObjectCache(object): + """Path to Activity/Buddy object cache + + On notification of a new object of either type the + PresenceService client stores the object's representation + in this object. + + XXX Why not just sub-class dict? We're only adding two + methods then and we would have all of the other + standard operations on dictionaries. + """ def __init__(self): + """Initialise the cache""" self._cache = {} def get(self, object_path): + """Retrieve specified object from the cache + + object_path -- full dbus path to the object + + returns a presence.buddy.Buddy or presence.activity.Activity + instance or None if the object_path is not yet cached. + + XXX could be written as return self._cache.get( object_path ) + """ try: return self._cache[object_path] except KeyError: return None def add(self, obj): + """Adds given presence object to the cache + + obj -- presence Buddy or Activity representation, the object's + object_path() method is used as the key for storage + + returns None + + XXX should raise an error on collisions, shouldn't it? or + return True/False to say whether the item was actually + added + """ op = obj.object_path() if not self._cache.has_key(op): self._cache[op] = obj def remove(self, object_path): + """Remove the given presence object from the cache + + object_path -- full dbus path to the object + + returns None + + XXX does two checks instead of one with a try:except for the + keyerror, normal case of deleting existing penalised as + a result. + + try: + return self._cache.pop( key ) + except KeyError: + return None + """ if self._cache.has_key(object_path): del self._cache[object_path] @@ -46,7 +101,13 @@ DBUS_PATH = "/org/laptop/Sugar/Presence" class PresenceService(gobject.GObject): - + """UI-side interface to the dbus presence service + + This class provides UI programmers with simplified access + to the dbus service of the same name. It allows for observing + various events from the presence service as GObject events, + as well as some basic introspection queries. + """ __gsignals__ = { 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), @@ -71,6 +132,7 @@ class PresenceService(gobject.GObject): def __init__(self): + """Initialise the service and connect to events""" gobject.GObject.__init__(self) self._objcache = ObjectCache() self._bus = dbus.SessionBus() @@ -84,6 +146,17 @@ class PresenceService(gobject.GObject): self._ps.connect_to_signal('PrivateInvitation', self._private_invitation_cb) def _new_object(self, object_path): + """Turn new object path into (cached) Buddy/Activity instance + + object_path -- full dbus path of the new object, must be + prefixed with either of _PS_BUDDY_OP or _PS_ACTIVITY_OP + + Note that this method is called throughout the class whenever + the representation of the object is required, it is not only + called when the object is first discovered. + + returns presence Buddy or Activity representation + """ obj = self._objcache.get(object_path) if not obj: if object_path.startswith(self._PS_BUDDY_OP): @@ -102,52 +175,79 @@ class PresenceService(gobject.GObject): pass def _emit_buddy_appeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" self.emit('buddy-appeared', self._new_object(object_path)) return False def _buddy_appeared_cb(self, op): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_buddy_appeared_signal, op) def _emit_buddy_disappeared_signal(self, object_path): + """Emit GObject event with presence.buddy.Buddy object""" self.emit('buddy-disappeared', self._new_object(object_path)) return False def _buddy_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_buddy_disappeared_signal, object_path) def _emit_activity_invitation_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" self.emit('activity-invitation', self._new_object(object_path)) return False def _activity_invitation_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_activity_invitation_signal, object_path) def _emit_private_invitation_signal(self, bus_name, connection, channel): + """Emit GObject event with bus_name, connection and channel + + XXX This seems to generate the wrong GObject event? It generates + 'service-disappeared' instead of private-invitation for some + reason. That event doesn't even seem to be registered? + """ self.emit('service-disappeared', bus_name, connection, channel) return False def _private_invitation_cb(self, bus_name, connection, channel): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_service_disappeared_signal, bus_name, connection, channel) def _emit_activity_appeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" self.emit('activity-appeared', self._new_object(object_path)) return False def _activity_appeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_activity_appeared_signal, object_path) def _emit_activity_disappeared_signal(self, object_path): + """Emit GObject event with presence.activity.Activity object""" self.emit('activity-disappeared', self._new_object(object_path)) return False def _activity_disappeared_cb(self, object_path): + """Callback for dbus event (forwards to method to emit GObject event)""" gobject.idle_add(self._emit_activity_disappeared_signal, object_path) def get(self, object_path): + """Retrieve given object path as a Buddy/Activity object + + XXX This is basically just an alias for _new_object, i.e. it + just adds an extra function-call to the operation. + """ return self._new_object(object_path) def get_activities(self): + """Retrieve set of all activities from service + + returns list of Activity objects for all object paths + the service reports exist (using GetActivities) + """ resp = self._ps.GetActivities() acts = [] for item in resp: @@ -155,6 +255,13 @@ class PresenceService(gobject.GObject): return acts def get_activity(self, activity_id): + """Retrieve single Activity object for the given unique id + + activity_id -- unique ID for the activity + + returns single Activity object or None if the activity + is not found using GetActivityById on the service + """ try: act_op = self._ps.GetActivityById(activity_id) except dbus.exceptions.DBusException: @@ -162,6 +269,11 @@ class PresenceService(gobject.GObject): return self._new_object(act_op) def get_buddies(self): + """Retrieve set of all buddies from service + + returns list of Buddy objects for all object paths + the service reports exist (using GetBuddies) + """ resp = self._ps.GetBuddies() buddies = [] for item in resp: @@ -169,6 +281,14 @@ class PresenceService(gobject.GObject): return buddies def get_buddy(self, key): + """Retrieve single Buddy object for the given public key + + key -- buddy's public encryption key + + returns single Buddy object or None if the activity + is not found using GetBuddyByPublicKey on the + service + """ try: buddy_op = self._ps.GetBuddyByPublicKey(dbus.ByteArray(key)) except dbus.exceptions.DBusException: @@ -176,6 +296,12 @@ class PresenceService(gobject.GObject): return self._new_object(buddy_op) def get_owner(self): + """Retrieves "owner" as a Buddy + + XXX check that it really is a Buddy that's produced, what is + this the owner of? Shouldn't it be getting an activity + and then asking who the owner of that is? + """ try: owner_op = self._ps.GetOwner() except dbus.exceptions.DBusException: @@ -183,13 +309,27 @@ class PresenceService(gobject.GObject): return self._new_object(owner_op) def _share_activity_cb(self, activity, op): + """Notify with GObject event of successful sharing of activity""" self.emit("activity-shared", True, self._new_object(op), None) def _share_activity_error_cb(self, activity, err): + """Notify with GObject event of unsuccessful sharing of activity""" logging.debug("Error sharing activity %s: %s" % (activity.get_id(), err)) self.emit("activity-shared", False, None, err) def share_activity(self, activity, properties={}): + """Ask presence service to ask the activity to share itself + + Uses the ShareActivity method on the service to ask for the + sharing of the given activity. Arranges to emit activity-shared + event with: + + (success, Activity, err) + + on success/failure. + + returns None + """ actid = activity.get_id() atype = activity.get_service_name() name = activity.props.title @@ -199,6 +339,10 @@ class PresenceService(gobject.GObject): class _MockPresenceService(gobject.GObject): + """Test fixture allowing testing of items that use PresenceService + + See PresenceService for usage and purpose + """ __gsignals__ = { 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), @@ -238,6 +382,7 @@ class _MockPresenceService(gobject.GObject): _ps = None def get_instance(): + """Retrieve this process' view of the PresenceService""" global _ps if not _ps: _ps = PresenceService()