Coverage for pyTooling / Attributes / __init__.py: 95%

114 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-28 12:48 +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# Copyright 2007-2016 Patrick Lehmann - Dresden, Germany # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33This Python module offers the base implementation of .NET-like attributes realized with class-based Python decorators. 

34This module comes also with a mixin-class to ease using classes having annotated methods. 

35 

36The annotated data is stored as instances of :class:`~pyTooling.Attributes.Attribute` classes in an additional field per 

37class, method or function. By default, this field is called ``__pyattr__``. 

38 

39.. hint:: 

40 

41 See :ref:`high-level help <ATTR>` for explanations and usage examples. 

42""" 

43from enum import IntFlag 

44from types import MethodType, FunctionType, ModuleType 

45from typing import Callable, List, TypeVar, Dict, Any, Iterable, Union, Type, Tuple, Generator, ClassVar 

46from typing import Optional as Nullable 

47 

48try: 

49 from pyTooling.Decorators import export, readonly 

50 from pyTooling.Common import getFullyQualifiedName 

51except (ImportError, ModuleNotFoundError): # pragma: no cover 

52 print("[pyTooling.Attributes] Could not import from 'pyTooling.*'!") 

53 

54 try: 

55 from Decorators import export, readonly 

56 from Common import getFullyQualifiedName 

57 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

58 print("[pyTooling.Attributes] Could not import directly!") 

59 raise ex 

60 

61 

62__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"] 

63 

64Entity = TypeVar("Entity", bound=Union[Type, Callable]) 

65"""A type variable for functions, methods or classes.""" 

66 

67TAttr = TypeVar("TAttr", bound='Attribute') 

68"""A type variable for :class:`~pyTooling.Attributes.Attribute`.""" 

69 

70TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None] 

71"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an 

72iterable of those.""" 

73 

74ATTRIBUTES_MEMBER_NAME: str = "__pyattr__" 

75"""Field name on entities (function, class, method) to store pyTooling.Attributes.""" 

76 

77 

78@export 

79class AttributeScope(IntFlag): 

80 """ 

81 An enumeration of possible entities an attribute can be applied to. 

82 

83 Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities. 

84 Supported language entities are: classes, methods or functions. Class fields or module variables are not supported. 

85 """ 

86 Class = 1 #: Attribute can be applied to classes. 

87 Method = 2 #: Attribute can be applied to methods. 

88 Function = 4 #: Attribute can be applied to functions. 

89 Any = Class + Method + Function #: Attribute can be applied to any language entity. 

90 

91 

92@export 

93class Attribute: # (metaclass=ExtendedType, slots=True): 

94 """Base-class for all pyTooling attributes.""" 

95# __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes. 

96 _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to. 

97 _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to. 

98 _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to. 

99 _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with. 

100 

101 # Ensure each derived class has its own instances of class variables. 

102 def __init_subclass__(cls, **kwargs: Any) -> None: 

103 """ 

104 Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the 

105 usage of that Attribute. 

106 """ 

107 super().__init_subclass__(**kwargs) 

108 cls._functions = [] 

109 cls._classes = [] 

110 cls._methods = [] 

111 

112 # Make all classes derived from Attribute callable, so they can be used as a decorator. 

113 def __call__(self, entity: Entity) -> Entity: 

114 """ 

115 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse 

116 lookups. 

117 

118 :param entity: Entity (function, class, method), to attach an attribute to. 

119 :returns: Same entity, with attached attribute. 

120 :raises TypeError: If parameter 'entity' is not a function, class nor method. 

121 """ 

122 self._AppendAttribute(entity, self) 

123 

124 return entity 

125 

126 @staticmethod 

127 def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None: 

128 """ 

129 Append an attribute to a language entity (class, method, function). 

130 

131 .. hint:: 

132 

133 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method. 

134 

135 .. code-block:: Python 

136 

137 class GroupAttribute(Attribute): 

138 def __call__(self, entity: Entity) -> Entity: 

139 self._AppendAttribute(entity, SimpleAttribute(...)) 

140 self._AppendAttribute(entity, SimpleAttribute(...)) 

141 

142 return entity 

143 

144 :param entity: Entity, the attribute is attached to. 

145 :param attribute: Attribute to attach. 

146 :raises TypeError: If parameter 'entity' is not a class, method or function. 

147 """ 

148 if isinstance(entity, MethodType): 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true

149 attribute._methods.append(entity) 

150 elif isinstance(entity, FunctionType): 

151 attribute._functions.append(entity) 

152 elif isinstance(entity, type): 152 ↛ 155line 152 didn't jump to line 155 because the condition on line 152 was always true

153 attribute._classes.append(entity) 

154 else: 

155 ex = TypeError(f"Parameter 'entity' is not a function, class nor method.") 

156 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.") 

157 raise ex 

158 

159 if hasattr(entity, ATTRIBUTES_MEMBER_NAME): 

160 getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute) 

161 else: 

162 setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ]) 

163 

164 @property 

165 def Scope(cls) -> AttributeScope: 

166 return cls._scope 

167 

168 @classmethod 

169 def GetFunctions(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: 

170 """ 

171 Return a generator for all functions, where this attribute is attached to. 

172 

173 The resulting item stream can be filtered by: 

174 * ``scope`` - when the item is a nested class in scope ``scope``. 

175 

176 :param scope: Undocumented. 

177 :returns: A sequence of functions where this attribute is attached to. 

178 """ 

179 if scope is None: 

180 for c in cls._functions: 

181 yield c 

182 elif isinstance(scope, ModuleType): 

183 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType)) 

184 for c in cls._functions: 

185 if c in elementsInScope: 185 ↛ 184line 185 didn't jump to line 184 because the condition on line 185 was always true

186 yield c 

187 else: 

188 raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.") 

189 

190 @classmethod 

191 def GetClasses(cls, scope: Union[Type, ModuleType, None] = None, subclassOf: Nullable[Type] = None) -> Generator[TAttr, None, None]: 

192 # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]: 

193 """ 

194 Return a generator for all classes, where this attribute is attached to. 

195 

196 The resulting item stream can be filtered by: 

197 * ``scope`` - when the item is a nested class in scope ``scope``. 

198 * ``subclassOf`` - when the item is a subclass of ``subclassOf``. 

199 

200 :param scope: Undocumented. 

201 :param subclassOf: An attribute class or tuple thereof, to filter for that attribute type or subtype. 

202 :returns: A sequence of classes where this attribute is attached to. 

203 """ 

204 from pyTooling.Common import isnestedclass 

205 

206 if scope is None: 

207 if subclassOf is None: 

208 for c in cls._classes: 

209 yield c 

210 else: 

211 for c in cls._classes: 

212 if issubclass(c, subclassOf): 

213 yield c 

214 elif subclassOf is None: 

215 if isinstance(scope, ModuleType): 

216 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type)) 

217 for c in cls._classes: 

218 if c in elementsInScope: 

219 yield c 

220 else: 

221 for c in cls._classes: 

222 if isnestedclass(c, scope): 

223 yield c 

224 else: 

225 for c in cls._classes: 

226 if isnestedclass(c, scope) and issubclass(c, subclassOf): 

227 yield c 

228 

229 @classmethod 

230 def GetMethods(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: 

231 """ 

232 Return a generator for all methods, where this attribute is attached to. 

233 

234 The resulting item stream can be filtered by: 

235 * ``scope`` - when the item is a nested class in scope ``scope``. 

236 

237 :param scope: Undocumented. 

238 :returns: A sequence of methods where this attribute is attached to. 

239 """ 

240 if scope is None: 

241 for c in cls._methods: 

242 yield c 

243 else: 

244 for m in cls._methods: 

245 if m.__classobj__ is scope: 

246 yield m 

247 

248 @classmethod 

249 def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]: 

250 """ 

251 Returns attached attributes of this kind for a given method. 

252 

253 :param method: 

254 :param includeSubClasses: 

255 :return: 

256 :raises TypeError: 

257 """ 

258 if hasattr(method, ATTRIBUTES_MEMBER_NAME): 

259 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) 

260 if isinstance(attributes, list): 260 ↛ 263line 260 didn't jump to line 263 because the condition on line 260 was always true

261 return tuple(attribute for attribute in attributes if isinstance(attribute, cls)) 

262 else: 

263 raise TypeError(f"Method '{method.__class__.__name__}{method.__name__}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's not a list of Attributes.") 

264 return tuple() 

265 

266 

267@export 

268class SimpleAttribute(Attribute): 

269 _args: Tuple[Any, ...] 

270 _kwargs: Dict[str, Any] 

271 

272 def __init__(self, *args, **kwargs) -> None: 

273 self._args = args 

274 self._kwargs = kwargs 

275 

276 @readonly 

277 def Args(self) -> Tuple[Any, ...]: 

278 return self._args 

279 

280 @readonly 

281 def KwArgs(self) -> Dict[str, Any]: 

282 return self._kwargs