A Complete Example

We have now given an overview of all the different dip modules. In this section we will present a complete example application - a simple editor for Python modules based on the QScintilla editor widget.

Below is a screenshot of the editor.

_images/editor.png

In subsequent sections we will present the complete source code a file at a time. We will not walk though each line of the code, but we will point out fragments of code of particular interest.

editor.py - Getting Started

The editor.py module is the editor’s main module.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


import sys

from PyQt4.QtGui import QApplication


# Every PyQt GUI application needs a QApplication.
app = QApplication(sys.argv, applicationName="Python Editor")


# Create a plugin manager.
from dip.plugins import PluginManager

plugin_manager = PluginManager()


# Add the infrastructure plugins we need.
from dip.io.plugins import FilesystemPlugin, IOManagerPlugin
from dip.shell.plugins import QMainWindowShellPlugin, ShellManagerPlugin
from dip.ui.plugins import QtToolkitPlugin

plugin_manager.contribute('dip.plugins.plugins',
        QtToolkitPlugin(plugin_manager=plugin_manager))

plugin_manager.contribute('dip.plugins.plugins',
        IOManagerPlugin(plugin_manager=plugin_manager))

plugin_manager.contribute('dip.plugins.plugins',
        FilesystemPlugin(plugin_manager=plugin_manager))

plugin_manager.contribute('dip.plugins.plugins',
        ShellManagerPlugin(plugin_manager=plugin_manager))

plugin_manager.contribute('dip.plugins.plugins',
        QMainWindowShellPlugin(plugin_manager=plugin_manager))


# Add the plugin that implements this application.
from py_editor.plugins import PyEditorPlugin

plugin_manager.contribute('dip.plugins.plugins',
        PyEditorPlugin(plugin_manager=plugin_manager))


# Enable the plugins.
plugin_manager.enable_plugins()


# Get the shell.
from dip.shell import IShell

shell = plugin_manager.service(IShell)


# Parse the command line and create a separate editor for each argument.  If
# there were no arguments then create an empty editor.
if len(sys.argv) == 1:
    tool = shell.shell_manager.new(shell)
    if tool is not None:
        shell.add_tool(tool)
else:
    for py_module in sys.argv[1:]:
        tool = shell.shell_manager.open(shell, py_module)
        if tool is not None:
            shell.add_tool(tool)


# Display the shell.
shell.widget.show()

# Enter the event loop.
sys.exit(app.exec_())

We have decided to implement the editor using plugins. The first part of the module, where we create and contribute a number of infrastructure plugins, will be common to most plugin-based applications.

If other toolkits were available we could decide to check to see if they could be used instead of the default Qt toolkit. For example, if PyKDE was able to be imported we could contribute a KDE toolkit instead and give the user a native KDE application.

The following lines of code then create and contribute our own editor plugin.

from py_editor.plugins import PyEditorPlugin

plugin_manager.contribute('dip.plugins.plugins',
        PyEditorPlugin(plugin_manager=plugin_manager))

After enabling the plugins and creating the shell, the following lines of code parse any command line arguments.

if len(sys.argv) == 1:
    tool = shell.shell_manager.new(shell)
    if tool is not None:
        shell.add_tool(tool)
else:
    for py_module in sys.argv[1:]:
        tool = shell.shell_manager.open(shell, py_module)
        if tool is not None:
            shell.add_tool(tool)

Each command line argument is assumed to be the location of a Python module to edit. If there were no command line arguments then the shell manager is asked to create a new, empty editor.

How the locations on the command line are interpreted depends on the storage plugins that have been enabled. We have only enabled the FilesystemPlugin and so a location will be interpreted as the name of a file. If the location is ambiguous, i.e. it is valid for more than one type of storage, then the user will be asked to specify the particular type of storage they want to use.

The plugins Package

The py_editor.plugins.__init__.py module defines the public API of the plugins package.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from .py_editor_plugin import PyEditorPlugin

The editor component consists of a single plugin. We have done this to keep things simple, but there is a strong argument that the data model (our Python module representation) and the editor should be put into separate plugins to ensure that they are properly decoupled.

The Plugin Definition

The py_editor.plugins.py_editor_plugin.py module is the editor’s plugin definition.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.plugins import ContributionTo, Plugin


class PyEditorPlugin(Plugin):
    """ The PyEditorPlugin class is the plugin definition for the simple Python
    editor.
    """

    # The description of the plugin as it will appear to the user.
    description = "Python editor"

    # The identifier of the plugin.
    id = 'dip.examples.py_editor'

    # The codecs contributed by this plugin.
    codecs = ContributionTo('dip.io.codecs')

    # The shell objects contributed by this plugin.
    objects = ContributionTo('dip.shell.objects')

    # The tools contributed by this plugin.
    tools = ContributionTo('dip.shell.tools')

    @codecs.default
    def codecs(self):
        """ Create the codecs factories. """

        from py_editor import PyModuleCodecsFactory

        return [PyModuleCodecsFactory()]

    @objects.default
    def objects(self):
        """ Create the shell object factories. """

        from py_editor import PyModuleFactory

        return [PyModuleFactory()]

    @tools.default
    def tools(self):
        """ Create the tool factories. """

        from py_editor import PyEditorFactory

        return [PyEditorFactory()]

A typical small application will implement a data model, the code to read and write a model from and to storage, and a tool that allows the user to perform some actions on a model. Our editor example does exactly that and the plugin definition creates instances of the relevant objects and contributes them to the 'dip.shell.objects', 'dip.io.codecs' and 'sip.shell.tools' extension point respectively.

The py_editor Package

The py_editor.__init__.py module defines the public API of the editor component.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from .i_py_module import IPyModule

from .py_editor import PyEditor
from .py_editor_factory import PyEditorFactory
from .py_module_codecs import PyModuleDecoder, PyModuleEncoder
from .py_module_codecs_factory import PyModuleCodecsFactory
from .py_module_factory import PyModuleFactory

# Make sure adapters get registered.
from . import ipymodule_ishellobject_adapter

These classes, along with those provided by the plugins package, allow other components to re-use our editor and data model and to extend them if required.

The IPyModule Interface

The py_editor.i_py_module.py module defines interface that an implementation of a Python module should implement.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.model import Interface, Str


class IPyModule(Interface):
    """ The IPyModule interface defines the API of the object that models a
    Python module.
    """

    # The text of the module.
    text = Str()

In our simple example a Python module only has a single attribute which is the text of the module. However it is easy to imagine how the interface might be developed in the future, for example by the addition of test cases.

Note that we have not called the interface IPyFile because that would (incorrectly) imply that the module was being stored in a file - but what an object is has no direct bearing on how it might be stored.

Letting the Shell Manage a Python Module

The py_editor.ipymodule_ishellobject_adapter.py module implements an adapter between the IPyModule and IShellObject interfaces.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.model import adapt, Adapter
from dip.shell import IDisplay, IShellObject

from .i_py_module import IPyModule


@adapt(IPyModule, to=IShellObject)
class IPyModuleIShellObjectAdapter(Adapter):
    """ The IPyModuleIShellObjectAdapter adapts a Python module to the
    IShellObject interface.
    """

    @IShellObject.location.observer
    def location(self, change):
        """ Invoked when the location of the object changes. """

        location = change.new

        if location is None:
            name = ''
        else:
            # If the location implements IDisplay then use it.
            idisplay = IDisplay(location, exception=False)
            if idisplay is None:
                name = location.location
            else:
                name = idisplay.name
                self.short_name = idisplay.short_name

        self.name = name if name != '' else "Untitled"

We are going to let the shell manage the lifecycle of a Python module as it is being edited and so the Python module object must implement the IShellObject interface. We could do this by having our IPyModule sub-class IShellObject but we achieve better decoupling if we use adaptation.

The main purpose of the adapter is to make sure that a Python module has a name. Of course a Python module does not have an implicit name, and is only referred to by its location in storage. Therefore the adapter uses the actual storage location as the name and will automatically update it when the location changes.

If the storage location implements the IDisplay interface then it may have a short name as well as its normal full name. If so the adapter takes advantage of that. dip’s filesystem storage locations do support IDisplay and provide the full path of a file name as the name, and just the file name part as the short name.

Reading and Writing a Python Module

The py_editor.py_module_codecs.py module implements the decoder and encoder used when reading and writing a Python module from and to storage.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from PyQt4.QtCore import QByteArray

from dip.io import IDecoder, IEncoder
from dip.model import implements, Model


@implements(IDecoder)
class PyModuleDecoder(Model):
    """ The PyModuleDecoder class implements a decoder for a Python module.
    """

    def decode(self, obj, source, location):
        """ A Python module is decoded from a byte stream.

        :param obj:
            is the Python module to populate from the decoded byte stream.
        :param source:
            is an iterator that will return the byte stream to be decoded.
        :param location:
            is the storage location where the encoded object is being read
            from.  It is mainly used for error reporting.
        :return:
            the Python module.
        """

        data = QByteArray()

        for block in source:
            data.append(block)

        # Assume the module is UTF-8 encoded.  A more complete implementation
        # would support PEP 263 by peeking at the first couple of lines.
        obj.text = str(data, encoding='utf8')

        return obj


@implements(IEncoder)
class PyModuleEncoder(Model):
    """ The PyModuleEncoder class implements an encoder for a Python module.
    """

    def encode(self, obj, location):
        """ A Python module is encoded as a byte stream.

        :param obj:
            is the Python module to encode.
        :param location:
            is the storage location where the encoded object will be written
            to.  It is mainly used for error reporting.
        :return:
            the next section of the encoded byte stream.
        """

        # Assume the module is UTF-8 encoded.  A more complete implementation
        # would support PEP 263 by peeking at the first couple of lines.
        yield obj.text.encode('utf8')

This code is straightforward - note the comments about properly supporting PEP 263.

The Codecs Factory

The py_editor.py_module_codecs_factory.py module implements the factory for the Python module decoder and encoder.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.io import ICodecsFactory
from dip.model import implements, Model

from .i_py_module import IPyModule
from .py_module_codecs import PyModuleDecoder, PyModuleEncoder


@implements(ICodecsFactory)
class PyModuleCodecsFactory(Model):
    """ The PyModuleCodecsFactory class is the factory for the codecs used to
    serialise a Python module.
    """

    # The identifier of the format.
    id = 'dip.examples.py_module.format'

    # The name of the format.
    name = "Python module"

    # The filter used with :class:`PyQt4.QtGui.QFileDialog`.
    filter = "Python files (*.py *.pyw)"

    def decodes(self, obj):
        """ Determine if the object can be decoded.

        :param obj:
            is the object.
        :return:
            ``True`` if the object can be decoded.
        """

        return isinstance(obj, IPyModule)

    def decoder(self):
        """ A new instance of the object decoder is created and returned.

        :return:
            a new instance of the object decoder.
        """

        return PyModuleDecoder(factory=self)

    def encodes(self, obj):
        """ Determine if the object can be encoded.

        :param obj:
            is the object.
        :return:
            ``True`` if the object can be encoded.
        """

        return isinstance(obj, IPyModule)

    def encoder(self):
        """ A new instance of the object encoder is created and returned.

        :return:
            a new instance of the object encoder.
        """

        return PyModuleEncoder(factory=self)

Again this code is straightforward.

The PyModule Object

The py_editor.py_module.py module contains the PyModule class which is our implementation of the IPyModule interface.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.model import implements, Instance, Model, Str

from .i_py_module import IPyModule
from .py_editor import PyEditor


@implements(IPyModule)
class PyModule(Model):
    """ The PyModule class is the default implementation of a Python module.
    """

    # For efficiency reasons we are going to use the editor itself (if it has
    # been created) to hold the text of the module.
    editor = Instance(PyEditor)

    # This is the module's text that needs to be stored in the editor as soon
    # as it is created.
    _pending_text = Str()

    @IPyModule.text.getter
    def text(self):
        """ The getter of the text. """

        return self._pending_text if self.editor is None else self.editor.text()

    @text.setter
    def text(self, text):
        """ The setter of the text. """

        if self.editor is None:
            self._pending_text = text
        else:
            self.editor.setText(text)

    @editor.setter
    def editor(self, editor):
        """ The setter of the editor. """

        # Load any pending text.
        editor.setText(self._pending_text)
        self._pending_text = ''

We could provide a much simpler implementation, but we choose to work around a potential inefficiency. The QScintilla widget stores the text being edited internally, however we need to make the text continually available as the text attribute of our PyModule class. The simple solution would be to update the text attribute with a copy of the text whenever it changes. Because this might be inefficient, we instead implement a getter for the text attribute so that a copy is taken only if it is actually needed.

    editor = Instance(PyEditor)

The above is the attribute containing the editor which will actually contain the text. It will be set by the factory that creates the editor tool.

    _pending_text = Str()

The above is the attribute containing any text that is set before the editor has been created. It is used to initialise the editor when it is created.

    @IPyModule.text.getter
    def text(self):
        """ The getter of the text. """

        return self._pending_text if self.editor is None else self.editor.text()

The above method is the getter for the text attribute that gets the text from the editor if it has been created, and from the _pending_text attribute if it hasn’t.

    @text.setter
    def text(self, text):
        """ The setter of the text. """

        if self.editor is None:
            self._pending_text = text
        else:
            self.editor.setText(text)

The above method is the setter for the text attribute that copies the text to the editor if it has been created, and saves it in the _pending_text attribute for later if it hasn’t.

    @editor.setter
    def editor(self, editor):
        """ The setter of the editor. """

        # Load any pending text.
        editor.setText(self._pending_text)
        self._pending_text = ''

The above method is the setter for the editor attribute that loads any pending text into the editor when it is first created.

The PyModule Object Factory

The py_editor.py_module_factory.py module implements the factory for our PyModule class.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.model import implements, Model
from dip.shell import IShellObjectFactory


@implements(IShellObjectFactory)
class PyModuleFactory(Model):
    """ The PyModuleFactory class is the factory for a :term:`shell object`
    that implements a Python module.
    """

    # The name of the type of the object.
    name = "Python module"

    # The identifier of a project's native format.
    native_format = 'dip.examples.py_module.format'

    def __call__(self, shell):
        """ Create an instance of the shell object.

        :param shell:
            is the shell.

        :return:
            the instance of the shell object.
        """

        from .py_module import PyModule

        return PyModule()

This code is straightforward.

The Editor Widget

The py_editor.py_editor.py module contains the editor widget itself. An important thing to note about this widget is that it is completely independent of dip.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from PyQt4.Qsci import QsciLexerPython, QsciScintilla


class PyEditor(QsciScintilla):
    """ The PyEditor class is the implementation of the Python editor. """

    def __init__(self, parent=None):
        """ Initialise the editor. """

        super(PyEditor, self).__init__(parent)

        self.setUtf8(True)
        self.setLexer(QsciLexerPython(self))
        self.setFolding(QsciScintilla.PlainFoldStyle, 1)
        self.setIndentationGuides(True)
        self.setIndentationWidth(4)

This code is a simple QScintilla sub-class that is configured to support Python syntax highlighting, code folding and indentation guides.

The important thing to note about it is that it knows nothing about dip. It can be made an integral part of a dip application without needing to change it. Equally it can be re-used easily in a non-dip application.

The Editor Widget Factory

The py_editor.py_editor_factory.py module implements the factory for our editor widget that allows it to be used as a tool in a shell.

# Copyright (c) 2010 Riverbank Computing Limited.
#
# This file is part of dip.
#
# This file may be used under the terms of the GNU General Public License
# v2 or v3 as published by the Free Software Foundation which can be found in
# the files LICENSE-GPL2.txt and LICENSE-GPL3.txt included in this package.
# In addition, as a special exception, Riverbank gives you certain additional
# rights.  These rights are described in the Riverbank GPL Exception, which
# can be found in the file GPL-Exception.txt in this package.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.


from dip.model import implements, Model, observe
from dip.shell import IShellObject, IToolFactory

from .i_py_module import IPyModule
from .py_module import PyModule


@implements(IToolFactory)
class PyEditorFactory(Model):
    """ The PyEditorFactory class is the factory for a :term:`tool` that
    allows a Python module to be edited.
    """

    # The name of the tool.
    name = "Python Editor"

    def __call__(self, obj, toolkit):
        """ Create an instance of the tool.

        :param obj:
            is the obj on which the tool is to operate on.
        :param toolkit:
            is the :class:`~dip.ui.IToolkit` implementation that can be used to
            create widgets.
        :return:
            the instance of the tool.  This must be a
            :class:`~PyQt4.QtGui.QWidget` instance and, therefore, can be
            adapted to the :class:`~dip.tools.ITool` interface.
        """

        from .py_editor import PyEditor

        py_editor = PyEditor()

        if isinstance(obj, PyModule):
            obj.editor = py_editor
        else:
            # The object doesn't use the editor to keep the text so we have to
            # do it the more obvious (but slower) way.
            py_editor.setText(obj.text)

            observe('text', obj, lambda c: py_editor.setText(c.new))
            py_editor.textChanged.connect(
                    lambda: setattr(obj, 'text', py_editor.text()))

        # Connect up the editor and the object's dirty flag.
        so = IShellObject(obj)

        observe('dirty', so, lambda c: py_editor.setModified(c.new))
        py_editor.modificationChanged.connect(
                lambda modification: setattr(so, 'dirty', modification))

        return py_editor

    def handles(self, obj):
        """ Check if the tool can handle an object.

        :param obj:
            is the object.
        :return:
            ``True`` if the tool can handle the object.
        """

        return isinstance(obj, IPyModule)

This is where we add the necessary code that integrates our dip-unaware PyEditor class.

        if isinstance(obj, PyModule):
            obj.editor = py_editor
        else:
            # The object doesn't use the editor to keep the text so we have to
            # do it the more obvious (but slower) way.
            py_editor.setText(obj.text)

            observe('text', obj, lambda c: py_editor.setText(c.new))
            py_editor.textChanged.connect(
                    lambda: setattr(obj, 'text', py_editor.text()))

Once we have created our PyEditor instance the above code fragment determines if the object being edited is a PyModule (so that we can use the editor to store the text) or is another implementation of the IPyModule interface (so that we have to fall back to the inefficient way of storing the text).

        so = IShellObject(obj)

        observe('dirty', so, lambda c: py_editor.setModified(c.new))
        py_editor.modificationChanged.connect(
                lambda modification: setattr(so, 'dirty', modification))

Irrespective of how the text is stored we have to synchronise the modification state of the editor with the value of the dirty attribute of the IShellObject interface. This is done by the code fragment above.