Coverage for pyTooling/Decorators/__init__.py: 86%
74 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ ___ _ __ __ _| |_ ___ _ __ ___ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __| #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ (_| (_) | | | (_| | || (_) | | \__ \ #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_| \__,_|\__\___/|_| |___/ #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""Decorators controlling visibility of entities in a Python module.
33.. hint:: See :ref:`high-level help <DECO>` for explanations and usage examples.
34"""
35import sys
36from functools import wraps
37from types import FunctionType
38from typing import Union, Type, TypeVar, Callable, Any, Optional as Nullable
40__all__ = ["export", "Param", "RetType", "Func", "T"]
43try:
44 # See https://stackoverflow.com/questions/47060133/python-3-type-hinting-for-decorator
45 from typing import ParamSpec # WORKAROUND: exists since Python 3.10
47 Param = ParamSpec("Param") #: A parameter specification for function or method
48 RetType = TypeVar("RetType") #: Type variable for a return type
49 Func = Callable[Param, RetType] #: Type specification for a function
50except ImportError: # pragma: no cover
51 Param = ... #: A parameter specification for function or method
52 RetType = TypeVar("RetType") #: Type variable for a return type
53 Func = Callable[..., RetType] #: Type specification for a function
56T = TypeVar("T", bound=Union[Type, FunctionType]) #: A type variable for a classes or functions.
57C = TypeVar("C", bound=Callable) #: A type variable for functions or methods.
60def export(entity: T) -> T:
61 """
62 Register the given function or class as publicly accessible in a module.
64 Creates or updates the ``__all__`` attribute in the module in which the decorated entity is defined to include the
65 name of the decorated entity.
67 +---------------------------------------------+------------------------------------------------+
68 | ``to_export.py`` | ``another_file.py`` |
69 +=============================================+================================================+
70 | .. code-block:: python | .. code-block:: python |
71 | | |
72 | from pyTooling.Decorators import export | from .to_export import * |
73 | | |
74 | @export | |
75 | def exported(): | # 'exported' will be listed in __all__ |
76 | pass | assert "exported" in globals() |
77 | | |
78 | def not_exported(): | # 'not_exported' won't be listed in __all__ |
79 | pass | assert "not_exported" not in globals() |
80 | | |
81 +---------------------------------------------+------------------------------------------------+
83 :param entity: The function or class to include in `__all__`.
84 :returns: The unmodified function or class.
85 :raises AttributeError: If parameter ``entity`` has no ``__module__`` member.
86 :raises TypeError: If parameter ``entity`` is not a top-level entity in a module.
87 :raises TypeError: If parameter ``entity`` has no ``__name__``.
88 """
89 # * Based on an idea by Duncan Booth:
90 # http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a
91 # * Improved via a suggestion by Dave Angel:
92 # http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1
94 if not hasattr(entity, "__module__"): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 raise AttributeError(f"{entity} has no __module__ attribute. Please ensure it is a top-level function or class reference defined in a module.")
97 if hasattr(entity, "__qualname__"): 97 ↛ 101line 97 didn't jump to line 101 because the condition on line 97 was always true
98 if any(i in entity.__qualname__ for i in (".", "<locals>", "<lambda>")):
99 raise TypeError(f"Only named top-level functions and classes may be exported, not {entity}")
101 if not hasattr(entity, "__name__") or entity.__name__ == "<lambda>": 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 raise TypeError(f"Entity must be a named top-level function or class, not {entity.__class__}")
104 try:
105 module = sys.modules[entity.__module__]
106 except KeyError:
107 raise ValueError(f"Module {entity.__module__} is not present in sys.modules. Please ensure it is in the import path before calling export().")
109 if hasattr(module, "__all__"):
110 if entity.__name__ not in module.__all__: # type: ignore 110 ↛ 115line 110 didn't jump to line 115 because the condition on line 110 was always true
111 module.__all__.append(entity.__name__) # type: ignore
112 else:
113 module.__all__ = [entity.__name__] # type: ignore
115 return entity
118@export
119def notimplemented(message: str) -> Callable:
120 """
121 Mark a method as *not implemented* and replace the implementation with a new method raising a :exc:`NotImplementedError`.
123 The original method is stored in ``<method>.__wrapped__`` and it's doc-string is copied to the replacing method. In
124 additional the field ``<method>.__notImplemented__`` is added.
126 .. admonition:: ``example.py``
128 .. code-block:: python
130 class Data:
131 @notimplemented
132 def method(self) -> bool:
133 '''This method needs to be implemented'''
134 return True
136 :param method: Method that is marked as *not implemented*.
137 :returns: Replacement method, which raises a :exc:`NotImplementedError`.
139 .. seealso::
141 * :func:`~pyTooling.Metaclasses.abstractmethod`
142 * :func:`~pyTooling.Metaclasses.mustoverride`
143 """
145 def decorator(method: C) -> C:
146 @wraps(method)
147 def func(*_, **__):
148 raise NotImplementedError(message)
150 func.__notImplemented__ = True
151 return func
153 return decorator
156# Further Reading:
157# * https://github.com/python/cpython/issues/89519#issuecomment-1397534245
158# * https://stackoverflow.com/questions/128573/using-property-on-classmethods/64738850#64738850
159# * https://stackoverflow.com/questions/128573/using-property-on-classmethods
160# * https://stackoverflow.com/questions/5189699/how-to-make-a-class-property
162@export
163def classproperty(method):
165 class Descriptor:
166 """A decorator adding properties to classes."""
167 _getter: Callable
168 _setter: Callable
170 def __init__(self, getter: Nullable[Callable] = None, setter: Nullable[Callable] = None) -> None:
171 self._getter = getter
172 self._setter = setter
173 self.__doc__ = getter.__doc__
175 def __get__(self, instance: Any, owner: Nullable[type] = None) -> Any:
176 return self._getter(owner)
178 def __set__(self, instance: Any, value: Any) -> None:
179 self._setter(instance.__class__, value)
181 def setter(self, setter: Callable):
182 return self.__class__(self._getter, setter)
184 descriptor = Descriptor(method)
185 return descriptor
188@export
189def readonly(func: Callable) -> property:
190 """
191 Marks a property as *read-only*.
193 The doc-string will be taken from the getter-function.
195 It will remove ``<property>.setter`` and ``<property>.deleter`` from the property descriptor.
197 :param func: Function to convert to a read-only property.
198 :returns: A property object with just a getter.
200 .. seealso::
202 :class:`property`
203 A decorator to convert getter, setter and deleter methods into a property applying the descriptor protocol.
204 """
205 prop = property(fget=func, fset=None, fdel=None, doc=func.__doc__)
207 return prop
210@export
211def InheritDocString(baseClass: type, merge: bool = False) -> Callable[[Union[Func, type]], Union[Func, type]]:
212 """
213 Copy the doc-string from given base-class to the method this decorator is applied to.
215 .. admonition:: ``example.py``
217 .. code-block:: python
219 from pyTooling.Decorators import InheritDocString
221 class Class1:
222 def method(self):
223 '''Method's doc-string.'''
225 class Class2(Class1):
226 @InheritDocString(Class1)
227 def method(self):
228 super().method()
230 :param baseClass: Base-class to copy the doc-string from to the new method being decorated.
231 :returns: Decorator function that copies the doc-string.
232 """
233 def decorator(param: Union[Func, type]) -> Union[Func, type]:
234 """
235 Decorator function, which copies the doc-string from base-class' method to method ``m``.
237 :param param: Method to which the doc-string from a method in ``baseClass`` (with same className) should be copied.
238 :returns: Same method, but with overwritten doc-string field (``__doc__``).
239 """
240 if isinstance(param, type):
241 baseDoc = baseClass.__doc__
242 elif callable(param): 242 ↛ 245line 242 didn't jump to line 245 because the condition on line 242 was always true
243 baseDoc = getattr(baseClass, param.__name__).__doc__
244 else:
245 return param
247 if merge:
248 if param.__doc__ is None: 248 ↛ 249line 248 didn't jump to line 249 because the condition on line 248 was never true
249 param.__doc__ = baseDoc
250 elif baseDoc is not None:
251 param.__doc__ = baseDoc + "\n\n" + param.__doc__
252 else:
253 param.__doc__ = baseDoc
255 return param
257 return decorator