Source code for pyTooling.Attributes

# ==================================================================================================================== #
#             _____           _ _                  _   _   _        _ _           _                                    #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _     / \ | |_| |_ _ __(_) |__  _   _| |_ ___  ___                         #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` |   / _ \| __| __| '__| | '_ \| | | | __/ _ \/ __|                        #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ / ___ \ |_| |_| |  | | |_) | |_| | ||  __/\__ \                        #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/   \_\__|\__|_|  |_|_.__/ \__,_|\__\___||___/                        #
# |_|    |___/                          |___/                                                                          #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany                                                             #
# Copyright 2007-2016 Patrick Lehmann - Dresden, Germany                                                               #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
This Python module offers the base implementation of .NET-like attributes realized with class-based Python decorators.
This module comes also with a mixin-class to ease using classes having annotated methods.

The annotated data is stored as instances of :class:`~pyTooling.Attributes.Attribute` classes in an additional field per
class, method or function. By default, this field is called ``__pyattr__``.

.. hint:: See :ref:`high-level help <ATTR>` for explanations and usage examples.
"""
from enum   import IntFlag
from sys    import version_info
from types  import MethodType, FunctionType, ModuleType
from typing import Callable, List, TypeVar, Dict, Any, Iterable, Union, Type, Tuple, Generator, ClassVar, Optional as Nullable

try:
	from pyTooling.Decorators import export, readonly
	from pyTooling.Common     import getFullyQualifiedName
except (ImportError, ModuleNotFoundError):  # pragma: no cover
	print("[pyTooling.Attributes] Could not import from 'pyTooling.*'!")

	try:
		from Decorators import export, readonly
		from Common     import getFullyQualifiedName
	except (ImportError, ModuleNotFoundError) as ex:  # pragma: no cover
		print("[pyTooling.Attributes] Could not import directly!")
		raise ex


__all__ = ["Entity", "TAttr", "TAttributeFilter", "ATTRIBUTES_MEMBER_NAME"]

Entity = TypeVar("Entity", bound=Union[Type, Callable])
"""A type variable for functions, methods or classes."""

TAttr = TypeVar("TAttr", bound='Attribute')
"""A type variable for :class:`~pyTooling.Attributes.Attribute`."""

TAttributeFilter = Union[Type[TAttr], Iterable[Type[TAttr]], None]
"""A type hint for a predicate parameter that accepts either a single :class:`~pyTooling.Attributes.Attribute` or an
iterable of those."""

ATTRIBUTES_MEMBER_NAME: str = "__pyattr__"
"""Field name on entities (function, class, method) to store pyTooling.Attributes."""


[docs] @export class AttributeScope(IntFlag): """ An enumeration of possible entities an attribute can be applied to. Values of this enumeration can be merged (or-ed) if an attribute can be applied to multiple language entities. Supported language entities are: classes, methods or functions. Class fields or module variables are not supported. """ Class = 1 #: Attribute can be applied to classes. Method = 2 #: Attribute can be applied to methods. Function = 4 #: Attribute can be applied to functions. Any = Class + Method + Function #: Attribute can be applied to any language entity.
[docs] @export class Attribute: # (metaclass=ExtendedType, slots=True): """Base-class for all pyTooling attributes.""" # __AttributesMemberName__: ClassVar[str] = "__pyattr__" #: Field name on entities (function, class, method) to store pyTooling.Attributes. _functions: ClassVar[List[Any]] = [] #: List of functions, this Attribute was attached to. _classes: ClassVar[List[Any]] = [] #: List of classes, this Attribute was attached to. _methods: ClassVar[List[Any]] = [] #: List of methods, this Attribute was attached to. _scope: ClassVar[AttributeScope] = AttributeScope.Any #: Allowed language construct this attribute can be used with. # Ensure each derived class has its own instances of class variables. def __init_subclass__(cls, **kwargs: Any) -> None: """ Ensure each derived class has its own instance of ``_functions``, ``_classes`` and ``_methods`` to register the usage of that Attribute. """ super().__init_subclass__(**kwargs) cls._functions = [] cls._classes = [] cls._methods = [] # Make all classes derived from Attribute callable, so they can be used as a decorator.
[docs] def __call__(self, entity: Entity) -> Entity: """ Attributes get attached to an entity (function, class, method) and an index is updated at the attribute for reverse lookups. :param entity: Entity (function, class, method), to attach an attribute to. :returns: Same entity, with attached attribute. :raises TypeError: If parameter 'entity' is not a function, class nor method. """ self._AppendAttribute(entity, self) return entity
[docs] @staticmethod def _AppendAttribute(entity: Entity, attribute: "Attribute") -> None: """ Append an attribute to a language entity (class, method, function). .. hint:: This method can be used in attribute groups to apply multiple attributes within ``__call__`` method. .. code-block:: Python class GroupAttribute(Attribute): def __call__(self, entity: Entity) -> Entity: self._AppendAttribute(entity, SimpleAttribute(...)) self._AppendAttribute(entity, SimpleAttribute(...)) return entity :param entity: Entity, the attribute is attached to. :param attribute: Attribute to attach. :raises TypeError: If parameter 'entity' is not a class, method or function. """ if isinstance(entity, MethodType): attribute._methods.append(entity) elif isinstance(entity, FunctionType): attribute._functions.append(entity) elif isinstance(entity, type): attribute._classes.append(entity) else: ex = TypeError(f"Parameter 'entity' is not a function, class nor method.") if version_info >= (3, 11): # pragma: no cover ex.add_note(f"Got type '{getFullyQualifiedName(entity)}'.") raise ex if hasattr(entity, ATTRIBUTES_MEMBER_NAME): getattr(entity, ATTRIBUTES_MEMBER_NAME).insert(0, attribute) else: setattr(entity, ATTRIBUTES_MEMBER_NAME, [attribute, ])
@property def Scope(cls) -> AttributeScope: return cls._scope
[docs] @classmethod def GetFunctions(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: """ Return a generator for all functions, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. :param scope: Undocumented. :returns: A sequence of functions where this attribute is attached to. """ if scope is None: for c in cls._functions: yield c elif isinstance(scope, ModuleType): elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, FunctionType)) for c in cls._functions: if c in elementsInScope: yield c else: raise NotImplementedError(f"Parameter 'scope' is a class isn't supported yet.")
[docs] @classmethod def GetClasses(cls, scope: Union[Type, ModuleType, None] = None, subclassOf: Nullable[Type] = None) -> Generator[TAttr, None, None]: # def GetClasses(cls, scope: Nullable[Type] = None, predicate: Nullable[TAttributeFilter] = None) -> Generator[TAttr, None, None]: """ Return a generator for all classes, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. * ``subclassOf`` - when the item is a subclass of ``subclassOf``. :param scope: Undocumented. :param subclassOf: An attribute class or tuple thereof, to filter for that attribute type or subtype. :returns: A sequence of classes where this attribute is attached to. """ from pyTooling.Common import isnestedclass if scope is None: if subclassOf is None: for c in cls._classes: yield c else: for c in cls._classes: if issubclass(c, subclassOf): yield c elif subclassOf is None: if isinstance(scope, ModuleType): elementsInScope = set(c for c in scope.__dict__.values() if isinstance(c, type)) for c in cls._classes: if c in elementsInScope: yield c else: for c in cls._classes: if isnestedclass(c, scope): yield c else: for c in cls._classes: if isnestedclass(c, scope) and issubclass(c, subclassOf): yield c
[docs] @classmethod def GetMethods(cls, scope: Nullable[Type] = None) -> Generator[TAttr, None, None]: """ Return a generator for all methods, where this attribute is attached to. The resulting item stream can be filtered by: * ``scope`` - when the item is a nested class in scope ``scope``. :param scope: Undocumented. :returns: A sequence of methods where this attribute is attached to. """ if scope is None: for c in cls._methods: yield c else: for m in cls._methods: if m.__classobj__ is scope: yield m
[docs] @classmethod def GetAttributes(cls, method: MethodType, includeSubClasses: bool = True) -> Tuple['Attribute', ...]: """ Returns attached attributes of this kind for a given method. :param method: :param includeSubClasses: :return: :raises TypeError: """ if hasattr(method, ATTRIBUTES_MEMBER_NAME): attributes = getattr(method, ATTRIBUTES_MEMBER_NAME) if isinstance(attributes, list): return tuple(attribute for attribute in attributes if isinstance(attribute, cls)) else: raise TypeError(f"Method '{method.__class__.__name__}{method.__name__}' has a '{ATTRIBUTES_MEMBER_NAME}' field, but it's not a list of Attributes.") return tuple()
[docs] @export class SimpleAttribute(Attribute): _args: Tuple[Any, ...] _kwargs: Dict[str, Any]
[docs] def __init__(self, *args, **kwargs) -> None: self._args = args self._kwargs = kwargs
@readonly def Args(self) -> Tuple[Any, ...]: return self._args @readonly def KwArgs(self) -> Dict[str, Any]: return self._kwargs