Support `@classmethod` in `Tracer.capture_method()`
Use case
Sometimes you want to trace a @classmethod. In my case I have an ORM-like (https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) library that provides some get, put, query, etc. class methods in a base class. I want tracing for all subclasses. Something like:
class Table:
@classmethod
@tracer.capture_method # Unclear which should nest innermost.
def get(cls, id: str):
db.get(table_name=cls.table_name, id=id)
table_name: str
class Users:
table_name = "users"
user = Users.get(123)
This doesn't work at the moment (fails with an error about get not having __module__ defined IIRC, at least if you nest @classmethod innermost).
Solution/User Experience
Either extend capture_method to cover @classmethods or add another decorator.
Ideally, as far as I'm concerned, we actually want to use the actual class (cls) in the sub-segment name. So in the example above "## user_module.Users.get". This provides more information in the span name than using Table. You could extend the same argument to the existing behavior of capture_method for instance methods. Although changing it there would be a breaking change (although probably benign).
Alternative solutions
Currently I've written my own, limited workaround:
_P = ParamSpec('_P')
_R_co = TypeVar('_R_co', covariant=True)
_Class = TypeVar('_Class', bound=type)
# Tracer.capture_method does not work for classmethods, so we need our own
# version.
def decorate_sync_classmethod(
func: Callable[Concatenate[_Class, _P], _R_co],
) -> Callable[Concatenate[_Class, _P], _R_co]:
'''Equivalent to `@Tracer.capture_method`, but for classmethods.
Only works on sync (i.e. not async) classmethods.
'''
@functools.wraps(func)
def wrapper(cls: _Class, *args: _P.args, **kwargs: _P.kwargs) -> _R_co:
method_name = f'{cls.__module__}.{cls.__qualname__}.{func.__name__}'
with _tracer.provider.in_subsegment(name=f"## {method_name}"):
return func(cls, *args, **kwargs)
return wrapper
Acknowledgment
- [X] This feature request meets Lambda Powertools Tenets
- [ ] Should this be considered in other Lambda Powertools languages? i.e. Java, TypeScript
We're on Python 3.9. Seems like starting with 3.10 __module__ is defined: https://docs.python.org/3/library/functions.html#classmethod. For older Python versions we might have to work around it by digging out the module from cls directly.
Hey Johan, thanks a lot for creating the feature request. I did a quick research and it turns out it’s more complex than I imagined it to be.
A classmethod returns a descriptor which is devoid from call. Therefore, we’d need to create a new decorator that is actually a class that decorates bounded methods that call unbounded function objects for it to work safely.
For now, we should update the Tracer documentation with a FAQ and/or new section that customers would need to swap the order @.*** being the top-most).
After that, I’d love to hear feedback from you on what a new decorator name would look like. I thought we could extend capture_method easily but the more I think about it the less ideal it is — e.g., what if the static method is async?
Suffice to say we welcome contributions and experiments in this area until we can prioritise it accordingly.
Thanks a lot for kicking this off; TIL.
https://github.com/GrahamDumpleton/wrapt/blob/master/blog/02-the-interaction-between-decorators-and-descriptors.md
On Wed, 15 Mar 2023 at 21:25, Johan Tibell @.***> wrote:
We're on Python 3.9. Seems like starting with 3.10 module is defined: https://docs.python.org/3/library/functions.html#classmethod. For older Python versions we might have to work around it by digging out the module from cls directly.
— Reply to this email directly, view it on GitHub https://github.com/awslabs/aws-lambda-powertools-python/issues/2011#issuecomment-1470791908, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAZPQBGYMI23Z4MIOO6X2ALW4IQVDANCNFSM6AAAAAAV37NQ3A . You are receiving this because you are subscribed to this thread.Message ID: @.*** com>