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

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. 

32 

33.. hint:: 

34 

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 

41 

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

43 

44 

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 

48 

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 

56 

57 

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. 

60 

61 

62def export(entity: T) -> T: 

63 """ 

64 Register the given function or class as publicly accessible in a module. 

65 

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. 

68 

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 +---------------------------------------------+------------------------------------------------+ 

84 

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 

95 

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.") 

98 

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}") 

102 

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__}") 

105 

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().") 

110 

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 

116 

117 return entity 

118 

119 

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`. 

124 

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. 

127 

128 .. admonition:: ``example.py`` 

129 

130 .. code-block:: python 

131 

132 class Data: 

133 @notimplemented 

134 def method(self) -> bool: 

135 '''This method needs to be implemented''' 

136 return True 

137 

138 :param method: Method that is marked as *not implemented*. 

139 :returns: Replacement method, which raises a :exc:`NotImplementedError`. 

140 

141 .. seealso:: 

142 

143 * :deco:`~pyTooling.MetaClasses.abstractmethod` 

144 * :deco:`~pyTooling.MetaClasses.mustoverride` 

145 """ 

146 

147 def decorator(method: C) -> C: 

148 @wraps(method) 

149 def func(*_, **__): 

150 raise NotImplementedError(message) 

151 

152 func.__notImplemented__ = True 

153 return func 

154 

155 return decorator 

156 

157 

158@export 

159class readonly(property): 

160 """ 

161 Marks a property as *read-only*. 

162 

163 The doc-string is taken from the getter-method, like :class:`property` does. 

164 

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. 

168 

169 .. seealso:: 

170 

171 :class:`property` 

172 A decorator to convert getter, setter and deleter methods into a property applying the descriptor protocol. 

173 """ 

174 

175 def setter(self, fset: Callable) -> NoReturn: 

176 """ 

177 Reject attaching a setter to a read-only property. 

178 

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 

185 

186 def deleter(self, fdel: Callable) -> NoReturn: 

187 """ 

188 Reject attaching a deleter to a read-only property. 

189 

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 

196 

197 

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. 

202 

203 .. admonition:: ``example.py`` 

204 

205 .. code-block:: python 

206 

207 from pyTooling.Decorators import InheritDocString 

208 

209 class Class1: 

210 def method(self): 

211 '''Method's doc-string.''' 

212 

213 class Class2(Class1): 

214 @InheritDocString(Class1) 

215 def method(self): 

216 super().method() 

217 

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``. 

224 

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 

234 

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 

242 

243 return param 

244 

245 return decorator