Coverage for pyTooling/Attributes/__init__.py: 95%
114 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 18:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 18: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.
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:: 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
45from typing import Optional as Nullable
47try:
48 from pyTooling.Decorators import export, readonly
49 from pyTooling.Common import getFullyQualifiedName
50except (ImportError, ModuleNotFoundError): # pragma: no cover
51 print("[pyTooling.Attributes] Could not import from 'pyTooling.*'!")
53 try:
54 from Decorators import export, readonly
55 from Common import getFullyQualifiedName
56 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
57 print("[pyTooling.Attributes] Could not import directly!")
58 raise ex
61__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]
63Entity = TypeVar("Entity", bound=Union[Type, Callable])
64"""A type variable for functions, methods or classes."""
66TAttr = TypeVar("TAttr", bound='Attribute')
67"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""
69TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None]
70"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an
71iterable of those."""
73ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
74"""Field name on entities (function, class, method) to store pyTooling.Attributes."""
77@export
78class AttributeScope(IntFlag):
79 """
80 An enumeration of possible entities an attribute can be applied to.
82 Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities.
83 Supported language entities are: classes, methods or functions. Class fields or module variables are not supported.
84 """
85 Class = 1 #: Attribute can be applied to classes.
86 Method = 2 #: Attribute can be applied to methods.
87 Function = 4 #: Attribute can be applied to functions.
88 Any = Class + Method + Function #: Attribute can be applied to any language entity.
91@export
92class Attribute: # (metaclass=ExtendedType, slots=True):
93 """Base-class for all pyTooling attributes."""
94# __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes.
95 _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to.
96 _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to.
97 _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to.
98 _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with.
100 # Ensure each derived class has its own instances of class variables.
101 def __init_subclass__(cls, **kwargs: Any) -> None:
102 """
103 Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the
104 usage of that Attribute.
105 """
106 super().__init_subclass__(**kwargs)
107 cls._functions = []
108 cls._classes = []
109 cls._methods = []
111 # Make all classes derived from Attribute callable, so they can be used as a decorator.
112 def __call__(self, entity: Entity) -> Entity:
113 """
114 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse
115 lookups.
117 :param entity: Entity (function, class, method), to attach an attribute to.
118 :returns: Same entity, with attached attribute.
119 :raises TypeError: If parameter 'entity' is not a function, class nor method.
120 """
121 self._AppendAttribute(entity, self)
123 return entity
125 @staticmethod
126 def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None:
127 """
128 Append an attribute to a language entity (class, method, function).
130 .. hint::
132 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
134 .. code-block:: Python
136 class GroupAttribute(Attribute):
137 def __call__(self, entity: Entity) -> Entity:
138 self._AppendAttribute(entity, SimpleAttribute(...))
139 self._AppendAttribute(entity, SimpleAttribute(...))
141 return entity
143 :param entity: Entity, the attribute is attached to.
144 :param attribute: Attribute to attach.
145 :raises TypeError: If parameter 'entity' is not a class, method or function.
146 """
147 if isinstance(entity, MethodType): 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 attribute._methods.append(entity)
149 elif isinstance(entity, FunctionType):
150 attribute._functions.append(entity)
151 elif isinstance(entity, type): 151 ↛ 154line 151 didn't jump to line 154 because the condition on line 151 was always true
152 attribute._classes.append(entity)
153 else:
154 ex = TypeError(f"Parameter 'entity' is not a function, class nor method.")
155 if version_info >= (3, 11): # pragma: no cover
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