Ancestration – Family Inheritance for Python

This project implements the so-called family inheritance for Python 2 and 3. It is based on the doctoral thesis of Patrick Lay “Entwurf eines Objektmodells für semistrukturierte Daten im Kontext von XML Content Management Systemen” (Rheinische Friedrich-Wilhelms Universität Bonn, 2006) and is developed as part of the diploma thesis of Michael Pohl “Architektur und Implementierung des Objektmodells für ein Web Application Framework” (Rheinische Friedrich-Wilhelms Universität Bonn, 2013-2014).

Family Inheritance

Family inheritance is a concept in which classes and other attributes are contained in a structure named family. A family (the child family) can extend another family (the super family), which works similarily to class inheritance:

  • All non-class attributes of the super family are made available to the child family by copying. All family functions, specially decorated module-level function, are redefined to use the globals of the child family.

  • For each family class (i.e. a class included in family inheritance) of the super family an empty class with the same name is defined, inheriting from the original class in addition to the other bases.

    All family classes use a breadth-first-search attribute (and method) resolution order, instead of the depth-first-search Python normally uses. When a family class is redefined (either by defining it again or by redefining an inherited class), it’s child family classes are redefined as well to use the new class as a base class in exchange for the original one, this is propagated through the family inheritance tree.

This library allows to use Python modules as families and to make Python new-style classes family classes, to include them in the family inheritance.

Defining a Family

A module can be made a family by calling family() in it, preferably as the first statement (after the necessary import). This function takes a module as the optional argument extends, which specifies the super family of the calling child family, and returns the super family module. On calling family() all attributes of the super family are copied to the new family module, if they are not yet defined there. All inherited family classes are newly defined to inherit from the super family base and their in-family base.

You can also retrieve attributes from family(): the names of the attribute values returned specify the family to extend, the last one must be called. Thus instead of calling family('foo.bar') to extend the family foo.bar you can write family.foo.bar().

Classes and non-lambda functions can be integrated into the family inheritance by calling adopt() with them as arguments, thus making the family classes and family functions. The objects can also be specified as strings, then they are retrieved as attributes from the module adopt() is called in.

The following pattern has proven useful for the definition of larger families:

  • The family is a Python package and thus also a module, i.e. a directory containing at least the file __init__.py, which contains the module code of the package.
  • Related functionality is encapsulated in modules below the family package, with the module names beginning with a underscore.
  • adopt() is used in the family package’s __init__.py after it has been made a family by calling family().

This way it is apparent that the modules are not to be used individually.

Defining a Family Class

New-style classes of a family can be made family classes by setting their metaclass to family_class or use the latter as a class decorator. In addition to inheriting from the extented classes (as specified on definition of a class), a family class also inherits from the equally named class of the super family. To force inheriting an attribute from the the super family base class, assign an iterable of names as the attribute FAMILY_INHERIT of the family class.

Important notes: A family class uses a breadth-first-search for determining the order of its base classes, as opposed to the depth-first-search Python normally uses, resulting in a different attribute/method resolution order. Please also note that making a class a family class makes it have a special metaclass, even if the class was made a family class by using a decorator. This metaclass is a subclass of abc.ABCMeta, making it possible to use the abc.abstractmethod() decorator and to use abstract classes as mixin-classes.

A family class may extend only one family class and an arbitrary number of normal Python classes. Inherited family classes can be overridden by respecifying them (i.e. defining a class with the same name), then the inheritance tree is modified so that other inherited or newly defined family classes, extending this class, use the overridden version.

The bases of family classes are redefined, so that:

  • The first base is the in-family base, if the class has one.
  • If there is a super family, the next base (either second or first) is the super family base class, if it has one.
  • After that come all non-family base classes.
  • If there is neither an in-family base, nor a super family base and no other bases, the only base is object.

Thus on redefinition of a family class you do not have to specify the in-family base class again, it is automatically made the first base. Similarily you may specify the in-family base or super family base anywhere in the list of base classes, they will be moved to the front.

On family classes the attributes in_family_base and super_family_base are defined, containing the base class of the class’ family or the super family respectively. In addition there is a function reload_family defined on family modules, which must be used instead of the built-in reload function on families.

To allow for using the current family module to look up attributes, instead of the namespace, use class_module as a class descriptor. By using the descriptor attribute’s value, code defined earlier can use overridden classes or other module attributes. class_module can also be called with a class or instance as its argument to get its module.

Defining a Family Function

A non-lambda module-level function can be registered by name as a family function, by decorating it with family_function(). Family functions are accessible in child families under the same name, but the function’s globals dictionary is redefined to be the child family module’s dictionary. This way the function uses overridden classes or other redefined objects in the child family.

Example

Suppose a family family_a is defined:

from ancestration import family, family_class, family_method, adopt, class_module

family()


@family_function
def class_a():
    return ClassA


@family_class
class ClassA(object)
    def test(self):
        return False


class ClassB(ClassA)
    module = class_module

    def class_a(self):
        return class_module(self).ClassA


class ClassC(ClassA)
    @classmethod
    def class_b(cls):
        return cls.module.ClassB


adopt(ClassB, 'ClassC')

Then a family family_b can be defined:

from ancestration import family

super_family = family.family_a()


@family_class
class ClassA(object)
    def test(self):
        return True


@family_class
class ClassC(object) # the in-family base does not have to be specified
    @classmethod
    def super_family(cls):
        return super_family

Accordingly the following would be true:

>>> import family_a, family_b 
>>> family_a.ClassB().test() is False 
True
>>> family_b.ClassB().test() is True 
True
>>> family_b.ClassC().test() is True 
True
>>> (family_a.class_a() is family_a.ClassA and
... family_b.class_a() is family_b.ClassA) 
True
>>> (family_a.ClassB().class_a() is family_a.ClassA and
... family_b.ClassB().class_a() is family_b.ClassA) 
True
>>> (family_a.ClassC.class_b() is family_a.ClassB and
... family_b.ClassB.class_b() is family_b.ClassB) 
True
>>> family_b.ClassC.super_family is family_a 
True
>>> (family_b.ClassC.in_family_base is family_b.ClassB and
... family_b.ClassC.super_family_base is family_a.ClassC) 
True

Defining Family Inheritance

ancestration.family(extends=None)

Makes the module it was called in a family module. See Family Inheritance and Defining a Family for more information.

Parameters:

extends – The family module to be the super family or None to define a root family. If the value is neither a module or None it is assumed to be the name of the family module, which is then imported and used.

Raises:
  • ancestration.FamilyInheritanceError – if there is an error in the specification of the family module.
  • ImportError – if the super family module was specified by name and it could not be found.
Returns:

The super family module.

You can also retrieve attributes from this function and from the returned attribute values and call the last (without arguments) to specify extends. Each attribute-retrieval step denotes a module, thus instead of calling family('foo.bar') you can write family.foo.bar().

class ancestration.family_class(*args)

To be used as a metaclass or as a class decorator. This includes the class in the family inheritance. See Family Inheritance and Defining a Family Class for more information.

Parameters:

args – Must contain either only the class (if used as a class decorator) or the name, the tuple of bases and the dictionary of the class to create (if used as a metaclass).

Raises:
Returns:

The family class object.

ancestration.family_function(func)

A function decorator that registers the name of the decorated non-lambda function as a family function. This means in child family modules the same function is available under the same name, but there it uses the child family’s dictionary for global lookup. Thus it uses overridden classes, functions and so on.

Note that this only works for functions defined and accessible as attributes of the family module, not for methods or nested functions.

Parameters:

func – The function to be made a family function.

Raises:
Returns:

func

ancestration.adopt(*args, **kargs)

A function which integrates the classes and functions given as arguments into the family inheritance, thus making them family classes and family functions. The arguments may also be strings, in this case the object is looked up in the module which is given in the named argument module and defaults to the current (family) module.

A callback function may be supplied which is called with each adopted object. If the returned value is different from None the class/function is replaced by this value.

Parameters:
  • args – The functions and classes to integrate into family inheritance, may also be given as strings. If none are given but the module argument is specified, all classes and functions of that module are adopted.
  • kargs – The named argument callback may be given as a function with one argument, which is called with each adopted object. The named argument module may be a module or a string denoting a module. If a module string starts with a ., it is resolved starting with the module where adopt() is called from. To create custom adoption-functions using this function, specify the named stack_depth argument with a number denoting the number of calls from the module where it is called until adopt() is called (a function to be called instead of adopt(), directly calling should specify a 1). If the named argument include_attributes is given and either True or an iterable of strings, all or the given non-class and non-function attributes will also be imported.
Raises:
  • FamilyInheritanceError – if an argument specifies neither a class nor function.
  • ImportError – If the module given in the argument module as a string could not be imported.
ancestration.class_module

A data-descriptor (use as a class attribute) being a proxy to access attributes of the module of a class or instance. An access to the descriptor attribute raises an ImportError if the class’s module can not be found. Setting or deleting an attribute of this descriptor on an instance is not possible.

It is also callable, which returns the module of a class or instance’s class.

Parameters:

cls_or_obj – The class or instance of the class to compute the module of.

Raises:
  • AttributeError – on old-style class instances.
  • ImportError – if the module cannot be found.
Returns:

The module of the class.

ancestration.LAZY_CLS_ATTR

Provides a way to define a class attribute, shadowed by a descriptor, which is lazily evaluated at first access, e.g. after its class has been made a family class. Retrieve attributes and items from instances of this class or call them, to create another instance. Assign such an instance to a class attribute, then on first retrieval of this attribute the value is computed and saved for cached retrieval.

Example:

>>> class Test(object):
...     lazy = LAZY_CLS_ATTR.foo[0]['bar']('world')
>>> Test.foo = [{'bar': lambda name: 'Hello {}!'.format(name)}]
>>> print(Test.lazy)
Hello world!

A ValueError is raised on retrieving items, calling or on access to the created class attribute, if no attribute was retrieved first:

>>> class Test(object):
...     lazy = LAZY_CLS_ATTR
>>> Test.lazy
Traceback (most recent call last):
  ...
ValueError: First retrieve an attribute from ancestration.LAZY_CLS_ATTR before assigning to a class attribute.
>>> class Test(object):
...     lazy = LAZY_CLS_ATTR()
Traceback (most recent call last):
  ...
ValueError: First retrieve an attribute from ancestration.LAZY_CLS_ATTR before calling.
>>> class Test(object):
...     lazy = LAZY_CLS_ATTR[0]
Traceback (most recent call last):
  ...
ValueError: First retrieve an attribute from ancestration.LAZY_CLS_ATTR before retrieving items.
exception ancestration.FamilyInheritanceError

Raised if there is a problem with the family inheritance.