Skip to content

Command type inference with decorators that mutate types

I have encountered a problem with type inference in commands when upgrading to PyTango 10.1. It occurs when declaring a command with a decorator that changes the type of the argument.

As a simplified example, I have a decorator to call a method with an additional string argument:

import functools

def add_str_arg(method):
    functools.wraps(method)
    def wrapper(self):
        return method(self, "added arg")
    return wrapper

A use case for this is to generate a unique ID and inject it into the method (in which case it wouldn't be a fixed string).

I define a device with a command that uses the decorator on the method:

from tango.server import Device, command

class TestCommand(Device):

    @command
    @add_str_arg
    def Test(self, arg: str) -> None:
        print(f"Test called with {arg}")

The idea being that the Test command has no argument, but the underlying method is called with the injected argument.

In PyTango 10.0.3, the input is correctly reported as DevVoid:

In [1]: t.get_command_config("Test")
Out[1]: CommandInfo(cmd_name = 'Test', cmd_tag = 0, disp_level = <DispLevel.OPERATOR: 0>, in_type = <CmdArgType.DevVoid: 0>, in_type_desc = 'Uninitialised', out_type = <CmdArgType.DevVoid: 0>, out_type_desc = 'Uninitialised')

In PyTango 10.1.1, the input is reported as DevString:

In [2]: t.get_command_config("Test")
Out[2]: CommandInfo(cmd_name = 'Test', cmd_tag = 0, disp_level = <DispLevel.OPERATOR: 0>, in_type = <CmdArgType.DevString: 8>, in_type_desc = ':param arg: (not documented)\n:type arg: DevString', out_type = <CmdArgType.DevVoid: 0>, out_type_desc = 'No output parameter (DevVoid)')

and attempts to call the command with or without an argument fail:

In [3]: t.Test()
API_IncompatibleCmdArgumentType: Incompatible argument type, expected type is : Tango::DevString
(For more detailed information type: tango_error)

In [4]: t.Test("hello")
PyDs_PythonError: TypeError: TestCommand.Test() takes 1 positional argument but 2 were given

(For more detailed information type: tango_error)

The problem is that command is "seeing through" the wrapper and doing type inference on the underlying method. I note in passing that the description of the input is wrong too.

My first thought was: can I work around this by explicitly using @command(dtype_in=None)? That doesn't work because None is the default value of dtype_in, and that causes command to use type inference.

After reading the documentation to remind myself about other ways of specifying types in PyTango, I realised that using @command(dtype_in="None") (i.e. as a string) will work, since it isn't the default and therefore overrides the type inference.

The other thing that works is to remove the type hint from arg, which prevents command from doing type inference in the first place.

More generally, this type inference problem will occur any time you mutate the command input or output type with a decorator. In most cases (all except None), it can be worked around by setting the type explicitly in command. Code that worked with pre-10.1 versions of PyTango will be doing that anyway. The corner case is when the intended input or output type is None, which will cause problems when upgrading from previous versions of PyTango.

Edited by Mark Ashdown