0

I'm experiencing some really weird problems in Python when trying to inherit from a class with a metaclass. I have this:

class NotifierMetaclass(type):

    def __new__(cls, name, bases, dct):

        attrs = ((name, value) for name, value in dct.items()
                    if not name.startswith('__'))

        def wrap_method(meth):
            return instance_wrapper()(meth) # instance_wrapper is a decorator of my own

        def is_callable(value):
            return hasattr(value, '__call__')

        decorated_meth = dict(
            (name, value) if not is_callable(value)
            else (name, wrap_method(value))
            for name, value in attrs
        )

        return super(NotifierMetaclass, cls).__new__(
            cls, name, bases, decorated_meth
        )


class Notifier(object):

    def __init__(self, instance):
        self._i = instance

    __metaclass__ = NotifierMetaclass

And then, in notifiers.py:

from helpers import Notifier

class CommentNotifier(Notifier):

    def __notification__(self, notification):
        return '%s has commented on your board' % self.sender

    def __notify__(self):
        receivers = self.retrieve_users()
        notif_type = self.__notificationtype__()
        for user in receivers:
            Notification.objects.create(
                object_id=self.id,
                receiver=user,
                sender_id=self.sender_id,
                type=notif_type
            )

However, when I try to import CommentNotifier it returns Notifier. In the shell:

$ python
Python 2.7.3 (default, Apr 20 2012, 22:44:07) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from logic.notifiers import CommentNotifier
>>> CommentNotifier
<class 'helpers.CommentNotifier'>

In fact, this is (at least that's what I think) actually the same problem I got a week ago with some Django models. At first I thought it was related to the way Django works, but now I suspect it's more like a Python "problem" with metaclasses and inheritance.
Is this a known issue or am I simply doing things wrong? Hope you can help me.
EDIT: I forgot to mention that I attribute this "error" to metaclasses because if I don't give a metaclass to Notifier it works as expected.

Community
  • 1
  • 1
cronos2
  • 288
  • 5
  • 13
  • 1
    Your example works correctly for me. Are you you didn't omit something important in the "stuff" sections? Please provide an example that actually exhibits the behavior you mean. – BrenBarn Aug 25 '12 at 17:31
  • Just edited the code for the real case – cronos2 Aug 25 '12 at 20:32
  • I get `` (using only two files, `helpers.py` and `notifiers.py`), and `` if I put it in a package. Try to cut it down even more, and stick some print statements in various places to rule out the possibility that you're not importing the version you think you are. – DSM Aug 25 '12 at 20:44
  • I get the behavior @DSM sees as well. I believe your problem is not that it is importing the wrong class, but that it is giving it the wrong name (or at the least the wrong `__module__`). [This question](http://stackoverflow.com/questions/11209521/base-metaclass-overriding-new-generates-classes-with-a-wrong-module) mentions the same issue but has no answers. It would seem that the `__module__` is incorrectly set to the module of the metaclass rather than the module of the class. I'm still trying to figure it out. . . – BrenBarn Aug 25 '12 at 20:46

1 Answers1

2

Okay, I think I figured it out. The correct class is being imported. It just has the wrong name. You should be able to see this if you set attributes on the classes. If you put someJunk = "Notifier" in the Notifier definition and someJunk = "CommentNotifier" in the CommentNotifier definition, then when you import CommentNotifier it will have the right value.

The problem is that in creating your attrs, you exclude all double-underscore attributes, including __module__. When you call the superclass __new__, you pass in your attrs, which does not have a __module__ entry, so Python creates one for you. But since this code is executing inside the file containing the metaclass, the module is incorrectly set to the metaclass's file and not the actual class's file.

I'm not seeing the behavior you observe for the actual name of the class, only for the module. That is, for me the imported class is named metafile.CommentNotifier, where metafile is the file containing the metaclass. It should be named submeta.CommentNotifier, where submeta is the file containing the CommentNotifierClass. I'm not sure why you're seeing it for __name__ as well, but it wouldn't surprise me if some subtle handling of module/name assignment varies across different Python versions.

__notify__ and __notification__ are not Python magic methods. It appears that you're excluding double-underscore methods because you are using double underscores to indicate something for your own purposes. You shouldn't do this. Use some other prefix for your own methods (like _Notifier or something) if you must, then exclude those methods and leave the double-underscore ones alone. Excluding double-underscore methods could cause other problems. In particular, it will cause failure if you ever decide to define a real magic method (e.g., __str__) on a class that uses this metaclass.

(To clarify: you can use methods that begin with double underscores if you want, as private attributes, although this still is probably not a good idea. If you do this, though, you need to make sure you only do your special processing on those attributes, and not ones that begin and end with double underscores, which are Python-internal magic methods. What you shouldn't do is create your own names that begin and end with double underscores, like __notify__.)

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • +1, but I find the class name discrepancy really puzzling. I'm using 2.7.3 as well. – DSM Aug 25 '12 at 21:00
  • Not +1 but +1000, you really gave me a Python lesson. First of all I want to apologize for all the confusion "helpers.Notifier" has generated. It was in fact "helpers.CommentNotifier" what the interpreter said, but I somehow modified the text without noticing. Sorry about that. Secondly, your guess is completely right, if I omit the check of `'__'` it actually sets correctly the `__module__` property and keeps the `__foo__` methods. And last, but not least, it's the fact of the methods naming. I've never been fully satisfied with the `__foo__` naming, but it worked. I should reconsider it. – cronos2 Aug 25 '12 at 23:41