Search code examples
pythonpython-decorators

Register data against class properties using decorators


First off this is NOT about XML serialization - its just a good use case to use as an example.

I want to be able to markup properties within a class then later have a serializer be able to read that meta data and make use of it.

I've looked at Class with a registry of methods based on decorators, but the approach here only seems to work with methods (not properties) and I also need to accept arguments into the markup decorators (OutputAsXmlAttribute & OutputAsXmlElement).

class MyXmlInvoice(object):

    @OutputAsXmlAttribute('invoiceNo', 'http://my.company.com/invoice-schema')
    @property
    def id(self) -> 'str':
        pass

    @id.setter
    def id(self, value: str):
        pass


    @OutputAsXmlElement('Price', 'http://my.company.com/invoice-schema')
    @property
    def price(self) -> 'int':
        pass

    @price.setter
    def price(self, value: int):
        pass


# This is the use case for the objects where MyXmlSerializer uses 
# the meta data on price & id to influence the serialization
xmlInvoice = MyXmlInvoice()
xmlInvoice.price = 3
xmlInvoice.id = "0001"

xmlSerializer = MyXmlSerializer()
xmlData = xmlSerializer.Serialize(xmlInvoice)

Solution

  • A Python function is just an object, and can receive additional attributes. Unfortunately, properties cannot so you must unwrap them. That means that you could start with:

    def OutputAsXmlElement(label, schema):
        def deco(p):
            f = p.fget if isinstance(p, property) else p
            f._label = label
            f._schema = schema
            f._iselement = True
            return p
        return deco
    
    def OutputAsXmlAttribute(label, schema):
        def deco(p):
            f = p.fget if isinstance(p, property) else p
            f._label = label
            f._schema = schema
            f._iselement = False
            return p
        return deco
    

    Then through the inspect module, you can access those special attributes. For example here is a way to find all decorated members of your example class, with their label and schema:

    for x, y in ((x,y) for x,y in inspect.getmembers(xmlInvoice.__class__)
             if not x.startswith('__')):
        if isinstance(y, property):
            f = y.fget
        else:
            f = y
        print(x, getattr(f, '_label', None), getattr(f, '_schema', None), getattr(f, '_element', None))