Source code for pyTooling.MetaClasses

# ==================================================================================================================== #
#             _____           _ _               __  __      _         ____ _                                           #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  |  \/  | ___| |_ __ _ / ___| | __ _ ___ ___  ___  ___                   #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |\/| |/ _ \ __/ _` | |   | |/ _` / __/ __|/ _ \/ __|                  #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |  | |  __/ || (_| | |___| | (_| \__ \__ \  __/\__ \                  #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_|  |_|\___|\__\__,_|\____|_|\__,_|___/___/\___||___/                  #
# |_|    |___/                          |___/                                                                          #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#   Sven Köhler                                                                                                        #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2024 Patrick Lehmann - Bötzingen, 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                                                                                  #
# ==================================================================================================================== #
#
"""
The MetaClasses package implements Python meta-classes (classes to construct other classes in Python).

.. hint:: See :ref:`high-level help <META>` for explanations and usage examples.
"""
from functools  import wraps
# from inspect    import signature, Parameter
from threading  import Condition
from types      import FunctionType  #, MethodType
from typing     import Any, Tuple, List, Dict, Callable, Generator, Set, Iterator, Iterable, Union
from typing     import Type, TypeVar, Generic, _GenericAlias, ClassVar, Optional as Nullable

try:
	from pyTooling.Exceptions import ToolingException
	from pyTooling.Decorators import export
	from pyTooling.Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope
except (ImportError, ModuleNotFoundError):  # pragma: no cover
	print("[pyTooling.MetaClasses] Could not import from 'pyTooling.*'!")

	try:
		from Exceptions import ToolingException
		from Decorators import export
		from Attributes import ATTRIBUTES_MEMBER_NAME, AttributeScope
	except (ImportError, ModuleNotFoundError) as ex:  # pragma: no cover
		print("[pyTooling.MetaClasses] Could not import directly!")
		raise ex


__all__ = ["M"]

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

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


[docs] @export class ExtendedTypeError(ToolingException): """The exception is raised by the meta-class :class:`~pyTooling.Metaclasses.ExtendedType`."""
[docs] @export class BaseClassWithoutSlotsError(ExtendedTypeError): """ The exception is raised when a class using ``__slots__`` inherits from at-least one base-class not using ``__slots__``. .. seealso:: * :ref:`Python data model for slots <slots>` * :term:`Glossary entry __slots__ <__slots__>` """
[docs] @export class BaseClassWithNonEmptySlotsError(ExtendedTypeError): pass
[docs] @export class BaseClassIsNotAMixinError(ExtendedTypeError): pass
[docs] @export class DuplicateFieldInSlotsError(ExtendedTypeError): pass
[docs] @export class AbstractClassError(ExtendedTypeError): """ The exception is raised, when a class contains methods marked with *abstractmethod* or *mustoverride*. .. seealso:: :func:`@abstractmethod <pyTooling.MetaClasses.abstractmethod>` |rarr| Mark a method as *abstract*. :func:`@mustoverride <pyTooling.MetaClasses.mustoverride>` |rarr| Mark a method as *must overrride*. :exc:`~MustOverrideClassError` |rarr| Exception raised, if a method is marked as *must-override*. """
[docs] @export class MustOverrideClassError(AbstractClassError): """ The exception is raised, when a class contains methods marked with *must-override*. .. seealso:: :func:`@abstractmethod <pyTooling.MetaClasses.abstractmethod>` |rarr| Mark a method as *abstract*. :func:`@mustoverride <pyTooling.MetaClasses.mustoverride>` |rarr| Mark a method as *must overrride*. :exc:`~AbstractClassError` |rarr| Exception raised, if a method is marked as *abstract*. """
# """ # Metaclass that allows multiple dispatch of methods based on method signatures. # # .. seealso: # # `Python Cookbook - Multiple dispatch with function annotations <https://GitHub.com/dabeaz/python-cookbook/blob/master/src/9/multiple_dispatch_with_function_annotations/example1.py?ts=2>`__ # """ M = TypeVar("M", bound=Callable) #: A type variable for methods.
[docs] @export def slotted(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError(f"Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, slots=True)
[docs] @export def mixin(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError(f"Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, mixin=True)
[docs] @export def singleton(cls): if cls.__class__ is type: metacls = ExtendedType elif issubclass(cls.__class__, ExtendedType): metacls = cls.__class__ for method in cls.__methods__: delattr(method, "__classobj__") else: raise ExtendedTypeError(f"Class uses an incompatible meta-class.") # FIXME: create exception for it? bases = tuple(base for base in cls.__bases__ if base is not object) slots = cls.__dict__["__slots__"] if "__slots__" in cls.__dict__ else tuple() members = { "__qualname__": cls.__qualname__ } for key, value in cls.__dict__.items(): if key not in slots: members[key] = value return metacls(cls.__name__, bases, members, singleton=True)
[docs] @export def abstractmethod(method: M) -> M: """ Mark a method as *abstract* and replace the implementation with a new method raising a :exc:`NotImplementedError`. The original method is stored in ``<method>.__wrapped__`` and it's doc-string is copied to the replacing method. In additional field ``<method>.__abstract__`` is added. .. warning:: This decorator should be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.AbstractClassError` at instantiation. .. admonition:: ``example.py`` .. code-block:: python class Data(mataclass=ExtendedType): @abstractmethod def method(self) -> bool: '''This method needs to be implemented''' :param method: Method that is marked as *abstract*. :returns: Replacement method, which raises a :exc:`NotImplementedError`. .. seealso:: * :exc:`~pyTooling.Exceptions.AbstractClassError` * :func:`~pyTooling.Metaclasses.mustoverride` * :func:`~pyTooling.Metaclasses.notimplemented` """ @wraps(method) def func(self): raise NotImplementedError(f"Method '{method.__name__}' is abstract and needs to be overridden in a derived class.") func.__abstract__ = True return func
[docs] @export def mustoverride(method: M) -> M: """ Mark a method as *must-override*. The returned function is the original function, but with an additional field ``<method>.____mustOverride__``, so a meta-class can identify a *must-override* method and raise an error. Such an error is not raised if the method is overridden by an inheriting class. A *must-override* methods can offer a partial implementation, which is called via ``super()...``. .. warning:: This decorator needs to be used in combination with meta-class :class:`~pyTooling.Metaclasses.ExtendedType`. Otherwise, an abstract class itself doesn't throw a :exc:`~pyTooling.Exceptions.MustOverrideClassError` at instantiation. .. admonition:: ``example.py`` .. code-block:: python class Data(mataclass=ExtendedType): @mustoverride def method(self): '''This is a very basic implementation''' :param method: Method that is marked as *must-override*. :returns: Same method, but with additional ``<method>.__mustOverride__`` field. .. seealso:: * :exc:`~pyTooling.Exceptions.MustOverrideClassError` * :func:`~pyTooling.Metaclasses.abstractmethod` * :func:`~pyTooling.Metaclasses.notimplemented` """ method.__mustOverride__ = True return method
# @export # def overloadable(method: M) -> M: # method.__overloadable__ = True # return method # @export # class DispatchableMethod: # """Represents a single multimethod.""" # # _methods: Dict[Tuple, Callable] # __name__: str # __slots__ = ("_methods", "__name__") # # def __init__(self, name: str) -> None: # self.__name__ = name # self._methods = {} # # def __call__(self, *args: Any): # """Call a method based on type signature of the arguments.""" # types = tuple(type(arg) for arg in args[1:]) # meth = self._methods.get(types, None) # if meth: # return meth(*args) # else: # raise TypeError(f"No matching method for types {types}.") # # def __get__(self, instance, cls): # Starting with Python 3.11+, use typing.Self as return type # """Descriptor method needed to make calls work in a class.""" # if instance is not None: # return MethodType(self, instance) # else: # return self # # def register(self, method: Callable) -> None: # """Register a new method as a dispatchable.""" # # # Build a signature from the method's type annotations # sig = signature(method) # types: List[Type] = [] # # for name, parameter in sig.parameters.items(): # if name == "self": # continue # # if parameter.annotation is Parameter.empty: # raise TypeError(f"Parameter '{name}' in method '{method.__name__}' must be annotated with a type.") # # if not isinstance(parameter.annotation, type): # raise TypeError(f"Parameter '{name}' in method '{method.__name__}' annotation must be a type.") # # if parameter.default is not Parameter.empty: # self._methods[tuple(types)] = method # # types.append(parameter.annotation) # # self._methods[tuple(types)] = method # @export # class DispatchDictionary(dict): # """Special dictionary to build dispatchable methods in a metaclass.""" # # def __setitem__(self, key: str, value: Any): # if callable(value) and key in self: # # If key already exists, it must be a dispatchable method or callable # currentValue = self[key] # if isinstance(currentValue, DispatchableMethod): # currentValue.register(value) # else: # dispatchable = DispatchableMethod(key) # dispatchable.register(currentValue) # dispatchable.register(value) # # super().__setitem__(key, dispatchable) # else: # super().__setitem__(key, value)
[docs] @export class ExtendedType(type): """ An updates meta-class to construct new classes with an extended feature set. .. todo:: META::ExtendedType Needs documentation. .. todo:: META::ExtendedType allow __dict__ and __weakref__ if slotted is enabled Features: * Store object members more efficiently in ``__slots__`` instead of ``_dict__``. * Allow only a single instance to be created (:term:`singleton`). * Define methods as :term:`abstract <abstract method>` or :term:`must-override <mustoverride method>` and prohibit instantiation of :term:`abstract classes <abstract class>`. .. #* Allow method overloading and dispatch overloads based on argument signatures. """ # @classmethod # def __prepare__(cls, className, baseClasses, slots: bool = False, mixin: bool = False, singleton: bool = False): # return DispatchDictionary()
[docs] def __new__(self, className: str, baseClasses: Tuple[type], members: Dict[str, Any], slots: bool = False, mixin: bool = False, singleton: bool = False) -> "ExtendedType": """ Construct a new class using this :term:`meta-class`. :param className: The name of the class to construct. :param baseClasses: The tuple of :term:`base-classes <base-class>` the class is derived from. :param members: The dictionary of members for the constructed class. :param slots: If true, store object attributes in :term:`__slots__ <slots>` instead of ``__dict__``. :param mixin: If true, make the class a :term:`Mixin-Class`. If false, create slots if ``slots`` is true. If none, preserve behavior of primary base-class. :param singleton: If true, make the class a :term:`Singleton`. :returns: The new class. :raises AttributeError: If base-class has no '__slots__' attribute. :raises AttributeError: If slot already exists in base-class. """ # Inherit 'slots' feature from primary base-class if len(baseClasses) > 0: primaryBaseClass = baseClasses[0] if isinstance(primaryBaseClass, self): slots = primaryBaseClass.__slotted__ # Compute slots and mixin-slots from annotated fields as well as class- and object-fields with initial values. classFields, objectFields = self._computeSlots(className, baseClasses, members, slots, mixin) # Compute abstract methods abstractMethods, members = self._checkForAbstractMethods(baseClasses, members) # Create a new class newClass = type.__new__(self, className, baseClasses, members) # Apply class fields for fieldName, typeAnnotation in classFields.items(): setattr(newClass, fieldName, typeAnnotation) # Search in inheritance tree for abstract methods newClass.__abstractMethods__ = abstractMethods newClass.__isAbstract__ = self._wrapNewMethodIfAbstract(newClass) newClass.__isSingleton__ = self._wrapNewMethodIfSingleton(newClass, singleton) # Check for inherited class attributes attributes = [] setattr(newClass, ATTRIBUTES_MEMBER_NAME, attributes) for base in baseClasses: if hasattr(base, ATTRIBUTES_MEMBER_NAME): pyAttr = getattr(base, ATTRIBUTES_MEMBER_NAME) for att in pyAttr: if AttributeScope.Class in att.Scope: attributes.append(att) att.__class__._classes.append(newClass) # Check methods for attributes methods, methodsWithAttributes = self._findMethods(newClass, baseClasses, members) # Add new fields for found methods newClass.__methods__ = tuple(methods) newClass.__methodsWithAttributes__ = tuple(methodsWithAttributes) # Additional methods on a class def HasClassAttributes(self) -> bool: """ Check if class has Attributes. :return: ``True``, if the class has Attributes. """ try: return len(self.__pyattr__) > 0 except AttributeError: return False def HasMethodAttributes(self) -> bool: """ Check if class has any method with Attributes. :return: ``True``, if the class has any method with Attributes. """ try: return len(self.__methodsWithAttributes__) > 0 except AttributeError: return False def GetMethodsWithAttributes(self, predicate: Nullable[TAttributeFilter[TAttr]] = None) -> Dict[Callable, Tuple["Attribute", ...]]: """ :param predicate: :return: :raises ValueError: :raises ValueError: """ try: from ..Attributes import Attribute except (ImportError, ModuleNotFoundError): # pragma: no cover try: from Attributes import Attribute except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover raise ex if predicate is None: predicate = Attribute elif isinstance(predicate, Iterable): for attribute in predicate: if not issubclass(attribute, Attribute): raise ValueError(f"Parameter 'predicate' contains an element which is not a sub-class of 'Attribute'.") predicate = tuple(predicate) elif not issubclass(predicate, Attribute): raise ValueError(f"Parameter 'predicate' is not a sub-class of 'Attribute'.") methodAttributePairs = {} for method in newClass.__methodsWithAttributes__: matchingAttributes = [] for attribute in method.__pyattr__: if isinstance(attribute, predicate): matchingAttributes.append(attribute) if len(matchingAttributes) > 0: methodAttributePairs[method] = tuple(matchingAttributes) return methodAttributePairs newClass.HasClassAttributes = classmethod(property(HasClassAttributes, doc=HasClassAttributes.__doc__)) newClass.HasMethodAttributes = classmethod(property(HasMethodAttributes, doc=HasMethodAttributes.__doc__)) newClass.GetMethodsWithAttributes = classmethod(GetMethodsWithAttributes) GetMethodsWithAttributes.__qualname__ = f"{className}.{GetMethodsWithAttributes.__name__}" # GetMethods(predicate) -> dict[method, list[attribute]] / generator # GetClassAtrributes -> list[attributes] / generator # MethodHasAttributes(predicate) -> bool # GetAttribute return newClass
@classmethod def _findMethods(self, newClass: "ExtendedType", baseClasses: Tuple[type], members: Dict[str, Any]): try: from ..Attributes import Attribute except (ImportError, ModuleNotFoundError): # pragma: no cover try: from Attributes import Attribute except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover raise ex # Embedded bind function due to circular dependencies. def bind(instance: object, func: FunctionType, methodName: Nullable[str] = None): if methodName is None: methodName = func.__name__ boundMethod = func.__get__(instance, instance.__class__) setattr(instance, methodName, boundMethod) return boundMethod methods = [] methodsWithAttributes = [] attributeIndex = {} for base in baseClasses: if hasattr(base, "__methodsWithAttributes__"): methodsWithAttributes.extend(base.__methodsWithAttributes__) for memberName, member in members.items(): if isinstance(member, FunctionType): method = newClass.__dict__[memberName] if hasattr(method, "__classobj__") and getattr(method, "__classobj__") is not newClass: raise TypeError(f"Method '{memberName}' is used by multiple classes: {method.__classobj__} and {newClass}.") else: setattr(method, "__classobj__", newClass) def GetAttributes(inst: Any, predicate: Nullable[Type[Attribute]] = None) -> Tuple[Attribute, ...]: results = [] try: for attribute in inst.__pyattr__: # type: Attribute if isinstance(attribute, predicate): results.append(attribute) return tuple(results) except AttributeError: return tuple() method.GetAttributes = bind(method, GetAttributes) methods.append(method) # print(f" convert function: '{memberName}' to method") # print(f" {member}") if "__pyattr__" in member.__dict__: attributes = member.__pyattr__ # type: List[Attribute] if isinstance(attributes, list) and len(attributes) > 0: methodsWithAttributes.append(member) for attribute in attributes: attribute._functions.remove(method) attribute._methods.append(method) # print(f" attributes: {attribute.__class__.__name__}") if attribute not in attributeIndex: attributeIndex[attribute] = [member] else: attributeIndex[attribute].append(member) # else: # print(f" But has no attributes.") # else: # print(f" ?? {memberName}") return methods, methodsWithAttributes @classmethod def _computeSlots(self, className, baseClasses, members, slots, mixin): # Compute which field are listed in __slots__ and which need to be initialized in an instance or class. slottedFields = [] objectFields = {} classFields = {} if slots or mixin: # If slots are used, all base classes must use __slots__. for baseClass in self._iterateBaseClasses(baseClasses): # Exclude object as a special case if baseClass is object or baseClass is Generic: continue if not hasattr(baseClass, "__slots__"): ex = BaseClassWithoutSlotsError(f"Base-classes '{baseClass.__name__}' doesn't use '__slots__'.") ex.add_note(f"All base-classes of a class using '__slots__' must use '__slots__' itself.") raise ex # FIXME: should have a check for non-empty slots on secondary base-classes too # Copy all field names from primary base-class' __slots__, which are later needed for error checking. inheritedSlottedFields = {} if len(baseClasses) > 0: for base in reversed(baseClasses[0].mro()): # Exclude object as a special case if base is object or base is Generic: continue for annotation in base.__slots__: inheritedSlottedFields[annotation] = base # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. annotations: Dict[str, Any] = members.get("__annotations__", {}) for fieldName, typeAnnotation in annotations.items(): if fieldName in inheritedSlottedFields: cls = inheritedSlottedFields[fieldName] raise AttributeError(f"Slot '{fieldName}' already exists in base-class '{cls.__module__}.{cls.__name__}'.") # If annotated field is a ClassVar, and it has an initial value # * copy field and initial value to classFields dictionary # * remove field from members if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: classFields[fieldName] = members[fieldName] del members[fieldName] # If an annotated field has an initial value # * copy field and initial value to objectFields dictionary # * remove field from members elif fieldName in members: slottedFields.append(fieldName) objectFields[fieldName] = members[fieldName] del members[fieldName] else: slottedFields.append(fieldName) mixinSlots = self._aggregateMixinSlots(className, baseClasses) else: # When adding annotated fields to slottedFields, check if name was not used in inheritance hierarchy. annotations: Dict[str, Any] = members.get("__annotations__", {}) for fieldName, typeAnnotation in annotations.items(): # If annotated field is a ClassVar, and it has an initial value # * copy field and initial value to classFields dictionary # * remove field from members if isinstance(typeAnnotation, _GenericAlias) and typeAnnotation.__origin__ is ClassVar and fieldName in members: classFields[fieldName] = members[fieldName] del members[fieldName] # FIXME: search for fields without annotation if mixin: mixinSlots.extend(slottedFields) members["__slotted__"] = True members["__slots__"] = tuple() members["__isMixin__"] = True members["__mixinSlots__"] = tuple(mixinSlots) elif slots: slottedFields.extend(mixinSlots) members["__slotted__"] = True members["__slots__"] = tuple(slottedFields) members["__isMixin__"] = False members["__mixinSlots__"] = tuple() else: members["__slotted__"] = False # NO __slots__ members["__isMixin__"] = False members["__mixinSlots__"] = tuple() return classFields, objectFields @classmethod def _aggregateMixinSlots(self, className, baseClasses): mixinSlots = [] if len(baseClasses) > 0: # If class has base-classes ensure only the primary inheritance path uses slots and all secondary inheritance # paths have an empty slots tuple. Otherwise, raise a BaseClassWithNonEmptySlotsError. inheritancePaths = [path for path in self._iterateBaseClassPaths(baseClasses)] primaryInharitancePath: Set[type] = set(inheritancePaths[0]) for typePath in inheritancePaths[1:]: for t in typePath: if hasattr(t, "__slots__") and len(t.__slots__) != 0 and t not in primaryInharitancePath: ex = BaseClassWithNonEmptySlotsError(f"Base-class '{t.__name__}' has non-empty __slots__ and can't be used as a direct or indirect base-class for '{className}'.") ex.add_note(f"In Python, only one inheritance branch can use non-empty __slots__.") # ex.add_note(f"With ExtendedType, only the primary base-class can use non-empty __slots__.") # ex.add_note(f"Secondary base-classes should be marked as mixin-classes.") raise ex # If current class is set to be a mixin, then aggregate all mixinSlots in a list. # Ensure all base-classes are either constructed # * by meta-class ExtendedType, or # * use no slots, or # * are typing.Generic # If it was constructed by ExtendedType, then ensure this class itself is a mixin-class. for baseClass in baseClasses: # type: ExtendedType if isinstance(baseClass, _GenericAlias) and baseClass.__origin__ is Generic: pass elif baseClass.__class__ is self and baseClass.__isMixin__: mixinSlots.extend(baseClass.__mixinSlots__) elif hasattr(baseClass, "__mixinSlots__"): mixinSlots.extend(baseClass.__mixinSlots__) return mixinSlots @classmethod def _iterateBaseClasses(metacls, baseClasses: Tuple[type]) -> Generator[type, None, None]: if len(baseClasses) == 0: return visited: Set[type] = set() iteratorStack: List[Iterator[type]] = list() for baseClass in baseClasses: yield baseClass visited.add(baseClass) iteratorStack.append(iter(baseClass.__bases__)) while True: try: base = next(iteratorStack[-1]) # type: type if base not in visited: yield base if len(base.__bases__) > 0: iteratorStack.append(iter(base.__bases__)) else: continue except StopIteration: iteratorStack.pop() if len(iteratorStack) == 0: break
[docs] @classmethod def _iterateBaseClassPaths(metacls, baseClasses: Tuple[type]) -> Generator[Tuple[type, ...], None, None]: """ Return a generator to iterate all possible inheritance paths for a given list of base-classes. An inheritance path is expressed as a tuple of base-classes from current base-class (left-most item) to :class:`object` (right-most item). :param baseClasses: List (tuple) of base-classes. :returns: Generator to iterate all inheritance paths. An inheritance path is a tuple of types (base-classes). """ if len(baseClasses) == 0: return typeStack: List[type] = list() iteratorStack: List[Iterator[type]] = list() for baseClass in baseClasses: typeStack.append(baseClass) iteratorStack.append(iter(baseClass.__bases__)) while True: try: base = next(iteratorStack[-1]) # type: type typeStack.append(base) if len(base.__bases__) == 0: yield tuple(typeStack) typeStack.pop() else: iteratorStack.append(iter(base.__bases__)) except StopIteration: typeStack.pop() iteratorStack.pop() if len(typeStack) == 0: break
[docs] @classmethod def _checkForAbstractMethods(metacls, baseClasses: Tuple[type], members: Dict[str, Any]) -> Tuple[Dict[str, Callable], Dict[str, Any]]: """ Check if the current class contains abstract methods and return a tuple of them. These abstract methods might be inherited from any base-class. If there are inherited abstract methods, check if they are now implemented (overridden) by the current class that's right now constructed. :param baseClasses: The tuple of :term:`base-classes <base-class>` the class is derived from. :param members: The dictionary of members for the constructed class. :returns: A tuple of abstract method's names. """ abstractMethods = {} if baseClasses: # Aggregate all abstract methods from all base-classes. for baseClass in baseClasses: if hasattr(baseClass, "__abstractMethods__"): abstractMethods.update(baseClass.__abstractMethods__) for base in baseClasses: for key, value in base.__dict__.items(): if (key in abstractMethods and isinstance(value, FunctionType) and not (hasattr(value, "__abstract__") or hasattr(value, "__mustOverride__"))): def outer(method): @wraps(method) def inner(cls, *args: Any, **kwargs: Any): return method(cls, *args, **kwargs) return inner members[key] = outer(value) # Check if methods are marked: # * If so, add them to list of abstract methods # * If not, method is now implemented and removed from list for memberName, member in members.items(): if callable(member): if ((hasattr(member, "__abstract__") and member.__abstract__) or (hasattr(member, "__mustOverride__") and member.__mustOverride__)): abstractMethods[memberName] = member elif memberName in abstractMethods: del abstractMethods[memberName] return abstractMethods, members
[docs] @classmethod def _wrapNewMethodIfSingleton(metacls, newClass, singleton: bool) -> bool: """ If a class is a singleton, wrap the ``_new__`` method, so it returns a cached object, if a first object was created. Only the first object creation initializes the object. This implementation is threadsafe. :param newClass: The newly constructed class for further modifications. :param singleton: If ``True``, the class allows only a single instance to exist. :returns: ``True``, if the class is a singleton. """ if hasattr(newClass, "__isSingleton__"): singleton = newClass.__isSingleton__ if singleton: oldnew = newClass.__new__ if hasattr(oldnew, "__singleton_wrapper__"): oldnew = oldnew.__wrapped__ oldinit = newClass.__init__ if hasattr(oldinit, "__singleton_wrapper__"): oldinit = oldinit.__wrapped__ @wraps(oldnew) def singleton_new(cls, *args: Any, **kwargs: Any): with cls.__singletonInstanceCond__: if cls.__singletonInstanceCache__ is None: obj = oldnew(cls, *args, **kwargs) cls.__singletonInstanceCache__ = obj else: obj = cls.__singletonInstanceCache__ return obj @wraps(oldinit) def singleton_init(self, *args: Any, **kwargs: Any): cls = self.__class__ cv = cls.__singletonInstanceCond__ with cv: if cls.__singletonInstanceInit__: oldinit(self, *args, **kwargs) cls.__singletonInstanceInit__ = False cv.notify_all() elif args or kwargs: raise ValueError(f"A further instance of a singleton can't be reinitialized with parameters.") else: while cls.__singletonInstanceInit__: cv.wait() singleton_new.__singleton_wrapper__ = True singleton_init.__singleton_wrapper__ = True newClass.__new__ = singleton_new newClass.__init__ = singleton_init newClass.__singletonInstanceCond__ = Condition() newClass.__singletonInstanceInit__ = True newClass.__singletonInstanceCache__ = None return True return False
[docs] @classmethod def _wrapNewMethodIfAbstract(metacls, newClass) -> bool: """ If the class has abstract methods, replace the ``_new__`` method, so it raises an exception. :param newClass: The newly constructed class for further modifications. :returns: ``True``, if the class is abstract. :raises AbstractClassError: If the class is abstract and can't be instantiated. """ # Replace '__new__' by a variant to throw an error on not overridden methods if len(newClass.__abstractMethods__) > 0: oldnew = newClass.__new__ if hasattr(oldnew, "__raises_abstract_class_error__"): oldnew = oldnew.__wrapped__ @wraps(oldnew) def abstract_new(cls, *_, **__): raise AbstractClassError(f"""Class '{cls.__name__}' is abstract. The following methods: '{"', '".join(newClass.__abstractMethods__)}' need to be overridden in a derived class.""") abstract_new.__raises_abstract_class_error__ = True newClass.__new__ = abstract_new return True # Handle classes which are not abstract, especially derived classes, if not abstract anymore else: # skip intermediate 'new' function if class isn't abstract anymore try: if newClass.__new__.__raises_abstract_class_error__: origNew = newClass.__new__.__wrapped__ # WORKAROUND: __new__ checks tp_new and implements different behavior # Bugreport: https://github.com/python/cpython/issues/105888 if origNew is object.__new__: @wraps(object.__new__) def wrapped_new(inst, *_, **__): return object.__new__(inst) newClass.__new__ = wrapped_new else: newClass.__new__ = origNew elif newClass.__new__.__isSingleton__: raise Exception(f"Found a singleton wrapper around an AbstractError raising method. This case is not handled yet.") except AttributeError as ex: # WORKAROUND: # AttributeError.name was added in Python 3.10. For version <3.10 use a string contains operation. try: if ex.name != "__raises_abstract_class_error__": raise ex except AttributeError: if "__raises_abstract_class_error__" not in str(ex): raise ex return False
[docs] @export class SlottedObject(metaclass=ExtendedType, slots=True): """Classes derived from this class will store all members in ``__slots__``."""