11

It seems as if that when I have an abstract base class that inherits from gevent.Greenlet (which inherits from the C extension module greenlet: https://github.com/python-greenlet/greenlet) then classes that implement it do not raise any of the abc errors about unimplemented methods.

class ActorBase(gevent.Greenlet):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        print "foo"

class ActorBaseTest(ActorBase):
    def bar(self):
        print "bar"

abt = ActorBaseTest()  # no errors!

If I inherit from object it fails as expected:

class ActorBase(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        print "foo"

class ActorBaseTest(ActorBase):
    def bar(self):
        print "bar"

>>> abt = ActorBaseTest()
Traceback (most recent call last):
  File "/home/dw/.virtualenvs/prj/local/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 2827, in run_code
exec code_obj in self.user_global_ns, self.user_ns
  File "<ipython-input-6-d67a142e7297>", line 1, in <module>
    abt = ActorBaseTest()
TypeError: Can't instantiate abstract class ActorBaseTest with abstract methods foo

What is the right way to implement this functionality?

Dustin Wyatt
  • 4,046
  • 5
  • 31
  • 60
  • In general, it should be OK. However, `gevent.Greenlet` may not be a new-style class, which could interfere with the ABC machinery. – chepner Dec 06 '13 at 21:23
  • Oops, you're right. gevent.Greenlet inherits from a C extension module. I'll edit question to ask what a good way to implement this use-case is. – Dustin Wyatt Dec 06 '13 at 21:40
  • 1
    Inheriting from a type implemented in C shouldn't be a problem, in general. There must be something specific to the `greenlet.greenlet` class itself that's interfering. – Mark Dickinson Dec 07 '13 at 08:00

1 Answers1

7

The cause of your problem is that it's the object.__new__ method that does the check for instantiation of an abstract class, and in this case object.__new__ isn't being invoked: gevent.Greenlet inherits from greenlet.greenlet, and greenlet.greenlet is a C extension type whose __new__ implementation doesn't call object.__new__ at any point (see the green_new function in the greenlet C source).

You can see the same effect by subclassing some of the other builtin types that implement their own __new__ method and don't refer back to object.__new__ (the float type, for example). The issue is not particular to C extension types, though: you can also replicate it with pure Python types. Consider the code below:

import abc

class A(object):
    def __new__(cls):
        # self = object.__new__(cls)
        return 42

class B(A):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        pass

b = B()  # No exception.

The class B is correctly registered as an abstract class (internally, its Py_TPFLAGS_IS_ABSTRACT bit is set in the tp_flags field), but object.__new__ is never called, so there's no error when B is instantiated. However, if you uncomment the self = object.__new__(cls) method call in A, you'll see the expected error on instantiation.

As far as the 'right way' to implement this, unfortunately I think the right way is to fix the greenlet type so that its __new__ method calls object.__new__. I guess you could add a __new__ method to ActorBase that explicitly calls both the base class __new__ and object.__new__ (and throws away the result of the latter), but I'd consider that an ugly workaround rather than the 'right way'. (EDIT: And on top of that, it doesn't work. I get TypeError: object.__new__(ActorBase) is not safe, use greenlet.greenlet.__new__() from the object.__new__ call.) I've opened an issue on the greenlet tracker.


EDIT: This problem seemed somewhat familiar, and I just did some digging into the Enthought Traits source, which defines a CHasTraits class implemented in C which does play nicely with ABCs. And its __new__ method starts like this (comments are from the original source, not mine):

PyObject *
has_traits_new ( PyTypeObject * type, PyObject * args, PyObject * kwds ) {

    // Call PyBaseObject_Type.tp_new to do the actual construction.
    // This allows things like ABCMeta machinery to work correctly
    // which is implemented at the C level.
    has_traits_object * obj = (has_traits_object *) PyBaseObject_Type.tp_new(type, empty_tuple, empty_dict);

So perhaps the long-term solution is to persuade the greenlet folks to do something similar.

Mark Dickinson
  • 29,088
  • 9
  • 83
  • 120
  • For anyone who comes along later, the issue @Mark Dickinson raised on the greenlet tracker led to greenlet implementing a fix. The problem I posted about is now fixed for greenlets. – Dustin Wyatt Aug 02 '20 at 21:44