Coverage for pyTooling/Attributes/__init__.py: 93%
117 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +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 class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the
106 usage of that Attribute.
107 """
108 super().__init_subclass__(**kwargs)
109 cls._functions = []
110 cls._classes = []
111 cls._methods = []
113 # Make all classes derived from Attribute callable, so they can be used as a decorator.
114 def __call__(self, entity: Entity) -> Entity:
115 """
116 Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse
117 lookups.
119 :param entity: Entity (function, class, method), to attach an attribute to.
120 :returns: Same entity, with attached attribute.
121 :raises TypeError: If parameter 'entity' is not a function, class nor method.
122 """
123 self._AppendAttribute(entity, self)
125 return entity
127 @staticmethod
128 def _AppendAttribute(entity: Entity, attribute: Attribute) -> None:
129 """
130 Append an attribute to a language entity (class, method, function).
132 .. hint::
134 This method can be used in attribute groups to apply multiple attributes within ``__call__`` method.
136 .. code-block:: Python
138 class GroupAttribute(Attribute):
139 def __call__(self, entity: Entity) -> Entity:
140 self._AppendAttribute(entity, SimpleAttribute(...))
141 self._AppendAttribute(entity, SimpleAttribute(...))
143 return entity
145 :param entity: Entity, the attribute is attached to.
146 :param attribute: Attribute to attach.
147 :raises TypeError: If parameter 'entity' is not a class, method or function.
148 """
149 if isinstance(entity, MethodType): 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 attribute._methods.append(entity)
151 elif isinstance(entity, FunctionType):
152 attribute._functions.append(entity)
153 elif isinstance(entity, type): 153 ↛ 156line 153 didn't jump to line 156 because the condition on line 153 was always true
154 attribute._classes.append(entity)
155 else:
156 ex = TypeError(f"Parameter 'entity' is not a function, class nor method.")
157 ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.")
158 raise ex
160 if hasattr(entity, ATTRIBUTES_MEMBER_NAME):
161 getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute)
162 else:
163 setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ])
165 @readonly
166 def Scope(cls) -> AttributeScope:
167 """
168 Read-only property to access the scope this attribute searches in (:attr:`_scope`).
170 :returns: The scope this attribute searches in.
171 """
172 return cls._scope
174 @classmethod
175 def GetFunctions(cls, scope: Nullable[type] = None) -> Generator[TAttr, None, None]:
176 """
177 Return a generator for all functions, where this attribute is attached to.
179 The resulting item stream can be filtered by:
180 * ``scope`` - when the item is a nested class in scope ``scope``.
182 :param scope: Optional, module the functions have to be defined in; ``None`` accepts every function.
183 :returns: A sequence of functions where this attribute is attached to.
184 :raises NotImplementedError: If this abstract method is not overridden by a derived class.
185 """
186 if scope is None:
187 for c in cls._functions:
188 yield c
189 elif isinstance(scope, ModuleType):
190 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType))
191 for c in cls._functions:
192 if c in elementsInScope: 192 ↛ 191line 192 didn't jump to line 191 because the condition on line 192 was always true
193 yield c
194 else:
195 raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.")
197 @classmethod
198 def GetClasses(cls, scope: Nullable[type | ModuleType] = None, subclassOf: Nullable[type] = None) -> Generator[TAttr, None, None]:
199 # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]:
200 """
201 Return a generator for all classes, where this attribute is attached to.
203 The resulting item stream can be filtered by:
204 * ``scope`` - when the item is a nested class in scope ``scope``.
205 * ``subclassOf`` - when the item is a subclass of ``subclassOf``.
207 :param scope: Optional, class or module the classes have to be nested in or defined in; ``None`` accepts every
208 class.
209 :param subclassOf: Optional, an attribute class or tuple thereof, to filter for that attribute type or subtype.
210 :returns: A sequence of classes where this attribute is attached to.
211 """
212 from pyTooling.Common import isnestedclass
214 if scope is None:
215 if subclassOf is None:
216 for c in cls._classes:
217 yield c
218 else:
219 for c in cls._classes:
220 if issubclass(c, subclassOf):
221 yield c
222 elif subclassOf is None:
223 if isinstance(scope, ModuleType):
224 elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type))
225 for c in cls._classes:
226 if c in elementsInScope:
227 yield c
228 else:
229 for c in cls._classes:
230 if isnestedclass(c, scope):
231 yield c
232 else:
233 for c in cls._classes:
234 if isnestedclass(c, scope) and issubclass(c, subclassOf):
235 yield c
237 @classmethod
238 def GetMethods(cls, scope: Nullable[type] = None) -> Generator[TAttr, None, None]:
239 """
240 Return a generator for all methods, where this attribute is attached to.
242 The resulting item stream can be filtered by:
243 * ``scope`` - when the item is a nested class in scope ``scope``.
245 :param scope: Optional, class or module the methods' classes have to be nested in or defined in; ``None``
246 accepts every method.
247 :returns: A sequence of methods where this attribute is attached to.
248 """
249 if scope is None:
250 for c in cls._methods:
251 yield c
252 else:
253 for m in cls._methods:
254 if m.__classobj__ is scope:
255 yield m
257 @classmethod
258 def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> tuple[Attribute, ...]:
259 """
260 Returns attached attributes of this kind for a given method.
262 :param method: Method to search attributes for.
263 :param includeSubClasses: Optional, if ``True``, attributes of derived attribute classes are included too.
264 :returns: Tuple of attached attributes of this kind.
265 :raises TypeError:
266 """
267 if hasattr(method, ATTRIBUTES_MEMBER_NAME):
268 attributes = getattr(method, ATTRIBUTES_MEMBER_NAME)
269 if isinstance(attributes, list): 269 ↛ 272line 269 didn't jump to line 272 because the condition on line 269 was always true
270 return tuple(attribute for attribute in attributes if isinstance(attribute, cls))
271 else:
272 methodName = getFullyQualifiedName(method)
273 ex = TypeError(f"Method '{methodName}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's no list.")
274 ex.add_note(f"Got type '{getFullyQualifiedName(attributes)}'.")
275 raise ex
276 return tuple()
279@export
280class SimpleAttribute(Attribute):
281 """
282 A generic attribute preserving the parameters it was applied with.
284 It needs no derived class per use case: whatever is passed to it is available from :attr:`Args` and :attr:`KwArgs`,
285 which makes it the quickest way to mark a class, method or function and read the marking back.
286 """
287 _args: tuple[Any, ...] #: Positional parameters the attribute was applied with.
288 _kwargs: dict[str, Any] #: Named parameters the attribute was applied with.
290 def __init__(self, *args: Any, **kwargs: Any) -> None:
291 """
292 Initialize the attribute, preserving whatever parameters it was applied with.
294 :param args: Positional parameters, readable from :attr:`Args`.
295 :param kwargs: Named parameters, readable from :attr:`KwArgs`.
296 """
297 self._args = args
298 self._kwargs = kwargs
300 @readonly
301 def Args(self) -> tuple[Any, ...]:
302 """
303 Read-only property to access the positional parameters this attribute was created with (:attr:`_args`).
305 :returns: Tuple of positional parameters.
306 """
307 return self._args
309 @readonly
310 def KwArgs(self) -> dict[str, Any]:
311 """
312 Read-only property to access the named parameters this attribute was created with (:attr:`_kwargs`).
314 :returns: Dictionary of named parameters.
315 """
316 return self._kwargs