Welcome | Get started | Dive into Lino | Contribute | Topics | Reference | More

Plugin inheritance

plugin inheritance

When you extend some existing Lino plugin by inheriting everything of it (its models, views, methods, fixtures and admin commands) and then overriding some of it.

plugin extension

The extension of an existing plugin using plugin inheritance.

Plugins aren't classes, they are packages, so plugin inheritance is not "real" inheritance but rather a series of guidelines and programming patterns.

Plugin inheritance is intensively used by Lino's plugin libraries.

plugin library

A collection of reusable plugins that are designed to work together. For example lino.modlib, lino_xl.lib, lino_noi.lib, lino_voga.lib, lino_welfare.modlib.

A simple example

A simple example of plugin inheritance is the lino_book.projects.min2 project: it defines a lino_book.projects.min2.modlib.contacts plugin, which inherts from lino_xl.lib.contacts by adding a series of mixins to some of its models. Look at the code and at the resulting application!

Overriding models

As a more complex example let's look at Lino Voga. It uses Lino's standard calendar module lino_xl.lib.cal, but extends the Room model defined there:

  • it adds two fields tariff and calendar

  • it adds another base class (the ContactRelated mixin)

  • it overrides the save() method to add some specific behaviour

Here is the relevant application code which defines the Voga version of cal.Room:

from lino_xl.lib.cal.models import Room
from lino_xl.lib.contacts.models import ContactRelated

class Room(Room, ContactRelated):

    tariff = dd.ForeignKey('products.Product', ...)
    calendar = dd.ForeignKey('cal.Calendar', ...)

def save(self, *args, **kwargs):
    super(Room, self). save(*args, **kwargs)

    # add specific behaviour

For this to work, the library version of cal.Room (i.e. lino_xl.lib.cal.Room) must have abstract=True.

But only in this special case. The general case is that when an application installs lino_xl.lib.cal , it gets (among others) a new model cal.Room. We wouldn't want to force every application which uses lino_xl.lib.cal to override the Room model just to make it concrete.

There is no way in Django to make a model abstract "afterwards". When it is declared as abstract, then you must override it in order to get a concrete model. When it is not abstract, then you cannot override it by a model of same name (Django complains if you try).

In other words: The abstractness of certain models in a plugin depends on whether the plugin is going to be extended.

So how can the library version know whether the Room model should be abstract or not?

To solve this problem, Lino offers the is_abstract_model method. Usage example:

class Room(dd.BabelNamed):
    class Meta:
        abstract = dd.is_abstract_model(__name__, 'Room')
        verbose_name = _("Room")
        verbose_name_plural = _("Rooms")

The trick here is that the lino_voga/lib/cal/__init__.py file now contains this information in the extends_models attribute:

from lino_xl.lib.cal import Plugin

class Plugin(Plugin):

    extends_models = ['Room']

We use a central place where models modules can ask whether it wants a given model to be abstract or not.

The implementation of is_abstract_model has evolved in time. The first implementation used a simple set of strings in a class attribute of lino.core.site.Site. That might have been a standard Django setting. But as things got more and more complex, it became difficult to define this manually. And it was redundant because every plugin does know which library models it is going to override. But how to load that information from a plugin before actually importing it? We then discovered that Django doesn't use the __init__.py files of installed plugins. And of course we were lucky to have a lino.core.site.Site class that is being instantiated before settings have finished to load...

Plugin namespaces

Some packages in a plugin library exist only because the library wants to provide different variants of a same plugin. We want them to be interchangeable, so they must have the same Django app_name. That's why we introduce and additional module level in order to differentiate them.

Examples of plugin namespaces are lino_xl.lib.statbel, lino_xl.lib.online and lino_voga.lib.roger.

Application developers can easily switch from the default version of the countries plugin to the "statbel" version of the same plugin. Since we don't add an additional plugin but replace the default version, we can use the get_plugin_modifiers method:

def get_plugin_modifiers(self, **kw):
    kw = super(Site, self).get_plugin_modifiers(**kw)
    kw.update(courses='lino_voga.lib.roger.courses')
    return kw

Overriding other things

Overriding other Python objects (ChoiceList, Action, Plugin) is straightforward.

But the fixtures, config and management subdirs need special attention when doing plugin inheritance.

The config directory

The config subdirectories are handled automatically as expected: Lino scans first the config subdirectory of the child, then those of the parents.

Inheriting fixtures and django-admin commands

When you extend a plugin that has a fixtures package, then you must decide whether you want to inherit these fixtures.

There are good chances that you actually just want to inherit them without changing anything. In that case you must define a wrapper fixture for each fixture you want to inherit, which imports at least objects from its "parent" fixture.

For example the fixtures package of lino_voga.lib.cal contains a suite of one-line modules, one for each fixture defined by its parent, the lino_xl.lib.cal plugin. Each of these wrapper fixtures has just one import statement like this:

from lino_xl.lib.cal.fixtures.demo import objects

There is currently no easier way to inherit the default behaviour. Keep in mind that your fixtures may do something else, or you may decide to not inherit some fixture from your parent.

There is a possible pitfall: when you create a new fixture in a plugin, then the users of your plugin will not automatically get notified that you added a new fixture and that they must create a wrapper if they want it as well.

management

A similar approach is necessary for django-admin commands. Django discovers them by checking whether the plugin has a subpackage management and then calling os.listdir() on that module's "commands" subdirectory. (See Django's core/management/__init__.py file.) So when you extent a plugin which has admin commands, you must create a pseudo command