# ==================================================================================================================== #
# _____ _ _ ____ _ #
# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ ___ _ __ __ _| |_ ___ _ __ ___ #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __| #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ (_| (_) | | | (_| | || (_) | | \__ \ #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_| \__,_|\__\___/|_| |___/ #
# |_| |___/ |___/ #
# ==================================================================================================================== #
# 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