Source code for oop_ext.interface._interface

# mypy: disallow-untyped-defs
# mypy: disallow-any-decorated
"""
This module provides a basic interface concept.

To use, create an interface:


class IMyCalculator(Interface):

    def Sum(self, *values):
        ...


Have classes implement an interface:

class MyCalculatorImpl(object):

    ImplementsInterface(IMyCalculator)

    ...


Then, to check an interface:


impl = MyCalculatorImpl()

if IsImplementation(impl, IMyCalculator):
    ...


Or if it *needs* to check an interface:

AssertImplements(impl, IMyCalculator)


"""
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import Dict
from typing import FrozenSet
from typing import Generic
from typing import List
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
from typing import cast

import inspect
import sys
from contextlib import suppress
from functools import lru_cache

from oop_ext.foundation.cached_method import ImmutableParamsCachedMethod
from oop_ext.foundation.decorators import Deprecated
from oop_ext.foundation.is_frozen import IsDevelopment
from oop_ext.foundation.types_ import Method

if TYPE_CHECKING:
    from traceback import StackSummary


# TypeCheckingSupport acts as a subclass during type checking so the type-checker
# recognizes Interface subclasses as Protocol implementations.
# When subclassing a Protocol, the subclass must also explicitly subclass Protocol again
# for the type checker to recognize them:
#
# https://www.python.org/dev/peps/pep-0544/#merging-and-extending-protocols
#
# Quote:
# Subclassing a protocol class would not turn the subclass into a protocol unless
# it also has typing.Protocol as an explicit base class.
#
# We declare TypeCheckingSupport as an alias to Protocol only during type checking,
# because Protocol is a complex meta class and brings a bunch of problems when trying to use
# it as an Interface superclass, namely declaring new methods
# (__init__, __init_subclass__, __getitem__, etc) which our Interface code now assumes
# should be part of the interface.
#
# By using a custom alias, only really enabled during type-checking, we hide that detail
# from users and avoid the runtime problems.
if TYPE_CHECKING:
    from typing_extensions import Protocol as TypeCheckingSupport
else:
    TypeCheckingSupport = object


class InterfaceError(RuntimeError):
    pass


class BadImplementationError(InterfaceError):
    pass


# InterfaceType should be changed to ``Type[Interface]`` after https://github.com/python/mypy/issues/5374
# is fixed.
InterfaceType = Type[Any]


class InterfaceImplementationMetaClass(type):
    def __new__(cls, name: str, bases: Tuple, dct: Dict) -> Any:
        C = type.__new__(cls, name, bases, dct)
        if IsDevelopment():  # Only doing check in dev mode.
            for I in dct.get("__implements__", []):
                # Will do full checking this first time, and also cache the results
                AssertImplements(C, I)
        return C


T = TypeVar("T")


class InterfaceImplementorStub(Generic[T]):
    """
    A helper for acting as a stub for some object (in this way, we're only able to access
    attributes declared directly in the interface.

    It forwards the calls to the actual implementor (the wrapped object)
    """

    def __init__(self, wrapped: T, implemented_interface: Type["Interface"]) -> None:
        self.__wrapped = wrapped
        self.__implemented_interface = implemented_interface

        (
            self.__interface_methods,
            self.__attrs,
        ) = cache_interface_attrs.GetInterfaceMethodsAndAttrs(implemented_interface)

    def GetWrappedFromImplementorStub(self) -> T:
        """
        Really big and awkward name because we don't want name-clashes
        """
        return self.__wrapped

    def __getattr__(self, attr: str) -> Any:
        if attr not in self.__attrs and attr not in self.__interface_methods:
            raise AttributeError(
                "Error. The interface {} does not have the attribute '{}' declared.".format(
                    self.__implemented_interface, attr
                )
            )
        return getattr(self.__wrapped, attr)

    def __getitem__(self, *args: Any, **kwargs: Any) -> Any:
        if "__getitem__" not in self.__interface_methods:
            raise AttributeError(
                "Error. The interface {} does not have the attribute '{}' declared.".format(
                    self.__implemented_interface, "__getitem__"
                )
            )
        return self.__wrapped.__getitem__(*args, **kwargs)  # type:ignore[index]

    def __setitem__(self, *args: Any, **kwargs: Any) -> Any:
        if "__setitem__" not in self.__interface_methods:
            raise AttributeError(
                "Error. The interface {} does not have the attribute '{}' declared.".format(
                    self.__implemented_interface, "__setitem__"
                )
            )
        return self.__wrapped.__setitem__(*args, **kwargs)  # type:ignore[index]

    def __repr__(self) -> str:
        return "<InterfaceImplementorStub %s>" % self.__wrapped

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        if "__call__" not in self.__interface_methods:
            raise AttributeError(
                "Error. The interface {} does not have the attribute '{}' declared.".format(
                    self.__implemented_interface, "__call__"
                )
            )
        return self.__wrapped.__call__(*args, **kwargs)  # type:ignore[operator]


# Instance to check if we are receiving an argument during Interface.__new__
_SENTINEL = object()


[docs] class Interface(TypeCheckingSupport): """Base class for interfaces. A interface describes a behavior that some objects must implement. **TypeCheckingSupport** .. versionadded:: 1.1.0 Interfaces that which to support static type checkers such as ``mypy`` also need to subclass from this class: .. code-block:: python from oop_ext.interface import Interface, TypeCheckingSupport class IDataSaver(Interface, TypeCheckingSupport): ... The ``TypeCheckingSupport`` exists solely for the benefit of type checkers, and has zero runtime cost associated with it. """ def __new__(cls, class_: Any = _SENTINEL) -> Any: """ The __new__ method of an interface has two behaviors: 1. Receiving a class In this case the class is verified to implement the interface, and returned unchanged. 2. Receiving an instance In this case the instance is verified to implement the interface, and an ``InterfaceImplementorStub`` is returned. A note about type checking: mypy has restrictions about what __new__ can do (it must return an instance of the class), and this is violated here, so we can't really use type checking in __new__ for the cases used by Interface. """ # if no class is given, raise InterfaceError('trying to instantiate interface') # check if class_or_object implements this interface from ._adaptable_interface import IAdaptable if class_ is _SENTINEL: raise InterfaceError("Can't instantiate Interface.") else: if isinstance(class_, type): # We're doing something as Interface(InterfaceImpl) -- not instancing _AssertImplementsFullChecking(class_, cls, check_attr=False) return class_ elif isinstance(class_, InterfaceImplementorStub): return class_ else: implemented_interfaces = GetImplementedInterfaces(class_) if cls in implemented_interfaces: return InterfaceImplementorStub(class_, cls) elif IAdaptable in implemented_interfaces: adapter = class_.GetAdapter(cls) if adapter is not None: return InterfaceImplementorStub(adapter, cls) # We're doing something as Interface(InterfaceImpl()) -- instancing _AssertImplementsFullChecking(class_, cls, check_attr=True) return InterfaceImplementorStub(class_, cls) if not TYPE_CHECKING: @classmethod def __init_subclass__(cls, **kwargs: object) -> None: for name in dir(cls): obj = getattr(cls, name) if _IsMethod(obj): sig = inspect.signature(obj) try: hash(sig) except TypeError: raise TypeError( f"Method {cls.__name__}.{name} contains unhashable arguments:\n{sig}" ) from None
def _GetClassForInterfaceChecking(class_or_instance: Any) -> Type: if _IsClass(class_or_instance): return class_or_instance # is class elif isinstance(class_or_instance, InterfaceImplementorStub): return _GetClassForInterfaceChecking( class_or_instance.GetWrappedFromImplementorStub() ) return class_or_instance.__class__ # is instance def _IsClass(obj: object) -> bool: return isinstance(obj, type) if TYPE_CHECKING: def _CheckIsInterfaceSubclass(interface: Any) -> None: pass else: def _CheckIsInterfaceSubclass(interface: Any) -> None: """ Checks that the given class is an Interface subclass. Note: during type-checking this is a no-op, because during type checking Interface subclasses Protocol, however issubclass will only work in Protocol subclasses decorated with @runtime_checkable; given this is not really used during type-checking, a no-op during type checking seems a good solution. """ # noinspection PyProtocol is_interface = issubclass(interface, Interface) if not is_interface: raise InterfaceError( "To check against an interface, an interface is required (received: %s -- mro:%s)" % (interface, interface.__mro__) ) def IsImplementation( class_or_instance: Any, interface: InterfaceType, *, requires_declaration: bool = True, ) -> bool: """ :param class_or_instance: type or classobj or object :param interface: the interface type to check. :param requires_declaration: If `True`, the Interface must have been explicitly declared through :py:func:`ImplementsInterface` for `class_or_interface` to be considered an implementation of `interface`. Otherwise it'd only check if `class_or_interface` has all methods defined on `interface`. Note that `class_or_instance` attributes are never evaluated. Alternatively, it's possible to use :py:func:`DeclareClassImplements` from outside the class in order to tell which interfaces are implemented. :rtype: bool :see: :py:func:`.AssertImplements` """ _CheckIsInterfaceSubclass(interface) class_ = _GetClassForInterfaceChecking(class_or_instance) is_implementation, _reason = _CheckIfClassImplements( class_, interface, requires_declaration=requires_declaration ) # Check older revisions of this file for helper debug code in this place. return is_implementation def IsImplementationOfAny( class_or_instance: Any, interfaces: Sequence[InterfaceType], *, requires_declaration: bool = True, ) -> bool: """ Check if the class or instance implements any of the given interfaces :param class_or_instance: type or classobj or object. :param interfaces: interfaces to check. :param requires_declaration: If `True`, the Interface must have been explicitly declared through :py:func:`ImplementsInterface` for `class_or_interface` to be considered an implementation of `interface`. Alternatively, it's possible to use :py:func:`DeclareClassImplements` from outside the class in order to tell that interfaces are implemented. :see: :py:func:`.IsImplementation` """ return any( IsImplementation( class_or_instance, x, requires_declaration=requires_declaration ) for x in interfaces ) def AssertImplements( class_or_instance: Any, interface: InterfaceType, *, requires_declaration: bool = True, ) -> None: """ If given a class, will try to match the class against a given interface. If given an object (instance), will try to match the class of the given object. Each method will need to match the interface exactly, including type annotations. The exception is that implementations might declare generic signatures (like ``*args, **kwargs`` or ``*args: object, **kwargs: object``), which will then bypass this checking. This is done so wrappers/decorators can be used together in interfaces. :type class_or_instance: type or classobj or object :type interface: Interface :type requires_declaration: bool If `True`, the Interface must have been explicitly declared through :py:func:`ImplementsInterface` for `class_or_interface` to be considered an implementation of `interface`. Alternatively, it's possible to use :py:func:`DeclareClassImplements` from outside the class in order to tell that interfaces are implemented. :raises BadImplementationError: If the object's class does not implement the given :arg interface:. :raises InterfaceError: In case the :arg interface: object is not really an interface. .. attention:: Caching Will do a full checking only once, and then cache the result for the given class. .. attention:: Runtime modifications Runtime modifications in the instances (appending methods or attributed) won't affect implementation checking (after the first check), because what is really being tested is the class. """ class_ = _GetClassForInterfaceChecking(class_or_instance) is_implementation, reason = _CheckIfClassImplements( class_, interface, requires_declaration=requires_declaration ) assert is_implementation, reason # Using explicit memoization, because we need to forget some values at some times __ImplementsCache: Dict[ Tuple[Type, InterfaceType, bool], Tuple[bool, Optional[str]] ] = {} __ImplementedInterfacesCache: Dict[Type, FrozenSet[InterfaceType]] = {} def _CheckIfClassImplements( class_: Type, interface: InterfaceType, *, requires_declaration: bool = True ) -> Tuple[bool, Optional[str]]: """ :type class_: type or classobj :param class_: A class type (NOT an instance of the class). :type interface: Interface :type requires_declaration: bool See :py:func:`IsImplementation` :rtype: (bool, str) or (bool, None) :returns: (is_implementation, reason) If the class doesn't implement the given interface, will return False, and a message stating the reason (missing methods, etc.). The message may be None. """ with suppress(KeyError): return __ImplementsCache[(class_, interface, requires_declaration)] assert _IsClass(class_) is_implementation = True reason = None # Exception: Null implements every Interface (useful for Null Object Pattern and for testing) from oop_ext.foundation.types_ import Null if not issubclass(class_, Null): try: _AssertImplementsFullChecking(class_, interface, check_attr=False) except BadImplementationError as e: is_implementation = False from oop_ext.foundation.exceptions import ExceptionToUnicode reason = ExceptionToUnicode(e) if ( is_implementation and requires_declaration and not _IsInterfaceDeclared(class_, interface) ): is_implementation = False reason = ( "The class or object '{}' does not declare that it implements interface '{}' " "and 'requires_declaration' is True.".format(class_, interface) ) result = (is_implementation, reason) __ImplementsCache[(class_, interface, requires_declaration)] = result return result def _IsImplementationFullChecking( class_or_instance: Any, interface: InterfaceType ) -> bool: """ Used internally by Attribute. :see: :py:func:`._AssertImplementsFullChecking` :type class_or_instance: type or instance :param class_or_instance: Class or instance to check :param Interface interface: Interface to check :rtype: bool :returns: If it implements the interface """ try: _AssertImplementsFullChecking(class_or_instance, interface) except BadImplementationError: return False else: return True def _IsInterfaceDeclared(class_: Optional[Type], interface: InterfaceType) -> bool: """ :type interface: Interface :param interface: The target interface. :rtype: True if the object declares the interface passed and False otherwise. Note that to declare an interface, the class MUST have declared it with :py:func:`ImplementsInterface` """ if class_ is None: return False _CheckIsInterfaceSubclass(interface) declared_interfaces = GetImplementedInterfaces(class_) # This set will include all interfaces (and its subclasses) declared for the given object declared_and_subclasses: Set[Type] = set() for implemented in declared_interfaces: declared_and_subclasses.update(implemented.__mro__) # Discarding object (it will always be returned in the mro collection) declared_and_subclasses.discard(object) return interface in declared_and_subclasses if not TYPE_CHECKING: class Attribute: """ """ _do_not_check_instance = object() def __init__( self, attribute_type: Type, instance: object = _do_not_check_instance ) -> None: """ :param type attribute_type: Will check the attribute type in the implementation against this type. Checks if the attribute is a direct instance of attribute_type, or of it implements it. :param object instance: If passed, will check for *equality* against this instance. The default is to not check for equality. """ self.attribute_type = attribute_type self.instance = instance def Match(self, attribute: object) -> Tuple[bool, Optional[str]]: """ :param object attribute: Object that will be compared to see if it matches the expected interface. :returns: If the given object implements or inherits from the interface expected by this attribute, will return (True, None), otherwise will return (False, message), where message is an error message of why there was a mismatch (may be None also). """ msg = None if isinstance(attribute, self.attribute_type): return (True, None) if self.instance is not self._do_not_check_instance: if self.instance == attribute: return (True, None) else: return ( False, "The instance ({}) does not match the expected one ({}).".format( self.instance, attribute ), ) try: if _IsImplementationFullChecking(attribute, self.attribute_type): return (True, msg) except InterfaceError as exception_msg: # Necessary because whenever a value is compared to an interface it does not inherits # from, IsImplementation raises an InterfaceError. In this context, an error like that # will mean that our candidate attribute is in fact not matching the interface, so we # capture this error and return False. msg = exception_msg return (False, None) class ReadOnlyAttribute(Attribute): """ This is an attribute that should be treated as read-only (note that usually this means that the related property should be also declared as read-only). """ else: # Type checking interfaces for Attribute and ReadOnlyAttribute: they # should be considered simple wrappers by the type checker, as that are handled # by Interface during runtime when checking if an implementation has the required # attributes. def Attribute( # type:ignore[empty-body] attribute_type: Type[T], instance: object = ... ) -> T: ... def ReadOnlyAttribute( # type:ignore[empty-body] attribute_type: Type[T], instance: object = ... ) -> T: ... class CacheInterfaceAttrs: """ Cache for holding the attrs for a given interface (separated by attrs and methods). """ _ATTRIBUTE_CLASSES: Tuple[Any, ...] = (Attribute, ReadOnlyAttribute) INTERFACE_OWN_METHODS = { i for i in dir(Interface) if inspect.isfunction(getattr(Interface, i)) } FUTURE_OBJECT_ATTRS = ( "next", "__long__", "__nonzero__", "__unicode__", "__native__", ) @classmethod def RegisterAttributeClass( cls: Type["CacheInterfaceAttrs"], attribute_class: Type[object] ) -> Set[object]: """ Registers a class to be considered as an attribute class. This provides a way of extending the Interface behavior by declaring new attributes classes such as ScalarAttribute. :param Attribute attribute_class: An Attribute class to register as an attribute class. :return set(Attribute): Returns a set with all the current attribute classes. """ result: Set[Any] = set(cls._ATTRIBUTE_CLASSES) result.add(attribute_class) cls._ATTRIBUTE_CLASSES = tuple(result) return result def __GetInterfaceMethodsAndAttrs( self, interface: InterfaceType ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ :type interface: the interface from where the methods and attributes should be gotten. :param interface: :rtype: the interface methods and attributes available in a given interface. """ all_attrs = dir(interface) interface_methods = dict() interface_attrs = dict() interface_vars = vars(interface) # Python 3+ changed how functions are represented, so it isn't possible anymore to # determine if a function is a method BEFORE it is bound to an object. # For this reason, it is necessary to also search by functions on Python and to filter out # functions like `__new__`, which are part of `Interface` class implementation and not part # expected interface. for attr in all_attrs: # If name inherited from `Interface`, check if isn't in list of reserved names if attr not in interface_vars: if attr in self.INTERFACE_OWN_METHODS: continue val = getattr(interface, attr) if type(val) in self._ATTRIBUTE_CLASSES: interface_attrs[attr] = val if _IsMethod(val): interface_methods[attr] = val return interface_methods, interface_attrs cache: ImmutableParamsCachedMethod def GetInterfaceMethodsAndAttrs( self, interface: InterfaceType ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ We have to make the creation of the ImmutableParamsCacheManager lazy because otherwise we'd enter a cyclic import. :type interface: the interface from where the methods and attributes should be gotten :param interface: (used as the cache-key) :rtype: @see: CacheInterfaceAttrs.__GetInterfaceMethodsAndAttrs """ try: cache = self.cache except AttributeError: cache = self.cache = ImmutableParamsCachedMethod( self.__GetInterfaceMethodsAndAttrs ) return cache(interface) # cache for the interface attrs (for Methods and Attrs). cache_interface_attrs = CacheInterfaceAttrs() def _IsMethod(member: object) -> bool: """ Consider method the following: 1) Methods 2) Functions 3) instances of Method (should it be implementors of "IMethod"?) """ from unittest import mock return ( inspect.isfunction(member) or inspect.ismethod(member) or isinstance(member, Method) or isinstance(member, mock.MagicMock) ) @Deprecated(AssertImplements) def AssertImplementsFullChecking( class_or_instance: object, interface: InterfaceType, check_attr: bool = True ) -> None: return AssertImplements(class_or_instance, interface) # set of methods that might be declared in interfaces but should be not be required by implementations _INTERFACE_METHODS_TO_IGNORE = {"__init_subclass__"} def _AssertImplementsFullChecking( class_or_instance: Any, interface: InterfaceType, check_attr: bool = True ) -> None: """ Used internally. This method will check each member of the given instance (or class) comparing them against the ones declared in the interface, making sure that it actually implements it even if it does not declare it so using ImplementsInterface. .. note:: Slow This method is *slow*, so make sure to never use it in hot-spots. :raises BadImplementationError: If :arg class_or_instance: doesn't implement this interface. """ # Moved from the file to avoid cyclic import: from oop_ext.foundation.types_ import Null _CheckIsInterfaceSubclass(interface) if isinstance(class_or_instance, Null): return try: classname = class_or_instance.__name__ except: classname = class_or_instance.__class__.__name__ if classname == "InterfaceImplementorStub": _AssertImplementsFullChecking( class_or_instance.GetWrappedFromImplementorStub(), interface, check_attr ) ( interface_methods, interface_attrs, ) = cache_interface_attrs.GetInterfaceMethodsAndAttrs(interface) if check_attr: for attr_name, val in interface_attrs.items(): if hasattr(class_or_instance, attr_name): attr = getattr(class_or_instance, attr_name) match, match_msg = val.Match(attr) if not match: msg = "Attribute %r for class %s does not match the interface %s" msg = msg % (attr_name, class_or_instance, interface) if match_msg: msg += ": " + match_msg raise BadImplementationError(msg) else: msg = "Attribute %r is missing in class %s and it is required by interface %s" msg = msg % (attr_name, class_or_instance, interface) raise BadImplementationError(msg) def GetSignature(method: Any) -> inspect.Signature: """ Get the inspect.Signature object for the method, considering the possibility of instances of Method, in which case, we must obtain the arguments of the instance "__call__" method. The returned signature is also stripped of any type annotation information, as we don't want to check them at runtime. """ if isinstance(method, Method): signature = inspect.signature(type(method).__call__) else: signature = inspect.signature(method) new_parameters = [ p.replace(annotation=inspect.Signature.empty) for p in signature.parameters.values() ] return signature.replace( parameters=new_parameters, return_annotation=inspect.Signature.empty ) acceptable_impl_signatures = _GetGenericImplementationSignatures() class_ = _GetClassForInterfaceChecking(class_or_instance) for name in interface_methods: if name in _INTERFACE_METHODS_TO_IGNORE: continue try: cls_or_obj_method = getattr(class_or_instance, name) if not _IsMethod(cls_or_obj_method): raise AttributeError except AttributeError: msg = "Method %r is missing in class %r (required by interface %r)" raise BadImplementationError(msg % (name, classname, interface.__name__)) else: interface_method = interface_methods[name] # get the signature from the class because inspect.signature for bound methods # doesn't include "self" cls_method = getattr(class_, name) impl_sig = GetSignature(cls_method) try: hash(impl_sig) except TypeError: raise TypeError( f"Implementation {class_.__name__}.{name} contains unhashable arguments:\n{impl_sig}" ) if impl_sig in acceptable_impl_signatures: continue interface_sig = GetSignature(interface_method) if interface_sig != impl_sig: msg = ( f"\n" f"Method {classname}.{name} signature:\n" f" {impl_sig}\n" f"differs from defined in interface {interface.__name__}\n" f" {interface_sig}" ) raise BadImplementationError(msg) DEBUG = False
[docs] def ImplementsInterface(*interfaces: Any, no_check: bool = False) -> Callable[[T], T]: """ Make sure a class implements the given interfaces. Must be used in as class decorator: .. code-block:: python @ImplementsInterface(IFoo) class Foo(object): ... :param no_check: If ``True``, does not check if the class implements the declared interfaces during import time. """ called = [False] class Check: def __init__(self) -> None: def _OnDie(ref: Any) -> None: # We may just use warnings.warn in the future, after our # codebase is properly 'sanitized', instead of handle_exception. # # This is to prevent users of doing an ImplementsInterface() # without using it as a decorator. if not called[0]: created_at_line: Union[str, "StackSummary"] if not DEBUG: created_at_line = ( "\nSet DEBUG == True in: {} to see location.".format( __file__ ) ) else: # This may be slow, so, just do it if DEBUG is enabled. import traceback created_at_line = traceback.extract_stack( sys._getframe(), limit=10 ) if isinstance(created_at_line, str): created_at_str = created_at_line else: created_at_str = "".join(traceback.format_list(created_at_line)) raise AssertionError( "A call with ImplementsInterface({}) was not properly done as a class decorator.\nCreated at: {}".format( ", ".join( tuple( str(getattr(x, "__name__", x)) for x in interfaces ) ), created_at_str, ) ) import weakref self._ref = weakref.ref(self, _OnDie) def __call__(self, type_: T) -> T: called[0] = True namespace = type_ curr = getattr(namespace, "__implements__", None) if curr is not None: all_interfaces = curr + interfaces else: all_interfaces = interfaces namespace.__implements__ = all_interfaces # type:ignore[attr-defined] if not no_check: if IsDevelopment(): # Only doing check in dev mode. for I in interfaces: # Will do full checking this first time, and also cache the results AssertImplements(type_, I) return type_ def __bool__(self) -> NoReturn: called[0] = True raise RuntimeError( "Invalid attempt to test interface.ImplementsInterface(). Did you " "mean interface.IsImplementation()?" ) return Check()
def DeclareClassImplements(class_: Type, *interfaces: InterfaceType) -> None: """ This is a way to tell, from outside of the class, that a given :arg class_: implements the given :arg interfaces:. .. attention:: Use Implements whenever possible This method should be used only when you can't use :py:func:`Implements`, or when you can't change the code of the class being declared, i.e., when you: * Can't add metaclass because the class already has one * Class can't depend on the library where the interface is defined * Class is defined from bindings * Class is defined in an external library * Class is defined by generated code :type interfaces: list(Interface) :type class_: type :raises BadImplementationError: If, after checking the methods, :arg class_: doesn't really implement the :arg interface:. .. note:: Inheritance When you use this method to declare that a base class implements a given interface, you should *also* use this in the derived classes, it does not propagate automatically to the derived classes. See testDeclareClassImplements. """ assert _IsClass(class_) from itertools import chain old_implements = getattr(class_, "__implements__", []) class_.__implements__ = list(chain(old_implements, interfaces)) # This check must be done *after* adding the interfaces to __implements__, because it will # also check that the interfaces are declared there. try: for interface in interfaces: # Forget any previous checks __ImplementsCache.pop((class_, interface, False), None) __ImplementsCache.pop((class_, interface, True), None) __ImplementedInterfacesCache.pop(class_, None) AssertImplements(class_, interface, requires_declaration=False) except: # Roll back... class_.__implements__ = old_implements raise def _GetMROForOldStyleClass(class_: Type) -> List[Type]: """ :type class_: classobj :param class_: An old-style class :rtype: list(classobj) :return: A list with all the bases in the older MRO (method resolution order) """ def _CalculateMro(class_: Any, mro: Any) -> Any: for base in class_.__bases__: if base not in mro: mro.append(base) _CalculateMro(base, mro) mro = [class_] _CalculateMro(class_, mro) return mro def _GetClassImplementedInterfaces(class_: Type) -> FrozenSet[InterfaceType]: with suppress(KeyError): return __ImplementedInterfacesCache[class_] implemented_interfaces = set() mro = inspect.getmro(class_) for c in mro: interfaces = getattr(c, "__implements__", ()) for interface in interfaces: interface_mro = inspect.getmro(interface) for interface_type in interface_mro: if interface_type in (Interface, object): continue implemented_interfaces.add(interface_type) result = frozenset(implemented_interfaces) __ImplementedInterfacesCache[class_] = result return result def GetImplementedInterfaces(class_or_object: Any) -> FrozenSet[InterfaceType]: """ Return the interfaces implemented by the object or class passed. """ class_ = _GetClassForInterfaceChecking(class_or_object) # we have to build the cache attribute given the name of the class, otherwise setting in a base # class before a subclass may give errors. return _GetClassImplementedInterfaces(class_)
[docs] def GetProxy(interface: InterfaceType, obj: T) -> T: """ Obtains a *proxy object* for ``obj``, which contains only methods and attributes declared in ``interface``. Usage: .. code-block:: python def run_simulation(params: SimulationParameters, saver: IDataSaver) -> None: data = calculate(params) proxy = GetProxy(IDataSaver, saver) proxy.save(data) Note however this is redundant when used with type checkers: ``saver: IDataSaver`` already tells the type checker to only allow access to legal members. This is useful however when type annotating legacy code which doesn't have any type annotations and makes use of the runtime mechanism to ensure interface compliance. .. note:: As of ``mypy 0.812``, there's `a bug <https://github.com/python/mypy/issues/5374>`__ that prevents :func:`GetProxy <oop_ext.interface.GetProxy>` from being properly type annotated. Hopefully this will be improved in the future. """ x = interface(obj) # type:ignore[call-arg] return x
@Deprecated(AssertImplements) def AssertDeclaresInterface( class_or_instance: object, interface: InterfaceType ) -> None: AssertImplements(class_or_instance, interface) @lru_cache(maxsize=1) def _GetGenericImplementationSignatures() -> FrozenSet[inspect.Signature]: """ Return a set of signatures that should always be considered a match against interface methods: they represent generic signatures that are commonly used by wrappers. Only list variants without type annotations, as they are stripped by the method which uses this information. """ def func1(*args, **kwargs): # type:ignore[no-untyped-def] ... def func2(self, *args, **kwargs): # type:ignore[no-untyped-def] ... def func3(cls, *args, **kwargs): # type:ignore[no-untyped-def] ... return frozenset( { inspect.Signature.from_callable(func1), inspect.Signature.from_callable(func2), inspect.Signature.from_callable(func3), } )