.. _architecture: 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 :func:`~dip.model.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 --------------- :mod:`dip.model` ................ The :mod:`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 :class:`~dip.model.Model` class. Sub-classes define attributes as class attributes using :term:`attribute type` classes such as :class:`~dip.model.Bool` and :class:`~dip.model.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 :class:`~dip.model.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 :term:`interfaces` and :term:`adapters`. The section :ref:`model-tutorial` contains a full tutorial for this module. :mod:`dip.ui` ............. The :mod:`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 :term:`model`-:term:`view`-:term:`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 :term:`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 :term:`toolkit` specific implementation of the view that is adapted to the factory specific :term:`interface`. For example, assuming the default PyQt5 toolkit, the :class:`~dip.ui.Dialog` factory will create a :class:`~PyQt5.QtWidgets.QDialog` instance that has been adapted to the :class:`~dip.ui.IDialog` interface. An application can then choose to use the dialog using the API defined by the :class:`~dip.ui.IDialog` interface or the :class:`~PyQt5.QtWidgets.QDialog` API on the unadapted dialog. Other commonly used view factories include :class:`~dip.ui.Form` and :class:`~dip.ui.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 :class:`~PyQt5.QtWidgets.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 :term:`interfaces` contained in the :mod:`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 :meth:`~dip.ui.IDialog.execute` method of the :class:`~dip.ui.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 :func:`~dip.model.unadapted` function to get a reference to the actual :class:`~PyQt5.QtWidgets.QDialog` that was created by the view factory. We are then calling the toolkit specific :meth:`~PyQt5.QtWidgets.QDialog.exec` method to enter the dialog's event loop. This will only work if we know the toolkit uses :class:`~PyQt5.QtWidgets.QDialog` (or a sub-class) to implement dialogs. The section :ref:`ui-tutorial` contains a full tutorial for this module. :mod:`dip.pui` .............. The :mod:`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 :term:`view` factory implemented by the :mod:`dip.ui` module this module implements a callable of the same name that will create the toolkit specific object. For example calling :func:`dip.pui.Dialog` will create a toolkit specific object that is adapted to the :class:`~dip.ui.IDialog` interface. Calling an *instance* of the :class:`dip.ui.Dialog` class will create a similar object. While it is recommended that user interfaces are created declaratively using the :mod:`dip.ui` module it is sometimes necessary to create new elements, in a toolkit independent way, and add them to an existing user interface. :mod:`dip.toolkits` ................... The :mod:`dip.toolkits` module implements the :term:`toolkits` that are included with dip. The default toolkit uses :mod:`PyQt5`. A toolkit is an implementation of the :class:`~dip.toolkits.IToolkit` interface. The name of the default toolkit can be set using the :envvar:`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 (:meth:`~dip.toolkits.IToolkit.get_open_file` in this case). :mod:`dip.shell` ................ The :mod:`dip.shell` module contains classes that implement :term:`shells`. A shell is an abstraction of an application and its user interface. The application's functionality is implemented as a set of :term:`tools`. A tool will define and manage a number of :term:`actions` and :term:`views`. A shell will visualise any tool actions and provide a number of :term:`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 :class:`~dip.ui.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 :class:`~PyQt5.QtWidgets.QMainWindow` to implement a main window. dip includes the following tools: - :class:`~dip.shell.tools.dirty.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. - :class:`~dip.shell.tools.form.FormTool` is the base class of tools that create form based views for editing models. - :class:`~dip.shell.tools.model_manager.ModelManagerTool` is a tool that manages the lifecycle of models on behalf of an application. It implements (with the help of the :mod:`dip.io` module) the standard ``New``, ``Open``, ``Save``, ``Save As`` and ``Close`` actions. - :class:`~dip.shell.tools.quit.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. - :class:`~dip.shell.tools.whats_this.WhatsThisTool` is a tool that implements the standard ``What's This?`` action. The section :ref:`shell-tutorial` contains a full tutorial for this module. :mod:`dip.settings` ................... The :mod:`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 :class:`~dip.settings.SettingsManager` is used to load all the user's settings for the application. The :meth:`~dip.settings.ISettingsManager.restore` method is called to read and restore the settings for the models that are passed to it. The :meth:`~dip.settings.ISettingsManager.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 :class:`~dip.settings.ISettings` interface (or provide an appropriate adapter). A :term:`toolkit` will usually provide adapters between toolkit-specific widgets and :class:`~dip.settings.ISettings` so that the geometry and configuration of a user interface can be remembered by passing the list of views to :meth:`~dip.settings.ISettingsManager.restore` before the application's event loop is entered and then to :meth:`~dip.settings.ISettingsManager.save` after the event loop exits. The section :ref:`ui-tutorial` contains a section describing the use of this module in more detail. :mod:`dip.publish` .................. The :mod:`dip.publish` module provides a set of classes that implement a simple publish/subscribe framework. While the :mod:`dip.model` module provides a low-level notification mechanism for changes to model attributes, the :mod:`dip.publish` module allows a :term:`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 :class:`~dip.publish.IPublicationManager` interface. A :term:`shell` contains such an implementation and tools added to the shell are automatically registered with the publication manager if they implement either of the :class:`~dip.publish.IPublisher` or :class:`~dip.publish.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 :attr:`~dip.publish.ISubscriber.subscription` attribute to receive notifications of events related to that type of model. dip defines some well known events. For example the :class:`~dip.shell.tools.model_manager.ModelManagerTool` tool publishes the ``dip.events.opened`` and ``dip.events.closed`` events whenever it opens and closes a :term:`managed model`. :mod:`dip.io` ............. The :mod:`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 :ref:`io-tutorial` contains a full tutorial for this module. :mod:`dip.plugins` .................. The :mod:`dip.plugins` module provides facilities for structuring an application as a set of :term:`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 :ref:`plugins-tutorial` contains a full tutorial for this module. :mod:`dip.automate` ................... The :mod:`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 :mod:`~PyQt5.QtTest` module is used. dip also includes the ``dip-automate`` tool which runs an application under the control of an automation script. The section :ref:`automate-tutorial` contains a full tutorial for this module. :mod:`dip.developer` .................... The :mod:`dip.developer` module implements a number of tools that can be included in an application to help in the debugging of that application.