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

113 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 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 sys import version_info 

43from types import MethodType, FunctionType, ModuleType 

44from typing import Callable, List, TypeVar, Dict, Any, Iterable, Union, Type, Tuple, Generator, ClassVar, 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 if version_info >= (3, 11): # pragma: no cover 

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

156 raise ex 

157 

158 if hasattr(entity, ATTRIBUTES_MEMBER_NAME): 

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

160 else: 

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

162 

163 @property 

164 def Scope(cls) -> AttributeScope: 

165 return cls._scope 

166 

167 @classmethod 

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

169 """ 

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

171 

172 The resulting item stream can be filtered by: 

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

174 

175 :param scope: Undocumented. 

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

177 """ 

178 if scope is None: 

179 for c in cls._functions: 

180 yield c 

181 elif isinstance(scope, ModuleType): 

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

183 for c in cls._functions: 

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

185 yield c 

186 else: 

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

188 

189 @classmethod 

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

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

192 """ 

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

194 

195 The resulting item stream can be filtered by: 

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

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

198 

199 :param scope: Undocumented. 

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

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

202 """ 

203 from pyTooling.Common import isnestedclass 

204 

205 if scope is None: 

206 if subclassOf is None: 

207 for c in cls._classes: 

208 yield c 

209 else: 

210 for c in cls._classes: 

211 if issubclass(c, subclassOf): 

212 yield c 

213 elif subclassOf is None: 

214 if isinstance(scope, ModuleType): 

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

216 for c in cls._classes: 

217 if c in elementsInScope: 

218 yield c 

219 else: 

220 for c in cls._classes: 

221 if isnestedclass(c, scope): 

222 yield c 

223 else: 

224 for c in cls._classes: 

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

226 yield c 

227 

228 @classmethod 

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

230 """ 

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

232 

233 The resulting item stream can be filtered by: 

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

235 

236 :param scope: Undocumented. 

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

238 """ 

239 if scope is None: 

240 for c in cls._methods: 

241 yield c 

242 else: 

243 for m in cls._methods: 

244 if m.__classobj__ is scope: 

245 yield m 

246 

247 @classmethod 

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

249 """ 

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

251 

252 :param method: 

253 :param includeSubClasses: 

254 :return: 

255 :raises TypeError: 

256 """ 

257 if hasattr(method, ATTRIBUTES_MEMBER_NAME): 

258 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) 

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

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

261 else: 

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

263 return tuple() 

264 

265 

266@export 

267class SimpleAttribute(Attribute): 

268 _args: Tuple[Any, ...] 

269 _kwargs: Dict[str, Any] 

270 

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

272 self._args = args 

273 self._kwargs = kwargs 

274 

275 @readonly 

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

277 return self._args 

278 

279 @readonly 

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

281 return self._kwargs