Coverage for pyTooling / Attributes / __init__.py: 95%
114 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 12:48 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 12:48 +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.
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__``.
39.. hint::
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
48try:
49 from pyTooling.Decorators import export, readonly
50 from pyTooling.Common import getFullyQualifiedName
51except (ImportError, ModuleNotFoundError): # pragma: no cover
52 print("[pyTooling.Attributes] Could not import from 'pyTooling.*'!")
54 try:
55 from Decorators import export, readonly
56 from Common import getFullyQualifiedName
57 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
58 print("[pyTooling.Attributes] Could not import directly!")
59 raise ex
62__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]
64Entity = TypeVar("Entity", bound=Union[Type, Callable])
65"""A type variable for functions, methods or classes."""
67TAttr = TypeVar("TAttr", bound='Attribute')
68"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""
70TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None]
71"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an
72iterable of those."""
74ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
75"""Field name on entities (function, class, method) to store pyTooling.Attributes."""
78@export
79class AttributeScope(IntFlag):
80 """
81 An enumeration of possible entities an attribute can be applied to.
83 Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities.
84 Supported language entities are: classes, methods or functions. Class fields or module variables are not supported.
85 """
86 Class = 1 #: Attribute can be applied to classes.
87 Method = 2 #: Attribute can be applied to methods.
88 Function = 4 #: Attribute can be applied to functions.
89 Any = Class + Method + Function #: Attribute can be applied to any language entity.
92@export
93class Attribute: # (metaclass=ExtendedType, slots=True):
94 """Base-class for all pyTooling attributes."""
95# __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes.
96 _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to.
97 _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to.
98 _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to.
99 _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with.
101 # Ensure each derived class has its own instances of class variables.
102 def __init_subclass__(cls, **kwargs: Any) -> None:
103 """
104 Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the
105 usage of that Attribute.
106 """
107 super().__init_subclass__(**kwargs)
108 cls._functions = []
109 cls._classes = []
110 cls._methods = []
112 # Make all classes derived from Attribute callable, so they can be used as a decorator.
113 def __call__(self, entity: Entity) -> Entity:
114 """
115 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse
116 lookups.
118 :param entity: Entity (function, class, method), to attach an attribute to.
119 :returns: Same entity, with attached attribute.
120 :raises TypeError: If parameter 'entity' is not a function, class nor method.
121 """
122 self._AppendAttribute(entity, self)
124 return entity
126 @staticmethod
127 def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None:
128 """
129 Append an attribute to a language entity (class, method, function).
131 .. hint::
133 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
135 .. code-block:: Python
137 class GroupAttribute(Attribute):
138 def __call__(self, entity: Entity) -> Entity:
139 self._AppendAttribute(entity, SimpleAttribute(...))
140 self._AppendAttribute(entity, SimpleAttribute(...))
142 return entity
144 :param entity: Entity, the attribute is attached to.
145 :param attribute: Attribute to attach.
146 :raises TypeError: If parameter 'entity' is not a class, method or function.
147 """
148 if isinstance(entity, MethodType): 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true
149 attribute._methods.append(entity)
150 elif isinstance(entity, FunctionType):
151 attribute._functions.append(entity)
152 elif isinstance(entity, type): 152 ↛ 155line 152 didn't jump to line 155 because the condition on line 152 was always true
153 attribute._classes.append(entity)
154 else:
155 ex = TypeError(f"Parameter 'entity' is not a function, class nor method.")
156 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
157 raise ex
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, ])
164 @property
165 def Scope(cls) -> AttributeScope:
166 return cls._scope
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.
173 The resulting item stream can be filtered by:
174 * ``scope`` - when the item is a nested class in scope ``scope``.
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.")
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.
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``.
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
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
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.
234 The resulting item stream can be filtered by:
235 * ``scope`` - when the item is a nested class in scope ``scope``.
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
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.
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()
267@export
268class SimpleAttribute(Attribute):
269 _args: Tuple[Any, ...]
270 _kwargs: Dict[str, Any]
272 def __init__(self, *args, **kwargs) -> None:
273 self._args = args
274 self._kwargs = kwargs
276 @readonly
277 def Args(self) -> Tuple[Any, ...]:
278 return self._args
280 @readonly
281 def KwArgs(self) -> Dict[str, Any]:
282 return self._kwargs