Coverage for pyTooling / Attributes / __init__.py: 95%
114 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:22 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 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.
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 types import MethodType, FunctionType, ModuleType
43from typing import Callable, List, TypeVar, Dict, Any, Iterable, Union, Type, Tuple, Generator, ClassVar
44from typing import Optional as Nullable
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.*'!")
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
60__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]
62Entity = TypeVar("Entity", bound=Union[Type, Callable])
63"""A type variable for functions, methods or classes."""
65TAttr = TypeVar("TAttr", bound='Attribute')
66"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""
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."""
72ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
73"""Field name on entities (function, class, method) to store pyTooling.Attributes."""
76@export
77class AttributeScope(IntFlag):
78 """
79 An enumeration of possible entities an attribute can be applied to.
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.
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.
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 = []
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.
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)
122 return entity
124 @staticmethod
125 def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None:
126 """
127 Append an attribute to a language entity (class, method, function).
129 .. hint::
131 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
133 .. code-block:: Python
135 class GroupAttribute(Attribute):
136 def __call__(self, entity: Entity) -> Entity:
137 self._AppendAttribute(entity, SimpleAttribute(...))
138 self._AppendAttribute(entity, SimpleAttribute(...))
140 return entity
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 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
155 raise ex
157 if hasattr(entity, ATTRIBUTES_MEMBER_NAME):
158 getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute)
159 else:
160 setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ])
162 @property
163 def Scope(cls) -> AttributeScope:
164 return cls._scope
166 @classmethod
167 def GetFunctions(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]:
168 """
169 Return a generator for all functions, where this attribute is attached to.
171 The resulting item stream can be filtered by:
172 * ``scope`` - when the item is a nested class in scope ``scope``.
174 :param scope: Undocumented.
175 :returns: A sequence of functions where this attribute is attached to.
176 """
177 if scope is None:
178 for c in cls._functions:
179 yield c
180 elif isinstance(scope, ModuleType):
181 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType))
182 for c in cls._functions:
183 if c in elementsInScope: 183 ↛ 182line 183 didn't jump to line 182 because the condition on line 183 was always true
184 yield c
185 else:
186 raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.")
188 @classmethod
189 def GetClasses(cls, scope: Union[Type, ModuleType, None] = None, subclassOf: Nullable[Type] = None) -> Generator[TAttr, None, None]:
190 # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]:
191 """
192 Return a generator for all classes, where this attribute is attached to.
194 The resulting item stream can be filtered by:
195 * ``scope`` - when the item is a nested class in scope ``scope``.
196 * ``subclassOf`` - when the item is a subclass of ``subclassOf``.
198 :param scope: Undocumented.
199 :param subclassOf: An attribute class or tuple thereof, to filter for that attribute type or subtype.
200 :returns: A sequence of classes where this attribute is attached to.
201 """
202 from pyTooling.Common import isnestedclass
204 if scope is None:
205 if subclassOf is None:
206 for c in cls._classes:
207 yield c
208 else:
209 for c in cls._classes:
210 if issubclass(c, subclassOf):
211 yield c
212 elif subclassOf is None:
213 if isinstance(scope, ModuleType):
214 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type))
215 for c in cls._classes:
216 if c in elementsInScope:
217 yield c
218 else:
219 for c in cls._classes:
220 if isnestedclass(c, scope):
221 yield c
222 else:
223 for c in cls._classes:
224 if isnestedclass(c, scope) and issubclass(c, subclassOf):
225 yield c
227 @classmethod
228 def GetMethods(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]:
229 """
230 Return a generator for all methods, where this attribute is attached to.
232 The resulting item stream can be filtered by:
233 * ``scope`` - when the item is a nested class in scope ``scope``.
235 :param scope: Undocumented.
236 :returns: A sequence of methods where this attribute is attached to.
237 """
238 if scope is None:
239 for c in cls._methods:
240 yield c
241 else:
242 for m in cls._methods:
243 if m.__classobj__ is scope:
244 yield m
246 @classmethod
247 def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]:
248 """
249 Returns attached attributes of this kind for a given method.
251 :param method:
252 :param includeSubClasses:
253 :return:
254 :raises TypeError:
255 """
256 if hasattr(method, ATTRIBUTES_MEMBER_NAME):
257 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME)
258 if isinstance(attributes, list): 258 ↛ 261line 258 didn't jump to line 261 because the condition on line 258 was always true
259 return tuple(attribute for attribute in attributes if isinstance(attribute, cls))
260 else:
261 raise TypeError(f"Method '{method.__class__.__name__}{method.__name__}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's not a list of Attributes.")
262 return tuple()
265@export
266class SimpleAttribute(Attribute):
267 _args: Tuple[Any, ...]
268 _kwargs: Dict[str, Any]
270 def __init__(self, *args, **kwargs) -> None:
271 self._args = args
272 self._kwargs = kwargs
274 @readonly
275 def Args(self) -> Tuple[Any, ...]:
276 return self._args
278 @readonly
279 def KwArgs(self) -> Dict[str, Any]:
280 return self._kwargs