Str method for python class which reconstitutes the class source

99 views Asked by At

How can I equip a class with a __str__ method which prints the source of the class itself excluding method definitions?

I am excluding method definitions because my use-case is that I am dynamically generating fixtures (using the fixtures package; this is not the same as django fixtures), and I would like to store those fixtures in files. For those who don't know, the fixtures package uses python classes to represent the fixture data.

This seems like the kind of thing that might even be built in to fixtures, as it seems to me that it would be a moderately common usecase.

1

There are 1 answers

5
kindall On BEST ANSWER

You can do this with a metaclass. Here I'm omitting private (starts with an underscore) and callable objects (methods, inner classes, etc.). Only attributes defined directly on the class are included (the base classes are correctly listed, so this shouldn't be an issue).

class ClassSourceMeta(type):
    def __str__(cls):
         return "class %s(%s):\n\n%s\n" % (cls.__name__, 
                ", ".join(k.__name__ for k in cls.__bases__), 
                "\n".join("    %s = %r" % (k, v) for (k, v) in
                cls.__dict__.items() if not (k.startswith("_") or callable(v))))
    __repr__ = __str__    

class Mine(object):
    __metaclass__ = ClassSourceMeta
    a = 42
    b = 13

print(Mine)

In Python 3 you need to specify the metaclass in another way:

class Mine(metaclass=ClassSourceMeta):
    a = 42
    b = 13

Note that the attributes need to have reasonable repr()s. If they are class instances you might end up with syntactically-invalid stuff like a = <__main__.Mine object at 0x02AA4870> unless you write a good __repr__ method for them. For basic stuff like numbers and strings, and even lists and dicts, you're covered.

Also note that the attributes won't necessarily come out in the same order they were defined; attributes are stored in a dict which doesn't preserve insertion order. Python won't care, though.

If the fixtures module already uses a metaclass, you might need to inherit from that rather than type, or monkey-patch their metaclass. On Python 2.x this will require making a method wrapper for the __str__ class:

from types import MethodType

def __str__(cls):
     return "class %s(%s):\n\n%s\n" % (cls.__name__, 
            ", ".join(k.__name__ for k in cls.__bases__), 
            "\n".join("    %s = %r" % (k, v) for (k, v) in
            cls.__dict__.items() if not (k.startswith("_") or callable(v))))

 MetaclassToPatch.__str__ = MethodType(__str__, None, MetaclassToPatch)
 MetaclassToPatch.__repr__ = MetaclassToPatch.__str__