FreeOpcUa/opcua-asyncio

`call_method` and `uamethod` unpack tuple type

tobiasbrinker opened this issue · 4 comments

Describe the bug

@uamethod and call_method unpack tuple type and change the expected type.

To Reproduce

Create a ua method that wraps something in a tuple. Execute the method. Result will not be packed into a tuple or list, but unpacked.

@uamethod
def put_into_tuple(parent, input: str) -> tuple[str]:
  return (input,)
    
def test_tuple_behaviour(client, idx):
  expected_result = ("test",)
  res = client.nodes.objects.call_method(f"{idx}:put_into_tuple", "test")
  assert res == expected_result  

# FAILED tests/test_sync.py::test_tuple_behaviour - AssertionError: assert 'test' == ('test',)

Expected behavior

Result is in a tuple or list e.g.
res == ['test'] or res == ('test', )

Version

Python-Version: 3.11
opcua-asyncio Version: master

In detail

A tuple with a single item ("test",) is converted into a list in line 112. The list is than unpacked in line 28 resulting in the loss of the tuple information. On the other hand, for a list with a single item like ["test"] this works fine as the list is not unpacked, see line 114.

For multiple elements this works as expected:

@uamethod
def put_multiple_into_tuple(parent, input1, input2) -> tuple[str]:
    return (input1, input2)
    
def test_multiple_inputs_tuple_behaviour(client, idx):
    expected_result = ["test1", "test2"]
    res = client.nodes.objects.call_method(
        f"{idx}:put_multiple_into_tuple", "test1", "test2"
    )
    assert res == expected_result

# Passed: ["test1", "test2"] == ["test1", "test2"]

Is this expected behaviour due to the OPC UA specification or a bug?

In OPC UA there are no tuples. But OPC UA allows multiple return values of a Method. So we unpack a tupple as return values. And if there is only one return value, we return the value instead an array of return values.

Ok, but why is the behaviour for lists different? For example the same code with a list instead of a tuple:

@uamethod
def put_into_list(parent, input: str) -> list[str]:
    return [input]
    
def test_list_behaviour(client, idx):
    expected_result = ["test"]
    res = client.nodes.objects.call_method(f"{idx}:put_into_list", "test")
    assert res == expected_result

# Passed: ["test"] == ["test"]

This works as expected, returning a list with a single element, but a tuple is unpacked into a single element.

Because in OPC UA you can return an OPC UA Array which is a list or chained lists, so there is no reason to convert it.

Ok, this was not was I was expecting. So the expected behaviour is?

Input: Tuple with 1 item -> Output: Item (without List or Tuple)
Input: List with 1 item -> Output: List with 1 item

Input: Tuple with 2 items -> Output: List with 2 items
Input: List with 2 items -> Output: List with 2 items