A possible clever trick for extension writing

Greg Ward greg at gerg.ca
Sat Nov 26 20:17:55 CST 2011


A common pattern in Mercurial extensions is the "dynamic subclass"
trick. E.g. here is the world's simplest extension using that trick:

"""
def reposetup(ui, repo):
    class simplerepo(repo.__class__):
        def tags(self):
            result = super(simplerepo, self).tags()
            result['first'] = self.lookup(0)
            return result
    repo.__class__ = simplerepo
"""

That works great, but it's a bit annoying that the simplerepo class is
indented 4 spaces more than most other Python classes. Wouldn't it be
nice if there was a way to write that class at module scope and save 4
precious spaces on the left hand of the screen? Take a look in
hgext/largefiles/reposetup.py to see what I'm talking about; there's
some freakishly deep-indented code in there.

So I started thinking about ways to make this better. Here's my first
idea: create the dynamic subclass more, well, dynamically. It relies
on the fact that type() has a second personality: call it like
type(name, bases, dict) and it creates a new class object. Here's the
code:

"""
# template is a classic class because simplerepo.__dict__
# is less work than dict(vars(simplerepo))
class simplerepo_template:
    def tags(self):
        self.ui.write('hello from simplerepo.tags()\n')
        result = super(self.__class__, self).tags()
        result['first'] = self.lookup(0)
        return result

def reposetup(ui, repo):
    repo.__class__ = type('simplerepo',
                          (repo.__class__,),
                          simplerepo_template.__dict__)
"""

Problem solved! By writing a "class" that is really just a namespace
used to populate the *real* class, which is created dynamically in
reposetup(), I have regained my precious 4 spaces on the left. This is
slightly more obscure than the original dynamic subclass trick, but
anyone who can wrap their head around

    class simplerepo(repo.__class__)

should be fine.

But of course, this doesn't solve the *real* problem. The *real*
problem is that I first heard about metaclasses in 1998, back when Jim
Fulton was still trying convince people there was a problem with
Python's type system. Ever since Python 2.2 came out I've been looking
for an excuse to write a metaclass. I may have finally found my
excuse: Mercurial extensions.

So here it is:

"""
class extensionmetaclass(type):
    def __new__(metacls, name, bases, dict):
        print "meta __new__: name=%r, bases=%r, dict=%r" % (name, bases, dict)
        (repo, template) = bases
        bases = (repo.__class__,)
        dict.update(template.__dict__)
        cls = type.__new__(metacls, name, bases, dict)
        print "cls=%r, __mro__=%r" % (cls, cls.__mro__)
        repo.__class__ = cls
        return cls

def reposetup(ui, repo):
    class simplerepo(repo, simplerepo_template):
        __metaclass__ = extensionmetaclass
"""

The idea is that extensionmetaclass would go in
mercurial/extensions.py for all extensions to use for dynamic
subclassing. Then each extension's reposetup() would do something like
the above.

If you don't have a clue what's going on here, the first two Google
hits from "python metaclass" are pretty good:

  http://www.ibm.com/developerworks/linux/library/l-pymeta/index.html
  http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python

In a nutshell, extensionmetaclass changes what happens when a "class"
statement is processed to create a class object at runtime.

Opinions? Is this a piece of pure evil that should be cast back into
the outer darkness? A brilliant idea that should be immediately
adopted and promoted? Cute, but overly clever for the problem at hand?

(Personally, I think it's all three, which probably means it's not
actually #2 -- but worth exploring and considering.)

Greg


More information about the Mercurial-devel mailing list