diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index f7aa582..398e495 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -73,3 +73,35 @@ def keyword_robot_types_and_bool_defaults(self, arg1, arg2=False): @keyword def keyword_exception_annotations(self, arg: 'NotHere'): return arg + + @keyword + def keyword_only_arguments(self, *varargs, some=111): + return f'{varargs}: {type(varargs)}, {some}: {type(some)}' + + @keyword + def keyword_only_arguments_no_default(self, *varargs, other): + return f'{varargs}, {other}' + + @keyword + def keyword_only_arguments_no_vararg(self, *, other): + return f'{other}: {type(other)}' + + @keyword + def keyword_only_arguments_many_positional_and_default(self, *varargs, one, two, three, four=True, five=None, six=False): + return f'{varargs}, {one}, {two}, {three}, {four}, {five}, {six}' + + @keyword + def keyword_only_arguments_default_and_no_default(self, *varargs, other, value=False): + return f'{varargs}, {other}, {value}' + + @keyword + def keyword_only_arguments_many(self, *varargs, some='value', other=None): + return f'{some}: {type(some)}, {other}: {type(other)}, {varargs}: {type(varargs)}' + + @keyword + def keyword_mandatory_and_keyword_only_arguments(self, arg: int, *vararg, some=True): + return f'{arg}, {vararg}, {some}' + + @keyword + def keyword_all_args(self, mandatory, positional=1, *varargs, other, value=False, **kwargs): + return True diff --git a/atest/DynamicTypesLibrary.py b/atest/DynamicTypesLibrary.py index 98449e2..8fbb0d6 100644 --- a/atest/DynamicTypesLibrary.py +++ b/atest/DynamicTypesLibrary.py @@ -75,3 +75,7 @@ def keyword_with_def_deco(self): @deco_wraps def keyword_wrapped(self, number=1, arg=''): return number, arg + + @keyword + def varargs_and_kwargs(self, *args, **kwargs): + return '%s, %s' % (args, kwargs) diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 25bde70..27f5bd7 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -47,12 +47,21 @@ Keyword Annonations And Robot Types Disbales Argument Conversion ${return} = DynamicTypesAnnotationsLibrary.Keyword Robot Types Disabled And Annotations 111 Should Match Regexp ${return} 111: <(class|type) 'str'> - Keyword Annonations And Robot Types Defined [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Robot Types And Bool Defaults tidii 111 Should Match Regexp ${return} tidii: <(class|type) 'str'>, 111: <(class|type) 'str'> +Keyword Annonations And Keyword Only Arguments + [Tags] py3 + ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments 1 ${1} some=222 + Should Match Regexp ${return} \\('1', 1\\): , 222: + +Keyword Only Arguments Without VarArg + [Tags] py3 + ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments No Vararg other=tidii + Should Match ${return} tidii: + *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3 Only ${py3} = DynamicTypesLibrary.Is Python 3 diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 320647e..ece0a40 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -22,16 +22,17 @@ import inspect import os import sys + try: import typing except ImportError: typing = None - from robot.api.deco import keyword # noqa F401 from robot import __version__ as robot_version PY2 = sys.version_info < (3,) +RF32 = robot_version > '3.2' __version__ = '1.0.1.dev1' @@ -101,36 +102,8 @@ def get_keyword_arguments(self, name): kw_method = self.__get_keyword(name) if kw_method is None: return None - args, defaults, varargs, kwargs = self.__get_arg_spec(kw_method) - if robot_version >= '3.2': - args += self.__new_default_spec(defaults) - else: - args += self.__old_default_spec(defaults) - if varargs: - args.append('*%s' % varargs) - if kwargs: - args.append('**%s' % kwargs) - return args - - def __new_default_spec(self, defaults): - return [(name, value) for name, value in defaults] - - def __old_default_spec(self, defaults): - return ['{}={}'.format(name, value) for name, value in defaults] - - def __get_arg_spec(self, kw): - if PY2: - spec = inspect.getargspec(kw) - keywords = spec.keywords - else: - spec = inspect.getfullargspec(kw) - keywords = spec.varkw - args = spec.args[1:] if inspect.ismethod(kw) else spec.args # drop self - defaults = spec.defaults or () - nargs = len(args) - len(defaults) - mandatory = args[:nargs] - defaults = zip(args[nargs:], defaults) - return mandatory, defaults, spec.varargs, keywords + spec = ArgumentSpec.from_function(kw_method) + return spec.get_arguments() def get_keyword_tags(self, name): self.__get_keyword_tags_supported = True @@ -181,8 +154,11 @@ def __get_typing_hints(self, method): return hints def __join_defaults_with_types(self, method, types): - _, defaults, _, _ = self.__get_arg_spec(method) - for name, value in defaults: + spec = ArgumentSpec.from_function(method) + for name, value in spec.defaults: + if name not in types and isinstance(value, (bool, type(None))): + types[name] = type(value) + for name, value in spec.kwonlydefaults: if name not in types and isinstance(value, (bool, type(None))): types[name] = type(value) return types @@ -220,3 +196,68 @@ class StaticCore(HybridCore): def __init__(self): HybridCore.__init__(self, []) + + +class ArgumentSpec(object): + + def __init__(self, positional=None, defaults=None, varargs=None, kwonlyargs=None, + kwonlydefaults=None, kwargs=None): + self.positional = positional or [] + self.defaults = defaults or [] + self.varargs = varargs + self.kwonlyargs = kwonlyargs or [] + self.kwonlydefaults = kwonlydefaults or [] + self.kwargs = kwargs + + def get_arguments(self): + args = self._format_positional(self.positional, self.defaults) + args += self._format_default(self.defaults) + if self.varargs: + args.append('*%s' % self.varargs) + args += self._format_positional(self.kwonlyargs, self.kwonlydefaults) + args += self._format_default(self.kwonlydefaults) + if self.kwargs: + args.append('**%s' % self.kwargs) + return args + + def _format_positional(self, positional, defaults): + for argument, _ in defaults: + positional.remove(argument) + return positional + + def _format_default(self, defaults): + if RF32: + return [default for default in defaults] + return ['%s=%s' % (argument, default) for argument, default in defaults] + + @classmethod + def from_function(cls, function): + if PY2: + spec = inspect.getargspec(function) + else: + spec = inspect.getfullargspec(function) + args = spec.args[1:] if inspect.ismethod(function) else spec.args # drop self + defaults = cls._get_defaults(spec) + kwonlyargs, kwonlydefaults, kwargs = cls._get_kw_args(spec) + return cls(positional=args, + defaults=defaults, + varargs=spec.varargs, + kwonlyargs=kwonlyargs, + kwonlydefaults=kwonlydefaults, + kwargs=kwargs) + + @classmethod + def _get_defaults(cls, spec): + if not spec.defaults: + return [] + names = spec.args[-len(spec.defaults):] + return list(zip(names, spec.defaults)) + + @classmethod + def _get_kw_args(cls, spec): + if PY2: + return [], [], spec.keywords + kwonlyargs = spec.kwonlyargs or [] + defaults = spec.kwonlydefaults or {} + kwonlydefaults = [(arg, name) for arg, name in defaults.items()] + return kwonlyargs, kwonlydefaults, spec.varkw diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 1339211..b7875c6 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -154,6 +154,11 @@ def test_dummy_magic_method(lib): assert types is None +def test_varargs(lib): + types = lib.get_keyword_types('varargs_and_kwargs') + assert types == {} + + @pytest.mark.skipif(PY2, reason='Only applicable on Python 3') def test_init_args_with_annotation(lib_types): types = lib_types.get_keyword_types('__init__') @@ -164,3 +169,27 @@ def test_init_args_with_annotation(lib_types): def test_exception_in_annotations(lib_types): types = lib_types.get_keyword_types('keyword_exception_annotations') assert types == {'arg': 'NotHere'} + + +@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') +def test_keyword_only_arguments(lib_types): + types = lib_types.get_keyword_types('keyword_only_arguments') + assert types == {} + + +@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') +def test_keyword_only_arguments_many(lib_types): + types = lib_types.get_keyword_types('keyword_only_arguments_many') + assert types == {'other': type(None)} + + +@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') +def test_keyword_only_arguments_many(lib_types): + types = lib_types.get_keyword_types('keyword_mandatory_and_keyword_only_arguments') + assert types == {'arg': int, 'some': bool} + + +@pytest.mark.skipif(PY2, reason='Only applicable on Python 3') +def test_keyword_only_arguments_many(lib_types): + types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default') + assert types == {'four': bool, 'five': type(None), 'six': bool} diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 28da85c..1a23920 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -3,9 +3,16 @@ import pytest from robot import __version__ as robot__version -from robotlibcore import HybridCore +from robotlibcore import HybridCore, PY2, ArgumentSpec from HybridLibrary import HybridLibrary from DynamicLibrary import DynamicLibrary +if not PY2: + from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary + + +@pytest.fixture(scope='module') +def dyn_lib(): + return DynamicLibrary() def test_keyword_names(): @@ -30,15 +37,12 @@ def test_keyword_names(): def test_dir(): expected = ['Custom name', 'Embedded arguments "${here}"', - '_DynamicCore__get_arg_spec', '_DynamicCore__get_keyword', '_DynamicCore__get_keyword_line', '_DynamicCore__get_keyword_path', '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__get_typing_hints', '_DynamicCore__join_defaults_with_types', - '_DynamicCore__new_default_spec', - '_DynamicCore__old_default_spec', '_HybridCore__get_members', '_HybridCore__get_members_from_instance', '_custom_name', @@ -70,14 +74,11 @@ def test_dir(): 'varargs_and_kwargs'] assert [a for a in dir(DynamicLibrary()) if a[:2] != '__'] == expected expected = [e for e in expected if e not in ('_DynamicCore__get_typing_hints', - '_DynamicCore__get_arg_spec', '_DynamicCore__get_keyword', '_DynamicCore__get_keyword_line', '_DynamicCore__get_keyword_path', '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__join_defaults_with_types', - '_DynamicCore__new_default_spec', - '_DynamicCore__old_default_spec', 'get_keyword_arguments', 'get_keyword_documentation', 'get_keyword_source', @@ -100,6 +101,7 @@ def test_getattr(): assert str(exc_info.value) == \ "'%s' object has no attribute 'non_existing'" % type(lib).__name__ + @pytest.mark.skipif(robot__version >= '3.2', reason='For RF 3.1') def test_get_keyword_arguments_rf31(): args = DynamicLibrary().get_keyword_arguments @@ -124,6 +126,148 @@ def test_get_keyword_arguments_rf32(): assert args('__foobar__') is None +def test_argument_spec_no_args(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.keyword_in_main) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +def test_argument_spec_mandatory(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.mandatory) + assert spec.positional == ['arg1', 'arg2'] + assert spec.defaults == [] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +def test_argument_spec_defaults(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.defaults) + assert spec.positional == ['arg1', 'arg2', 'arg3'] + assert spec.defaults == [('arg2', 'default'), ('arg3', 3)] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +def test_argument_spec_varargs_and_kwargs(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.varargs_and_kwargs) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs == 'args' + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs == 'kws' + + +def test_argument_spec_kwargs_only(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.kwargs_only) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs == 'kws' + + +def test_argument_spec_all_arguments(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.all_arguments) + assert spec.positional == ['mandatory', 'default'] + assert spec.defaults == [('default', 'value')] + assert spec.varargs == 'varargs' + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs == 'kwargs' + + +def test_argument_spec_init(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.__init__) + assert spec.positional == ['arg'] + assert spec.defaults == [('arg', None)] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +def test_argument_spec_keyword_only_arguments(): + lib = DynamicTypesAnnotationsLibrary(1) + spec = ArgumentSpec.from_function(lib.keyword_only_arguments) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs == 'varargs' + assert spec.kwonlyargs == ['some'] + assert spec.kwonlydefaults == [('some', 111)] + assert spec.kwargs is None + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +def test_argument_spec_keyword_only_arguments_no_default(): + lib = DynamicTypesAnnotationsLibrary(1) + spec = ArgumentSpec.from_function(lib.keyword_only_arguments_no_default) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs == 'varargs' + assert spec.kwonlyargs == ['other'] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +def test_argument_spec_keyword_only_arguments_no_vararg(): + lib = DynamicTypesAnnotationsLibrary(1) + spec = ArgumentSpec.from_function(lib.keyword_only_arguments_no_vararg) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs is None + assert spec.kwonlyargs == ['other'] + assert spec.kwonlydefaults == [] + assert spec.kwargs is None + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +def test_argument_spec_keyword_only_arguments_many_args(): + lib = DynamicTypesAnnotationsLibrary(1) + spec = ArgumentSpec.from_function(lib.keyword_only_arguments_many_positional_and_default) + assert spec.positional == [] + assert spec.defaults == [] + assert spec.varargs == 'varargs' + assert spec.kwonlyargs == ['one', 'two', 'three', 'four', 'five', 'six'] + assert spec.kwonlydefaults == [('four', True), ('five', None), ('six', False)] + assert spec.kwargs is None + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +@pytest.mark.skipif(robot__version < '3.2', reason='For RF 3.2 or greater') +def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): + args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments + assert args('keyword_only_arguments') == ['*varargs', ('some', 111)] + assert args('keyword_only_arguments_many') == ['*varargs', ('some', 'value'), ('other', None)] + assert args('keyword_only_arguments_no_default') == ['*varargs', 'other'] + assert args('keyword_only_arguments_default_and_no_default') == ['*varargs', 'other', ('value', False)] + all_args = [ 'mandatory', ('positional', 1), '*varargs', 'other', ('value', False), '**kwargs'] + assert args('keyword_all_args') == all_args + + +@pytest.mark.skipif(PY2, reason='Only for Python 3') +@pytest.mark.skipif(robot__version > '3.2', reason='For RF 3.1') +def test_keyword_only_arguments_for_get_keyword_arguments_rf31(): + args = DynamicTypesAnnotationsLibrary(1).get_keyword_arguments + assert args('keyword_only_arguments') == ['*varargs', 'some=111'] + assert args('keyword_only_arguments_many') == ['*varargs', 'some=value', 'other=None'] + assert args('keyword_only_arguments_no_default') == ['*varargs', 'other'] + assert args('keyword_only_arguments_default_and_no_default') == ['*varargs', 'other', 'value=False'] + all_args = ['mandatory', 'positional=1', '*varargs', 'other', 'value=False', '**kwargs'] + assert args('keyword_all_args') == all_args + + def test_get_keyword_documentation(): doc = DynamicLibrary().get_keyword_documentation assert doc('function') == ''