compclasses


Namecompclasses JSON
Version 0.4.2 PyPI version JSON
download
home_pageNone
SummaryPrefer composition over inheritance
upload_time2023-12-02 10:37:13
maintainerNone
docs_urlNone
authorFrancesco Bruzzesi
requires_python>=3.8
licenseMIT License Copyright (c) 2023 Francesco Bruzzesi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <img src="docs/img/compclass-logo.svg" width=180 height=180 align="right">

# Compclasses

![](https://img.shields.io/github/license/FBruzzesi/compclasses)
<img src ="docs/img/interrogate-shield.svg">
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
<img src ="docs/img/coverage.svg">

Like *dataclasses*, but for composition.

As the Gang of Four (probably) said:

> favor object composition over class inheritance.

However when we use composition in Python, we cannot access methods directly from the composed class, and we either re-define these methods from scratch, or access them using chaining.

This codebase wants to address such issue and make it easy to do so, by [delegating](https://en.wikipedia.org/wiki/Delegation_(object-oriented_programming)) the desired methods of a class to its attributes.

---

[**Documentation**](https://fbruzzesi.github.io/compclasses/) | [**Source Code**](https://github.com/fbruzzesi/compclasses/)

---

## Table of Content

- [Compclasses](#compclasses)
  * [Table of Content](#table-of-content)
  * [Alpha Notice](#alpha-notice)
  * [Installation](#installation)
  * [Getting Started](#getting-started)
    + [compclass (decorator)](#compclass-decorator)
    + [CompclassMeta (metaclass)](#compclassmeta-metaclass)
  * [Advanced usage](#advanced-usage)
  * [Why Composition (TL;DR)](#why-composition-tldr)
  * [Feedbacks](#feedbacks)
  * [Contributing](#contributing)
  * [Inspiration](#inspiration)
  * [Licence](#licence)

## Alpha Notice

This codebase is experimental and is working for my use cases. It is very probable that there are cases not covered and for which everything breaks. If you find them, please feel free to open an issue in the [issue page](https://github.com/FBruzzesi/compclasses/issues) of the repo.

## Installation

**compclasses** is published as a Python package on [pypi](https://pypi.org/), and it can be installed with pip, ideally by using a virtual environment.

From a terminal it is possible to install it with:

```bash
python -m pip install compclasses
```

## Getting Started

Let's suppose we have the following 3 classes, `Foo`, `Bar` and `Baz`:

- `Foo` and `Bar` are independent from one another;
- `Baz` get initialized with two class attributes (`_foo`, `_bar`) which are instances of the other two classes.

```python title="Classes definition"
class Foo:
    """Foo class"""

    def __init__(self, value: int):
        """foo init"""
        self._value = value

    def get_value(self):
        """get value attribute"""
        return self._value

    def hello(self, name: str) -> str:
        """Method with argument"""
        return f"Hello {name}, this is Foo!"


class Bar:
    """Bar class"""
    b: int = 1

    def __len__(self) -> int:
        """Custom len method"""
        return 42

class Baz:
    """Baz class"""

    def __init__(self, foo: Foo, bar: Bar):
        self._foo = foo
        self._bar = bar
```

Now let's instantiate them and try see how we would access the "inner" attributes/methods:

```python title="Naive approach"
foo = Foo(123)
bar = Bar()

baz = Baz(foo, bar)

baz._foo.get_value()  # -> 123
baz._foo.hello("GitHub")  # -> "Hello GitHub, this is Foo!"
baz._bar.__len__()  # -> 42

len(baz)  # -> TypeError: object of type 'Baz' has no len()
```

### compclass (decorator)

Using the [compclass](https://fbruzzesi.github.io/compclasses/api/compclass) decorator we can *forward* the methods that we want to the `Baz` class from its attributes at definition time:

```python title="Using compclass"
from compclasses import compclass

delegates = {
    "_foo": ( "get_value", "hello"),
    "_bar": ("__len__", )
}

@compclass(delegates=delegates)
class Baz:
    """Baz class"""

    def __init__(self, foo: Foo, bar: Bar):
        self._foo = foo
        self._bar = bar

baz = Baz(foo, bar)
baz.get_value()  # -> 123
baz.hello("GitHub")  # -> "Hello GitHub, this is Foo!"
len(baz)  # -> 42
```

We can see how now we can access the methods directly.

Remark that in the `delegates` dictionary, we have that:

- the keys correspond to the attribute names in the `Baz` class;
- each value should be an iterable of string corresponding to methods/attributes present in the class instance associated to the key-attribute.

The `compclass` decorator adds each attribute and method as a [property attribute](https://docs.python.org/3/library/functions.html#property), callable as
`self.<attr_name>` instead of `self.<delegatee_cls>.<attr_name>`

### CompclassMeta (metaclass)

The equivalent, but alternative, way of doing it is by using the [`CompclassMeta`](https://fbruzzesi.github.io/compclasses/api/compclassmeta.md) metaclass when you define the class.

```python
from compclasses import CompclassMeta

delegates = {
    "_foo": ( "get_value", "hello"),
    "_bar": ("__len__", )
}

class Baz(metaclass=CompclassMeta, delegates=delegates):
    """Baz class"""

    def __init__(self, foo: Foo, bar: Bar):
        self._foo = foo
        self._bar = bar

baz = Baz(foo, bar)
baz.get_value()  # -> 123
baz.hello("GitHub")  # -> "Hello GitHub, this is Foo!"
len(baz)  # -> 42
```

As you can see the syntax is nearly one-to-one with the `compclass` decorator, and the resulting behaviour is exactly the same!

## Advanced usage

Instead of using an iterable in the `delegates` dictionary, we suggest to use a [delegatee](https://fbruzzesi.github.io/compclasses/api/delegatee/) instance as a value.

This will yield more flexibility and features when decide to forward class attributes and methods.

Check the dedicated [documentation page](https://fbruzzesi.github.io/compclasses/user_guide/beyond_basics/) to get a better understanding and see more examples on how `delegatee` can be used, its pros and cons.

## Why Composition (TL;DR)

Overall, composition is a more flexible and transparent way to reuse code and design classes in Python. It allows to build classes that are customized to your specific needs, and it promotes code reuse and modularity.

A more detailed explanation is present in the [documentation page](https://fbruzzesi.github.io/compclasses/composition).

## Feedbacks

Any feedback, improvement/enhancement or issue is welcome in the [issue page](https://github.com/FBruzzesi/compclasses/issues) of the repo.

## Contributing

Please refer to the [contributing guidelines](https://fbruzzesi.github.io/compclasses/contribute) in the documentation site.

## Inspiration

This projects is inspired by the [forwardable](https://github.com/5long/forwardable) library, a "utility for easy object composition via delegation".

However I was looking for both more flexibility and more features. In particular:

- a clear separation between class definition and method forwarding;
- a validation step to make sure that changing something from the component doesn't break the class;
- the possibility to forward all the methods/attributes of a given component with a single instruction;
- the chance of adding prefix and/or suffix for each component;

Please refer to [Beyond the basics](user_guide/beyond_basics.md) page to see example usages.

## Licence

The project has a [MIT Licence](https://github.com/FBruzzesi/compclasses/blob/main/LICENSE)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "compclasses",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": "Francesco Bruzzesi",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/d3/fc/62df3245cd05e4f3f3ae509532b36fc77cfc78f90021bdc7ef5182a5ecea/compclasses-0.4.2.tar.gz",
    "platform": null,
    "description": "<img src=\"docs/img/compclass-logo.svg\" width=180 height=180 align=\"right\">\n\n# Compclasses\n\n![](https://img.shields.io/github/license/FBruzzesi/compclasses)\n<img src =\"docs/img/interrogate-shield.svg\">\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n<img src =\"docs/img/coverage.svg\">\n\nLike *dataclasses*, but for composition.\n\nAs the Gang of Four (probably) said:\n\n> favor object composition over class inheritance.\n\nHowever when we use composition in Python, we cannot access methods directly from the composed class, and we either re-define these methods from scratch, or access them using chaining.\n\nThis codebase wants to address such issue and make it easy to do so, by [delegating](https://en.wikipedia.org/wiki/Delegation_(object-oriented_programming)) the desired methods of a class to its attributes.\n\n---\n\n[**Documentation**](https://fbruzzesi.github.io/compclasses/) | [**Source Code**](https://github.com/fbruzzesi/compclasses/)\n\n---\n\n## Table of Content\n\n- [Compclasses](#compclasses)\n  * [Table of Content](#table-of-content)\n  * [Alpha Notice](#alpha-notice)\n  * [Installation](#installation)\n  * [Getting Started](#getting-started)\n    + [compclass (decorator)](#compclass-decorator)\n    + [CompclassMeta (metaclass)](#compclassmeta-metaclass)\n  * [Advanced usage](#advanced-usage)\n  * [Why Composition (TL;DR)](#why-composition-tldr)\n  * [Feedbacks](#feedbacks)\n  * [Contributing](#contributing)\n  * [Inspiration](#inspiration)\n  * [Licence](#licence)\n\n## Alpha Notice\n\nThis codebase is experimental and is working for my use cases. It is very probable that there are cases not covered and for which everything breaks. If you find them, please feel free to open an issue in the [issue page](https://github.com/FBruzzesi/compclasses/issues) of the repo.\n\n## Installation\n\n**compclasses** is published as a Python package on [pypi](https://pypi.org/), and it can be installed with pip, ideally by using a virtual environment.\n\nFrom a terminal it is possible to install it with:\n\n```bash\npython -m pip install compclasses\n```\n\n## Getting Started\n\nLet's suppose we have the following 3 classes, `Foo`, `Bar` and `Baz`:\n\n- `Foo` and `Bar` are independent from one another;\n- `Baz` get initialized with two class attributes (`_foo`, `_bar`) which are instances of the other two classes.\n\n```python title=\"Classes definition\"\nclass Foo:\n    \"\"\"Foo class\"\"\"\n\n    def __init__(self, value: int):\n        \"\"\"foo init\"\"\"\n        self._value = value\n\n    def get_value(self):\n        \"\"\"get value attribute\"\"\"\n        return self._value\n\n    def hello(self, name: str) -> str:\n        \"\"\"Method with argument\"\"\"\n        return f\"Hello {name}, this is Foo!\"\n\n\nclass Bar:\n    \"\"\"Bar class\"\"\"\n    b: int = 1\n\n    def __len__(self) -> int:\n        \"\"\"Custom len method\"\"\"\n        return 42\n\nclass Baz:\n    \"\"\"Baz class\"\"\"\n\n    def __init__(self, foo: Foo, bar: Bar):\n        self._foo = foo\n        self._bar = bar\n```\n\nNow let's instantiate them and try see how we would access the \"inner\" attributes/methods:\n\n```python title=\"Naive approach\"\nfoo = Foo(123)\nbar = Bar()\n\nbaz = Baz(foo, bar)\n\nbaz._foo.get_value()  # -> 123\nbaz._foo.hello(\"GitHub\")  # -> \"Hello GitHub, this is Foo!\"\nbaz._bar.__len__()  # -> 42\n\nlen(baz)  # -> TypeError: object of type 'Baz' has no len()\n```\n\n### compclass (decorator)\n\nUsing the [compclass](https://fbruzzesi.github.io/compclasses/api/compclass) decorator we can *forward* the methods that we want to the `Baz` class from its attributes at definition time:\n\n```python title=\"Using compclass\"\nfrom compclasses import compclass\n\ndelegates = {\n    \"_foo\": ( \"get_value\", \"hello\"),\n    \"_bar\": (\"__len__\", )\n}\n\n@compclass(delegates=delegates)\nclass Baz:\n    \"\"\"Baz class\"\"\"\n\n    def __init__(self, foo: Foo, bar: Bar):\n        self._foo = foo\n        self._bar = bar\n\nbaz = Baz(foo, bar)\nbaz.get_value()  # -> 123\nbaz.hello(\"GitHub\")  # -> \"Hello GitHub, this is Foo!\"\nlen(baz)  # -> 42\n```\n\nWe can see how now we can access the methods directly.\n\nRemark that in the `delegates` dictionary, we have that:\n\n- the keys correspond to the attribute names in the `Baz` class;\n- each value should be an iterable of string corresponding to methods/attributes present in the class instance associated to the key-attribute.\n\nThe `compclass` decorator adds each attribute and method as a [property attribute](https://docs.python.org/3/library/functions.html#property), callable as\n`self.<attr_name>` instead of `self.<delegatee_cls>.<attr_name>`\n\n### CompclassMeta (metaclass)\n\nThe equivalent, but alternative, way of doing it is by using the [`CompclassMeta`](https://fbruzzesi.github.io/compclasses/api/compclassmeta.md) metaclass when you define the class.\n\n```python\nfrom compclasses import CompclassMeta\n\ndelegates = {\n    \"_foo\": ( \"get_value\", \"hello\"),\n    \"_bar\": (\"__len__\", )\n}\n\nclass Baz(metaclass=CompclassMeta, delegates=delegates):\n    \"\"\"Baz class\"\"\"\n\n    def __init__(self, foo: Foo, bar: Bar):\n        self._foo = foo\n        self._bar = bar\n\nbaz = Baz(foo, bar)\nbaz.get_value()  # -> 123\nbaz.hello(\"GitHub\")  # -> \"Hello GitHub, this is Foo!\"\nlen(baz)  # -> 42\n```\n\nAs you can see the syntax is nearly one-to-one with the `compclass` decorator, and the resulting behaviour is exactly the same!\n\n## Advanced usage\n\nInstead of using an iterable in the `delegates` dictionary, we suggest to use a [delegatee](https://fbruzzesi.github.io/compclasses/api/delegatee/) instance as a value.\n\nThis will yield more flexibility and features when decide to forward class attributes and methods.\n\nCheck the dedicated [documentation page](https://fbruzzesi.github.io/compclasses/user_guide/beyond_basics/) to get a better understanding and see more examples on how `delegatee` can be used, its pros and cons.\n\n## Why Composition (TL;DR)\n\nOverall, composition is a more flexible and transparent way to reuse code and design classes in Python. It allows to build classes that are customized to your specific needs, and it promotes code reuse and modularity.\n\nA more detailed explanation is present in the [documentation page](https://fbruzzesi.github.io/compclasses/composition).\n\n## Feedbacks\n\nAny feedback, improvement/enhancement or issue is welcome in the [issue page](https://github.com/FBruzzesi/compclasses/issues) of the repo.\n\n## Contributing\n\nPlease refer to the [contributing guidelines](https://fbruzzesi.github.io/compclasses/contribute) in the documentation site.\n\n## Inspiration\n\nThis projects is inspired by the [forwardable](https://github.com/5long/forwardable) library, a \"utility for easy object composition via delegation\".\n\nHowever I was looking for both more flexibility and more features. In particular:\n\n- a clear separation between class definition and method forwarding;\n- a validation step to make sure that changing something from the component doesn't break the class;\n- the possibility to forward all the methods/attributes of a given component with a single instruction;\n- the chance of adding prefix and/or suffix for each component;\n\nPlease refer to [Beyond the basics](user_guide/beyond_basics.md) page to see example usages.\n\n## Licence\n\nThe project has a [MIT Licence](https://github.com/FBruzzesi/compclasses/blob/main/LICENSE)\n",
    "bugtrack_url": null,
    "license": "MIT License\n        \n        Copyright (c) 2023 Francesco Bruzzesi\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.",
    "summary": "Prefer composition over inheritance",
    "version": "0.4.2",
    "project_urls": {
        "documentation": "https://fbruzzesi.github.io/compclasses/",
        "issue-tracker": "https://github.com/fbruzzesi/compclasses/issues",
        "repository": "https://github.com/fbruzzesi/compclasses/"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "5255982e966364fe417d55870b6b6fad35b137ec1a08d00ffb1c691944d91666",
                "md5": "d7d612ff106aba8eacf3836fa6c000c6",
                "sha256": "e42f116b1ee7d466b251a5ce2f42e396c646729633f6ae87e361aa781b797358"
            },
            "downloads": -1,
            "filename": "compclasses-0.4.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d7d612ff106aba8eacf3836fa6c000c6",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 11598,
            "upload_time": "2023-12-02T10:37:10",
            "upload_time_iso_8601": "2023-12-02T10:37:10.781898Z",
            "url": "https://files.pythonhosted.org/packages/52/55/982e966364fe417d55870b6b6fad35b137ec1a08d00ffb1c691944d91666/compclasses-0.4.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d3fc62df3245cd05e4f3f3ae509532b36fc77cfc78f90021bdc7ef5182a5ecea",
                "md5": "bc621b7126a24d8474c93309559a5265",
                "sha256": "88ba26a75ca686a1d02b7b345a3fad9afa8b3ac143396889ebdd74991ce67162"
            },
            "downloads": -1,
            "filename": "compclasses-0.4.2.tar.gz",
            "has_sig": false,
            "md5_digest": "bc621b7126a24d8474c93309559a5265",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 58477,
            "upload_time": "2023-12-02T10:37:13",
            "upload_time_iso_8601": "2023-12-02T10:37:13.132661Z",
            "url": "https://files.pythonhosted.org/packages/d3/fc/62df3245cd05e4f3f3ae509532b36fc77cfc78f90021bdc7ef5182a5ecea/compclasses-0.4.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-12-02 10:37:13",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "fbruzzesi",
    "github_project": "compclasses",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "compclasses"
}
        
Elapsed time: 0.14075s