Skip to main content
https://gitlab.com/katyukha/extend-me/badges/master/pipeline.svg https://coveralls.io/repos/katyukha/extend-me/badge.png https://img.shields.io/badge/docs-passing-brightgreen.svg PyPI - Downloads

Extend Me - Class based extension/plugin library

This module provides mechanism of extension of your application based on ‘extension via inheritance’. Under this words I mean ability to define new extensions of application objects simply by subclassing of extensible classes of app.

For example we have app with class ‘Worker’ which we would like to make extensible (allowing third party modules to extend or change its behavior). Thinking strait, there are a lot of work to be done, to impelement mechanism of loading, registering, end enabling extension, with lot of glue code, which must define some entry points to connect extension and main app. But why not make it simpler, supposing that any subclass of ‘Worker’ will extend it? And this module provides implementation of this in two ways:

  • Explicit (by using metaclass ExtensibleType directly)
    • When using this way You will heve seperatly Base class to be subclassed by extension classes and class getter which will construct class based on all defined extensions using multiple inhertance

  • Implicit (by using Extensible class which use metaclass magic implicitly)
    • Extensible class takes care of all metaclass magic

      related to generation objects of correct class

How it Works

Metaclass (ExtensibleType) tracks all subclasses of class it is applied to, and provides method to build class based on all subclasses of base class, thus using all functionality of all subclasses. Thus generation of correct class is separate process which should be used everywhere where extensible class is requred.

To simplify this class Extensible was implemented. It has redefined method __new__ which automaticaly creates instances of correct class (class that inherited from base class and all its extensions’)

Examples

ExtensibleType

At the begining we should create a metaclass that will automaticaly gether all information about all extensions, and apply this metaclass to class we would like to enable extensions for:

>>> import six  # Used for Python 2/3 compatability
>>> mc = ExtensibleType._("Object")

>>> @six.add_metaclass(mc)
... class Object(object):
...     pass

Not method _ of ExtensibleType. This method is used to create metaclass for specific object. It receives one argument - string that will be used as name of class generated by this metaclass

Next we may define extension for this class. It is very simple. Just subclass previously defined class:

>>> class ObjectExtension(Object):
...     cool_attribute = 1
...     def method1(self):
...         return "Test"

So… at this momet we have base class and extension. And here is that core magic occures. Metaclass that was created at the begining automaticaly collects all subclasses of base class. So it is posible now to create new class that is subclass of all subclasses of base class using multiple inheritance. And metaclass mc will do it for You:

>>> cls = mc.get_class()

And now You can use cls for Your needs, instead of base class. It can do all that base class can, and all that extensions can:

>>> obj = cls()
>>> obj.method1()
'Test'
>>> obj.cool_attribute
1

ExtensibleByHashType

Same as ExtensibleType, but allows to build tree of classes for diferent names (types). Just look examples below.

First, create metaclass that will specify inheritance rules:

>>> import six  # Used for Python 2/3 compatability
>>> mc = ExtensibleByHashType._("Connector", hashattr='name')

Here we see aditional parametr in _ method: hashattr='name' which describes what meta attribute will be used as key(hash).

Next step - we have to create Base class with this metaclass. As example we will look into connection classes of openerp_proxy project:

>>> @six.add_metaclass(mc)
... class ConnectorBase(object):
...     # Base class for all connectors
...
...     def __init__(self, host, port, verbose=False):
...         self.host = host
...         self.port = port
...         self.verbose = verbose
...
...     def _get_service(self, name):
...         raise NotImplementedError
...
...     def get_service(self, name):
...         # Returns service for specified *name*
...         return self._get_service(name)

Base class describes only interface, and may be some part of abstract logic And as next step we will extend it in diferent ways to support different connection types:

>>> class ConnectorXMLRPC(ConnectorBase):
...     # XML-RPC connector
...     class Meta:
...         name = 'xml-rpc' # remember definition of metaclass?
...                          # this attribute is used as hash(key)
...                          # to unique identify each banch of extensions
...                          # of base class
...
...     def __init__(self, *args, **kwargs):
...         super(ConnectorXMLRPC, self).__init__(*args, **kwargs)
...         self.__services = {}
...
...     def get_service_url(self, service_name):
...         return 'http://%s:%s/xmlrpc/%s' % (self.host, self.port, service_name)
...
...     def _get_service(self, name):
...         service = self.__services.get(name, False)
...         if service is False:
...             service = XMLRPCProxy(self.get_service_url(name), verbose=self.verbose)
...             self.__services[name] = service
...         return service
...
...
... # Pay attention on base class.
>>> class ConnectorXMLRPCS(ConnectorXMLRPC):
...     # XML-RPCS Connector
...     class Meta:
...         name = 'xml-rpcs'
...
...     def get_service_url(self, service_name):
...         return 'https://%s:%s/xmlrpc/%s' % (self.host, self.port, service_name)

Code above creates two connectors: one for XML-RPC and one for XML-RPCS. Each of connectors may be extended by simple inheritance. And if required any extension may define new branch(key)(hash) as wee see in example above.

To use this connector mc has method get_class(name[, default=False]) wich will return class generated for hash=*name*:

>>> cls = mc.get_class('xml-rpc')
>>> [b.__name__ for b in cls.__bases__]
['ConnectorXMLRPC', 'ConnectorBase']
>>> cls.__name__
'Connector'

>>> cls = mc.get_class('xml-rpcs')
>>> [b.__name__ for b in cls.__bases__]
['ConnectorXMLRPCS', 'ConnectorBase']
>>> cls.__name__
'Connector'

Example above shows what classes will be generated for specified names. By default, if mc.get_class called with unregistered name (No extension with Meta.name == name defined) it will raise ValueError

If You want to allow creating of classes with not Meta.name defined, just pass default=True to mc.get_class:

>>> cls = mc.get_class('unexisting-protocol', default=True)
>>> [b.__name__ for b in cls.__bases__]
['ConnectorBase']
>>> cls.__name__
'Connector'

Extensible

This class provides one more level of abstraction, allowing to hide all metaclass magic behide the scene. So, using it You don’t need to worry about metaclasses and class creation process. Just inherit extensions form base class, and use in Your program instances of base class. Let’s see it in example:

>>> class MyCoolClass(Extensible):
...     my_attr_1 = 25
...     def my_method1(self, arg1):
...         print('Hello, %s' % arg1)

>>> class MyCoolClassExtension1(MyCoolClass):
...     def my_method1(self, arg1):
...         super(MyCoolClassExtension1, self).my_method1(arg1.upper())
...
...     def my_method2(self, arg1):
...         print("Good by, %s" % arg1)

And now using simply instances of base class You have all abilities that provided by extensions:

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

Metadata

Release files for extend-me 1.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for extend-me 1.1.5
File Size Uploaded
extend_me-1.1.5.tar.gz 15.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for extend-me 1.1.5
File Interpreter ABI Platform
extend_me-1.1.5-py2.py3-none-any.whl Python 3, Python 2 none any Details

Total release size: 32.6 kB

Release files / extend_me-1.1.5.tar.gz

Download URL extend_me-1.1.5.tar.gz
Size 15.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1f5879d97fa059793165907202bf0cd01f3b9dcfb2e885c991674aacc3e2b396
BLAKE2b-256 checksum
How to use checksums
57f3347662fa19ddce1a861b03eb16d605fb99a3196b72acbfe180da7165cd6f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.10.4

Release files / extend_me-1.1.5-py2.py3-none-any.whl

Download URL extend_me-1.1.5-py2.py3-none-any.whl
Size 17.5 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
01b63cd3bd74d8fab7167fb0ea02c358895643b7ee9c6e3cc9b975db09762d7c
BLAKE2b-256 checksum
How to use checksums
ef95ede232fa6fb6c4ba933f397ede7130791ec60d5f0599653f70a952128e09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.10.4

Release history Release notifications | RSS feed

This release

1.1.5 This release

2 release files

1.1.4

2 release files

1.1.3

1 release file

1.1.2

1 release file

1.1.1

1 release file

1.1.0

2 release files

1.0.0

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page