{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/amphibian-decorators/",
  "description": "Build Python decorators that work seamlessly with both sync and async functions using inspect.iscoroutinefunction for maximum flexibility.",
  "path": "/python/amphibian-decorators/",
  "publishedAt": "2022-02-06T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "Async"
  ],
  "textContent": "Whether you like it or not, the split world of sync and async functions in the Python\necosystem is something we'll have to live with; at least for now. So, having to write things\nthat work with both sync and async code is an inevitable part of the journey. Projects like\n[Starlette], [HTTPx] can give you some clever pointers on how to craft APIs that are\ncompatible with both sync and async code.\n\n> Lately, I've been calling constructs that are compatible with both synchronous and\n> asynchronous paradigms as Amphibian Constructs.\n\nSo, I wanted to write an amphibian decorator that'd work with both sync and async functions.\nLet's consider writing a trivial decorator that'll tag the wrapped function. Here, by\ntagging I mean, the decorator will attach a _tags attribute to the wrapped function where\nthe value of the tag can be passed as the function parameter.\n\nThis type of tagging can be helpful if you want to write code that'll classify functions\nbased on their tags and do interesting things with them. [Locust] uses this concept of\ntagging to select and deselect load-testing routines in the CLI. Also, @pytest.mark.\nutilizes a similar concept.\n\nHere's how you can do that:\n\nIn the above snippet:\n\n- The decorator tag is a variadic function that accepts the names of the tags.\n\n- I attached the tag to a function before dealing with the sync and async functions. The tag\n  attachment is done via func._tags = names statement. Placing them outside of the wrapped\n  function also makes sure that the attachment happens during the definition time of the\n  wrapped function; not during runtime. Otherwise, it'll raise AttributeError if you try to\n  access func._tags to inspect the tags.\n\n- Afterwards, I checked if the function is an async one via iscoroutinefunction function\n  from the inspect module. If the wrapped function is an async function, then it's\n  executed with the await statement. Otherwise, the function is a sync function and is\n  executed as usual.\n\nYou can play around with the decorator as follows:\n\nBreadcrumbs\n\nAstute readers might notice that the type annotations in this decorator are quite loose and\nit doesn't take advantage of Python 3.10's typing.ParamSpec type. This is intentional as\nit adds quite a bit of noise that might obfuscate the primary intent of the code snippet.\nAlso, typing a decorator that returns either a sync or async callable based on the control\nflow is tricky.\n\nFurther reading\n\n- [Amphibian decorator in Starlette's source code]\n\n\n\n\n[starlette]:\n    https://www.starlette.io/\n\n[httpx]:\n    https://www.python-httpx.org/\n\n[locust]:\n    http://docs.locust.io/en/stable/api.html#locust.tag\n\n[amphibian decorator in starlette's source code]:\n    https://github.com/encode/starlette/blob/424351cb231c67798a65c091b0b7d42790f5e444/starlette/authentication.py#L19",
  "title": "Amphibian decorators in Python"
}