Decorators with arguments?

Cameron Simpson cs at cskk.id.au
Thu May 14 19:02:39 EDT 2020


On 14May2020 08:28, Christopher de Vidal <cbdevidal.jk1 at gmail.com> wrote:
>Help please? Creating an MQTT-to-Firestore bridge and I know a decorator
>would help but I'm stumped how to create one. I've used decorators before
>but not with arguments.
>
>The Firestore collection.on_snapshot() method invokes a callback and sends
>it three parameters (collection_snapshot, changes, and read_time). I need
>the callback to also know the name of the collection so that I can publish
>to the equivalent MQTT topic name. I had thought to add a fourth parameter
>and I believe a decorator is the right approach but am stumped how to add
>that fourth parameter. How would I do this with the code below?

To start with, I'm not convinced a decorator is a good approach here.  
I'd use a small class.

Maybe you could provide an example of how you think the code would look 
_with_ a decorator (ignoring the implementation of the decorator itself) 
so that we can see what you'd like to use?

The thing about a decorator is that in normal use you use it to define a 
new named function; each function would tend to be different in what it 
does, otherwise you'd only have one function.

It sounds like you want a named function per collection name, but with 
the _same_ inner code (on_snapshot). But decorators, being applied to 
multiple functions, are usually for situations where the inner code 
varies and the decorator is just making a shim to call it in a 
particular way. You've got _one_ function and just want to attach it to 
multiple collection names, kind of the inverse.

To clear my mind, I'll lay out the class approach (untested). Then if I 
think you can do this with a decorator I'll try to sketch one.

    class MQTTAdaptor:

        def __init__(self, fbdb, collection_name):
            self.fbdb = fbdb
            self.collection_name = collection_name
            self.fbref = None
            self.subscribe()

        def subscribe(self):
            assert self.fbref is None
            self.fbref = db.collection(collection_name)
            self.fbref.on_snapshot(self.on_snapshot)

        def unsubscribe(self):
            self.fbref.unsubscribe()
            self.fbref = None

        def on_snapshot(self, col_snapshot, changes, read_time):
            col_name = self.collection_name
            data = {}
            for doc in col_snapshot:
                serial = doc.id
                contents = load_json(doc.to_dict()['value'])
                data[serial] = contents
            for change in changes:
                serial = change.document.id
                mqtt_topic = col_name + '/' + serial
                contents = data[serial]
                if change.type.name in ['ADDED', 'MODIFIED']:
                    mqtt.publish(mqtt_topic, contents)
                elif change.type.name == 'REMOVED':
                    mqtt.publish(mqtt_topic, None)
                else:
                    warning("unhandled change type: %r" % change.type.name) 

    adaptors = []
    for collection_name in 'cpu_temp', 'door_status':
        adaptors.append(MQTTAdaptor, db, collection_name)
    .... run for a while ...
    for adaptor in adaptors:
        adaptor.unsubscribe()

I've broken out the subscribe/unsubscribe as standalone methods just in 
case you want to resubscribe an adaptor later (eg turn them on and off).

So here we've got a little class that keeps the state (the subscription 
ref and the collection name) and has its own FB style on_snapshot which 
passes stuff on to MQTT.

If you want to do this with a decorator you've got a small problem: it 
is easy to make a shim like your on_snapshot callback, but if you want 
to do a nice unsubscribe at the end thend you need to keep the ref 
around somewhere. The class above provides a place to keep that.

With a decorator you need it to know where to store that ref. You can 
use a global registry (ugh) or you could make one (just a dict) and pass 
it to the decorator as well. We'll use a global and just use it in the 
decorator directly, since we'll use "db" the same way.

    # the registry
    adaptors = {}

    @adapt('cpu_temp')
    def cpu_temp_on_snapshot(collection_name, col_snapshot, changes, read_time):
        ... your existing code here ...

and then to subscribe:

    cpu_temp_col_ref = db.collection('cpu_temp')
    cpu_temp_col_watch = cpu_temp_col_ref.on_snapshot(cpu_temp_on_snapshot)

but then for the door_status you want the same function repeated:

    @adapt('door_status')
    def door_on_snapshot(collection_name, col_snapshot, changes, read_time):
        ... your existing code here ...

and the same longhand subscription.

You can see this isn't any better - you're writing out on_snapshot 
longhand every time.  Now, a decorator just accepts a function as its 
argument and returns a new function to be used in its place. So we could 
define on_snapshot once and decorate it:

    def named_snapshot(collection_name, col_snapshot, changes, read_time):
        ... the egneral function code here again ...

    cpu_temp_on_snapshot = adapt('cpu_temp')(named_snapshot)
    door_on_snapshot = adapt('door_status')(named_snapshot)

but (a) it doesn't look so much like a decorator any more and (b) you 
still have to to the explicit subscription.

However we could have the decorator do the subscription _and_ record the 
fbref for the unsubscription later. So not quite so bad.

So, how do you do the decorator-with-an-argument?

A decorator takes _one_ argument, a function, and returns a new 
function. The syntax:

    @foo
    def bah(...):
        ...

is just a pretty shorthand for:

    # original "bah"
    def bah(...):
        ....
    # switch out "bah" for a new function using the original "bah"
    bah = foo(bah)

However, you're allowed to write:

    @foo2(some-arguments...)
    def bah(...):
        ...

What's going on there? The expression:

    foo2(some-arguments...)

_itself_ returns a decorator. Which then decorates "bah".

So, making an @adapt decorator for your purpose... [... hack hack ...] 
Well I can't write one of any use. I just wrote an @adapt decorator, but 
it never calls the function it is passed when I do that (because the 
mqtt "on_snapshot()" function already exists and it calls that instead).  
So there's no use for a decorator :-(

Cheers,
Cameron Simpson <cs at cskk.id.au>


More information about the Python-list mailing list