Coverage for pyTooling/Attributes/__init__.py: 93%
117 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 19:04 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 19:04 +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.
43.. seealso::
45 :mod:`pyTooling.Attributes.ArgParse`
46 |rarr| Attributes describing a command line interface.
47 :mod:`pyTooling.MetaClasses`
48 |rarr| The meta-class that collects the attributes attached to a class' methods.
49 :mod:`pyTooling.Decorators`
50 |rarr| Decorators that modify an entity instead of marking it.
51"""
52from __future__ import annotations
54from enum import IntFlag
55from types import MethodType, FunctionType, ModuleType
56from typing import Callable, TypeVar, Any, Iterable, Union, Generator, ClassVar
57from typing import Optional as Nullable
59from pyTooling.Decorators import export, readonly
60from pyTooling.Common import getFullyQualifiedName
63__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]
65Entity = TypeVar("Entity", bound=Union[type, Callable[..., Any]])
66"""A type variable for functions, methods or classes."""
68TAttr = TypeVar("TAttr", bound='Attribute')
69"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""
71TAttributeFilter = Union[type[TAttr], Iterable[type[TAttr]], None]
72"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an
73iterable of those."""
75ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
76"""Field name on entities (function, class, method) to store pyTooling.Attributes."""
79@export
80class AttributeScope(IntFlag):
81 """
82 An enumeration of possible entities an attribute can be applied to.
84 Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities.
85 Supported language entities are: classes, methods or functions. Class fields or module variables are not supported.
86 """
87 Class = 1 #: Attribute can be applied to classes.
88 Method = 2 #: Attribute can be applied to methods.
89 Function = 4 #: Attribute can be applied to functions.
90 Any = Class + Method + Function #: Attribute can be applied to any language entity.
93@export
94class Attribute: # (metaclass=ExtendedType, slots=True):
95 """Base-class for all pyTooling attributes."""
96# __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes.
97 _functions: ClassVar[list[Any]] = [] #: List of functions, this Attribute was attached to.
98 _classes: ClassVar[list[Any]] = [] #: List of classes, this Attribute was attached to.
99 _methods: ClassVar[list[Any]] = [] #: List of methods, this Attribute was attached to.
100 _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with.
102 # Ensure each derived class has its own instances of class variables.
103 def __init_subclass__(cls, **kwargs: Any) -> None:
104 """
105 Ensure each derived attribute class gets its own registry of annotated entities.
107 The registries :attr:`_functions`, :attr:`_classes` and :attr:`_methods` are class variables, so a derived
108 attribute class would otherwise share the base-class' lists and report entities it was never attached to. Fresh
109 lists are assigned per derived class to prevent that.
111 :param kwargs: Class keyword arguments forwarded to the base-class.
112 """
113 super().__init_subclass__(**kwargs)
114 cls._functions = []
115 cls._classes = []
116 cls._methods = []
118 # Make all classes derived from Attribute callable, so they can be used as a decorator.
119 def __call__(self, entity: Entity) -> Entity:
120 """
121 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse
122 lookups.
124 :param entity: Entity (function, class, method), to attach an attribute to.
125 :returns: Same entity, with attached attribute.
126 :raises TypeError: If parameter 'entity' is not a function, class nor method.
127 """
128 self._AppendAttribute(entity, self)
130 return entity
132 @staticmethod
133 def _AppendAttribute(entity: Entity, attribute: Attribute) -> None:
134 """
135 Append an attribute to a language entity (class, method, function).
137 .. hint::
139 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
141 .. code-block:: Python
143 class GroupAttribute(Attribute):
144 def __call__(self, entity: Entity) -> Entity:
145 self._AppendAttribute(entity, SimpleAttribute(...))
146 self._AppendAttribute(entity, SimpleAttribute(...))
148 return entity
150 :param entity: Entity, the attribute is attached to.
151 :param attribute: Attribute to attach.
152 :raises TypeError: If parameter 'entity' is not a class, method or function.
153 """
154 if isinstance(entity, MethodType): 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 attribute._methods.append(entity)
156 elif isinstance(entity, FunctionType):
157 attribute._functions.append(entity)
158 elif isinstance(entity, type): 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was always true
159 attribute._classes.append(entity)
160 else:
161 ex = TypeError("Parameter 'entity' is not a function, class nor method.")
162 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
163 raise ex
165 if hasattr(entity, ATTRIBUTES_MEMBER_NAME):
166 getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute)
167 else:
168 setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ])
170 @readonly
171 def Scope(cls) -> AttributeScope:
172 """
173 Read-only property to access the scope this attribute searches in (:attr:`_scope`).
175 :returns: The scope this attribute searches in.
176 """
177 return cls._scope
179 @classmethod
180 def GetFunctions(cls, scope: Nullable[type] = None) -> Generator[TAttr, None, None]:
181 """
182 Return a generator for all functions, where this attribute is attached to.
184 The resulting item stream can be filtered by:
185 * ``scope`` - when the item is a nested class in scope ``scope``.
187 :param scope: Optional, module the functions have to be defined in; ``None`` accepts every function.
188 :returns: A sequence of functions where this attribute is attached to.
189 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
190 """
191 if scope is None:
192 for c in cls._functions:
193 yield c
194 elif isinstance(scope, ModuleType):
195 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType))
196 for c in cls._functions:
197 if c in elementsInScope: 197 ↛ 196line 197 didn't jump to line 196 because the condition on line 197 was always true
198 yield c
199 else:
200 raise NotImplementedError("Parameter 'scope' is a class isn't supported yet.")
202 @classmethod
203 def GetClasses(cls, scope: Nullable[type | ModuleType] = None, subclassOf: Nullable[type] = None) -> Generator[TAttr, None, None]:
204 # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]:
205 """
206 Return a generator for all classes, where this attribute is attached to.
208 The resulting item stream can be filtered by:
209 * ``scope`` - when the item is a nested class in scope ``scope``.
210 * ``subclassOf`` - when the item is a subclass of ``subclassOf``.
212 :param scope: Optional, class or module the classes have to be nested in or defined in; ``None`` accepts every
213 class.
214 :param subclassOf: Optional, an attribute class or tuple thereof, to filter for that attribute type or subtype.
215 :returns: A sequence of classes where this attribute is attached to.
216 """
217 from pyTooling.Common import isnestedclass
219 if scope is None:
220 if subclassOf is None:
221 for c in cls._classes:
222 yield c
223 else:
224 for c in cls._classes:
225 if issubclass(c, subclassOf):
226 yield c
227 elif subclassOf is None:
228 if isinstance(scope, ModuleType):
229 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type))
230 for c in cls._classes:
231 if c in elementsInScope:
232 yield c
233 else:
234 for c in cls._classes:
235 if isnestedclass(c, scope):
236 yield c
237 else:
238 for c in cls._classes:
239 if isnestedclass(c, scope) and issubclass(c, subclassOf):
240 yield c
242 @classmethod
243 def GetMethods(cls, scope: Nullable[type] = None) -> Generator[TAttr, None, None]:
244 """
245 Return a generator for all methods, where this attribute is attached to.
247 The resulting item stream can be filtered by:
248 * ``scope`` - when the item is a nested class in scope ``scope``.
250 :param scope: Optional, class or module the methods' classes have to be nested in or defined in; ``None``
251 accepts every method.
252 :returns: A sequence of methods where this attribute is attached to.
253 """
254 if scope is None:
255 for c in cls._methods:
256 yield c
257 else:
258 for m in cls._methods:
259 if m.__classobj__ is scope:
260 yield m
262 @classmethod
263 def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> tuple[Attribute, ...]:
264 """
265 Returns attached attributes of this kind for a given method.
267 :param method: Method to search attributes for.
268 :param includeSubClasses: Optional, if ``True``, attributes of derived attribute classes are included too.
269 :returns: Tuple of attached attributes of this kind.
270 :raises TypeError: If the method's attribute field is not a list.
271 """
272 if hasattr(method, ATTRIBUTES_MEMBER_NAME):
273 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME)
274 if isinstance(attributes, list): 274 ↛ 277line 274 didn't jump to line 277 because the condition on line 274 was always true
275 return tuple(attribute for attribute in attributes if isinstance(attribute, cls))
276 else:
277 methodName = getFullyQualifiedName(method)
278 ex = TypeError(f"Method '{methodName}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's no list.")
279 ex.add_note(f"Got type '{getFullyQualifiedName(attributes)}'.")
280 raise ex
281 return tuple()
284@export
285class SimpleAttribute(Attribute):
286 """
287 A generic attribute preserving the parameters it was applied with.
289 It needs no derived class per use case: whatever is passed to it is available from :attr:`Args` and :attr:`KwArgs`,
290 which makes it the quickest way to mark a class, method or function and read the marking back.
291 """
292 _args: tuple[Any, ...] #: Positional parameters the attribute was applied with.
293 _kwargs: dict[str, Any] #: Named parameters the attribute was applied with.
295 def __init__(self, *args: Any, **kwargs: Any) -> None:
296 """
297 Initialize the attribute, preserving whatever parameters it was applied with.
299 :param args: Positional parameters, readable from :attr:`Args`.
300 :param kwargs: Named parameters, readable from :attr:`KwArgs`.
301 """
302 self._args = args
303 self._kwargs = kwargs
305 @readonly
306 def Args(self) -> tuple[Any, ...]:
307 """
308 Read-only property to access the positional parameters this attribute was created with (:attr:`_args`).
310 :returns: Tuple of positional parameters.
311 """
312 return self._args
314 @readonly
315 def KwArgs(self) -> dict[str, Any]:
316 """
317 Read-only property to access the named parameters this attribute was created with (:attr:`_kwargs`).
319 :returns: Dictionary of named parameters.
320 """
321 return self._kwargs