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.
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.
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.