Coverage for pyTooling/Attributes/__init__.py: 95%
111 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
1# ==================================================================================================================== #
2# _____ _ _ _ _ _ _ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / \ | |_| |_ _ __(_) |__ _ _| |_ ___ ___ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __| #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| | | | |_) | |_| | || __/\__ \ #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \_\__|\__|_| |_|_.__/ \__,_|\__\___||___/ #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 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
48from pyTooling.Decorators import export, readonly
49from pyTooling.Common import getFullyQualifiedName
52__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]
54Entity = TypeVar("Entity", bound=Union[Type, Callable])
55"""A type variable for functions, methods or classes."""
57TAttr = TypeVar("TAttr", bound='Attribute')
58"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""
60TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None]
61"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an
62iterable of those."""
64ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
65"""Field name on entities (function, class, method) to store pyTooling.Attributes."""
68@export
69class AttributeScope(IntFlag):
70 """
71 An enumeration of possible entities an attribute can be applied to.
73 Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities.
74 Supported language entities are: classes, methods or functions. Class fields or module variables are not supported.
75 """
76 Class = 1 #: Attribute can be applied to classes.
77 Method = 2 #: Attribute can be applied to methods.
78 Function = 4 #: Attribute can be applied to functions.
79 Any = Class + Method + Function #: Attribute can be applied to any language entity.
82@export
83class Attribute: # (metaclass=ExtendedType, slots=True):
84 """Base-class for all pyTooling attributes."""
85# __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes.
86 _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to.
87 _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to.
88 _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to.
89 _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with.
91 # Ensure each derived class has its own instances of class variables.
92 def __init_subclass__(cls, **kwargs: Any) -> None:
93 """
94 Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the
95 usage of that Attribute.
96 """
97 super().__init_subclass__(**kwargs)
98 cls._functions = []
99 cls._classes = []
100 cls._methods = []
102 # Make all classes derived from Attribute callable, so they can be used as a decorator.
103 def __call__(self, entity: Entity) -> Entity:
104 """
105 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse
106 lookups.
108 :param entity: Entity (function, class, method), to attach an attribute to.
109 :returns: Same entity, with attached attribute.
110 :raises TypeError: If parameter 'entity' is not a function, class nor method.
111 """
112 self._AppendAttribute(entity, self)
114 return entity
116 @staticmethod
117 def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None:
118 """
119 Append an attribute to a language entity (class, method, function).
121 .. hint::
123 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
125 .. code-block:: Python
127 class GroupAttribute(Attribute):
128 def __call__(self, entity: Entity) -> Entity:
129 self._AppendAttribute(entity, SimpleAttribute(...))
130 self._AppendAttribute(entity, SimpleAttribute(...))
132 return entity
134 :param entity: Entity, the attribute is attached to.
135 :param attribute: Attribute to attach.
136 :raises TypeError: If parameter 'entity' is not a class, method or function.
137 """
138 if isinstance(entity, MethodType): 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 attribute._methods.append(entity)
140 elif isinstance(entity, FunctionType):
141 attribute._functions.append(entity)
142 elif isinstance(entity, type): 142 ↛ 145line 142 didn't jump to line 145 because the condition on line 142 was always true
143 attribute._classes.append(entity)
144 else:
145 ex = TypeError(f"Parameter 'entity' is not a function, class nor method.")
146 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
147 raise ex
149 if hasattr(entity, ATTRIBUTES_MEMBER_NAME):
150 getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute)
151 else:
152 setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ])
154 @readonly
155 def Scope(cls) -> AttributeScope:
156 """
157 Read-only property to access the scope this attribute searches in (:attr:`_scope`).
159 :returns: The scope this attribute searches in.
160 """
161 return cls._scope
163 @classmethod
164 def GetFunctions(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]:
165 """
166 Return a generator for all functions, where this attribute is attached to.
168 The resulting item stream can be filtered by:
169 * ``scope`` - when the item is a nested class in scope ``scope``.
171 :param scope: Undocumented.
172 :returns: A sequence of functions where this attribute is attached to.
173 """
174 if scope is None:
175 for c in cls._functions:
176 yield c
177 elif isinstance(scope, ModuleType):
178 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType))
179 for c in cls._functions:
180 if c in elementsInScope: 180 ↛ 179line 180 didn't jump to line 179 because the condition on line 180 was always true
181 yield c
182 else:
183 raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.")
185 @classmethod
186 def GetClasses(cls, scope: Nullable[Type | ModuleType] = None, subclassOf: Nullable[Type] = None) -> Generator[TAttr, None, None]:
187 # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]:
188 """
189 Return a generator for all classes, where this attribute is attached to.
191 The resulting item stream can be filtered by:
192 * ``scope`` - when the item is a nested class in scope ``scope``.
193 * ``subclassOf`` - when the item is a subclass of ``subclassOf``.
195 :param scope: Undocumented.
196 :param subclassOf: An attribute class or tuple thereof, to filter for that attribute type or subtype.
197 :returns: A sequence of classes where this attribute is attached to.
198 """
199 from pyTooling.Common import isnestedclass
201 if scope is None:
202 if subclassOf is None:
203 for c in cls._classes:
204 yield c
205 else:
206 for c in cls._classes:
207 if issubclass(c, subclassOf):
208 yield c
209 elif subclassOf is None:
210 if isinstance(scope, ModuleType):
211 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type))
212 for c in cls._classes:
213 if c in elementsInScope:
214 yield c
215 else:
216 for c in cls._classes:
217 if isnestedclass(c, scope):
218 yield c
219 else:
220 for c in cls._classes:
221 if isnestedclass(c, scope) and issubclass(c, subclassOf):
222 yield c
224 @classmethod
225 def GetMethods(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]:
226 """
227 Return a generator for all methods, where this attribute is attached to.
229 The resulting item stream can be filtered by:
230 * ``scope`` - when the item is a nested class in scope ``scope``.
232 :param scope: Undocumented.
233 :returns: A sequence of methods where this attribute is attached to.
234 """
235 if scope is None:
236 for c in cls._methods:
237 yield c
238 else:
239 for m in cls._methods:
240 if m.__classobj__ is scope:
241 yield m
243 @classmethod
244 def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]:
245 """
246 Returns attached attributes of this kind for a given method.
248 :param method: Method to search attributes for.
249 :param includeSubClasses: If ``True``, attributes of derived attribute classes are included too.
250 :returns: Tuple of attached attributes of this kind.
251 :raises TypeError:
252 """
253 if hasattr(method, ATTRIBUTES_MEMBER_NAME):
254 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME)
255 if isinstance(attributes, list): 255 ↛ 258line 255 didn't jump to line 258 because the condition on line 255 was always true
256 return tuple(attribute for attribute in attributes if isinstance(attribute, cls))
257 else:
258 raise TypeError(f"Method '{method.__class__.__name__}{method.__name__}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's not a list of Attributes.")
259 return tuple()
262@export
263class SimpleAttribute(Attribute):
264 _args: Tuple[Any, ...]
265 _kwargs: Dict[str, Any]
267 def __init__(self, *args, **kwargs) -> None:
268 self._args = args
269 self._kwargs = kwargs
271 @readonly
272 def Args(self) -> Tuple[Any, ...]:
273 """
274 Read-only property to access the positional parameters this attribute was created with (:attr:`_args`).
276 :returns: Tuple of positional parameters.
277 """
278 return self._args
280 @readonly
281 def KwArgs(self) -> Dict[str, Any]:
282 """
283 Read-only property to access the named parameters this attribute was created with (:attr:`_kwargs`).
285 :returns: Dictionary of named parameters.
286 """
287 return self._kwargs