0

The Context

I have a python application with a relatively involved class hierarchy. It needs to work with python 2.6 up to python 3.5 (a big range, I know!), and I've been having particular problems with ABCs. I'm using the six library's with_metaclass to ease some of the hurt, but it's still problematic.

One particular set of classes has been giving me trouble. Here's what it looks like in a simplified form:

from abc import ABCMeta
from six import with_metaclass

# SomeParentABC is another ABC, in case it is relevant
class MyABC(with_metaclass(ABCMeta, SomeParentABC)):
    def __init__(self, important_attr):
        self.important_attr = important_attr
    def gamma(self):
         self.important_attr += ' gamma'

class MyChild1(MyABC):
    def __repr__(self):
        return "MyChild1(imporant_attr=%s)" % important_attr
    def alpha(self):
         self.important_attr += ' alpha'

class MyChild2(MyABC):
    def __repr__(self):
        return "MyChild2(imporant_attr=%s)" % important_attr
    def beta(self):
         self.important_attr += ' beta'

There's a lot of gamma like functions bundled in MyABC, and a few subclass specific functions like alpha and beta. I want all of the subclasses of MyABC to inherit the same __init__ and gamma attributes, and then pile on their own specific characteristics.

The Problem

The issue is that in order for MyChild1 and MyChild2 to share code for __init__, MyABC needs to have a concrete initializer. In Python 3, everything is working just fine, but in Python 2, when the initializer is concrete, I fail to get TypeErrors when instantiating MyABC.

I have a segment in my testsuite that looks something like this

def test_MyABC_really_is_abstract():
    try:
        MyABC('attr value')
    # ideally more sophistication here to get the right kind of TypeError,
    # but I've been lazy for now
    except TypeError:
        pass
    else:
        assert False

Somehow, in Python 2.7 (I assume 2.6, but haven't bothered to check) this test is failing.

MyABC doesn't have any other abstract properties, but it isn't meaningful to instantiate a class that has gamma without also having either alpha or beta. For now, I've been getting by with a DRY violation by just duplicating the __init__ function in MyChild1 and MyChild2, but as time goes on this is becoming more and more burdensome.

How can I give a Python 2 ABC a concrete initializer without making it instantiable, while maintaining Python 3 compatibility? In other words, I want trying to instantiate MyABC to throw TypeErrors in Python 2 and Python 3, but it only throws them in Python 3.

with_metaclass

I believe it is relevant to see the code for with_metaclass here. This is provided under the existing License and Copyright of the six project, (c) 2010-2014 Bejamin Peterson

def with_metaclass(meta, *bases):
    """Create a base class with a metaclass."""
    # This requires a bit of explanation: the basic idea is to make a dummy
    # metaclass for one level of class instantiation that replaces itself with
    # the actual metaclass.
    class metaclass(meta):
        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)
    return type.__new__(metaclass, 'temporary_class', (), {})
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
sirosen
  • 1,716
  • 13
  • 17
  • 1
    What is the *full* traceback when the `TypeError` is thrown in Python 2? – Martijn Pieters Aug 09 '14 at 17:21
  • @MartijnPieters I'm not sure what you're asking for here. There is no `TypeError` thrown in Python2. That is exactly the problem: I want one to be thrown because `MyABC` is meant to be abstract. – sirosen Aug 09 '14 at 17:26
  • Right, that wasn't clear then; you expect it to be thrown but isn't. – Martijn Pieters Aug 09 '14 at 17:27
  • 1
    That's handled by `type.__new__`; I don't see any overrides here for `__new__`; is `SomeParentABC` doing that perhaps? – Martijn Pieters Aug 09 '14 at 17:29
  • @MartijnPieters I've just edited the question to include the `with_metaclass` code, which does edit `__new__`. I admit to that it gives me a headache to read. – sirosen Aug 09 '14 at 17:36

1 Answers1

2

The six.with_metaclass() meta class may be incompatible with ABCs as it overrides type.__new__; this could interfere with the normal procedure for testing for concrete methods.

Try using the @six.add_metaclass() class decorator instead:

from abc import ABCMeta from six import add_metaclass

@add_metaclass(ABCMeta)
class MyABC(SomeParentABC):
    def __init__(self, important_attr):
        self.important_attr = important_attr
    def gamma(self):
         self.important_attr += ' gamma'

Demo:

>>> from abc import ABCMeta, abstractmethod
>>> from six import add_metaclass
>>> @add_metaclass(ABCMeta)
... class MyABC(object):
...     @abstractmethod
...     def gamma(self): pass
... 
>>> MyABC()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyABC with abstract methods gamma

Note that you do need to have abstract methods without concrete implementations for the TypeError to be raised!

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • This is definitely help if the right direction, but I'm still able to instantiate a class which has used `@add_metaclass(ABCMeta)`. Specifically, if I decorate with `add_metaclass` and inherit from `object`, then `MyABC('some value')` still works. – sirosen Aug 09 '14 at 18:03
  • @sirosen: I accidentally left in the `with_metaclass()` call; removed now. – Martijn Pieters Aug 09 '14 at 18:07
  • @sirosen: that said, I am having trouble reproducing your case with `six.with_metaclass`. – Martijn Pieters Aug 09 '14 at 18:11
  • I don't think the `with_metaclass` usage was the issue though. In python 2.7 and 3.2 (my local testing versions), I am still able to instantiate MyABC if it is identical to the above, but inherits directly from `object`. Does it need to have an abstract property or abstract method to raise the `TypeError`? (Also, thanks so much for taking so much time and effort on this!) – sirosen Aug 09 '14 at 18:11
  • @sirosen: yes, I was assuming your *parent* ABC meta had those. – Martijn Pieters Aug 09 '14 at 18:14