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

113 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-13 22:36 +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 @property 

155 def Scope(cls) -> AttributeScope: 

156 return cls._scope 

157 

158 @classmethod 

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

160 """ 

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

162 

163 The resulting item stream can be filtered by: 

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

165 

166 :param scope: Undocumented. 

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

168 """ 

169 if scope is None: 

170 for c in cls._functions: 

171 yield c 

172 elif isinstance(scope, ModuleType): 

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

174 for c in cls._functions: 

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

176 yield c 

177 else: 

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

179 

180 @classmethod 

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

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

183 """ 

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

185 

186 The resulting item stream can be filtered by: 

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

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

189 

190 :param scope: Undocumented. 

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

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

193 """ 

194 from pyTooling.Common import isnestedclass 

195 

196 if scope is None: 

197 if subclassOf is None: 

198 for c in cls._classes: 

199 yield c 

200 else: 

201 for c in cls._classes: 

202 if issubclass(c, subclassOf): 

203 yield c 

204 elif subclassOf is None: 

205 if isinstance(scope, ModuleType): 

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

207 for c in cls._classes: 

208 if c in elementsInScope: 

209 yield c 

210 else: 

211 for c in cls._classes: 

212 if isnestedclass(c, scope): 

213 yield c 

214 else: 

215 for c in cls._classes: 

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

217 yield c 

218 

219 @classmethod 

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

221 """ 

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

223 

224 The resulting item stream can be filtered by: 

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

226 

227 :param scope: Undocumented. 

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

229 """ 

230 if scope is None: 

231 for c in cls._methods: 

232 yield c 

233 else: 

234 for m in cls._methods: 

235 if m.__classobj__ is scope: 

236 yield m 

237 

238 @classmethod 

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

240 """ 

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

242 

243 :param method: 

244 :param includeSubClasses: 

245 :return: 

246 :raises TypeError: 

247 """ 

248 if hasattr(method, ATTRIBUTES_MEMBER_NAME): 

249 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) 

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

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

252 else: 

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

254 return tuple() 

255 

256 

257@export 

258class SimpleAttribute(Attribute): 

259 _args: Tuple[Any, ...] 

260 _kwargs: Dict[str, Any] 

261 

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

263 self._args = args 

264 self._kwargs = kwargs 

265 

266 @readonly 

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

268 return self._args 

269 

270 @readonly 

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

272 return self._kwargs