1

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.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • 4
    Have you looked at [`inspect.getsource`](https://docs.python.org/2/library/inspect.html#inspect.getsource) and the related functions in the `inspect` module? That would be a good starting point. You will probably need to retrieve the source of the class and manually remove the method definitions. If you encounter problems doing that, you can ask a more specific question showing your attempt. – BrenBarn Jun 03 '14 at 17:54
  • Not sure if this is what you are asking for but the inspect module has an inspect.getsource(obj) method which could potentially be what you are looking for. Although I'm not sure what dependencies there are for it to be able to read that source. – Bob Jun 03 '14 at 17:54
  • Dynamically generating text to be executed as code? This usually strikes me as the wrong approach. Things like inheritance, closures, `eval()`, `compile()` etc seem to cover most/all dynamic-environment use cases well. – Brian Cain Jun 03 '14 at 17:58
  • Do the fixtures need to be human-readable when stored? If not, why not just pickle them? – JAB Jun 03 '14 at 18:04
  • @JAB For those who don't know, the fixtures package uses python classes to represent the fixture data. – Marcin Jun 03 '14 at 18:40
  • @Marcin You stated that in your question. Most Python classes are pickleable, and as far as I know those used for fixtures are no exception. (Though until recently, inner classes were not picklable, I believe, so that might require a newer version of Python.) – JAB Jun 04 '14 at 11:10
  • Yeah, looks like inner classes weren't picklable until Python 3.4, and you probably would still need to customize the pickling procedure given the default behavior for classes versus class instances, but I was looking at it from the perspective of storage efficiency as pickle supports binary formats for whatever is pickled. – JAB Jun 04 '14 at 11:24

1 Answers1

1

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__
kindall
  • 178,883
  • 35
  • 278
  • 309
  • Awesome, thank you! I'll need to massage this a little to also deal with inner classes (the fixtures module uses inner classes to represent database rows/instances, which inner classes contain the key value pairs). I realise that the inner classes scenario is not explicitly stated in my question. In fact, I can use a different metaclass for the outer class and inner classes to do this cleanly. – Marcin Jun 03 '14 at 18:46
  • Inner classes are doable... basically give them the same metaclass, then you have to have some code to indent them. Unfortunately I don't think my one-liner is feasible if you're doing that. – kindall Jun 03 '14 at 18:59
  • Actually, this doesn't work for me. Your test code does work. I'm patching the existing metaclasses. Ho hum. – Marcin Jun 03 '14 at 19:28
  • You probably need to make a method wrapper for it. I'll see if I can figure it out after lunch. – kindall Jun 03 '14 at 19:41
  • Thanks. For my usecase, I've solved this by explicitly calling functions based on your approach, which works because I only have two levels. – Marcin Jun 03 '14 at 20:07