Skip to main content

@propert - Static and class-level properties in python

Project description

🍵 @propert - Static and class-level properties in python

⚙️ Features

You may know the @property decorator in python, which sadly does not work with static and class methods.

Introducing @propert - a small library to add static and class-level properties to your code:

from propert import classproperty, staticproperty

class Test:
    @classproperty
    def class_prop(cls):
        return ...

    @staticproperty
    def static_prop():
        return ...

📝 Overview

🏷️ Setters and Deleters

All properties provided by this library support setters and deleters:

class Test(PropertBase):
    @classproperty
    def class_prop(cls):
        return ...

    @class_prop.setter
    def class_prop(cls, value: ...):
        ...

    @class_prop.deleter
    def class_prop(cls):
        ...

x = Test.class_prop # calls getter
Test.class_prop = x # calls setter
del Test.class_prop # calls deleter

Metaclass Patching

Note, that you will have to apply the PropertMeta metaclass to your class to enable custom setters and deleters. There are multiple ways to achieve this:

class Test(PropertBase):
    ...

class Test(metaclass=PropertMeta):
    ...

class Test(propert(OtherBase)):
    ...

class Test(enable_propert_modifications(OtherBase)):
    ...

You can also only enable setters (or just deleters) for a class:

class Test(propert.enable_setters(OtherBase)):
    ...

class Test(enable_propert_setters(OtherBase)):
    ...

📦 Cached Properties

There are cached properties equivalent to the @cached_property in the functools library:

from propert import cached_classproperty, cached_staticproperty

class Test:
    @cached_classproperty
    def cached_class_prop(cls):
        return ...

    @cached_staticproperty
    def cached_static_prop():
        return ...

A custom setter on a cached property can return a value that will be saved in the cache. Alternatively, it can return propert.CACHE_RESET to reset the cache or propert.NO_VALUE to not change the cache.

Calling del Test.cached_class_prop will reset the cached value by default causing the property to be re-evaluated on the next access. A custom deleter for a cached property should return True to reset the cache or False to leave the cache unchanged. This allows conditional cache invalidation.

[!WARNING] In order for cached properties to reset when calling del Test.cached_prop, the Test class must be patched with the PropertMeta metaclass. See Metaclass Patching for more info.

🔎 Introspection

There is a special type of properties that can be used to introspect the property when a getter, setter or deleter is called:

from propert import introspected_classproperty, introspected_staticproperty

class Test:
    @introspected_classproperty
    def class_prop(cls, prop):
        prop._some_internal_var = 42
        return ...

    @introspected_staticproperty
    def static_prop(prop):
        prop._some_internal_var = 42
        return ...

This behaviour can be useful if you want to declare custom property subclasses meant for decorating methods requiring access to the property object itself.

🤏 Shorthands

You can also use shorthands to avoid importing and typing the full name of each decorator:

from propert import propert

class Test:
    @propert
    def class_prop(cls):
        return ...

    @propert
    def static_prop():
        return ...

    @propert.cached
    def cached_class_prop(cls):
        return ...

    @propert.cached
    def cached_static_prop():
        return ...

This will automatically determine if the property is a class or static property based on the number of arguments.

💎 Static-, Class- and Autoproperties

@staticproperty @classproperty @autoproperty
Syntax def prop(): ... def prop(cls): ... def prop(cls_or_self): ...
x = Test.prop cls = Test cls_or_self = Test
x = Test().prop cls = Test cls_or_self = Test()
Supports Cache ✅ (instance and class share cache)
Supports Introspection
Supported by @propert shorthand

🎨 Your code, your style

Every syntax you can think of is supported with propert to suit your needs. For example, all of these are valid ways to create a cached class property:

class Test:
    @propert(cached=True)
    def prop(cls):
        return ...

    @propert.cached
    def prop(cls):
        return ...

    @classproperty(cached=True)
    def prop(cls):
        return ...

    @cached_classproperty
    def prop():
        return ...

    prop = propert(lambda cls: ..., cached=True)
    prop = propert.cached(lambda cls: ...)
    prop = classproperty(lambda cls: ..., cached=True)
    prop = cached_classproperty(lambda cls: ...)

Or let's say your entire project uses the @propert.introspected shorthand and now you need an introspected class property that is also cached:

class Test:
    @propert.introspected
    def prop_1(cls, prop):
        return ...

    @propert.introspected
    def prop_2(cls, prop):
        return ...

    @propert.introspected(cached=True)
    def prop_cached(cls, prop):
        return ...

    @propert.introspected
    def prop_3(cls, prop):
        return ...

Blends right in, doesn't it?

📊 Type Checking and MyPy

Classmethod/Staticmethod

If you are planning to use this library with mypy, you may need to add @classmethod or @staticmethod between the decorator and the property method, like this:

class Test:
    @classproperty
    @classmethod
    def class_property(cls):
        return ...

    @staticproperty
    @staticmethod
    def static_property():
        return ...

    @classproperty
    def class_property_instance(cls):
        # will work, but mypy will assume that cls: Self@Test instead of cls: Type[Self@Test]
        return ...

Using a classmethod will also solve the issue, that type checkers show the wrong type annotation in the method of a classproperty: cls: Self@Test instead of cls: Type[Self@Test], even though at runtime, the type will be Type[Test].

Redefinition

Additionally, when using custom setters and deleters, you may run into a redefinition error. You can solve that error in three ways:

class Test:
    @classproperty
    def class_prop(cls):
        return ...

    # 1. Use and underscore as the function name for setters and deleters
    @class_prop.setter
    def _(cls, value: ...):
        ...

    @class_prop.deleter
    def _(cls):
        ...

    # 2. Suppress the redefinition error
    @class_prop.setter # type: ignore[no-redef]
    def class_prop(cls, value: ...):
        ...

    # 3. Use a different function name for setters and deleters (not recommended)
    @class_prop.setter
    def class_prop_set(cls, value: int) -> None:
        pass

    @class_prop.deleter
    def class_prop_del(cls) -> None:
        pass

Invalid Assignment

Finally, when using setters, mypy will not allow you to assign to the property, because it thinks that it is a method (and/or a property object of some sort). You can suppress this warning by using # type: ignore[assignment, method-assign].

Runtime

The library itself will still work without the @classmethod or @staticmethod decorators, but mypy and type-checking may show warnings. The same applies for the "redefinitions" and "method assignments", which are properly resolved at runtime but may cause issues with type-checking.

Plugin

In the future, a mypy plugin might be added, but this will require at least the following PR to be merged: https://github.com/python/mypy/pull/9925

💻 Development

📁 Code structure

The code is structured as follows:

  • src/propert/ contains the source code
  • tests/ contains the tests
  • tests/test_mypy.py contains the mypy tests

Most of the actual logic is in the src/propert/base.py and src/propert/meta.py files. The src/propert/common.py file contains some values that are used by both files. All remaining files primarily contain type hints and some decision logic for the shorthands they represent.

📦 Distribution

To build the package, you can do the following:

uv run build
Publishing

💡 This section is primarily relevant for the maintainers of this package (me), as it requires permission to push a package to the propert repository on PyPI.

uv run publish --token <token>

🎯 Tests

To run all tests, you can do the following:

uv run pytest
uv run mypy src tests/test_mypy.py

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

propert-0.1.4.tar.gz (18.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

propert-0.1.4-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

File details

Details for the file propert-0.1.4.tar.gz.

File metadata

  • Download URL: propert-0.1.4.tar.gz
  • Upload date:
  • Size: 18.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.14

File hashes

Hashes for propert-0.1.4.tar.gz
Algorithm Hash digest
SHA256 febd0ceb21b1f736931f49e97c04874c63eaf583f518184d5648cf4d8c553d79
MD5 de0423d31644167a088d89ba113588f9
BLAKE2b-256 e736cdfdee8d35d19726e1214de51368f50c3e832e79d556d075983be352bda3

See more details on using hashes here.

File details

Details for the file propert-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: propert-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 17.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.14

File hashes

Hashes for propert-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 3cf030d63523c8643cdf43821ac1c53d19d9a16664099f7f86a4f7af2ec9be20
MD5 170ba9431383e150d500bc5a210390a7
BLAKE2b-256 4895c212872e97ffbec84e2d59135343d5930bc756fac9167f793632084369da

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page