Source code for advanced_descriptors.advanced_property

#!/usr/bin/env python

#    Copyright 2016 - 2022 Alexey Stepanov aka penguinolog
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at

#         http://www.apache.org/licenses/LICENSE-2.0

#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Property with class-wide getter."""

from __future__ import annotations

# Standard Library
import typing

if typing.TYPE_CHECKING:
    # Standard Library
    from collections.abc import Callable

__all__ = ("AdvancedProperty",)

_OwnerClassT = typing.TypeVar("_OwnerClassT")
_ReturnT = typing.TypeVar("_ReturnT")
_ClassReturnT = typing.TypeVar("_ClassReturnT")


[docs] class AdvancedProperty(property, typing.Generic[_OwnerClassT, _ReturnT, _ClassReturnT]): """Property with class-wide getter. This property allows implementation of read-only getter for classes in additional to normal set of getter/setter/deleter on instance. Implements almost full @property interface, except __doc__ due to class-wide nature. .. versionadded:: 2.1.0 Inherit property .. note:: If class-wide setter/deleter required: use normal property in metaclass. Usage examples: >>> class LikeNormalProperty: ... def __init__(self): ... self.val = 42 ... @AdvancedProperty ... def prop(self): ... return self.val ... @prop.setter ... def prop(self, new_val): ... self.val = new_val ... @prop.deleter ... def prop(self): ... self.val = 0 >>> test_instance = LikeNormalProperty() >>> test_instance.prop 42 >>> test_instance.prop=43 >>> test_instance.prop 43 >>> del test_instance.prop >>> test_instance.prop 0 # But access from class is restricted instead of normal property return. >>> LikeNormalProperty.prop Traceback (most recent call last): ... AttributeError >>> class ExtendedProperty: ... def __init__(self): ... self.val = 21 ... @AdvancedProperty ... def prop(self): ... return self.val ... @prop.setter ... def prop(self, new_val): ... self.val = new_val ... @prop.deleter ... def prop(self): ... self.val = -1 ... @prop.cgetter ... def prop(cls): ... return 'self.val' >>> test_instance = ExtendedProperty() >>> test_instance.prop 21 >>> test_instance.prop=20 >>> test_instance.prop 20 >>> del test_instance.prop >>> test_instance.prop -1 # Class getter is set and differs from normal getter >>> ExtendedProperty.prop 'self.val' >>> class ClassProperty: ... def _getter(cls): ... return cls ... prop = AdvancedProperty(fcget=_getter) # special case >>> ClassProperty.prop is ClassProperty True >>> ClassProperty().prop is ClassProperty # class wide property is used True """
[docs] def __init__( self, fget: Callable[[_OwnerClassT], _ReturnT] | None = None, fset: Callable[[_OwnerClassT, _ReturnT], None] | None = None, fdel: Callable[[_OwnerClassT], None] | None = None, fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None = None, ) -> None: """Advanced property main entry point. :param fget: normal getter. :type fget: Callable[[typing.Any, ], typing.Any] | None :param fset: normal setter. :type fset: Callable[[typing.Any, typing.Any], None] | None :param fdel: normal deleter. :type fdel: Callable[[typing.Any, ], None] | None :param fcget: class getter. Used as normal, if normal is None. :type fcget: Callable[[typing.Any, ], typing.Any] | None .. note:: doc argument is not supported due to class wide getter usage. """ super().__init__(fget=fget, fset=fset, fdel=fdel) self.__fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None = fcget
@typing.overload def __get__(self, instance: None, owner: type[_OwnerClassT]) -> _ClassReturnT: """Class method.""" @typing.overload def __get__(self, instance: _OwnerClassT, owner: type[_OwnerClassT] | None = None) -> _ReturnT: """Normal method.""" def __get__( self, instance: _OwnerClassT | None, owner: type[_OwnerClassT] | None = None, ) -> _ClassReturnT | _ReturnT: """Get descriptor. :param instance: Owner class instance. Filled only if instance created, else None. :type instance: owner | None :param owner: Owner class for property. :return: getter call result if getter presents :rtype: typing.Any :raises AttributeError: Getter is not available """ if owner is not None and (instance is None or self.fget is None): if self.__fcget is None: raise AttributeError() return self.__fcget(owner) return super().__get__(instance, owner) # type: ignore[no-any-return] @property def fcget(self) -> Callable[[type[_OwnerClassT]], _ClassReturnT] | None: """Class wide getter instance. :return: Class wide getter instance :rtype: Callable[[typing.Any, ], typing.Any] | None """ return self.__fcget
[docs] def cgetter( self, fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None, ) -> AdvancedProperty[_OwnerClassT, _ReturnT, _ClassReturnT]: """Descriptor to change the class wide getter on a property. :param fcget: new class-wide getter. :type fcget: Callable[[typing.Any, ], typing.Any] | None :return: AdvancedProperty :rtype: AdvancedProperty """ self.__fcget = fcget return self
if __name__ == "__main__": # pragma: no cover # Standard Library import doctest doctest.testmod(verbose=True)