Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you verify duck-typed interfaces in python?

Tags:

python

class ITestType(object):
  """ Sample interface type """

  __metaclass__ = ABCMeta

  @abstractmethod
  def requiredCall(self):
    return

class TestType1(object):
  """ Valid type? """
  def requiredCall(self):
    pass

class TestType2(ITestType):
  """ Valid type """
  def requiredCall(self):
    pass

class TestType3(ITestType):
  """ Invalid type """
  pass

In the example above issubclass(TypeType*, ITestType) will return true for 2 and false for 1 and 3.

Is there an alternative way to use issubclass, or an alternative method for interface testing that will allow 1 and 2 to pass, but reject 3?

It'd be extremely helpful for me to be able to use duck typing rather than explicitly binding classes to abstract types, but also allow object checking when duck-typed objects pass through particular interfaces.

Yes, I'm aware that python people don't like interfaces, and the standard methodology is "find it when it fails and wrap everything in exceptions" but also completely irrelevant to my question. No, I cannot simply not use interfaces in this project.

edit:

Perfect! For anyone else who finds this question, here's the an example of how to use subclasshook:

class ITestType(object):
  """ Sample interface type """

  __metaclass__ = ABCMeta

  @abstractmethod
  def requiredCall(self):
    return

  @classmethod
  def __subclasshook__(cls, C):
    required = ["requiredCall"]
    rtn = True
    for r in required:
      if not any(r in B.__dict__ for B in C.__mro__):
        rtn = NotImplemented
    return rtn
like image 898
Doug Avatar asked Feb 10 '12 06:02

Doug


3 Answers

Check out the ABC module. You can define an abstract base class that provides a __subclasshook__ method that defines whether a particular class "is a subclass" of the abstract base class based on any criteria you like -- such as "it has methods X, Y and Z" or whatever. Then you can use issubclass() or isinstance() to detect interfaces on classes and instances.

like image 156
kindall Avatar answered Sep 17 '22 15:09

kindall


this is a couple of years late but here's the way I did it:

import abc

class MetaClass(object):
    __metaclass__ = abc.ABCMeta

    [...]

    @classmethod
    def __subclasshook__(cls, C):
        if C.__abstractmethods__:
            print C.__abstractmethods__
            return False
        else:
            return True

If C is an attempted class of MetaClass, then C.__abstractmethods__ will be empty only if C implements all the abstract methods.

See here for the details: https://www.python.org/dev/peps/pep-3119/#the-abc-module-an-abc-support-framework (it's under "Implementation" but a search for __abstractmethods__ should get you to the right paragraph)

Where has this worked for me:

I can create MetaClass. I can then subclass BaseClass and MetaClass to create SubClass which needs some extra functionality. But I have a need to cast an instance of BaseClass down to SubClass by changing the __cls__ attribute since I don't own BaseClass but I get instances of it that I want to cast down.

However, if I improperly implement SubClass, I can still cast down unless I use the above __subclasshook__ and just add a subclass check when I do the cast down process (which I should do anyway since I want to only try to cast a parent class down). If someone requests, I can provide a MWE for this.

ETA: Here is a MWE. I think what I was suggesting before was incorrect so the following appears to do what I intended.

The goal is to be able to convert a BaseClass object to SubClass and back. From SubClass to BaseClass is easy. But from BaseClass to SubClass not so much. The standard thing to do is update the __class__ attribute but that leaves an opening when SubClass is not really a subclass or derives from a abstract meta class but is not properly implemented.

Below, the conversion is done in the convert method that BaseMetaClass implements. However, in that logic, I check two things. One, in order to convert to the subclass, I check if it is indeed a subclass. Second, I check the attribute __abstractmethods__ to see if it's empty. If it is, then it's also a properly implemented metaclass. Failure leads to a TypeError. Otherwise the object is converted.

import abc


class BaseMetaClass(object):
    __metaclass__ = abc.ABCMeta

    @classmethod
    @abc.abstractmethod
    def convert(cls, obj):
        if issubclass(cls, type(obj)):
            if cls.__abstractmethods__:
                msg = (
                    '{0} not a proper subclass of BaseMetaClass: '
                    'missing method(s)\n\t'
                ).format(
                    cls.__name__
                )
                mthd_list = ',\n\t'.join(
                    map(
                        lambda s: cls.__name__ + '.' + s,
                        sorted(cls.__abstractmethods__)
                    )
                )
                raise TypeError(msg + mthd_list)

            else:
                obj.__class__ = cls
                return obj
        else:
            msg = '{0} not subclass of {1}'.format(
                cls.__name__,
                type(obj).__name__
            )
            raise TypeError(msg)

    @abc.abstractmethod
    def abstractmethod(self):
        return


class BaseClass(object):

    def __init__(self, x):
        self.x = x

    def __str__(self):
        s0 = "BaseClass:\n"
        s1 = "x: {0}".format(self.x)
        return s0 + s1


class AnotherBaseClass(object):

    def __init__(self, z):
        self.z = z

    def __str__(self):
        s0 = "AnotherBaseClass:\n"
        s1 = "z: {0}".format(self.z)
        return s0 + s1


class GoodSubClass(BaseMetaClass, BaseClass):

    def __init__(self, x, y):
        super(GoodSubClass, self).__init__(x)
        self.y = y

    @classmethod
    def convert(cls, obj, y):
        super(GoodSubClass, cls).convert(obj)
        obj.y = y

    def to_base(self):
        return BaseClass(self.x)

    def abstractmethod(self):
        print "This is the abstract method"

    def __str__(self):
        s0 = "SubClass:\n"
        s1 = "x: {0}\n".format(self.x)
        s2 = "y: {0}".format(self.y)
        return s0 + s1 + s2


class BadSubClass(BaseMetaClass, BaseClass):

    def __init__(self, x, y):
        super(BadSubClass, self).__init__(x)
        self.y = y

    @classmethod
    def convert(cls, obj, y):
        super(BadSubClass, cls).convert(obj)
        obj.y = y

    def __str__(self):
        s0 = "SubClass:\n"
        s1 = "x: {0}\n".format(self.x)
        s2 = "y: {0}".format(self.y)
        return s0 + s1 + s2


base1 = BaseClass(1)
print "BaseClass instance"
print base1
print


GoodSubClass.convert(base1, 2)
print "Successfully casting BaseClass to GoodSubClass"
print base1
print

print "Cannot cast BaseClass to BadSubClass"
base1 = BaseClass(1)
try:
    BadSubClass.convert(base1, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print


print "Cannot cast AnotherBaseCelass to GoodSubClass"
anotherbase = AnotherBaseClass(5)
try:
    GoodSubClass.convert(anotherbase, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print

print "Cannot cast AnotherBaseCelass to BadSubClass"
anotherbase = AnotherBaseClass(5)
try:
    BadSubClass.convert(anotherbase, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print


# BaseClass instance
# BaseClass:
# x: 1

# Successfully casting BaseClass to GoodSubClass
# SubClass:
# x: 1
# y: 2

# Cannot cast BaseClass to BadSubClass
# TypeError: BadSubClass not a proper subclass of BaseMetaClass: missing method(s)
#     BadSubClass.abstractmethod

# Cannot cast AnotherBaseCelass to GoodSubClass
# TypeError: GoodSubClass not subclass of AnotherBaseClass

# Cannot cast AnotherBaseCelass to BadSubClass
# TypeError: BadSubClass not subclass of AnotherBaseClass
like image 40
Alex Avatar answered Sep 17 '22 15:09

Alex


Here's an alternative that works very well in practice too, without the cumbersome checking the entire dictionary on every class instance creation.

(py2 and py3 compatible)

Usage:

class Bar():
  required_property_1 = ''

  def required_method(self):
    pass

# Module compile time check that Foo implements Bar
@implements(Bar)
class Foo(UnknownBaseClassUnrelatedToBar):
  required_property_1

   def required_method(self):
     pass

# Run time check that Foo uses @implements or defines its own __implements() member
def accepts_bar(self, anything):
  if not has_api(anything, Bar):
    raise Exception('Target does not implement Bar')
  ...

You can also do obvious things like @implements(Stream, Folder, Bar), when they all require some of the same methods, which makes this more useful practically than inheritance.

Code:

import inspect


def implements(*T):
  def inner(cls):
    cls.__implements = []
    for t in T:

      # Look for required methods
      t_methods = inspect.getmembers(t, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x))
      c_methods = inspect.getmembers(cls, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x))
      sig = {}
      for i in t_methods:
        name = 'method:%s' % i[0]
        if not name.startswith("method:__"):
          sig[name] = False
      for i in c_methods:
        name = 'method:%s' % i[0]
        if name in sig.keys():
          sig[name] = True

      # Look for required properties
      t_props = [i for i in inspect.getmembers(t) if i not in t_methods]
      c_props = [i for i in inspect.getmembers(cls) if i not in c_methods]
      for i in t_props:
        name = 'property:%s' % i[0]
        if not name.startswith("property:__"):
          sig[name] = False
      for i in c_props:
        name = 'property:%s' % i[0]
        if name in sig.keys():
          sig[name] = True

      missing = False
      for i in sig.keys():
        if not sig[i]:
          missing = True
      if missing:
        raise ImplementsException(cls, t, sig)
      cls.__implements.append(t)
    return cls
  return inner


def has_api(instance, T):
  """ Runtime check for T in type identity """
  rtn = False
  if instance is not None and T is not None:
    if inspect.isclass(instance):
      if hasattr(instance, "__implements"):
        if T in instance.__implements:
          rtn = True
    else:
      if hasattr(instance.__class__, "__implements"):
        if T in instance.__class__.__implements:
          rtn = True
  return rtn


class ImplementsException(Exception):
  def __init__(self, cls, T, signature):
    msg = "Invalid @implements decorator on '%s' for interface '%s': %r" % (cls.__name__, T.__name__, signature)
    super(ImplementsException, self).__init__(msg)
    self.signature = signature
like image 28
Doug Avatar answered Sep 20 '22 15:09

Doug