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

111 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# 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 

48from pyTooling.Decorators import export, readonly 

49from pyTooling.Common import getFullyQualifiedName 

50 

51 

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

53 

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

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

56 

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

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

59 

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

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

62iterable of those.""" 

63 

64ATTRIBUTES_MEMBER_NAME: str = "__pyattr__" 

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

66 

67 

68@export 

69class AttributeScope(IntFlag): 

70 """ 

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

72 

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

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

75 """ 

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

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

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

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

80 

81 

82@export 

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

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

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

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

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

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

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

90 

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

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

93 """ 

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

95 usage of that Attribute. 

96 """ 

97 super().__init_subclass__(**kwargs) 

98 cls._functions = [] 

99 cls._classes = [] 

100 cls._methods = [] 

101 

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

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

104 """ 

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

106 lookups. 

107 

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

109 :returns: Same entity, with attached attribute. 

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

111 """ 

112 self._AppendAttribute(entity, self) 

113 

114 return entity 

115 

116 @staticmethod 

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

118 """ 

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

120 

121 .. hint:: 

122 

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

124 

125 .. code-block:: Python 

126 

127 class GroupAttribute(Attribute): 

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

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

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

131 

132 return entity 

133 

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

135 :param attribute: Attribute to attach. 

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

137 """ 

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

139 attribute._methods.append(entity) 

140 elif isinstance(entity, FunctionType): 

141 attribute._functions.append(entity) 

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

143 attribute._classes.append(entity) 

144 else: 

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

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

147 raise ex 

148 

149 if hasattr(entity, ATTRIBUTES_MEMBER_NAME): 

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

151 else: 

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

153 

154 @readonly 

155 def Scope(cls) -> AttributeScope: 

156 """ 

157 Read-only property to access the scope this attribute searches in (:attr:`_scope`). 

158 

159 :returns: The scope this attribute searches in. 

160 """ 

161 return cls._scope 

162 

163 @classmethod 

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

165 """ 

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

167 

168 The resulting item stream can be filtered by: 

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

170 

171 :param scope: Undocumented. 

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

173 """ 

174 if scope is None: 

175 for c in cls._functions: 

176 yield c 

177 elif isinstance(scope, ModuleType): 

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

179 for c in cls._functions: 

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

181 yield c 

182 else: 

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

184 

185 @classmethod 

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

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

188 """ 

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

190 

191 The resulting item stream can be filtered by: 

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

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

194 

195 :param scope: Undocumented. 

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

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

198 """ 

199 from pyTooling.Common import isnestedclass 

200 

201 if scope is None: 

202 if subclassOf is None: 

203 for c in cls._classes: 

204 yield c 

205 else: 

206 for c in cls._classes: 

207 if issubclass(c, subclassOf): 

208 yield c 

209 elif subclassOf is None: 

210 if isinstance(scope, ModuleType): 

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

212 for c in cls._classes: 

213 if c in elementsInScope: 

214 yield c 

215 else: 

216 for c in cls._classes: 

217 if isnestedclass(c, scope): 

218 yield c 

219 else: 

220 for c in cls._classes: 

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

222 yield c 

223 

224 @classmethod 

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

226 """ 

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

228 

229 The resulting item stream can be filtered by: 

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

231 

232 :param scope: Undocumented. 

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

234 """ 

235 if scope is None: 

236 for c in cls._methods: 

237 yield c 

238 else: 

239 for m in cls._methods: 

240 if m.__classobj__ is scope: 

241 yield m 

242 

243 @classmethod 

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

245 """ 

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

247 

248 :param method: Method to search attributes for. 

249 :param includeSubClasses: If ``True``, attributes of derived attribute classes are included too. 

250 :returns: Tuple of attached attributes of this kind. 

251 :raises TypeError: 

252 """ 

253 if hasattr(method, ATTRIBUTES_MEMBER_NAME): 

254 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) 

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

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

257 else: 

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

259 return tuple() 

260 

261 

262@export 

263class SimpleAttribute(Attribute): 

264 _args: Tuple[Any, ...] 

265 _kwargs: Dict[str, Any] 

266 

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

268 self._args = args 

269 self._kwargs = kwargs 

270 

271 @readonly 

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

273 """ 

274 Read-only property to access the positional parameters this attribute was created with (:attr:`_args`). 

275 

276 :returns: Tuple of positional parameters. 

277 """ 

278 return self._args 

279 

280 @readonly 

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

282 """ 

283 Read-only property to access the named parameters this attribute was created with (:attr:`_kwargs`). 

284 

285 :returns: Dictionary of named parameters. 

286 """ 

287 return self._kwargs