Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server method decorated with uamethod do not convert to correct dtypes #1784

Open
GPla opened this issue Feb 4, 2025 · 4 comments
Open

Server method decorated with uamethod do not convert to correct dtypes #1784

GPla opened this issue Feb 4, 2025 · 4 comments

Comments

@GPla
Copy link

GPla commented Feb 4, 2025

Describe the bug

A server method decorated with @uamethod does not convert to the dtypes specified when registering the method at the server. Integers are always converted to Int64 and floats to Double.

To Reproduce

Adapted from the sync examples:

# %%

from asyncua.sync import Server
from asyncua import ua, uamethod


def print_result(func):
    def wrapped(*args, **kwargs):
        result = func(*args, **kwargs)
        print(result)
        return result

    return wrapped


@print_result
@uamethod
def multiply(parent, x, y):
    print("multiply method call with parameters: ", x, y)
    return x * y


# set up our server
server = Server()
server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")

# set up our own namespace, not really necessary but should as spec
uri = "http://examples.freeopcua.github.io"
idx = server.register_namespace(uri)

# populating our address space
myobj = server.nodes.objects.add_method(
    idx,
    "multiply",
    multiply,
    [ua.VariantType.Int16, ua.VariantType.Int16],
    [ua.VariantType.Int16],
)

# starting!
server.start()

#%%

from asyncua.sync import Client, ThreadLoop

with ThreadLoop() as tloop:
        with Client("opc.tcp://localhost:4840/freeopcua/server/", tloop=tloop) as client:
            uri = "http://examples.freeopcua.github.io"
            idx = client.get_namespace_index(uri)
            obj = client.nodes.objects
            res = obj.call_method(f"{idx}:multiply", 3, 2)

Printed output:

multiply method call with parameters:  3 2
[Variant(Value=6, VariantType=<VariantType.Int64: 8>, Dimensions=None, is_array=False)]

Expected behavior

The return type should be Int16 as in add_method instead of Int64.

Problem

The conversion in uamethod is performed by calling _format_call_outputs and to_variant if no Variant is returned by the method; a new Variant with VariantType=None is initialized.
Thereby, the types are guessed and the types from the method registration are ignored.

From methods.py:

def uamethod(func):
    """
    Method decorator to automatically convert
    arguments and output to and from variants
    """

    if iscoroutinefunction(func):
        async def wrapper(parent, *args):
            func_args = _format_call_inputs(parent, *args)
            result = await func(*func_args)
            return _format_call_outputs(result)

    else:
        def wrapper(parent, *args):
            func_args = _format_call_inputs(parent, *args)
            result = func(*func_args)
            return _format_call_outputs(result)
    return wrapper

def _format_call_outputs(result):
    if result is None:
        return []
    elif isinstance(result, ua.CallMethodResult):
        result.OutputArguments = to_variant(*result.OutputArguments)
        return result
    elif isinstance(result, ua.StatusCode):
        return result
    elif isinstance(result, tuple):
        return to_variant(*result)
    else:
        return to_variant(result)


def to_variant(*args: Iterable) -> List[ua.Variant]:
    """Create a list of ua.Variants from a given iterable of arguments."""
    uaargs: List[ua.Variant] = []
    for arg in args:
        if not isinstance(arg, ua.Variant):
            arg = ua.Variant(arg)
        uaargs.append(arg)
    return uaargs

Version

Python-Version: 3.11
opcua-asyncio Version (e.g. master branch, 0.9): 1.1.5

@GPla GPla changed the title Server method decorated with uamethod does convert to correct dtypes Server method decorated with uamethod do not convert to correct dtypes Feb 4, 2025
@AndreasHeine
Copy link
Member

AndreasHeine commented Feb 6, 2025

@GPla the main issue is there is no "int16" in python... and if your method does not return the correct VariantType asyncua can only guess and guess what a python int is an int64 the same thing applies with other types as well. due to the fact that OPC UA is strict on typing i recommend you to always specify the correct VariantType!

@GPla
Copy link
Author

GPla commented Feb 6, 2025

I understand why this is the case. However, it would be nice if it was mentioned in the docstring of uamethod. In general, this implementation is very loose on the type system (understandable with python), but this also leads to ugly errors in the serialization (without uamethod), e.g., if I pass a string to a return param of Int, the serializer will error.

My problem with this implementation is, that the information is known through the registration of the method. Now, this information should be used to verify the types. This job should actually reside with the MethodService for input and output arguments.

@oroulet
Copy link
Member

oroulet commented Feb 6, 2025

you are right. I do not see it as a very big issue because if you care about types you should not use that wrapper, but MR is welcome.

@AndreasHeine
Copy link
Member

AndreasHeine commented Feb 6, 2025

https://github.com/AndreasHeine/opcua-asyncio/blob/master/examples%2Fserver-extension-object-as-method-argument.py

here us a good example!

edit @GPla :

if you define the "ua.Argument()" and the wrapper before binding you could use the info to validate contents:

    inarg_extobj = ua.Argument()
    inarg_extobj.Name = "In"
    inarg_extobj.DataType = ua.NodeId(12079, 0)
    inarg_extobj.ValueRank = -1
    inarg_extobj.ArrayDimensions = []
    inarg_extobj.Description = ua.LocalizedText("Wanted AxisInformation")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants