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