extendable


Nameextendable JSON
Version 1.2.1 PyPI version JSON
download
home_page
SummaryA lib to define class extendable at runtime.
upload_time2023-09-19 14:19:30
maintainer
docs_urlNone
author
requires_python>=3.6
license
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![CI](https://github.com/lmignon/extendable/actions/workflows/ci.yml/badge.svg)](https://github.com/lmignon/extendable/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/lmignon/extendable/branch/master/graph/badge.svg?token=LXD34T420H)](https://codecov.io/gh/lmignon/extendable)
# Extendable

## About

Extendable is a module that aims to provide a way to define extensible python
classes. It is designed to provide developers with a convenient and flexible way
to extend the functionality of their Python applications. By leveraging the "extendable"
library, developers can easily create modular and customizable components that
can be added or modified without modifying the core codebase. This library utilizes
Python's object-oriented programming features and provides a simple and intuitive
interface for extending and customizing applications. It aims to enhance code
reusability, maintainability, and overall development efficiency. It implements
the extension by inheritance and composition pattern. It's inspired by the way
odoo implements its models. Others patterns can be used to make your code pluggable
and this library doesn't replace them.

## Quick start

Let's define a first python class.

```python
from extendable import ExtendableMeta

class Person(metaclass=ExtendableMeta):

    def __init__(self, name: str):
        self.name = name

    def __repr__(self) -> str:
        return self.name

```

Someone using the module where the class is defined would need to extend the
person definition with a firstname field.

```python
from extendable import ExtendableMeta

class PersonExt(Person, extends=Person):
    def __init__(self, name: str):
        super().__init__(name)
        self._firstname = None

    @property
    def firstname(self) -> str:
        return self._firstname

    @firstname.setter
    def firstname(self, value:str) -> None:
        self._firstname = value

    def __repr__(self) -> str:
        res = super().__repr__()
        return f"{res}, {self.firstname or ''}"
```
At this time we have defined that `PersonExt` extends the initial definition
of `Person`. To finalyse the process, we need to instruct the runtime that
all our class declarations are done by building the final class definitions and
making it available into the current execution context.

```python
from extendable import context, registry

_registry = registry.ExtendableClassesRegistry()
context.extendable_registry.set(_registry)
_registry.init_registry()

```

Once it's done the `Person` and `PersonExt` classes can be used interchangeably
into your code since both represent the same class...

```python
p = Person("Mignon")
p.firstname = "Laurent"
print (p)
#> Mignon, Laurent
```

> :warning: This way of extending a predefined behaviour must be used carefully and in
> accordance with the [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)
> It doesn't replace others design patterns that can be used to make your code pluggable.

## How it works

 Behind the scenes, the "extendable" library utilizes several key concepts and
 mechanisms to enable its functionality. Overall, the "extendable" library leverages
 metaclasses, registry initialization, and dynamic loading to provide a flexible
 and modular approach to extending Python classes. By utilizing these mechanisms,
 developers can easily enhance the functionality of their applications without
 the need for extensive modifications to the core codebase.

### Metaclasses

The metaclass do 2 things.

* It collects the definitions of the declared class and gathers information about
  its attributes, methods, and other characteristics. These definitions are stored
  in a global registry by module. This registry is a map of module name to a list
  of class definitions.
* This information is then used to build a class object that acts as a proxy or
  blueprint for the actual concrete class that will be created later when the registry
  is initialized based on the aggregated definition of all the classes declared
  to extend the initial class...

### Registry initialization

The registry initialization is the process that build the final class definition.
To make your blueprint class work, you need to initialize the registry. This is
done by calling the `init_registry` method of the registry object. This method
will build the final class definition by aggregating all the definitions of the
classes declared to extend the initial class through a class hierarchy. The
order of the classes in the hierarchy is important. This order is by default
the order in which the classes are loaded by the python interpreter. For advanced
usage, you can change this order or even the list of definitions used to build the
final class definition. This is done by calling the `init_registry` method with
the list of modules[^1] to load as argument.

[^1]: When you specify a module into the list of modules to load, the wildcart
      character `*` is allowed at the end of the module name to load all the
      submodules of the module. Otherwise, only the module itself is loaded.

```python
from extendable import registry

_registry = registry.ExtendableClassesRegistry()
_registry.init_registry(["module1", "module2.*"])
```

Once the registry is initialized, it must be made available into the current
execution context so the blueprint class can use it. To do so you must set the
registry into the `extendable_registry` context variable. This is done by
calling the `set` method of the `extendable_registry` context variable.

```python
from extendable import context, registry

_registry = registry.ExtendableClassesRegistry()
context.extendable_registry.set(_registry)
_registry.init_registry()
```

### Dynamic loading

All of this is made possible by the dynamic loading capabilities of Python.
The concept of dynamic loading in Python refers to the ability to load and execute
code at runtime, rather than during the initial compilation or execution phase.
It allows developers to dynamically import and use modules, classes, functions
or variables based on certain conditions or user input. The dynamic loading
can also be applied at class instantiation time. This is the mechanism used by
the "extendable" library to instantiate the final class definition when you call
the constructor of the blueprint class. This is done by implementing the
`__call__` method into the metaclass to return the final class definition instead
of the blueprint class itself. The same applies to pretend that the blueprint
class is the final class definition through the implementation of the `__subclasscheck__`
method into the metaclass.

## Development

To run tests, use `tox`. You will get a test coverage report in `htmlcov/index.html`.
An easy way to install tox is `pipx install tox`.

This project uses pre-commit to enforce linting (among which black for code formating,
isort for sorting imports, and mypy for type checking).

To make sure linters run locally on each of your commits, install pre-commit
(`pipx install pre-commit` is recommended), and run `pre-commit install` in your
local clone of the extendable repository.

To release:

 * run ``bumpversion patch|minor|major` --list`
 * Check the `new_version` value returned by the previous command
 * run `towncrier build`.
 * Inspect and commit the updated HISTORY.rst.
 * `git tag {new_version} ; git push --tags`.

## Contributing

All kind of contributions are welcome.


            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "extendable",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": "",
    "keywords": "",
    "author": "",
    "author_email": "Laurent Mignon <laurent.mignon@acsone.eu>",
    "download_url": "https://files.pythonhosted.org/packages/68/d9/24f7439c0511c7ead44bbf7c9d44e6137f78e184a4e4c750b1b7717cc522/extendable-1.2.1.tar.gz",
    "platform": null,
    "description": "[![CI](https://github.com/lmignon/extendable/actions/workflows/ci.yml/badge.svg)](https://github.com/lmignon/extendable/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/lmignon/extendable/branch/master/graph/badge.svg?token=LXD34T420H)](https://codecov.io/gh/lmignon/extendable)\n# Extendable\n\n## About\n\nExtendable is a module that aims to provide a way to define extensible python\nclasses. It is designed to provide developers with a convenient and flexible way\nto extend the functionality of their Python applications. By leveraging the \"extendable\"\nlibrary, developers can easily create modular and customizable components that\ncan be added or modified without modifying the core codebase. This library utilizes\nPython's object-oriented programming features and provides a simple and intuitive\ninterface for extending and customizing applications. It aims to enhance code\nreusability, maintainability, and overall development efficiency. It implements\nthe extension by inheritance and composition pattern. It's inspired by the way\nodoo implements its models. Others patterns can be used to make your code pluggable\nand this library doesn't replace them.\n\n## Quick start\n\nLet's define a first python class.\n\n```python\nfrom extendable import ExtendableMeta\n\nclass Person(metaclass=ExtendableMeta):\n\n    def __init__(self, name: str):\n        self.name = name\n\n    def __repr__(self) -> str:\n        return self.name\n\n```\n\nSomeone using the module where the class is defined would need to extend the\nperson definition with a firstname field.\n\n```python\nfrom extendable import ExtendableMeta\n\nclass PersonExt(Person, extends=Person):\n    def __init__(self, name: str):\n        super().__init__(name)\n        self._firstname = None\n\n    @property\n    def firstname(self) -> str:\n        return self._firstname\n\n    @firstname.setter\n    def firstname(self, value:str) -> None:\n        self._firstname = value\n\n    def __repr__(self) -> str:\n        res = super().__repr__()\n        return f\"{res}, {self.firstname or ''}\"\n```\nAt this time we have defined that `PersonExt` extends the initial definition\nof `Person`. To finalyse the process, we need to instruct the runtime that\nall our class declarations are done by building the final class definitions and\nmaking it available into the current execution context.\n\n```python\nfrom extendable import context, registry\n\n_registry = registry.ExtendableClassesRegistry()\ncontext.extendable_registry.set(_registry)\n_registry.init_registry()\n\n```\n\nOnce it's done the `Person` and `PersonExt` classes can be used interchangeably\ninto your code since both represent the same class...\n\n```python\np = Person(\"Mignon\")\np.firstname = \"Laurent\"\nprint (p)\n#> Mignon, Laurent\n```\n\n> :warning: This way of extending a predefined behaviour must be used carefully and in\n> accordance with the [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)\n> It doesn't replace others design patterns that can be used to make your code pluggable.\n\n## How it works\n\n Behind the scenes, the \"extendable\" library utilizes several key concepts and\n mechanisms to enable its functionality. Overall, the \"extendable\" library leverages\n metaclasses, registry initialization, and dynamic loading to provide a flexible\n and modular approach to extending Python classes. By utilizing these mechanisms,\n developers can easily enhance the functionality of their applications without\n the need for extensive modifications to the core codebase.\n\n### Metaclasses\n\nThe metaclass do 2 things.\n\n* It collects the definitions of the declared class and gathers information about\n  its attributes, methods, and other characteristics. These definitions are stored\n  in a global registry by module. This registry is a map of module name to a list\n  of class definitions.\n* This information is then used to build a class object that acts as a proxy or\n  blueprint for the actual concrete class that will be created later when the registry\n  is initialized based on the aggregated definition of all the classes declared\n  to extend the initial class...\n\n### Registry initialization\n\nThe registry initialization is the process that build the final class definition.\nTo make your blueprint class work, you need to initialize the registry. This is\ndone by calling the `init_registry` method of the registry object. This method\nwill build the final class definition by aggregating all the definitions of the\nclasses declared to extend the initial class through a class hierarchy. The\norder of the classes in the hierarchy is important. This order is by default\nthe order in which the classes are loaded by the python interpreter. For advanced\nusage, you can change this order or even the list of definitions used to build the\nfinal class definition. This is done by calling the `init_registry` method with\nthe list of modules[^1] to load as argument.\n\n[^1]: When you specify a module into the list of modules to load, the wildcart\n      character `*` is allowed at the end of the module name to load all the\n      submodules of the module. Otherwise, only the module itself is loaded.\n\n```python\nfrom extendable import registry\n\n_registry = registry.ExtendableClassesRegistry()\n_registry.init_registry([\"module1\", \"module2.*\"])\n```\n\nOnce the registry is initialized, it must be made available into the current\nexecution context so the blueprint class can use it. To do so you must set the\nregistry into the `extendable_registry` context variable. This is done by\ncalling the `set` method of the `extendable_registry` context variable.\n\n```python\nfrom extendable import context, registry\n\n_registry = registry.ExtendableClassesRegistry()\ncontext.extendable_registry.set(_registry)\n_registry.init_registry()\n```\n\n### Dynamic loading\n\nAll of this is made possible by the dynamic loading capabilities of Python.\nThe concept of dynamic loading in Python refers to the ability to load and execute\ncode at runtime, rather than during the initial compilation or execution phase.\nIt allows developers to dynamically import and use modules, classes, functions\nor variables based on certain conditions or user input. The dynamic loading\ncan also be applied at class instantiation time. This is the mechanism used by\nthe \"extendable\" library to instantiate the final class definition when you call\nthe constructor of the blueprint class. This is done by implementing the\n`__call__` method into the metaclass to return the final class definition instead\nof the blueprint class itself. The same applies to pretend that the blueprint\nclass is the final class definition through the implementation of the `__subclasscheck__`\nmethod into the metaclass.\n\n## Development\n\nTo run tests, use `tox`. You will get a test coverage report in `htmlcov/index.html`.\nAn easy way to install tox is `pipx install tox`.\n\nThis project uses pre-commit to enforce linting (among which black for code formating,\nisort for sorting imports, and mypy for type checking).\n\nTo make sure linters run locally on each of your commits, install pre-commit\n(`pipx install pre-commit` is recommended), and run `pre-commit install` in your\nlocal clone of the extendable repository.\n\nTo release:\n\n * run ``bumpversion patch|minor|major` --list`\n * Check the `new_version` value returned by the previous command\n * run `towncrier build`.\n * Inspect and commit the updated HISTORY.rst.\n * `git tag {new_version} ; git push --tags`.\n\n## Contributing\n\nAll kind of contributions are welcome.\n\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "A lib to define class extendable at runtime.",
    "version": "1.2.1",
    "project_urls": {
        "Changelog": "https://github.com/lmignon/extendable/blob/master/HISTORY.rst",
        "Source": "https://github.com/lmignon/extendable"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "82c9732acc1340c32fac822f0d7678ae69170149317c2569b006bb044e74881e",
                "md5": "bc9996f0e9dffb50e4c62be756fbd272",
                "sha256": "b51259d835745932106e997fc1f9dcb98e6b1b7fa0fd64589586e870bf7a24ba"
            },
            "downloads": -1,
            "filename": "extendable-1.2.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "bc9996f0e9dffb50e4c62be756fbd272",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 12242,
            "upload_time": "2023-09-19T14:19:28",
            "upload_time_iso_8601": "2023-09-19T14:19:28.654090Z",
            "url": "https://files.pythonhosted.org/packages/82/c9/732acc1340c32fac822f0d7678ae69170149317c2569b006bb044e74881e/extendable-1.2.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "68d924f7439c0511c7ead44bbf7c9d44e6137f78e184a4e4c750b1b7717cc522",
                "md5": "f09178ec77658edf01d5b80ee31ed83e",
                "sha256": "594d5a00e04e30687f2441a0d61c696fbbe3864943fe5611170c210d8c45f476"
            },
            "downloads": -1,
            "filename": "extendable-1.2.1.tar.gz",
            "has_sig": false,
            "md5_digest": "f09178ec77658edf01d5b80ee31ed83e",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 12997,
            "upload_time": "2023-09-19T14:19:30",
            "upload_time_iso_8601": "2023-09-19T14:19:30.042905Z",
            "url": "https://files.pythonhosted.org/packages/68/d9/24f7439c0511c7ead44bbf7c9d44e6137f78e184a4e4c750b1b7717cc522/extendable-1.2.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-09-19 14:19:30",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "lmignon",
    "github_project": "extendable",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "extendable"
}
        
Elapsed time: 0.11796s