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 the dip.io module) the standard New, Open, Save, Save As and Close actions.
  • QuitTool is a tool that manages the user’s ability to quit the application. It implements the standard Quit 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 standard What'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.