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

114 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-04 18: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# 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:: See :ref:`high-level help <ATTR>` for explanations and usage examples. 

40""" 

41from enum import IntFlag 

42from sys import version_info 

43from types import MethodType, FunctionType, ModuleType 

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

45from typing import Optional as Nullable 

46 

47try: 

48 from pyTooling.Decorators import export, readonly 

49 from pyTooling.Common import getFullyQualifiedName 

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

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

52 

53 try: 

54 from Decorators import export, readonly 

55 from Common import getFullyQualifiedName 

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

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

58 raise ex 

59 

60 

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

62 

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

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

65 

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

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

68 

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

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

71iterable of those.""" 

72 

73ATTRIBUTES_MEMBER_NAME: str = "__pyattr__" 

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

75 

76 

77@export 

78class AttributeScope(IntFlag): 

79 """ 

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

81 

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

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

84 """ 

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

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

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

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

89 

90 

91@export 

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

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

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

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

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

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

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

99 

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

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

102 """ 

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

104 usage of that Attribute. 

105 """ 

106 super().__init_subclass__(**kwargs) 

107 cls._functions = [] 

108 cls._classes = [] 

109 cls._methods = [] 

110 

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

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

113 """ 

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

115 lookups. 

116 

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

118 :returns: Same entity, with attached attribute. 

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

120 """ 

121 self._AppendAttribute(entity, self) 

122 

123 return entity 

124 

125 @staticmethod 

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

127 """ 

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

129 

130 .. hint:: 

131 

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

133 

134 .. code-block:: Python 

135 

136 class GroupAttribute(Attribute): 

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

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

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

140 

141 return entity 

142 

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

144 :param attribute: Attribute to attach. 

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

146 """ 

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

148 attribute._methods.append(entity) 

149 elif isinstance(entity, FunctionType): 

150 attribute._functions.append(entity) 

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

152 attribute._classes.append(entity) 

153 else: 

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

155 if version_info >= (3, 11): # pragma: no cover 

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