Python KIOSlave Tutorial

Contents

Introduction

The Python kioslave presents the contents of a Python module (the kio_python module itself) as a simple, read-only filing system. Although not a particularly useful example, it explains how to implement basic features that are common to many kioslaves, and should be simple enough to demonstrate how kioslaves work without introducing too many specialised functions.

This document outlines the source code and structure of the Python kioslave and aims to be a useful guide to those wishing to write kioslaves in either Python or C++.

It is convenient to examine the source code as it is written in the kio_python.py file. However, we can at least group some of the methods in order to provide an overview of the kioslave and separate out the details of the initialisation.

Initialisation

Each kioslave requires various classes from the Qt and KDE frameworks in order to function. These are imported from the qt, kio and kdecore modules. In addition, the os, site, and sys modules provide useful functions that are either necessary or just very useful. We group these imports together for conciseness:

import os, site, sys
from qt import QByteArray, QDataStream, QFile, QFileInfo, QString, \
               QStringList, IO_ReadOnly, IO_WriteOnly, SIGNAL
from kio import KIO
from kdecore import KURL

The inspect module is used to read the source code of Python objects:

import inspect

We define an exception that is used to indicate failure in certain kioslave operations:

class OperationFailed(Exception):

    pass

We omit the debugging code to keep this tutorial fairly brief. This can be examined in the source code.

The KIOSlave Class

We define a class which will be used to create instances of the kioslave. The class must be derived from the KIO.SlaveBase class so that it can communicate with clients via the standard DCOP mechanism. Various operations are supported if the appropriate method (virtual functions in C++) are reimplemented in our subclass.

Note that the name of the class is not important; it is only used by a factory function that we provide later.

Standard Methods

__init__

An initialisation method, or constructor, is written which calls the base class and initialises some useful attributes, or instance variables. Note that the name of the kioslave is passed to the base class's __init__ method:

class SlaveClass(KIO.SlaveBase):

    def __init__(self, pool, app):
    
        KIO.SlaveBase.__init__(self, "python", pool, app)

setHost

The kioslave does not allow the user to specify a host name when supplying a URL. The setHost method is implemented in the following way to enforce this behaviour:

def setHost(self, host, port, user, passwd):

    if unicode(host) != u"":
    
        self.error(KIO.ERR_MALFORMED_URL, host)
        return

Since the kioslave only provides a read-only view of the contents of a Python module, we only need to implement methods that return information about objects; methods that support writing operations are not implemented. We implement the stat, mimetype, get and listDir methods.

stat

The stat method provides information about objects in the virtual filing system. These are specified by URLs, and it is up to this method to return accurate information for valid URLs or return an error if the URL does not correspond to a known object. We begin by using a specialised method (defined later) to retrieve information about the specified object:

def stat(self, url):

    try:
    
        name, obj = self.find_object_from_url(url)
    
    except OperationFailed:
    
        self.error(KIO.ERR_DOES_NOT_EXIST, url.path())
        return

We use the custom OperationFailed exception to indicate that no suitable object could be found for the URL given, and we use the error method to inform the application.

For URLs that correspond to objects in the virtual filing system, we can construct generic information that lets the application decide how to handle the object using the build_entry method (defined later):

entry = self.build_entry(name, obj)

if entry != []:

    self.statEntry(entry)
    self.finished()

else:

    self.error(KIO.ERR_DOES_NOT_EXIST, url.path())

If suitable information could be found for the object then we inform the application using the statEntry method and call finished to let the application know that the operation completed successfully. If, for some unknown reason, suitable information was not available, we return with an error as before.

Note that, when exiting with an error, it is not necessary to call finished.

mimetype

The mimetype method provides the application with information about the MIME type of the object specified by a URL:

def mimetype(self, url):

    try:
    
        name, obj = self.find_object_from_url(url)
    
    except OperationFailed:
    
        self.error(KIO.ERR_DOES_NOT_EXIST, url.path())
        return

Once again, we return with an error if an object could not be found.

If an object is found, we can return one of two MIME types using the mimeType method: in the case of a container object we return a suitable MIME type for a directory, and for everything else we return the MIME type for plain text:

if self.is_container(obj):

    self.mimeType("inode/directory")

else:

    self.mimeType("text/plain")

self.finished()

Whether an object is a container is decided by the is_container function (defined later).

After informing the application about the MIME type of the object we finish the operation with a call to the finished method.

The mimetype method does not need to be implemented. However, if it is not then calling applications will have to retrieve data for each object provided by the kioslave in order to determine their MIME types, and this is obviously less efficient.

get

The get method returns the contents of an object, specified by a URL, to the calling application:

def get(self, url):

    try:
    
        name, obj = self.find_object_from_url(url)
    
    except OperationFailed:
    
        self.error(KIO.ERR_DOES_NOT_EXIST, url.path())
        return

Again, we return with an error if the URL does not refer to a valid object.

If a valid object is found, we must check whether it is a directory. The get method is not supposed to return data for directories; instead, we must return with the following error:

if self.is_container(obj):

    self.error(KIO.ERR_IS_DIRECTORY, url.path())
    return

For this kioslave, the code that declares the MIME type and sends the data to the application is straightforward. More complex kioslaves may have to perform detailed checks to ensure that each object is represented properly to applications. Here, we simply declare that the data is HTML and use the data method to send a character-based representation of the object to the application:

self.mimeType("text/html")

self.data((
    u"<html>\n"
    u"<head>\n"
    u' <meta http-equiv="Content-Type" content="text/html; charset=%s">\n'
    u' <title>%s</title>\n'
    u"</head>\n"
    u"<body>\n"
    u"<h1>%s</h1>\n" % (self.html_repr(self.encoding),
                        self.html_repr(name),
                        self.html_repr(name))
    ).encode(self.encoding))

The first piece of data sent to the application is the start of the HTML document. The details of the object are found using functions from the inspect module:

source_file = None
source = None

try:

    source_file = inspect.getabsfile(obj)
    source_lines, source_start = inspect.getsourcelines(obj)
    source = u"".join(source_lines)

except (TypeError, IOError):

    source = repr(obj)
    source_start = None

if source_file is not None:

    self.data((
        u'Defined in <strong><a href="file:%s">%s</a></strong>' % (
            self.html_repr(source_file), self.html_repr(source_file))
        ).encode(self.encoding))
    
    if source_start is not None:
    
        self.data((
            u" (line %s)" % self.html_repr(source_start)
            ).encode(self.encoding))
    
    self.data(u"\n".encode(self.encoding))

self.data((
    u"<pre>\n"
    u"%s\n"
    u"</pre>\n" % self.html_repr(source)
    ).encode(self.encoding))

self.data((
    u"</body>\n"
    u"</html>\n"
    ).encode(self.encoding))

self.data(QByteArray())

self.finished()

The final call to data indicates that no more data is waiting to be sent. We finish the operation by calling the finished method.

listDir

The listDir method is called by applications to obtain a list of files for a directory represented by a URL. Usually, the application will have called stat via the client interface, and discovered that a given URL corresponds to a directory in the virtual filing system. It is clearly important that the stat method works, and that the kioslave can determine the type of objects correctly and reliably.

The specified URL is checked in the same way as for previous methods:

def listDir(self, url):

    try:
    
        name, obj = self.find_object_from_url(url)
    
    except OperationFailed:
    
        self.error(KIO.ERR_DOES_NOT_EXIST, url.path())
        return

In this method, we can only return sensible results if the object found is a directory. Therefore, we return with an error if the object is a file:

if not self.is_container(obj):

    self.error(KIO.ERR_IS_FILE, url.path())
    return

We represent certain Python objects as directories if they have an internal dictionary that contains at least one entry. For each of these dictionary entries, we return the generic information returned by build_entry to the application using the listEntry method:

for name in obj.__dict__.keys():

    entry = self.build_entry(name, obj.__dict__[name])
    
    if entry != []:
    
        self.listEntry(entry, False)

self.listEntry([], True)
self.finished()

After all the dictionary entries have been examined, the final listEntry call with the True argument indicates to the application that the listing is complete. We finish the operation with a call to the finished method.

Specialised Methods

build_entry

The build_entry method is a custom method and not part of the standard kioslave API. It creates a list of generic filing system properties for the named object specified:

def build_entry(self, name, obj):

    entry = []
    name = self.encode_name(name)
    length = len(unicode(obj).encode(site.encoding))

Each object can be named and its length measured.

The two different types of object are represented in different ways. Containers are represented as directories, and are given the appropriate file permissions, file type and MIME type:

if self.is_container(obj):

    permissions = 0544
    filetype = os.path.stat.S_IFDIR
    mimetype = "inode/directory"

All other objects (such as strings, integers, functions) are represented as files:

else:

    permissions = 0644
    filetype = os.path.stat.S_IFREG
    mimetype = "text/html"

Each object is described by a list of "atoms" with defined properties. We create each atom and append it to the list:

atom = KIO.UDSAtom()
atom.m_uds = KIO.UDS_NAME
atom.m_str = name

entry.append(atom)

atom = KIO.UDSAtom()
atom.m_uds = KIO.UDS_SIZE
atom.m_long = length

entry.append(atom)

atom = KIO.UDSAtom()
atom.m_uds = KIO.UDS_ACCESS
atom.m_long = permissions

entry.append(atom)

atom = KIO.UDSAtom()
atom.m_uds = KIO.UDS_FILE_TYPE
atom.m_long = filetype

entry.append(atom)

atom = KIO.UDSAtom()
atom.m_uds = KIO.UDS_MIME_TYPE
atom.m_str = mimetype

entry.append(atom)

return entry

The list of atoms is returned to the caller.

Note that if the stat method is implemented then the list of entries must include the UDE_FILE_TYPE atom or the whole system may not work at all.

find_object_from_url

The find_object_from_url is not part of the standard kioslave API. It tries to find a Python object within a predefined module that corresponds to the specified URL:

def find_object_from_url(self, url):

    url.cleanPath(1)
    
    path = unicode(url.path(-1))

We obtain a path from the URL that contains no trailing directory separator. This path can be used to specify a Python object within the module's object hierarchy.

To navigate the object tree, we split the URL at each directory separator, and we define the object that represents the topmost item in the tree:

elements = filter(lambda element: element != u"", path.split(u"/"))

name = u"kio_python"
obj = sys.modules["kio_python"]

The name of the topmost item is not important; we define it only for consistency with the following code:

while elements != []:

    name = elements.pop(0)
    
    if name in obj.__dict__.keys():
    
        obj = obj.__dict__[name]
        
        if not self.is_container(obj) and elements != []:
        
            raise OperationFailed
  
  else:
  
      raise OperationFailed

return name, obj

If the URL does not refer to a valid object, the OperationFailed exception is raised; otherwise, the name used to locate the object is returned with the object itself. Note that if the URL contains only a single directory separator (i.e. "python:/") the initial values of name and obj are returned.

is_container

The is_container method simply determines whether a given Python object should be represented as a directory:

def is_container(self, obj):

    return hasattr(obj, "__dict__") and obj.__dict__.keys() != []

If the object has a dictionary of attributes, and this contains at least one entry, the object is considered to be a container.

encode_name

The encode_name method provides quick and simple way to escape strings of characters that will be passed back to the application for use in URLs:

def encode_name(self, name):

    new = u""
    name = unicode(name)
    
    for c in name:
    
        if c == u"/":
            new = new + u"%2f"
        elif c == u"%":
            new = new + u"%25"
        else:
            new = new + c
    
    return new

Generally, Python objects do not contain characters that need to be escaped.

Other Standard Methods

Although not entirely necessary, we implement disconnectSlave and dispatchLoop methods:

def disconnectSlave(self):

    return

def dispatchLoop(self):

    KIO.SlaveBase.dispatchLoop(self)

When implementing the dispatchLoop method, we must call the corresponding method of the base class.

The Loader Library

Each Python kioslave needs to be launched by a library written in C++. Fortunately, the process of building a suitable library for each kioslave is automatic. The developer only needs to provide a details.py file containing information about the Python kioslave class and the module which contains it.

Here's an excerpt from the details.py file for this kioslave:

def find_details():

    details = {
        "name":             "Python",
        "pretty name":      "Python",
        "comment":          "A kioslave for displaying information about the Python interpreter",
        "factory":          "SlaveClass",
        "icon":             ""
    }
    ...

The values declared in the details.py file follow various conventions described in existing files. When the setup.py script provided with the is run using the build command, these values are written to a header file and compiled into a loader library:

python setup.py build

A desktop file is also created; this provides information to KDE about the kioslave.

The library, desktop file and Python module are installed into a suitable location when the install command is used:

python setup.py install

KDE uses the desktop file to find and load the library. In turn, this ensures that a Python interpreter is running, imports the Python module, instantiates the kioslave class, and starts the dispatch loop.

Notes

Although the Python kioslave provides a consistent view of the objects within the kio_python module, it is important to note that more than one kioslave may be created. Each of these will display the contents of the modules used in different instances of the kioslave. Generally, the contents of these will all be the same. However, it is important to examine the consistency of data returned from a number kioslaves that all rely on the same source of information, particularly if each of them is able to modify that information.