From 3c4de5b19bc2d49ac888a250725816c476380cba Mon Sep 17 00:00:00 2001 From: Josh Heidecker Date: Thu, 2 Jan 2025 17:19:10 -0500 Subject: [PATCH] Enable argument type conversion based on default values --- src/robotremoteserver.py | 13 +++++++++--- test/atest/types.robot | 21 +++++++++++++++++++ test/libs/Types.py | 16 +++++++++++++++ test/utest/test_argsdocs.py | 41 +++++++++++++++++++++++++++++++++++-- 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 test/atest/types.robot create mode 100644 test/libs/Types.py diff --git a/src/robotremoteserver.py b/src/robotremoteserver.py index 85ec555..7dd4a5d 100644 --- a/src/robotremoteserver.py +++ b/src/robotremoteserver.py @@ -15,6 +15,7 @@ from __future__ import print_function +import contextlib import inspect import os import re @@ -27,7 +28,7 @@ if sys.version_info < (3,): from SimpleXMLRPCServer import SimpleXMLRPCServer from StringIO import StringIO - from xmlrpclib import Binary, ServerProxy + from xmlrpclib import Binary, Fault, ServerProxy, dumps, loads from collections import Mapping PY2, PY3 = True, False def getfullargspec(func): @@ -35,7 +36,7 @@ def getfullargspec(func): else: from inspect import getfullargspec from io import StringIO - from xmlrpc.client import Binary, ServerProxy + from xmlrpc.client import Binary, Fault, ServerProxy, dumps, loads from xmlrpc.server import SimpleXMLRPCServer from collections.abc import Mapping PY2, PY3 = False, True @@ -277,6 +278,12 @@ def is_function_or_method(item): return inspect.isfunction(item) or inspect.ismethod(item) +def is_marshallable(item): + with contextlib.suppress(Fault, TypeError, OverflowError): + return item == loads(dumps((item,)), use_builtin_types=True)[0][0] + return False + + class StaticRemoteLibrary(object): def __init__(self, library): @@ -316,7 +323,7 @@ def get_keyword_arguments(self, name): args = args[1:] # drop 'self' if defaults: args, names = args[:-len(defaults)], args[-len(defaults):] - args += ['%s=%s' % (n, d) for n, d in zip(names, defaults)] + args += [(n, d) if is_marshallable(d) else '%s=%s' % (n, d) for n, d in zip(names, defaults)] if varargs: args.append('*%s' % varargs) if kwargs: diff --git a/test/atest/types.robot b/test/atest/types.robot new file mode 100644 index 0000000..2771ecd --- /dev/null +++ b/test/atest/types.robot @@ -0,0 +1,21 @@ +*** Settings *** +Documentation Testing argument type conversion using a class based library. +Resource resource.robot +Suite Setup Start And Import Remote Library Types.py +Suite Teardown Stop Remote Library + +*** Test Cases *** +Default None + Default None + Default None abc + Default None arg=abc + +Default Float + Default Float + Default Float 5 + Default Float arg=5 + +Default Bytes + Default Bytes + Default Bytes abc + Default Bytes arg=abc diff --git a/test/libs/Types.py b/test/libs/Types.py new file mode 100644 index 0000000..7dbe7be --- /dev/null +++ b/test/libs/Types.py @@ -0,0 +1,16 @@ +class Types(object): + def default_none(self, arg=None): + assert arg is None or isinstance(arg, str) and '=' not in arg, f'{arg=}' + + def default_float(self, arg=0.0): + assert isinstance(arg, float), f'{arg=}' + + def default_bytes(self, arg=b''): + assert isinstance(arg, bytes) and b'=' not in arg, f'{arg=}' + + +if __name__ == '__main__': + import sys + from robotremoteserver import RobotRemoteServer + + RobotRemoteServer(Types(), '127.0.0.1', *sys.argv[1:]) diff --git a/test/utest/test_argsdocs.py b/test/utest/test_argsdocs.py index dbf9d09..26cf385 100755 --- a/test/utest/test_argsdocs.py +++ b/test/utest/test_argsdocs.py @@ -1,10 +1,17 @@ """Module doc - used in tests""" +import datetime +import decimal import unittest from robotremoteserver import RemoteLibraryFactory +class NonMarshallable: + def __repr__(self): + return f'{self.__class__.__name__}()' + + class LibraryWithArgsAndDocs: """Intro doc""" @@ -17,6 +24,18 @@ def keyword(self, k1, k2=2, *k3): def no_doc_or_args(self): pass + def marshallable_defaults(self, k1=True, k2=0, k3=0.0, k4='', k5=[], k6={}, k7=datetime.datetime.min, k8=b''): + pass + + def non_marshallable_defaults(self, k1=NonMarshallable(), k2=2147483648, k3=decimal.Decimal(0), k4=None, k5=[None]): + pass + + def nested_marshallable_defaults(self, k1={'one': [1], 'two': False}): + pass + + def mixed_defaults(self, k1=True, k2=None): + pass + def keyword_in_module(m1, m2=3, *m3): """Module keyword doc""" @@ -66,16 +85,34 @@ def _test_doc(self, name, expected, library=LibraryWithArgsAndDocs(None)): class TestArgs(unittest.TestCase): def test_keyword_args(self): - self._test_args('keyword', ['k1', 'k2=2', '*k3']) + self._test_args('keyword', ['k1', ('k2', 2), '*k3']) def test_keyword_args_when_no_args(self): self._test_args('no_doc_or_args', []) def test_keyword_args_from_module_keyword(self): import test_argsdocs - self._test_args('keyword_in_module', ['m1', 'm2=3', '*m3'], + self._test_args('keyword_in_module', ['m1', ('m2', 3), '*m3'], test_argsdocs) + def test_marshallable_defaults(self): + self._test_args( + 'marshallable_defaults', + [ + ('k1', True), ('k2', 0), ('k3', 0.0), ('k4', ''), ('k5', []), ('k6', {}), + ('k7', datetime.datetime.min), ('k8', b'') + ] + ) + def test_non_marshallable_defaults(self): + self._test_args('non_marshallable_defaults', + ['k1=NonMarshallable()', 'k2=2147483648', 'k3=0', 'k4=None', 'k5=[None]']) + + def test_nested_marshallable_defaults(self): + self._test_args('nested_marshallable_defaults', [('k1', {'one': [1], 'two': False})]) + + def test_mixed_defaults(self): + self._test_args('mixed_defaults', [('k1', True), 'k2=None']) + def _test_args(self, name, expected, library=LibraryWithArgsAndDocs(None)): library = RemoteLibraryFactory(library) self.assertEquals(library.get_keyword_arguments(name), expected)