Source code for pyTooling.Decorators

# ==================================================================================================================== #
#             _____           _ _               ____                           _                                       #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  |  _ \  ___  ___ ___  _ __ __ _| |_ ___  _ __ ___                       #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __|                      #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| |  __/ (_| (_) | | | (_| | || (_) | |  \__ \                      #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_|  \__,_|\__\___/|_|  |___/                      #
# |_|    |___/                          |___/                                                                          #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2024 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# 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.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""Decorators controlling visibility of entities in a Python module.

.. hint:: See :ref:`high-level help <DECO>` for explanations and usage examples.
"""
import sys
from functools import wraps
from types     import FunctionType
from typing    import Union, Type, TypeVar, Callable, Any, Optional as Nullable

__all__ = ["export", "Param", "RetType", "Func", "T"]


try:
	# See https://stackoverflow.com/questions/47060133/python-3-type-hinting-for-decorator
	from typing import ParamSpec                     # WORKAROUND: exists since Python 3.10

	Param = ParamSpec("Param")                       #: A parameter specification for function or method
	RetType = TypeVar("RetType")                     #: Type variable for a return type
	Func = Callable[Param, RetType]                  #: Type specification for a function
except ImportError:  # pragma: no cover
	Param = ...                                      #: A parameter specification for function or method
	RetType = TypeVar("RetType")                     #: Type variable for a return type
	Func = Callable[..., RetType]                    #: Type specification for a function


T = TypeVar("T", bound=Union[Type, FunctionType])  #: A type variable for a classes or functions.
C = TypeVar("C", bound=Callable)                   #: A type variable for functions or methods.


[docs] def export(entity: T) -> T: """ Register the given function or class as publicly accessible in a module. Creates or updates the ``__all__`` attribute in the module in which the decorated entity is defined to include the name of the decorated entity. +---------------------------------------------+------------------------------------------------+ | ``to_export.py`` | ``another_file.py`` | +=============================================+================================================+ | .. code-block:: python | .. code-block:: python | | | | | from pyTooling.Decorators import export | from .to_export import * | | | | | @export | | | def exported(): | # 'exported' will be listed in __all__ | | pass | assert "exported" in globals() | | | | | def not_exported(): | # 'not_exported' won't be listed in __all__ | | pass | assert "not_exported" not in globals() | | | | +---------------------------------------------+------------------------------------------------+ :param entity: The function or class to include in `__all__`. :returns: The unmodified function or class. :raises AttributeError: If parameter ``entity`` has no ``__module__`` member. :raises TypeError: If parameter ``entity`` is not a top-level entity in a module. :raises TypeError: If parameter ``entity`` has no ``__name__``. """ # * Based on an idea by Duncan Booth: # http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a # * Improved via a suggestion by Dave Angel: # http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1 if not hasattr(entity, "__module__"): raise AttributeError(f"{entity} has no __module__ attribute. Please ensure it is a top-level function or class reference defined in a module.") if hasattr(entity, "__qualname__"): if any(i in entity.__qualname__ for i in (".", "<locals>", "<lambda>")): raise TypeError(f"Only named top-level functions and classes may be exported, not {entity}") if not hasattr(entity, "__name__") or entity.__name__ == "<lambda>": raise TypeError(f"Entity must be a named top-level function or class, not {entity.__class__}") try: module = sys.modules[entity.__module__] except KeyError: raise ValueError(f"Module {entity.__module__} is not present in sys.modules. Please ensure it is in the import path before calling export().") if hasattr(module, "__all__"): if entity.__name__ not in module.__all__: # type: ignore module.__all__.append(entity.__name__) # type: ignore else: module.__all__ = [entity.__name__] # type: ignore return entity
[docs] @export def notimplemented(message: str) -> Callable: """ Mark a method as *not implemented* and replace the implementation with a new method raising a :exc:`NotImplementedError`. The original method is stored in ``<method>.__wrapped__`` and it's doc-string is copied to the replacing method. In additional the field ``<method>.__notImplemented__`` is added. .. admonition:: ``example.py`` .. code-block:: python class Data: @notimplemented def method(self) -> bool: '''This method needs to be implemented''' return True :param method: Method that is marked as *not implemented*. :returns: Replacement method, which raises a :exc:`NotImplementedError`. .. seealso:: * :func:`~pyTooling.Metaclasses.abstractmethod` * :func:`~pyTooling.Metaclasses.mustoverride` """ def decorator(method: C) -> C: @wraps(method) def func(*_, **__): raise NotImplementedError(message) func.__notImplemented__ = True return func return decorator
[docs] @export def classproperty(method): class Descriptor: """A decorator adding properties to classes.""" _getter: Callable _setter: Callable def __init__(self, getter: Nullable[Callable] = None, setter: Nullable[Callable] = None) -> None: self._getter = getter self._setter = setter self.__doc__ = getter.__doc__ def __get__(self, instance: Any, owner: Nullable[type] = None) -> Any: return self._getter(owner) def __set__(self, instance: Any, value: Any) -> None: self._setter(instance.__class__, value) def setter(self, setter: Callable): return self.__class__(self._getter, setter) descriptor = Descriptor(method) return descriptor
[docs] @export def readonly(func: Callable) -> property: """ Marks a property as *read-only*. It will remove ``<property>.setter`` and ``<property>.deleter``. :param func: :return: """ prop = property(fget=func, fset=None, fdel=None, doc=func.__doc__) return prop
[docs] @export def InheritDocString(baseClass: type) -> Callable[[Func], Func]: """ Copy the doc-string from given base-class to the method this decorator is applied to. .. admonition:: ``example.py`` .. code-block:: python from pyTooling.Decorators import InheritDocString class Class1: def method(self): '''Method's doc-string.''' class Class2(Class1): @InheritDocString(Class1) def method(self): super().method() :param baseClass: Base-class to copy the doc-string from to the new method being decorated. :returns: Decorator function that copies the doc-string. """ def decorator(m: Func) -> Func: """ Decorator function, which copies the doc-string from base-class' method to method ``m``. :param m: Method to which the doc-string from a method in ``baseClass`` (with same className) should be copied. :returns: Same method, but with overwritten doc-string field (``__doc__``). """ m.__doc__ = getattr(baseClass, m.__name__).__doc__ return m return decorator