The order of command decorator with other decorators matters when using argument type annotations

The pytango tutorial, the order the @command decorator with respect to other decorators is not strict.

However, if I am using type annotations to provide the dtype_in for my command, then standard decorators will break the determination of dtype_in based on type annotations. Even if the decorator is using functools.wraps.

For example, consider the following device:

import tango
from tango.server import Device, run, command
import functools
import inspect

def my_decorator(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        print("before")
        result = f(*args, **kwargs)
        print("after")
        return result

    return wrapped

class MyDevice(Device):
    @command
    @my_decorator
    def MyCmdFirst(self, argin: str) -> str:
        return argin

    @my_decorator
    @command
    def MyCmdSecond(self, argin: str) -> str:
        return argin

if __name__ == '__main__':
    run((MyDevice,))

I can successfully invoke MyCmdSecond("foo"), however, when I invoke MyCmdFirst("foo") I get a TypeError:

TypeError: Invalid input argument for command MyCmdFirst: 'foo' cannot be converted t
o type DevVoid

It is interesting to look at the introspection information for these the functions being passed to @command. If I add the following to my_decorator:

def my_decorator(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        print("before")
        result = f(*args, **kwargs)
        print("after")
        return result

    print(f"f signature: {inspect.signature(f)}")
    print(f"wrapped signature: {inspect.signature(wrapped)}")

    print(f"f fullargspec: {inspect.getfullargspec(f)}")
    print(f"wrapped fullargspec: {inspect.getfullargspec(wrapped)}")

    return wrapped

I get the following output when MyCmdFirst is passed to my_decorator:

f signature: (self, argin: str) -> str
wrapped signature: (self, argin: str) -> str
f fullargspec: FullArgSpec(args=['self', 'argin'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={'return': <class 'str'>, 'argin
': <class 'str'>})
wrapped fullargspec: FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={'return': <class 'str'>})

So, functools.wraps() is updating things so that the inspect.signature()s match, however, @command is using the inspect.getfullargspec() and these differ.

If I add wrapped.__siganture__ = inspect.signature(f) to my_decorator then the inspect.getfullargspec() match and both commands work.

I would expect that my_decorator is pretty typical in that it is a decorator which doesn't care about the arguments and return type of the function it is wrapping. I would expect this to be re-orderable with @command provided the decorator is using functools.wraps and I would not expect it to have to provide a __signature__ as this is pretty unusual.

Would it be possible to only use inspect.signature() in @command to support this case?

Edited Jun 09, 2025 by Thomas Ives
Assignee Loading
Time tracking Loading