Source code for advanced_descriptors.separate_class_method

#!/usr/bin/env python

#    Copyright 2017 - 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.

"""Separate class and instance methods with the same name."""

from __future__ import annotations

# Standard Library
import functools
import typing

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

__all__ = ("SeparateClassMethod",)

_MethodReturnT = typing.TypeVar("_MethodReturnT")
_ClassMethodReturnT = typing.TypeVar("_ClassMethodReturnT")


[docs] class SeparateClassMethod(typing.Generic[_MethodReturnT, _ClassMethodReturnT]): """Separate class method and instance methods with the same name. Usage examples: >>> class WithNormalMethod: # no wrapper should be used ... def __init__(self): ... self.val=42 ... @SeparateClassMethod ... def set_val(self, new_val): ... self.val=new_val >>> tst_obj = WithNormalMethod() >>> tst_obj.val 42 >>> tst_obj.set_val(21) >>> tst_obj.val 21 >>> WithNormalMethod.set_val() # It's not defined Traceback (most recent call last): ... AttributeError >>> class WithSeparateMethod: ... val = 33 ... def __init__(self): ... self.val=42 ... @SeparateClassMethod ... def set_val(self, new_val): ... self.val=new_val ... @set_val.class_method ... def set_val(cls, new_val): ... cls.val=new_val >>> tst_obj = WithSeparateMethod() >>> WithSeparateMethod.val 33 >>> tst_obj.val 42 >>> tst_obj.set_val(21) # normal method >>> tst_obj.val 21 >>> WithSeparateMethod.val 33 >>> WithSeparateMethod.set_val(44) # class method (not instance) >>> tst_obj.val 21 >>> WithSeparateMethod.val 44 >>> class WithClassMethod: # @classmethod should be used ... def _func(cls): ... return cls ... meth = SeparateClassMethod(cmeth=_func) >>> WithClassMethod.meth() is WithClassMethod True >>> WithClassMethod().meth() is WithClassMethod True """ __slots__ = ("__instance_method", "__class_method", "__owner", "__name")
[docs] def __init__( self, imeth: Callable[..., _MethodReturnT] | None = None, cmeth: Callable[..., _ClassMethodReturnT] | None = None, ) -> None: """Separate class method and instance methods. :param imeth: Instance method :type imeth: Callable[..., _MethodReturnT] | None :param cmeth: Class method :type cmeth: Callable[..., _ClassMethodReturnT] | None """ self.__instance_method: Callable[..., _MethodReturnT] | None = imeth self.__class_method: Callable[..., _ClassMethodReturnT] | None = cmeth self.__owner: type | None = None self.__name: str = ""
def __set_name__(self, owner: type | None, name: str) -> None: """Set __name__ and __objclass__ property.""" self.__owner = owner self.__name = name @typing.overload def __get__(self, instance: None, owner: typing.Any) -> Callable[..., _ClassMethodReturnT]: """Class method.""" @typing.overload def __get__(self, instance: typing.Any, owner: typing.Any) -> Callable[..., _MethodReturnT]: """Normal method.""" def __get__( self, instance: typing.Any | None, owner: typing.Any, ) -> Callable[..., _MethodReturnT | _ClassMethodReturnT]: """Get descriptor. :return: class method or instance method depends on call behavior :rtype: Callable :raises AttributeError: Not implemented getter for class method and called class context. """ if instance is None or self.__instance_method is None: if self.__class_method is None: raise AttributeError() @functools.wraps(self.__class_method) def class_method(*args: typing.Any, **kwargs: typing.Any) -> _ClassMethodReturnT: """Bound class method. :return: bound class method result :rtype: typing.Any """ return self.__class_method(owner, *args, **kwargs) # type: ignore[misc] return class_method @functools.wraps(self.__instance_method) def instance_method(*args: typing.Any, **kwargs: typing.Any) -> _MethodReturnT: """Bound instance method. :return: bound instance method result :rtype: typing.Any """ return self.__instance_method(instance, *args, **kwargs) # type: ignore[misc] return instance_method @property def __objclass__(self) -> type | None: # pragma: no cover """Read-only owner. :return: property owner class :rtype: type | None """ return self.__owner @property def __name__(self) -> str: # pragma: no cover """Read-only name. :return: attribute name (may be overridden) :rtype: str """ return self.__name
[docs] def instance_method( self, imeth: Callable[..., _MethodReturnT] | None, ) -> SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT]: """Descriptor to change instance method. :param imeth: New instance method. :type imeth: Callable[..., _MethodReturnT] | None :return: SeparateClassMethod :rtype: SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT] """ self.__instance_method = imeth return self
[docs] def class_method( self, cmeth: Callable[..., _ClassMethodReturnT] | None, ) -> SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT]: """Descriptor to change class method. :param cmeth: New class method. :type cmeth: Callable[..., _ClassMethodReturnT] | None :return: SeparateClassMethod :rtype: SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT] """ self.__class_method = cmeth return self
@property def imeth(self) -> Callable[..., _MethodReturnT] | None: """Instance method instance. :rtype: Callable[..., _MethodReturnT] | None """ return self.__instance_method @property def cmeth(self) -> Callable[..., _ClassMethodReturnT] | None: """Class method instance. :rtype: Callable[..., _ClassMethodReturnT] | None """ return self.__class_method
if __name__ == "__main__": # pragma: no cover # Standard Library import doctest doctest.testmod(verbose=True)