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

114 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 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# 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 types import MethodType, FunctionType, ModuleType 

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

44from typing import Optional as Nullable 

45 

46try: 

47 from pyTooling.Decorators import export, readonly 

48 from pyTooling.Common import getFullyQualifiedName 

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

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

51 

52 try: 

53 from Decorators import export, readonly 

54 from Common import getFullyQualifiedName 

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

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

57 raise ex 

58 

59 

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

61 

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

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

64 

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

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

67 

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

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

70iterable of those.""" 

71 

72ATTRIBUTES_MEMBER_NAME: str = "__pyattr__" 

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

74 

75 

76@export 

77class AttributeScope(IntFlag): 

78 """ 

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

80 

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

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

83 """ 

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

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

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

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

88 

89 

90@export 

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

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

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

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

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

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

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

98 

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

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

101 """ 

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

103 usage of that Attribute. 

104 """ 

105 super().__init_subclass__(**kwargs) 

106 cls._functions = [] 

107 cls._classes = [] 

108 cls._methods = [] 

109 

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

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

112 """ 

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

114 lookups. 

115 

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

117 :returns: Same entity, with attached attribute. 

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

119 """ 

120 self._AppendAttribute(entity, self) 

121 

122 return entity 

123 

124 @staticmethod 

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

126 """ 

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

128 

129 .. hint:: 

130 

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

132 

133 .. code-block:: Python 

134 

135 class GroupAttribute(Attribute): 

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

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

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

139 

140 return entity 

141 

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

143 :param attribute: Attribute to attach. 

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

145 """ 

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

147 attribute._methods.append(entity) 

148 elif isinstance(entity, FunctionType): 

149 attribute._functions.append(entity) 

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

151 attribute._classes.append(entity) 

152 else: 

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

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

155 raise ex 

156 

157 if hasattr(entity, ATTRIBUTES_MEMBER_NAME): 

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

159 else: 

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

161 

162 @property 

163 def Scope(cls) -> AttributeScope: 

164 return cls._scope 

165 

166 @classmethod 

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

168 """ 

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

170 

171 The resulting item stream can be filtered by: 

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

173 

174 :param scope: Undocumented. 

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

176 """ 

177 if scope is None: 

178 for c in cls._functions: 

179 yield c 

180 elif isinstance(scope, ModuleType): 

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

182 for c in cls._functions: 

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

184 yield c 

185 else: 

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

187 

188 @classmethod 

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

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

191 """ 

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

193 

194 The resulting item stream can be filtered by: 

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

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

197 

198 :param scope: Undocumented. 

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

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

201 """ 

202 from pyTooling.Common import isnestedclass 

203 

204 if scope is None: 

205 if subclassOf is None: 

206 for c in cls._classes: 

207 yield c 

208 else: 

209 for c in cls._classes: 

210 if issubclass(c, subclassOf): 

211 yield c 

212 elif subclassOf is None: 

213 if isinstance(scope, ModuleType): 

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

215 for c in cls._classes: 

216 if c in elementsInScope: 

217 yield c 

218 else: 

219 for c in cls._classes: 

220 if isnestedclass(c, scope): 

221 yield c 

222 else: 

223 for c in cls._classes: 

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

225 yield c 

226 

227 @classmethod 

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

229 """ 

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

231 

232 The resulting item stream can be filtered by: 

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

234 

235 :param scope: Undocumented. 

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

237 """ 

238 if scope is None: 

239 for c in cls._methods: 

240 yield c 

241 else: 

242 for m in cls._methods: 

243 if m.__classobj__ is scope: 

244 yield m 

245 

246 @classmethod 

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

248 """ 

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

250 

251 :param method: 

252 :param includeSubClasses: 

253 :return: 

254 :raises TypeError: 

255 """ 

256 if hasattr(method, ATTRIBUTES_MEMBER_NAME): 

257 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) 

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

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

260 else: 

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

262 return tuple() 

263 

264 

265@export 

266class SimpleAttribute(Attribute): 

267 _args: Tuple[Any, ...] 

268 _kwargs: Dict[str, Any] 

269 

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

271 self._args = args 

272 self._kwargs = kwargs 

273 

274 @readonly 

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

276 return self._args 

277 

278 @readonly 

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

280 return self._kwargs