1

I am writing a program to transform a collection of XML documents into HTML. The documents require similar but not identical transformations, so I am hoping to abstract most of the details into a generic BaseParser class and then write an single subclass for each document that encapsulates the document-specific transformations. I am using the Python standard library xml.etree.ElementTree package for event-based parsing.

I would like to be able to write code like this, where the logic of the function is bundled together with when it should be called.

class CustomParser(BaseParser):
    @on_tag('word', {'lang':'en'})
    def found_en_word(self, tag, attrs):
        # do something

For this to work, the decorator needs register the found_en_word function in a class variable (or an instance variable, though it would be redundant for each instance to have its own copy), so that the control flow can be separated in the BaseParser class.

My current solution, shown below, is to use a metaclass to create a callbacks dictionary on the class.

class Meta(type):
    def __new__(cls, clsname, bases, dct):
        callbacks = {}
        for key, value in dct.items():
            if hasattr(value, '_on_tag'):
                callbacks[value._on_tag] = value
        ret = type(clsname, bases, dct)
        ret.callbacks = callbacks
        return ret

def on_tag(tag, attrs=None):
    def decorator(f):
        f._on_tag = (tag, attrs)
        return f
    return decorator

class BaseParser(metaclass=Meta):
    ...

Unfortunately it doesn't look like the metaclass is inherited the way I had hoped: it seems that the metaclass is used to construct a modified BaseParser class, from which CustomParser just inherits normally.

Can this construction be implemented, with or without metaclasses, in Python?

1 Answer 1

2

Your metaclass isn't constructing the class correctly. As described in the docs, you actually need to call type.__new__(Meta, clsname, bases, dct). By just calling type(clsname, bases, dct), you are constructing an ordinary class that is not an instance of your custom metaclass.

Once you fix that, you'll have another problem, which is that you're trying to use _on_tag as a dictionary key, but _on_tag contains a dictionary, and dictionaries aren't hashable. That's somewhat tangential to your main question, but you'll have to figure out some way to handle it (perhaps by making the user do @on_tag('word', ('lang', 'en')) instead of @on_tag('word', {'lang': 'en'})).

Sign up to request clarification or add additional context in comments.

1 Comment

Good catch, that addressed both my original problem and a problem I didn't know I had! I'll have the decorator convert the dictionary to a tuple so I can retain the nicer calling syntax.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.