Docstrings for modules all over sugar and shell.

These are just the doc strings I created as I was spelunking
through to see how Sugar manages launching applications.  The
resulting auto-documentation is neither polished or finished,
but it should help people reading the code somewhat.

There are a few minor code cleanups:

  * activityhandle (replacing C idiom for initialisation with
    a Python one)
  * bundle registry (using a parameterised directory name so
    that it shows up in the documentation)
  * validate_activity_id function, use isinstance( item, (str,unicode))
    for the query, rather than two separate checks with isinstance
This commit is contained in:
Mike C. Fletcher 2007-04-09 22:47:37 -04:00
parent 508a59b99b
commit 3f10890319
15 changed files with 373 additions and 18 deletions

View File

@ -25,6 +25,13 @@ from sugar.presence import presenceservice
from sugar import profile
class HomeActivity(gobject.GObject):
"""Activity which appears in the "Home View" of the Sugar shell
This class stores the Sugar Shell's metadata regarding a
given activity/application in the system. It interacts with
the sugar.activity.* modules extensively in order to
accomplish its tasks.
"""
__gsignals__ = {
'launch-timeout': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
@ -32,6 +39,15 @@ class HomeActivity(gobject.GObject):
}
def __init__(self, bundle, activity_id):
"""Initialise the HomeActivity
bundle -- sugar.activity.bundle.Bundle instance,
provides the information required to actually
create the new instance. This is, in effect,
the "type" of activity being created.
activity_id -- unique identifier for this instance
of the activity type
"""
gobject.GObject.__init__(self)
self._window = None
self._xid = None
@ -52,6 +68,8 @@ class HomeActivity(gobject.GObject):
self._launch_timeout_id = 0
def _launch_timeout_cb(self, user_data=None):
"""Callback for launch operation timeouts
"""
logging.debug("Activity %s (%s) launch timed out" %
(self._activity_id, self.get_type))
self._launch_timeout_id = 0
@ -78,17 +96,33 @@ class HomeActivity(gobject.GObject):
self._service = service
def get_service(self):
"""Retrieve the application's sugar introspection service
Note that non-native Sugar applications will not have
such a service, so the return value will be None in
those cases.
"""
return self._service
def get_title(self):
"""Retrieve the application's root window's suggested title"""
if not self._launched:
raise RuntimeError("Activity is still launching.")
return self._window.get_name()
def get_icon_name(self):
"""Retrieve the bundle's icon (file) name"""
return self._bundle.get_icon()
def get_icon_color(self):
"""Retrieve the appropriate icon colour for this activity
Uses activity_id to index into the PresenceService's
set of activity colours, if the PresenceService does not
have an entry (implying that this is not a Sugar-shared application)
uses the local user's profile.get_color() to determine the
colour for the icon.
"""
pservice = presenceservice.get_instance()
activity = pservice.get_activity(self._activity_id)
if activity != None:
@ -97,28 +131,53 @@ class HomeActivity(gobject.GObject):
return profile.get_color()
def get_activity_id(self):
"""Retrieve the "activity_id" passed in to our constructor
This is a "globally likely unique" identifier generated by
sugar.util.unique_id
"""
return self._activity_id
def get_xid(self):
"""Retrieve the X-windows ID of our root window"""
if not self._launched:
raise RuntimeError("Activity is still launching.")
return self._xid
def get_window(self):
"""Retrieve the X-windows root window of this application
This was stored by the set_window method, which was
called by HomeModel._add_activity, which was called
via a callback that looks for all 'window-opened'
events.
HomeModel currently uses a dbus service query on the
activity to determine to which HomeActivity the newly
launched window belongs.
"""
if not self._launched:
raise RuntimeError("Activity is still launching.")
return self._window
def get_type(self):
"""Retrieve bundle's "service_name" for future reference"""
return self._bundle.get_service_name()
def get_shared(self):
"""Return whether this activity is using Presence service sharing"""
if not self._launched:
raise RuntimeError("Activity is still launching.")
return self._service.get_shared()
def get_launch_time(self):
"""Return the time at which the activity was first launched
Format is floating-point time.time() value
(seconds since the epoch)
"""
return self._launch_time
def get_launched(self):
"""Return whether we have bound our top-level window yet"""
return self._launched

View File

@ -28,7 +28,19 @@ _SERVICE_NAME = "org.laptop.Activity"
_SERVICE_PATH = "/org/laptop/Activity"
class HomeModel(gobject.GObject):
"""Model of the "Home" view (activity management)
The HomeModel is basically the point of registration
for all running activities within Sugar. It traps
events that tell the system there is a new activity
being created (generated by the activity factories),
or removed, as well as those which tell us that the
currently focussed activity has changed.
The HomeModel tracks a set of HomeActivity instances,
which are tracking the window to activity mappings
the activity factories have set up.
"""
__gsignals__ = {
'activity-launched': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE,
@ -124,6 +136,16 @@ class HomeModel(gobject.GObject):
self.emit('activity-added', home_window)
def _add_activity(self, window):
"""Add the window to the set of windows we track
At the moment this requires that something somewhere
have registered a dbus service with the XID of the
new window that is used to bind the requested activity
to the window.
window -- gtk.Window instance representing a new
normal, top-level window
"""
bus = dbus.SessionBus()
xid = window.get_xid()
try:

View File

@ -1,3 +1,4 @@
"""OLPC Sugar User Interface"""
ZOOM_MESH = 0
ZOOM_FRIENDS = 1
ZOOM_HOME = 2

View File

@ -1,3 +1,8 @@
"""Base class for Python-coded activities
This is currently the only reference for what an
activity must do to participate in the Sugar desktop.
"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
@ -29,6 +34,26 @@ class Activity(Window, gtk.Container):
"""Base Activity class that all other Activities derive from."""
__gtype_name__ = 'SugarActivity'
def __init__(self, handle):
"""Initialise the Activity
handle -- sugar.activity.activityhandle.ActivityHandle
instance providing the activity id and access to the
presence service which *may* provide sharing for this
application
Side effects:
Sets the gdk screen DPI setting (resolution) to the
Sugar screen resolution.
Connects our "destroy" message to our _destroy_cb
method.
Creates a base gtk.Window within this window.
Creates an ActivityService (self._bus) servicing
this application.
"""
Window.__init__(self)
self.connect('destroy', self._destroy_cb)
@ -105,6 +130,7 @@ class Activity(Window, gtk.Container):
return False
def _destroy_cb(self, window):
"""Destroys our ActivityService and sharing service"""
if self._bus:
del self._bus
self._bus = None
@ -112,4 +138,6 @@ class Activity(Window, gtk.Container):
self._pservice.unregister_service(self._service)
def get_bundle_path():
"""Return the bundle path for the current process' bundle
"""
return os.environ['SUGAR_BUNDLE_PATH']

View File

@ -1,3 +1,4 @@
"""Shell side object which manages request to start activity"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
@ -31,6 +32,7 @@ _ACTIVITY_SERVICE_PATH = "/org/laptop/Activity"
_ACTIVITY_INTERFACE = "org.laptop.Activity"
def create_activity_id():
"""Generate a new, unique ID for this activity"""
pservice = presenceservice.get_instance()
# create a new unique activity ID
@ -52,6 +54,14 @@ def create_activity_id():
raise RuntimeError("Cannot generate unique activity id.")
class ActivityCreationHandler(gobject.GObject):
"""Sugar-side activity creation interface
This object uses a dbus method on the ActivityFactory
service to create the new activity. It generates
GObject events in response to the success/failure of
activity startup using callbacks to the service's
create call.
"""
__gsignals__ = {
'success': (gobject.SIGNAL_RUN_FIRST,
gobject.TYPE_NONE, ([])),
@ -61,6 +71,29 @@ class ActivityCreationHandler(gobject.GObject):
}
def __init__(self, service_name, activity_handle):
"""Initialise the handler
service_name -- used to retrieve the activity bundle
from the global BundleRegistry. This is what
determines what activity will be run.
activity_handle -- stores the values which are to
be passed to the service to uniquely identify
the activity to be created and the sharing
service that may or may not be connected with it
sugar.activity.activityhandle.ActivityHandle instance
calls the "create" method on the service for this
particular activity type and registers the
_reply_handler and _error_handler methods on that
call's results.
The specific service which creates new instances of this
particular type of activity is created during the activity
registration process in
sugar.activity.bundleregistry.BundleRegistry which creates
service definition files for each registered bundle type.
"""
gobject.GObject.__init__(self)
self._service_name = service_name
self._activity_handle = activity_handle
@ -75,16 +108,28 @@ class ActivityCreationHandler(gobject.GObject):
factory.create(self._activity_handle.get_dict(),
reply_handler=self._reply_handler,
error_handler=self._error_handler)
def get_activity_id(self):
"""Retrieve the unique identity for this activity"""
return self._activity_handle.activity_id
def _reply_handler(self, xid):
"""Reply from service regarding what window was created
xid -- X windows ID for the window that was just created
emits the "success" message (on ourselves)
"""
logging.debug("Activity created %s (%s)." %
(self._activity_handle.activity_id, self._service_name))
self.emit('success')
def _error_handler(self, err):
"""Reply from service with an error message (exception)
err -- exception object describing the error
emits the "error" message (on ourselves)
"""
logging.debug("Couldn't create activity %s (%s): %s" %
(self._activity_handle.activity_id, self._service_name, err))
self.emit('error', err)

View File

@ -35,9 +35,43 @@ gobject.threads_init()
dbus.glib.threads_init()
class ActivityFactoryService(dbus.service.Object):
"""D-Bus service that creates new instances of an activity"""
"""D-Bus service that creates instances of Python activities
The ActivityFactoryService is a dbus service created for
each Python based activity type (that is, each activity
bundle which declares a "class" in its activity.info file,
rather than an "exec").
The ActivityFactoryService is the actual process which
instantiates the Python classes for Sugar interfaces. That
is, your Python code runs in the same process as the
ActivityFactoryService itself.
The "service" process is created at the moment Sugar first
attempts to create an instance of the activity type. It
then remains in memory until the last instance of the
activity type is terminated.
"""
def __init__(self, service_name, activity_class):
"""Initialize the service to create activities of this type
service_name -- bundle's service name, this is used
to construct the dbus service name used to access
the created service.
activity_class -- dotted Python class name for the
Activity class which is to be instantiated by
the service. Assumed to be composed of a module
followed by a class.
if the module specified has a "start" attribute this object
will be called on service initialisation (before first
instance is created).
if the module specified has a "stop" attribute this object
will be called after every instance exits (note: may be
called multiple times for each time start is called!)
"""
self._activities = []
splitted_module = activity_class.rsplit('.', 1)
@ -60,6 +94,12 @@ class ActivityFactoryService(dbus.service.Object):
@dbus.service.method("com.redhat.Sugar.ActivityFactory", in_signature="a{ss}")
def create(self, handle):
"""Create a new instance of this activity
handle -- sugar.activity.activityhandle.ActivityHandle
compatible dictionary providing the instance-specific
values for the new instance
"""
activity_handle = activityhandle.create_from_dict(handle)
activity = self._constructor(activity_handle)
activity.present()
@ -70,6 +110,16 @@ class ActivityFactoryService(dbus.service.Object):
return activity.window.xid
def _activity_destroy_cb(self, activity):
"""On close of an instance's root window
Removes the activity from the tracked activities.
If our implementation module has a stop, calls
that.
If there are no more tracked activities, closes
the activity.
"""
self._activities.remove(activity)
if hasattr(self._module, 'stop'):

View File

@ -18,13 +18,47 @@
from sugar.presence import presenceservice
class ActivityHandle(object):
def __init__(self, activity_id):
"""Data structure storing simple activity metadata"""
def __init__(
self, activity_id, pservice_id=None,
object_id=None,uri=None
):
"""Initialise the handle from activity_id
activity_id -- unique id for the activity to be
created
pservice_id -- identity of the sharing service
for this activity in the PresenceService
object_id -- identity of the journal object
associated with the activity. It was used by
the journal prototype implementation, might
change when we do the real one.
When you resume an activity from the journal
the object_id will be passed in. It's optional
since new activities does not have an
associated object (yet).
XXX Not clear how this relates to the activity
id yet, i.e. not sure we really need both. TBF
uri -- URI associated with the activity. Used when
opening an external file or resource in the
activity, rather than a journal object
(downloads stored on the file system for
example or web pages)
"""
self.activity_id = activity_id
self.pservice_id = None
self.object_id = None
self.uri = None
self.pservice_id = pservice_id
self.object_id = object_id
self.uri = uri
def get_presence_service(self):
"""Retrieve the "sharing service" for this activity
Uses the PresenceService to find any existing dbus
service which provides sharing mechanisms for this
activity.
"""
if self.pservice_id:
pservice = presenceservice.get_instance()
return pservice.get_activity(self.pservice_id)
@ -32,6 +66,7 @@ class ActivityHandle(object):
return None
def get_dict(self):
"""Retrieve our settings as a dictionary"""
result = { 'activity_id' : self.activity_id }
if self.pservice_id:
result['pservice_id'] = self.pservice_id
@ -43,12 +78,11 @@ class ActivityHandle(object):
return result
def create_from_dict(handle_dict):
result = ActivityHandle(handle_dict['activity_id'])
if handle_dict.has_key('pservice_id'):
result.pservice_id = handle_dict['pservice_id']
if handle_dict.has_key('object_id'):
result.object_id = handle_dict['object_id']
if handle_dict.has_key('uri'):
result.uri = handle_dict['uri']
"""Create a handle from a dictionary of parameters"""
result = ActivityHandle(
handle_dict['activity_id'],
pservice_id = handle_dict.get( 'pservice_id' ),
object_id = handle_dict.get('object_id'),
uri = handle_dict.get('uri'),
)
return result

View File

@ -29,6 +29,21 @@ class ActivityService(dbus.service.Object):
tightly control what stuff passes through the dbus python bindings."""
def __init__(self, activity):
"""Initialise the service for the given activity
activity -- sugar.activity.activity.Activity instance,
must have already bound it's window (i.e. it must
have already initialised to the point of having
the X window available).
Creates dbus services that use the xid of the activity's
root window as discriminants among all active services
of this type. That is, the services are all available
as names/paths derived from the xid for the window.
The various methods exposed on dbus are just forwarded
to the client Activity object's equally-named methods.
"""
xid = activity.window.xid
service_name = _ACTIVITY_SERVICE_NAME + '%d' % xid
object_path = _ACTIVITY_SERVICE_PATH + "/%s" % xid

View File

@ -1,3 +1,4 @@
"""Metadata description of a given application/activity"""
import logging
import locale
import os
@ -8,7 +9,17 @@ from sugar import env
_PYTHON_FACTORY='sugar-activity-factory'
class Bundle:
"""Info about an activity bundle. Wraps the activity.info file."""
"""Metadata description of a given application/activity
The metadata is normally read from an activity.info file,
which is an INI-style configuration file read using the
standard Python ConfigParser module.
The format reference for the Bundle definition files is
available for further reference:
http://wiki.laptop.org/go/Activity_bundles
"""
def __init__(self, path):
self._name = None
self._icon = None

View File

@ -15,8 +15,25 @@ def _get_data_dirs():
return [ '/usr/local/share/', '/usr/share/' ]
class _ServiceManager(object):
"""Internal class responsible for creating dbus service files
DBUS services are defined in files which bind a service name
to the name of an executable which provides the service name.
In Sugar, the service files are automatically generated from
the activity registry (by this class). When an activity's
dbus launch service is requested, dbus will launch the
specified executable in order to allow it to provide the
requested activity-launching service.
In the case of activities which provide a "class", instead of
an "exec" attribute in their activity.info, the
sugar-activity-factory script is used with an appropriate
argument to service that bundle.
"""
SERVICE_DIRECTORY = '~/.local/share/dbus-1/services'
def __init__(self):
service_dir = os.path.expanduser('~/.local/share/dbus-1/services')
service_dir = os.path.expanduser(self.SERVICE_DIRECTORY)
if not os.path.isdir(service_dir):
os.makedirs(service_dir)

View File

@ -1,10 +1,25 @@
"""Simple date-representation model"""
import datetime
class Date(object):
"""Date-object storing a simple time.time() float
XXX not sure about the rationale for this class,
possibly it makes transfer over dbus easier?
"""
def __init__(self, timestamp):
"""Initialise via a timestamp (floating point value)"""
self._timestamp = timestamp
def __str__(self):
"""Produce a formatted date representation
Eventually this should produce a localised version
of the date. At the moment it always produces English
dates in long form with Today and Yesterday
special-cased and dates from this year not presenting
the year in the date.
"""
date = datetime.date.fromtimestamp(self._timestamp)
today = datetime.date.today()

View File

@ -1,3 +1,4 @@
"""Calculates file-paths for the Sugar working environment"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or

View File

@ -1,3 +1,4 @@
"""Logging module configuration for Sugar"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or

View File

@ -1,3 +1,4 @@
"""User settings/configuration loading"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
@ -23,6 +24,30 @@ from sugar import util
from sugar.graphics.xocolor import XoColor
class _Profile(object):
"""Local user's current options/profile information
User settings are stored in an INI-style configuration
file. This object uses the ConfigParser module to load
the settings. At the moment the only storage mechanism
is in the set_server_registered method, which loads the
file directly from disk, then dumps it back out again
immediately, rather than using the class.
The profile is also responsible for loading the user's
public and private ssh keys from disk.
Attributes:
name -- child's name
color -- XoColor for the child's icon
server -- school server with which the child is
associated
server_registered -- whether the child has registered
with the school server or not
pubkey -- public ssh key
privkey_hash -- SHA has of the child's public key
"""
def __init__(self):
self.name = None
self.color = None

View File

@ -1,3 +1,4 @@
"""Various utility functions"""
# Copyright (C) 2006, Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
@ -38,6 +39,19 @@ def _sha_data(data):
return sha_hash.digest()
def unique_id(data = ''):
"""Generate a likely-unique ID for whatever purpose
data -- suffix appended to working data before hashing
Returns a 40-character string with hexidecimal digits
representing an SHA hash of the time, a random digit
within a constrained range and the data passed.
Note: these are *not* crypotographically secure or
globally unique identifiers. While they are likely
to be unique-enough, no attempt is made to make
perfectly unique values.
"""
data_string = "%s%s%s" % (time.time(), random.randint(10000, 100000), data)
return printable_hash(_sha_data(data_string))
@ -49,7 +63,7 @@ def is_hex(s):
def validate_activity_id(actid):
"""Validate an activity ID."""
if not isinstance(actid, str) and not isinstance(actid, unicode):
if not isinstance(actid, (str,unicode)):
return False
if len(actid) != ACTIVITY_ID_LEN:
return False
@ -62,6 +76,23 @@ class _ServiceParser(ConfigParser):
return option
def write_service(name, bin, path):
"""Write a D-BUS service definition file
These are written by the bundleregistry when
a new activity is registered. They bind a
D-BUS bus-name with an executable which is
to provide the named service.
name -- D-BUS service name, must be a valid
filename/D-BUS name
bin -- executable providing named service
path -- directory into which to write the
name.service file
The service files themselves are written using
the _ServiceParser class, which is a subclass
of the standard ConfigParser class.
"""
service_cp = _ServiceParser()
section = 'D-BUS Service'
service_cp.add_section(section)