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.0.tar.gz (18.3 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.0-py3-none-any.whl (17.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for propert-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b8492833b9091bca098510edd5b6d88e9cffa51ba27f0e3243cc5b726cb50aae
MD5 50006dcfff03bbad322352402237410d
BLAKE2b-256 d74479e21d6783b62dd8c8f5beb59cab18d29e7fc815b3866ca4a3218a251195

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for propert-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4581b7f9cb3dff58e8525ededf9f78a1426a1e1e859907cf81187b096d584875
MD5 f11ca515846c5651a17489add5cfa82c
BLAKE2b-256 a656d6acb0193c6e9127a039f2eb85be4c90adfc07f0bc44e1f56ba47f84ce77

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