declare


Namedeclare JSON
Version 1.0.1 PyPI version JSON
download
home_pageNone
SummaryDeclare attributes
upload_time2024-04-19 10:47:26
maintainerNone
docs_urlNone
authorWill McGugan
requires_python<4.0,>=3.8
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # What?

Declare is a syntactical sugar for adding attributes to Python classes, with support for validation and *watching* attributes for changes.

Declare works well with type-checkers such as MyPy, even though in many cases you don't need to write type annotations.

## Example

let's look at a simple example.
The following code creates a class to represent a color, with attributes for red, green, blue, plus an alpha (transparency) component:

```python
import declare


class Color:
    """A color object with RGB, and alpha components."""

    red = declare.Int(0)
    green = declare.Int(0)
    blue = declare.Int(0)
    alpha = declare.Float(1.0)
```

If you construct a `Color` instance, it will have the four attributes.

Perhaps surprisingly, these attributes are typed -- without any explicit type annotations.
Mypy and other type-checkers will understand that `Color` instances have three `int` attributes, and an additional `float` for alpha.

### Validation

So far, there is little benefit over conventional attributes, but Declare adds a convenient way to add validation.

The following extends the `Color` class with validation for all four attributes:

```python
import declare


class Color:
    """A color object with RGB, and alpha components."""

    red = declare.Int(0)
    green = declare.Int(0)
    blue = declare.Int(0)
    alpha = declare.Float(1.0)

    @red.validate
    @green.validate
    @blue.validate
    def _validate_component(self, component: int) -> int:
        """Restrict RGB to 0 -> 255."""
        return max(0, min(255, component))

    @alpha.validate
    def _validate_alpha(self, alpha: float) -> float:
        return max(0.0, min(1.0, alpha))
```

If you were to attempt to assign a value to an attribute outside of its expected range, that value will be restricted to be within an upper and lower range.
In other words, setting `my_color.red=300` would actually set the `red` attribute to `255`.

You can do anything you wish in the validator to change the value being stored, or perhaps to raise an exception if the value is invalid.

### Watching

In addition to validating attributes, you can *watch* attributes for changes.
When an attribute has a watch method, that method will be called when the value changes.
The method will receive the original value and the new value being set.

Here's the color class extended with a watch method:

```python
import declare


class Color:
    """A color object with RGB, and alpha components."""

    red = declare.Int(0)
    green = declare.Int(0)
    blue = declare.Int(0)
    alpha = declare.Float(1.0)

    @red.validate
    @green.validate
    @blue.validate
    def _validate_component(self, component: int) -> int:
        """Restrict RGB to 0 -> 255."""
        return max(0, min(255, component))

    @alpha.validate
    def _validate_alpha(self, alpha: float) -> float:
        return max(0.0, min(1.0, alpha))

    @alpha.watch
    def _watch_alpha(self, old_alpha: float, alpha: float) -> None:
        print(f"alpha changed from {old_alpha} to {alpha}!")
```

The addition of the `@alpha.watch` decorator will cause the `_watch_alpha` method to be called when any value is assigned to the `alpha` attribute.

### Declare types

In the above code `declare.Int` and `declare.Float` are pre-defined declarations for standard types (there is also `Bool`, `Str`, and `Bytes`).
You can also also declare custom type by importing `Declare`.

let's add a `Palette` class which contains a name, and a list of colors:

```python
import declare
from declare import Declare


class Color:
    """A color object with RGB, and alpha components."""

    red = declare.Int(0)
    green = declare.Int(0)
    blue = declare.Int(0)
    alpha = declare.Float(1.0)

    @red.validate
    @green.validate
    @blue.validate
    def _validate_component(self, component: int) -> int:
        """Restrict RGB to 0 -> 255."""
        return max(0, min(255, component))

    @alpha.validate
    def _validate_alpha(self, alpha: float) -> float:
        return max(0.0, min(1.0, alpha))

    @alpha.watch
    def _watch_alpha(self, old_alpha: float, alpha: float) -> None:
        print(f"alpha changed from {old_alpha} to {alpha}!")


class Palette:
    """A container of colors."""
    name = declare.Str("")
    colors = Declare[list[Color]]([])

```

The `colors` attribute is created with this invocation: `Declare[list[Color]]([])`, which creates a list of colors, defaulting to an empty list.

Let's break that down a bit:

- `Declare` is the descriptor which create the attributes.
- `Declare[list[Color]]` tells the type checker you are declaring a list of Color objects.
- `Declare[list[Color]]([])` sets the default to an empty list. Note that this doesn't suffer from the classic Python issue of default mutable arguments. You will get a new instance every time you construct a `Palette`.


## Installation

Install via `pip` or your favorite package manager.

```bash
pip install declare
```

# Why?

Textual uses a similar approach to declare [reactive attributes](https://textual.textualize.io/guide/reactivity/), which are a useful general programming concept. Alas, without Textual as a runtime, it wouldn't be possible to use Textual's reactive attributes in another project. 

This library extracts some of the core features and makes them work without any other dependencies.

There is some overlap with dataclasses, [traitlets](https://traitlets.readthedocs.io/en/stable/using_traitlets.html), [Pydantic](https://docs.pydantic.dev/latest/), [attrs](https://www.attrs.org/en/stable/), and other similar projects.
But Declare isn't intended to replace any of these projects, which offer way more features.
In fact, you can add Declared attributes to the class objects created by these other libraries.

# How?

In a word: ["descriptors"](https://mathspp.com/blog/pydonts/describing-descriptors).
Python's descriptor protocol has been around forever, but remains a under used feature, IMHO.

# Who?

- [@willmcgugan](https://twitter.com/willmcgugan)
- [mastodon.social/@willmcgugan](https://mastodon.social/@willmcgugan)


# Thanks!

A huge thank you to [Chris Cardillo](https://github.com/chriscardillo) who very kindly let me have the `declare` name on Pypi.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "declare",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": "Will McGugan",
    "author_email": "willmcgugan@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/dd/40/b14c202ed926486fe0c9d478692480f0bca6380e0a9570af12be428bf7af/declare-1.0.1.tar.gz",
    "platform": null,
    "description": "# What?\n\nDeclare is a syntactical sugar for adding attributes to Python classes, with support for validation and *watching* attributes for changes.\n\nDeclare works well with type-checkers such as MyPy, even though in many cases you don't need to write type annotations.\n\n## Example\n\nlet's look at a simple example.\nThe following code creates a class to represent a color, with attributes for red, green, blue, plus an alpha (transparency) component:\n\n```python\nimport declare\n\n\nclass Color:\n    \"\"\"A color object with RGB, and alpha components.\"\"\"\n\n    red = declare.Int(0)\n    green = declare.Int(0)\n    blue = declare.Int(0)\n    alpha = declare.Float(1.0)\n```\n\nIf you construct a `Color` instance, it will have the four attributes.\n\nPerhaps surprisingly, these attributes are typed -- without any explicit type annotations.\nMypy and other type-checkers will understand that `Color` instances have three `int` attributes, and an additional `float` for alpha.\n\n### Validation\n\nSo far, there is little benefit over conventional attributes, but Declare adds a convenient way to add validation.\n\nThe following extends the `Color` class with validation for all four attributes:\n\n```python\nimport declare\n\n\nclass Color:\n    \"\"\"A color object with RGB, and alpha components.\"\"\"\n\n    red = declare.Int(0)\n    green = declare.Int(0)\n    blue = declare.Int(0)\n    alpha = declare.Float(1.0)\n\n    @red.validate\n    @green.validate\n    @blue.validate\n    def _validate_component(self, component: int) -> int:\n        \"\"\"Restrict RGB to 0 -> 255.\"\"\"\n        return max(0, min(255, component))\n\n    @alpha.validate\n    def _validate_alpha(self, alpha: float) -> float:\n        return max(0.0, min(1.0, alpha))\n```\n\nIf you were to attempt to assign a value to an attribute outside of its expected range, that value will be restricted to be within an upper and lower range.\nIn other words, setting `my_color.red=300` would actually set the `red` attribute to `255`.\n\nYou can do anything you wish in the validator to change the value being stored, or perhaps to raise an exception if the value is invalid.\n\n### Watching\n\nIn addition to validating attributes, you can *watch* attributes for changes.\nWhen an attribute has a watch method, that method will be called when the value changes.\nThe method will receive the original value and the new value being set.\n\nHere's the color class extended with a watch method:\n\n```python\nimport declare\n\n\nclass Color:\n    \"\"\"A color object with RGB, and alpha components.\"\"\"\n\n    red = declare.Int(0)\n    green = declare.Int(0)\n    blue = declare.Int(0)\n    alpha = declare.Float(1.0)\n\n    @red.validate\n    @green.validate\n    @blue.validate\n    def _validate_component(self, component: int) -> int:\n        \"\"\"Restrict RGB to 0 -> 255.\"\"\"\n        return max(0, min(255, component))\n\n    @alpha.validate\n    def _validate_alpha(self, alpha: float) -> float:\n        return max(0.0, min(1.0, alpha))\n\n    @alpha.watch\n    def _watch_alpha(self, old_alpha: float, alpha: float) -> None:\n        print(f\"alpha changed from {old_alpha} to {alpha}!\")\n```\n\nThe addition of the `@alpha.watch` decorator will cause the `_watch_alpha` method to be called when any value is assigned to the `alpha` attribute.\n\n### Declare types\n\nIn the above code `declare.Int` and `declare.Float` are pre-defined declarations for standard types (there is also `Bool`, `Str`, and `Bytes`).\nYou can also also declare custom type by importing `Declare`.\n\nlet's add a `Palette` class which contains a name, and a list of colors:\n\n```python\nimport declare\nfrom declare import Declare\n\n\nclass Color:\n    \"\"\"A color object with RGB, and alpha components.\"\"\"\n\n    red = declare.Int(0)\n    green = declare.Int(0)\n    blue = declare.Int(0)\n    alpha = declare.Float(1.0)\n\n    @red.validate\n    @green.validate\n    @blue.validate\n    def _validate_component(self, component: int) -> int:\n        \"\"\"Restrict RGB to 0 -> 255.\"\"\"\n        return max(0, min(255, component))\n\n    @alpha.validate\n    def _validate_alpha(self, alpha: float) -> float:\n        return max(0.0, min(1.0, alpha))\n\n    @alpha.watch\n    def _watch_alpha(self, old_alpha: float, alpha: float) -> None:\n        print(f\"alpha changed from {old_alpha} to {alpha}!\")\n\n\nclass Palette:\n    \"\"\"A container of colors.\"\"\"\n    name = declare.Str(\"\")\n    colors = Declare[list[Color]]([])\n\n```\n\nThe `colors` attribute is created with this invocation: `Declare[list[Color]]([])`, which creates a list of colors, defaulting to an empty list.\n\nLet's break that down a bit:\n\n- `Declare` is the descriptor which create the attributes.\n- `Declare[list[Color]]` tells the type checker you are declaring a list of Color objects.\n- `Declare[list[Color]]([])` sets the default to an empty list. Note that this doesn't suffer from the classic Python issue of default mutable arguments. You will get a new instance every time you construct a `Palette`.\n\n\n## Installation\n\nInstall via `pip` or your favorite package manager.\n\n```bash\npip install declare\n```\n\n# Why?\n\nTextual uses a similar approach to declare [reactive attributes](https://textual.textualize.io/guide/reactivity/), which are a useful general programming concept. Alas, without Textual as a runtime, it wouldn't be possible to use Textual's reactive attributes in another project. \n\nThis library extracts some of the core features and makes them work without any other dependencies.\n\nThere is some overlap with dataclasses, [traitlets](https://traitlets.readthedocs.io/en/stable/using_traitlets.html), [Pydantic](https://docs.pydantic.dev/latest/), [attrs](https://www.attrs.org/en/stable/), and other similar projects.\nBut Declare isn't intended to replace any of these projects, which offer way more features.\nIn fact, you can add Declared attributes to the class objects created by these other libraries.\n\n# How?\n\nIn a word: [\"descriptors\"](https://mathspp.com/blog/pydonts/describing-descriptors).\nPython's descriptor protocol has been around forever, but remains a under used feature, IMHO.\n\n# Who?\n\n- [@willmcgugan](https://twitter.com/willmcgugan)\n- [mastodon.social/@willmcgugan](https://mastodon.social/@willmcgugan)\n\n\n# Thanks!\n\nA huge thank you to [Chris Cardillo](https://github.com/chriscardillo) who very kindly let me have the `declare` name on Pypi.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Declare attributes",
    "version": "1.0.1",
    "project_urls": null,
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "d621c14312b6c633c48aab937d761f0f3229d32e936f144f30883003a7b3893b",
                "md5": "9e7ef09bcad6c1a233313b487f353422",
                "sha256": "5f2e6b384fe42f87a91feee566536e7c161d7bc9c38b03fde39f5e0a8124e89f"
            },
            "downloads": -1,
            "filename": "declare-1.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "9e7ef09bcad6c1a233313b487f353422",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.8",
            "size": 5909,
            "upload_time": "2024-04-19T10:47:25",
            "upload_time_iso_8601": "2024-04-19T10:47:25.039464Z",
            "url": "https://files.pythonhosted.org/packages/d6/21/c14312b6c633c48aab937d761f0f3229d32e936f144f30883003a7b3893b/declare-1.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "dd40b14c202ed926486fe0c9d478692480f0bca6380e0a9570af12be428bf7af",
                "md5": "6e8d1dd5771dcb0b34299f73d87b9130",
                "sha256": "4c3c35ed96dcbdf45f4beeeb14554582a04d49d0094a8ec9fd61d10a0f9aa7fb"
            },
            "downloads": -1,
            "filename": "declare-1.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "6e8d1dd5771dcb0b34299f73d87b9130",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.8",
            "size": 5235,
            "upload_time": "2024-04-19T10:47:26",
            "upload_time_iso_8601": "2024-04-19T10:47:26.248551Z",
            "url": "https://files.pythonhosted.org/packages/dd/40/b14c202ed926486fe0c9d478692480f0bca6380e0a9570af12be428bf7af/declare-1.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-19 10:47:26",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "declare"
}
        
Elapsed time: 0.24577s