Architecture¶
In this section we attempt to describe the overall architecture of dip and the philosophy behind it. This will include a summary of the purpose of each of the main modules that make up dip. Later sections will take the form of tutorials in using each of these modules.
Before we outline the philosophy of dip we first consider two issues: the realities of software development; and the myth of toolkit independence.
The Realities of Software Development¶
Software development is a messy business. Perhaps the most desired quality of a developer is hindsight - “I wish I’d done it differently at the start”.
Most successful applications start out small with limited scope. It’s only later, after more development turns it into something more widely used and with much more functionality than originally anticipated, that the hindsight kicks in and the need for a framework becomes apparent to help sort out the somewhat ad-hoc implementation.
Most frameworks require the developer to use the framework from the very start of development because they impose a particular way of doing things that don’t integrate particularly well with more ad-hoc approaches. Applying such a framework to an existing application often means rewriting that application, something that is both expensive and risky.
Far better is a framework that can be adopted a bit at a time providing a means by which an application can be re-designed and re-implemented incrementally almost as a side effect of normal maintenance and development. dip is such a framework.
The Myth of Toolkit Independence¶
Many application frameworks support the concept of GUI toolkit independence. In other words they allow you to develop your application so that it can be run without changes using any of the supported toolkits. This is typically implemented as a toolkit independent API and the framework defines wrappers that translate that API to the toolkit specific API.
The toolkit independent API is typically more abstract and requires fewer lines of code to create simple GUIs. However it is also less flexible and less appropriate for the more complex, application specific parts of the GUI. Because of this the framework will usually provide access to the underlying toolkit specific objects.
Most organizations and individual developers will choose a particular GUI toolkit and use it for all their applications. A typical application will use the toolkit independent, more abstract, API for the simple parts of the GUI and the toolkit dependent API, for their chosen toolkit, for the more complex parts of the GUI. In other words, for the vast majority of applications, the toolkit independence offered by frameworks is irrelevant. What does matter is how good the abstract API is in creating simple GUIs in few lines of code, and how easy it is to mix GUIs created by the abstract API and GUIs created with the toolkit specific API.
Of course toolkit independence is important in code that is likely to be shared between organizations and individuals that have made different toolkit choices. The most obvious example of such code is dip itself. Internally dip always uses the toolkit independent API.
dip ensures that it is easy to mix GUIs it creates with GUIs created by the
toolkit specific API. Like other frameworks dip implements the independent API
as a wrapper around the toolkit specific API. A toolkit independent object can
be converted to a toolkit specific object by calling a simple function
(specifically unadapted()
). Equally, when a toolkit specific
object is passed to dip to use it makes no difference whether that object was
originally created by dip or by another part of the application that knows
nothing about dip.
Philosophy¶
Like any framework the main purpose of dip is to provide functionality common to all applications and so allowing developers to concentrate their efforts on the functionality that is specific to their particular application.
Central to dip’s philosophy is that it acknowledges how software is actually developed, rather than how the text books would like it to be. It recognises that there is never a right way to do something - just that some approaches are more appropriate than others in any particular situation, and that situations change over time.
In particular:
- you choose to use as much or as little as you want
- it will keep out of the way when it’s not wanted
- you choose the degree of abstraction you want to use
- if you don’t like any of it, replace it.
The single most important feature of dip is that it is easy to adopt it a bit at a time. It can be used in part of an application while allowing other parts to remain unchanged. An application that has grown “organically” over time can be easily migrated to one that is well designed, component based, without needing to take time out to do a major rewrite.
Module Overview¶
dip.model
¶
The dip.model
module implements a declarative type system for Python.
It is used throughout the rest of dip.
The core of the module is the Model
class. Sub-classes
define attributes as class attributes using attribute type classes such
as Bool
and Str
. When such a sub-class
is instantiated corresponding instance attributes are automatically created.
A model will enforce an attribute’s type when rebinding it to a different value. It will also apply any additional constraints imposed by a particular type. All types can provide an default value (that may be overridden) for an attribute.
The initial values of attributes can be delayed until they are actually needed. This can be used to implement the lazy loading of Python modules.
An attribute can be observed and arbitrary Python code invoked when its value changes.
The MappingProxy
class allows the use of any Python mapping
object, with some limitations, to be used as a model.
The module also provides support for interfaces and adapters.
The section Getting Started with dip.model contains a full tutorial for this module.
dip.ui
¶
The dip.ui
module provides a set of classes that allow a user
interface, or just a part of a user interface, to be defined declaratively.
dip follows the conventional model-view-controller pattern. The user interacts with the view and the controller makes appropriate updates to the model. More specifically, the view contains a number of editors each of which is bound to an attribute of the model. A view may contain sub-views.
For each type of view dip provides a view factory. When an instance of the factory is called it is passed the model and an optional controller. A default controller will be created automatically if required. The factory creates a toolkit specific implementation of the view that is adapted to the factory specific interface.
For example, assuming the default PyQt5 toolkit, the Dialog
factory will create a QDialog
instance that has been
adapted to the IDialog
interface. An application can then
choose to use the dialog using the API defined by the IDialog
interface or the QDialog
API on the unadapted dialog.
Other commonly used view factories include Form
and
LineEditor
.
A user interface that is defined declaratively is often done so as a class attribute:
from dip.ui import Dialog, Form, LineEditor
class MyUIFactory:
# Declaratively define the dialog.
ui = Dialog(
Form(
LineEditor('name'),
LineEditor('address')
)
)
def __call__(self, model):
""" Create and return an instance of the dialog. """
return self.ui(model)
The 'name'
and 'address'
are the names of attributes in the model that
the QLineEdit
instances that are created are bound
to.
Actually the above example is unnecessarily long as dip has sensible defaults and will inspect the model to obtain any missing information. The definition of the dialog in this case only needs to be:
ui = Dialog('name', 'address')
Each view has a toolkit independent API. These are defined as
interfaces contained in the dip.ui
module. Access
to the toolkit independent API is gained by adapting the toolkit specific
object to the interface. Extending the previous example:
from dip.model import unadapted
from dip.ui import IDialog
ui_factory = MyUiFactory()
dialog_1 = ui_factory(model)
dialog_1.execute()
dialog_2 = ui_factory(model)
unadapted(dialog_2).exec()
In the case of dialog_1
we are calling the
execute()
method of the IDialog
interface to enter the dialog’s event loop. This will always work no matter
what toolkit is being used.
In the case of dialog_2
we are using the unadapted()
function to get a reference to the actual QDialog
that was created by the view factory. We are then calling the toolkit specific
exec()
method to enter the dialog’s event loop.
This will only work if we know the toolkit uses
QDialog
(or a sub-class) to implement dialogs.
The section Getting Started with dip.ui contains a full tutorial for this module.
dip.pui
¶
The dip.pui
module provides a set of classes that allow a user
interface, or just a part of a user interface, to be defined procedurally.
For every view factory implemented by the dip.ui
module this
module implements a callable of the same name that will create the toolkit
specific object. For example calling dip.pui.Dialog()
will create a
toolkit specific object that is adapted to the IDialog
interface. Calling an instance of the dip.ui.Dialog
class will
create a similar object.
While it is recommended that user interfaces are created declaratively using
the dip.ui
module it is sometimes necessary to create new elements, in a
toolkit independent way, and add them to an existing user interface.
dip.toolkits
¶
The dip.toolkits
module implements the toolkits that
are included with dip. The default toolkit uses PyQt5
.
A toolkit is an implementation of the IToolkit
interface. The name of the default toolkit can be set using the
DIP_TOOLKIT
environment variable. If the name contains a .
then
it is assumed to be the name of a module that contains a callable called
Toolkit
that will create the toolkit. Otherwise it is assumed that the
name refers to a toolkit included with dip.
Normally an application doesn’t need to worry about toolkits, however it may
want to change the standard behaviour of a toolkit. For example it may want to
use a custom version of a particular view, say a file selector dialog. All
that needs to be done is to sub-class an existing toolkit, reimplement the
appropriate methods (get_open_file()
in this
case).
dip.shell
¶
The dip.shell
module contains classes that implement
shells. A shell is an abstraction of an application and its
user interface. The application’s functionality is implemented as a set of
tools. A tool will define and manage a number of
actions and views. A shell will visualise any
tool actions and provide a number of areas in which views can be
placed.
The use of the shell abstraction by an application is entirely optional.
dip provides a shell implementation that uses MainWindow
. The
actions are visualised as menu items and tool buttons. The areas are
visualised as docks and tabbed widgets. The default PyQt5 toolkit uses a
QMainWindow
to implement a main window.
dip includes the following tools:
DirtyTool
is a tool that supports the feature common to many toolkits where some indication is given (usually in the main window’s title bar) that the application has unsaved data.FormTool
is the base class of tools that create form based views for editing models.ModelManagerTool
is a tool that manages the lifecycle of models on behalf of an application. It implements (with the help of thedip.io
module) the standardNew
,Open
,Save
,Save As
andClose
actions.QuitTool
is a tool that manages the user’s ability to quit the application. It implements the standardQuit
action and will allow other tools to veto an attempt to quit if, for example, there is unsaved data.WhatsThisTool
is a tool that implements the standardWhat's This?
action.
The section Getting Started with dip.shell contains a full tutorial for this module.
dip.settings
¶
The dip.settings
module provides a set of classes that provide support
for the reading and writing of user-specific settings from and to persistent
storage.
The singleton SettingsManager
is used to load all the
user’s settings for the application. The
restore()
method is called to read and
restore the settings for the models that are passed to it. The
save()
method is called to save and write
the settings for the models that are passed to it.
An individual setting has a string identifier. A model that wants to read and
write any number of settings must implement the
ISettings
interface (or provide an appropriate adapter).
A toolkit will usually provide adapters between toolkit-specific
widgets and ISettings
so that the geometry and
configuration of a user interface can be remembered by passing the list of
views to restore()
before the
application’s event loop is entered and then to
save()
after the event loop exits.
The section Getting Started with dip.ui contains a section describing the use of this module in more detail.
dip.publish
¶
The dip.publish
module provides a set of classes that implement a simple
publish/subscribe framework. While the dip.model
module provides a
low-level notification mechanism for changes to model attributes, the
dip.publish
module allows a tool to register an interest in
specific types of event rather than a specific model instance.
Published events are managed by a publication manager which is an
implementation of the IPublicationManager
interface. A
shell contains such an implementation and tools added to the shell are
automatically registered with the publication manager if they implement either
of the IPublisher
or ISubscriber
interfaces.
A published event is simply a string (typically using the dotted notation) and
a model that the event is related to. A subscriber specifies the type of model
it is interested in and observes the
subscription
attribute to receive
notifications of events related to that type of model.
dip defines some well known events. For example the
ModelManagerTool
tool publishes the
dip.events.opened
and dip.events.closed
events whenever it opens and
closes a managed model.
dip.io
¶
The dip.io
module provides a set of classes that enables models to be
completely decoupled from where they are stored and the data formats used to
store them. This allows support for new types of storage to be added without
requiring any application changes. It also allows models to be imported from
and exported to new data formats without requiring changes to existing code.
The section Getting Started with dip.io contains a full tutorial for this module.
dip.plugins
¶
The dip.plugins
module provides facilities for structuring an
application as a set of plugins. Plugins are completely
decoupled from each other which makes it easier to add new functionality in the
form of new plugins without requiring changes to existing code.
The section Getting Started with dip.plugins contains a full tutorial for this module.
dip.automate
¶
The dip.automate
module provides facilities for the automation of
applications. Automated applications do not need to be dip applications.
Applications do not need to be modified in order to be automated. (Although
there are steps that can be taken when writing applications that make
automation easier.)
The module defines an automation API that is used in automation scripts. A
toolkit will implement that API, typically by providing appropriate adapters,
using toolkit specific features. In the case of the default PyQt5 toolkit the
QtTest
module is used.
dip also includes the dip-automate
tool which runs an application under the
control of an automation script.
The section Getting Started with dip.automate contains a full tutorial for this module.
dip.developer
¶
The dip.developer
module implements a number of tools that can be
included in an application to help in the debugging of that application.