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
« 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.
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, 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 if version_info >= (3, 11): # pragma: no cover
155 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
156 raise ex
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, ])
163 @property
164 def Scope(cls) -> AttributeScope:
165 return cls._scope
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.
172 The resulting item stream can be filtered by:
173 * ``scope`` - when the item is a nested class in scope ``scope``.
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.")
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.
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``.
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
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
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.
233 The resulting item stream can be filtered by:
234 * ``scope`` - when the item is a nested class in scope ``scope``.
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
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.
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()
266@export
267class SimpleAttribute(Attribute):
268 _args: Tuple[Any, ...]
269 _kwargs: Dict[str, Any]
271 def __init__(self, *args, **kwargs) -> None:
272 self._args = args
273 self._kwargs = kwargs
275 @readonly
276 def Args(self) -> Tuple[Any, ...]:
277 return self._args
279 @readonly
280 def KwArgs(self) -> Dict[str, Any]:
281 return self._kwargs