Coverage for pyTooling/Decorators/__init__.py: 86%
63 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ ___ ___ _ __ __ _| |_ ___ _ __ ___ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ _ \/ __/ _ \| '__/ _` | __/ _ \| '__/ __| #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ (_| (_) | | | (_| | || (_) | | \__ \ #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___|\___\___/|_| \__,_|\__\___/|_| |___/ #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 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::
35 See :ref:`high-level help <DECO>` for explanations and usage examples.
36"""
37import sys
38from functools import wraps
39from types import FunctionType
40from typing import Union, Type, TypeVar, Callable, NoReturn
42__all__ = ["export", "Param", "RetType", "Func", "T"]
45try:
46 # See https://stackoverflow.com/questions/47060133/python-3-type-hinting-for-decorator
47 from typing import ParamSpec # WORKAROUND: exists since Python 3.10
49 Param = ParamSpec("Param") #: A parameter specification for function or method
50 RetType = TypeVar("RetType") #: Type variable for a return type
51 Func = Callable[Param, RetType] #: Type specification for a function
52except ImportError: # pragma: no cover
53 Param = ... #: A parameter specification for function or method
54 RetType = TypeVar("RetType") #: Type variable for a return type
55 Func = Callable[..., RetType] #: Type specification for a function
58T = TypeVar("T", bound=Union[Type, FunctionType]) #: A type variable for a classes or functions.
59C = TypeVar("C", bound=Callable) #: A type variable for functions or methods.
62def export(entity: T) -> T:
63 """
64 Register the given function or class as publicly accessible in a module.
66 Creates or updates the ``__all__`` attribute in the module in which the decorated entity is defined to include the
67 name of the decorated entity.
69 +---------------------------------------------+------------------------------------------------+
70 | ``to_export.py`` | ``another_file.py`` |
71 +=============================================+================================================+
72 | .. code-block:: python | .. code-block:: python |
73 | | |
74 | from pyTooling.Decorators import export | from .to_export import * |
75 | | |
76 | @export | |
77 | def exported(): | # 'exported' will be listed in __all__ |
78 | pass | assert "exported" in globals() |
79 | | |
80 | def not_exported(): | # 'not_exported' won't be listed in __all__ |
81 | pass | assert "not_exported" not in globals() |
82 | | |
83 +---------------------------------------------+------------------------------------------------+
85 :param entity: The function or class to include in `__all__`.
86 :returns: The unmodified function or class.
87 :raises AttributeError: If parameter ``entity`` has no ``__module__`` member.
88 :raises TypeError: If parameter ``entity`` is not a top-level entity in a module.
89 :raises TypeError: If parameter ``entity`` has no ``__name__``.
90 """
91 # * Based on an idea by Duncan Booth:
92 # http://groups.google.com/group/comp.lang.python/msg/11cbb03e09611b8a
93 # * Improved via a suggestion by Dave Angel:
94 # http://groups.google.com/group/comp.lang.python/msg/3d400fb22d8a42e1
96 if not hasattr(entity, "__module__"): 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 raise AttributeError(f"{entity} has no __module__ attribute. Please ensure it is a top-level function or class reference defined in a module.")
99 if hasattr(entity, "__qualname__"): 99 ↛ 103line 99 didn't jump to line 103 because the condition on line 99 was always true
100 if any(i in entity.__qualname__ for i in (".", "<locals>", "<lambda>")):
101 raise TypeError(f"Only named top-level functions and classes may be exported, not {entity}")
103 if not hasattr(entity, "__name__") or entity.__name__ == "<lambda>": 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 raise TypeError(f"Entity must be a named top-level function or class, not {entity.__class__}")
106 try:
107 module = sys.modules[entity.__module__]
108 except KeyError:
109 raise ValueError(f"Module {entity.__module__} is not present in sys.modules. Please ensure it is in the import path before calling export().")
111 if hasattr(module, "__all__"):
112 if entity.__name__ not in module.__all__: # type: ignore 112 ↛ 117line 112 didn't jump to line 117 because the condition on line 112 was always true
113 module.__all__.append(entity.__name__) # type: ignore
114 else:
115 module.__all__ = [entity.__name__] # type: ignore
117 return entity
120@export
121def notimplemented(message: str) -> Callable:
122 """
123 Mark a method as *not implemented* and replace the implementation with a new method raising a :exc:`NotImplementedError`.
125 The original method is stored in ``<method>.__wrapped__`` and it's doc-string is copied to the replacing method. In
126 additional the field ``<method>.__notImplemented__`` is added.
128 .. admonition:: ``example.py``
130 .. code-block:: python
132 class Data:
133 @notimplemented
134 def method(self) -> bool:
135 '''This method needs to be implemented'''
136 return True
138 :param method: Method that is marked as *not implemented*.
139 :returns: Replacement method, which raises a :exc:`NotImplementedError`.
141 .. seealso::
143 * :deco:`~pyTooling.MetaClasses.abstractmethod`
144 * :deco:`~pyTooling.MetaClasses.mustoverride`
145 """
147 def decorator(method: C) -> C:
148 @wraps(method)
149 def func(*_, **__):
150 raise NotImplementedError(message)
152 func.__notImplemented__ = True
153 return func
155 return decorator
158@export
159class readonly(property):
160 """
161 Marks a property as *read-only*.
163 The doc-string is taken from the getter-method, like :class:`property` does.
165 A plain :class:`property` hands out ``<property>.setter`` and ``<property>.deleter``, so a property declared as
166 read-only could be made writable again further down the class body. Both methods therefore raise an
167 :exc:`AttributeError` instead.
169 .. seealso::
171 :class:`property`
172 A decorator to convert getter, setter and deleter methods into a property applying the descriptor protocol.
173 """
175 def setter(self, fset: Callable) -> NoReturn:
176 """
177 Reject attaching a setter to a read-only property.
179 :param fset: The setter-method that was to be attached.
180 :raises AttributeError: Always, because a read-only property can't have a setter.
181 """
182 ex = AttributeError(f"Property '{self.fget.__name__}' is read-only, so it can't have a setter.")
183 ex.add_note(f"Use '@property' instead of '@readonly', if the property should be writable.")
184 raise ex
186 def deleter(self, fdel: Callable) -> NoReturn:
187 """
188 Reject attaching a deleter to a read-only property.
190 :param fdel: The deleter-method that was to be attached.
191 :raises AttributeError: Always, because a read-only property can't have a deleter.
192 """
193 ex = AttributeError(f"Property '{self.fget.__name__}' is read-only, so it can't have a deleter.")
194 ex.add_note(f"Use '@property' instead of '@readonly', if the property should be deletable.")
195 raise ex
198@export
199def InheritDocString(baseClass: type, merge: bool = False) -> Callable[[Func | type], Func | type]:
200 """
201 Copy the doc-string from given base-class to the method this decorator is applied to.
203 .. admonition:: ``example.py``
205 .. code-block:: python
207 from pyTooling.Decorators import InheritDocString
209 class Class1:
210 def method(self):
211 '''Method's doc-string.'''
213 class Class2(Class1):
214 @InheritDocString(Class1)
215 def method(self):
216 super().method()
218 :param baseClass: Base-class to copy the doc-string from to the new method being decorated.
219 :returns: Decorator function that copies the doc-string.
220 """
221 def decorator(param: Func | type) -> Func | type:
222 """
223 Decorator function, which copies the doc-string from base-class' method to method ``m``.
225 :param param: Method to which the doc-string from a method in ``baseClass`` (with same className) should be copied.
226 :returns: Same method, but with overwritten doc-string field (``__doc__``).
227 """
228 if isinstance(param, type):
229 baseDoc = baseClass.__doc__
230 elif callable(param): 230 ↛ 233line 230 didn't jump to line 233 because the condition on line 230 was always true
231 baseDoc = getattr(baseClass, param.__name__).__doc__
232 else:
233 return param
235 if merge:
236 if param.__doc__ is None: 236 ↛ 237line 236 didn't jump to line 237 because the condition on line 236 was never true
237 param.__doc__ = baseDoc
238 elif baseDoc is not None:
239 param.__doc__ = baseDoc + "\n\n" + param.__doc__
240 else:
241 param.__doc__ = baseDoc
243 return param
245 return decorator