Amphibian decorators in Python

Redowan Delowar February 6, 2022
Source

Whether you like it or not, the split world of sync and async functions in the Python ecosystem is something we'll have to live with; at least for now. So, having to write things that work with both sync and async code is an inevitable part of the journey. Projects like Starlette, HTTPx can give you some clever pointers on how to craft APIs that are compatible with both sync and async code.

Lately, I've been calling constructs that are compatible with both synchronous and asynchronous paradigms as Amphibian Constructs.

So, I wanted to write an amphibian decorator that'd work with both sync and async functions. Let's consider writing a trivial decorator that'll tag the wrapped function. Here, by tagging I mean, the decorator will attach a _tags attribute to the wrapped function where the value of the tag can be passed as the function parameter.

This type of tagging can be helpful if you want to write code that'll classify functions based on their tags and do interesting things with them. Locust uses this concept of tagging to select and deselect load-testing routines in the CLI. Also, @pytest.mark. utilizes a similar concept.

Here's how you can do that:

In the above snippet:

  • The decorator tag is a variadic function that accepts the names of the tags.

  • I attached the tag to a function before dealing with the sync and async functions. The tag attachment is done via func._tags = names statement. Placing them outside of the wrapped function also makes sure that the attachment happens during the definition time of the wrapped function; not during runtime. Otherwise, it'll raise AttributeError if you try to access func._tags to inspect the tags.

  • Afterwards, I checked if the function is an async one via iscoroutinefunction function from the inspect module. If the wrapped function is an async function, then it's executed with the await statement. Otherwise, the function is a sync function and is executed as usual.

You can play around with the decorator as follows:

Breadcrumbs

Astute readers might notice that the type annotations in this decorator are quite loose and it doesn't take advantage of Python 3.10's typing.ParamSpec type. This is intentional as it adds quite a bit of noise that might obfuscate the primary intent of the code snippet. Also, typing a decorator that returns either a sync or async callable based on the control flow is tricky.

Further reading

Discussion in the ATmosphere

Loading comments...