objectextensions

A basic framework for implementing an extension pattern


Keywords
extensions, plugins
License
MIT
Install
pip install objectextensions==2.0.1

Documentation

Object Extensions

A basic framework for implementing an extension pattern

Summary

The point of this framework is to provide a more modular alternative to object inheritance.

Consider the following use case: You have an abstract class Car intended to represent a car, and need a pattern that allows you to optionally add more features. For example, you may want to add a convertible roof or a touchscreen on the dashboard, but these features will not necessarily be added to every subclass of Car you create.

Applying standard OOP here means you would need to make a subclass every time a new combination of these optional features is needed. In the above case, you may need one subclass for a car with a convertible roof, one subclass for a car with a touchscreen, and one that has both features. As the amount of optional features increases, the amount of possible combinations skyrockets. This is not a scalable solution to the problem.

Object Extensions is an elegant way to handle scenarios such as this one. Rather than creating a new subclass for each possible combination, you create one extension representing each feature. When you need to create a car with a particular set of features, you take the parent class and pass it the exact set of extensions you want to apply via the .with_extensions() method as the need arises.

Note that this pattern is intended to be used alongside inheritance, not to replace it entirely. The two can be mixed without issue, such that (for example) a subclass could extend a parent class that has pre-applied extensions like so:

class SpecificCarModel(Car.with_extensions(TouchscreenDash)):
    pass

Quickstart

Setup

Below is an example of an extendable class, and an example extension that can be applied to it.

from objectextensions import Extendable


class HashList(Extendable):
    """
    A basic example class with some data and methods.
    Inheriting Extendable allows this class to be modified with extensions
    """

    def __init__(self, iterable=()):
        super().__init__()

        self.values = {}
        self.list = []

        for value in iterable:
            self.append(value)

    def append(self, item):
        self.list.append(item)
        self.values[item] = self.values.get(item, []) + [len(self.list) - 1]

    def index(self, item):
        """
        Returns all indexes containing the specified item.
        Much lower time complexity than in a typical list due to dict lookup usage
        """

        if item not in self.values:
            raise ValueError(f"{item} is not in hashlist")

        return self.values[item]
from objectextensions import Extension


class Listener(Extension):
    """
    This extension class is written to apply a counter to the HashList class,
    which increments any time .append() is called
    """

    @staticmethod
    def can_extend(target_cls):
        return issubclass(target_cls, HashList)

    @staticmethod
    def extend(target_cls):
        Extension._set(target_cls, "increment_append_count", Listener.__increment_append_count)

        Extension._wrap(target_cls, "__init__", Listener.__wrap_init)
        Extension._wrap(target_cls, 'append', Listener.__wrap_append)

    def __wrap_init(self, *args, **kwargs):
        Extension._set(self, "append_count", 0)
        yield

    def __wrap_append(self, *args, **kwargs):
        yield
        self.increment_append_count()

    def __increment_append_count(self):
        self.append_count += 1

Instantiation

HashListWithListeners = HashList.with_extensions(Listener)
my_hashlist = HashListWithListeners(iterable=[5,2,4])

or, for shorthand:

my_hashlist = HashList.with_extensions(Listener)(iterable=[5,2,4])

Result

>>> my_hashlist.append_count  # Attribute that was added by the Listener extension
3
>>> my_hashlist.append(7)  # Listener has wrapped this method with logic which increments .append_count
>>> my_hashlist.append_count
4

Functionality

Properties

Extendable.extensions
    Returns a reference to a tuple containing any applied extensions.
 

Extendable.extension_data
    Returns a snapshot of the instance's extension data.
    This is intended to hold metadata optionally provided by extensions for the sake of introspection,
    and for communication between extensions.
 

Methods

Extendable.with_extensions(cls, *extensions: Type[Extension])
    Returns a subclass with the provided extensions applied to it.
 

Extension.can_extend(target_cls: Type[Extendable])
    Abstract staticmethod which must be overridden.
    Should return a bool indicating whether this Extension can be applied to the target class.
 

Extension.extend(target_cls: Type[Extendable])
    Abstract staticmethod which must be overridden.
    Any modification of the target class should take place in this function.
 

Extension._wrap(target_cls: Type[Extendable], method_name: str,
                             gen_func: Callable[..., Generator[None, Any, None]])
    Used to wrap an existing method on the target class.
    Passes copies of the method parameters to the generator function provided.
    The generator function should yield once,
    with the yield statement receiving a copy of the result of executing the core method.
 

Extension._set(target: Union[Type[Extendable], Extendable], attribute_name: str, value: Any)
    Used to safely add a new attribute to an extendable class.

Note: It is possible but not recommended to modify an instance rather than a class using this method.
    Will raise an error if the attribute already exists (for example, if another extension has already added it)
    to ensure compatibility issues are flagged and can be dealt with easily.
 

Extension._set_property(target: Union[Type[Extendable], Extendable], property_name: str, value: Callable[[Extendable], Any])
    Used to safely add a new property to an extendable class.

Note: It is possible but not recommended to modify an instance rather than a class using this method.
    Will raise an error if the attribute already exists (for example, if another extension has already added it)
    to ensure compatibility issues are flagged and can be dealt with easily.
 

Extension._set_setter(target: Union[Type[Extendable], Extendable], setter_name: str, linked_property_name: str,
                                     value: Callable[[Extendable, Any], Any])
    Used to safely add a new setter to an extendable class.

Note: It is possible but not recommended to modify an instance rather than a class using this method.
    If the property this setter is paired with does not use the same attribute name,
    and the setter's name already exists on the class (for example, if another extension has already added it),
    an error will be raised. This is to ensure compatibility issues are flagged and can be dealt with easily.
 

Additional Info

  • As extensions do not properly invoke name mangling, adding private members via extensions is discouraged; doing so may lead to unintended behaviour. Using protected members instead is encouraged, as name mangling does not come into play in this case.