From 739b5d1f50ea898e6689cf1a6c5a0041319b9870 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 Apr 2020 00:30:28 +0300 Subject: [PATCH 01/18] Test varargs and kwargs type --- atest/DynamicTypesLibrary.py | 4 ++++ utest/test_get_keyword_types.py | 5 +++++ 2 files changed, 9 insertions(+) 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/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 1339211..08ebc9c 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__') From 958e3cd242e22dfb5779135a78934239a32bae42 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 Apr 2020 00:33:07 +0300 Subject: [PATCH 02/18] Support for keyword only arguments --- atest/DynamicTypesAnnotationsLibrary.py | 8 ++++++ src/robotlibcore.py | 35 ++++++++++++++++++++----- utest/test_robotlibcore.py | 28 +++++++++++++++++++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index f7aa582..22fc4ee 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -73,3 +73,11 @@ 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='value'): + return f'{some}: {type(some)}, {varargs}: {type(varargs)}' + + @keyword + def keyword_only_arguments_many(self, *varargs, some='value', other=None): + return f'{some}: {type(some)}, {other}: {type(other)}, {varargs}: {type(varargs)}' diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 320647e..9387c93 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -101,36 +101,59 @@ 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, defaults, varargs, kwargs, kwonlydefaults = self.__get_arg_spec(kw_method) + if self.__rf_31: args += self.__old_default_spec(defaults) + else: + args += self.__new_default_spec(defaults) if varargs: args.append('*%s' % varargs) if kwargs: args.append('**%s' % kwargs) + if kwonlydefaults: + args += self.__kwonlydefaults_spec(kwonlydefaults) return args + @property + def __rf_31(self): + return robot_version < '3.2' + 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 __kwonlydefaults_spec(self, kwonlydefaults): + args = [] + for argument, default_value in kwonlydefaults.items(): + if self.__rf_31: + args.append(self.__old_kwonlydefaults_spec(argument, default_value)) + else: + args.append(self.__new_kwonlydefaults_spec(argument, default_value)) + return args + + def __new_kwonlydefaults_spec(self, argument, default_value): + return (argument, default_value) + + def __old_kwonlydefaults_spec(self, argument, default_value): + return '%s=%s' % (argument, default_value) + def __get_arg_spec(self, kw): if PY2: spec = inspect.getargspec(kw) keywords = spec.keywords + kwonlydefaults = {} else: spec = inspect.getfullargspec(kw) keywords = spec.varkw + kwonlydefaults = spec.kwonlydefaults 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 + return mandatory, defaults, spec.varargs, keywords, kwonlydefaults def get_keyword_tags(self, name): self.__get_keyword_tags_supported = True @@ -181,7 +204,7 @@ def __get_typing_hints(self, method): return hints def __join_defaults_with_types(self, method, types): - _, defaults, _, _ = self.__get_arg_spec(method) + _, defaults, _, _, _ = self.__get_arg_spec(method) for name, value in defaults: if name not in types and isinstance(value, (bool, type(None))): types[name] = type(value) diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 28da85c..3f7e43a 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -3,9 +3,11 @@ import pytest from robot import __version__ as robot__version -from robotlibcore import HybridCore +from robotlibcore import HybridCore, PY2 from HybridLibrary import HybridLibrary from DynamicLibrary import DynamicLibrary +if not PY2: + from DynamicTypesAnnotationsLibrary import DynamicTypesAnnotationsLibrary def test_keyword_names(): @@ -37,8 +39,12 @@ def test_dir(): '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__get_typing_hints', '_DynamicCore__join_defaults_with_types', + '_DynamicCore__kwonlydefaults_spec', '_DynamicCore__new_default_spec', + '_DynamicCore__new_kwonlydefaults_spec', '_DynamicCore__old_default_spec', + '_DynamicCore__old_kwonlydefaults_spec', + '_DynamicCore__rf_31', '_HybridCore__get_members', '_HybridCore__get_members_from_instance', '_custom_name', @@ -76,8 +82,12 @@ def test_dir(): '_DynamicCore__get_keyword_path', '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__join_defaults_with_types', + '_DynamicCore__kwonlydefaults_spec', '_DynamicCore__new_default_spec', + '_DynamicCore__new_kwonlydefaults_spec', '_DynamicCore__old_default_spec', + '_DynamicCore__old_kwonlydefaults_spec', + '_DynamicCore__rf_31', 'get_keyword_arguments', 'get_keyword_documentation', 'get_keyword_source', @@ -124,6 +134,22 @@ def test_get_keyword_arguments_rf32(): assert args('__foobar__') 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', 'value')] + assert args('keyword_only_arguments_many') == ['*varargs', ('some', 'value'), ('other', None)] + + +@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=value'] + assert args('keyword_only_arguments_many') == ['*varargs', 'some=value', 'other=None'] + + def test_get_keyword_documentation(): doc = DynamicLibrary().get_keyword_documentation assert doc('function') == '' From 1fca1552e349e2461228a6e1bf7a135b8524a43a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 Apr 2020 01:32:00 +0300 Subject: [PATCH 03/18] Support keyword only arguments for get types --- atest/DynamicTypesAnnotationsLibrary.py | 4 ++++ src/robotlibcore.py | 6 +++++- utest/test_get_keyword_types.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 22fc4ee..0499dfd 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -81,3 +81,7 @@ def keyword_only_arguments(self, *varargs, some='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}' diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 9387c93..fc4aa81 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -204,10 +204,14 @@ def __get_typing_hints(self, method): return hints def __join_defaults_with_types(self, method, types): - _, defaults, _, _, _ = self.__get_arg_spec(method) + _, defaults, _, _, kwonlydefaults = self.__get_arg_spec(method) for name, value in defaults: if name not in types and isinstance(value, (bool, type(None))): types[name] = type(value) + if kwonlydefaults: + for name, value in kwonlydefaults.items(): + if name not in types and isinstance(value, (bool, type(None))): + types[name] = type(value) return types def get_keyword_source(self, keyword_name): diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 08ebc9c..15ac1bd 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -169,3 +169,21 @@ 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} From 962ec4766b13f4311e14eb86934da5a7a6f9a430 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 Apr 2020 01:40:02 +0300 Subject: [PATCH 04/18] Atest for keyword only arguments --- atest/DynamicTypesAnnotationsLibrary.py | 4 ++-- atest/tests_types.robot | 6 +++++- utest/test_robotlibcore.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 0499dfd..75ef468 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -75,8 +75,8 @@ def keyword_exception_annotations(self, arg: 'NotHere'): return arg @keyword - def keyword_only_arguments(self, *varargs, some='value'): - return f'{some}: {type(some)}, {varargs}: {type(varargs)}' + def keyword_only_arguments(self, *varargs, some=111): + return f'{varargs}: {type(varargs)}, {some}: {type(some)}' @keyword def keyword_only_arguments_many(self, *varargs, some='value', other=None): diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 25bde70..d3fb8cd 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -47,12 +47,16 @@ 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 ${return} ('1', 1): , 222: + *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3 Only ${py3} = DynamicTypesLibrary.Is Python 3 diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 3f7e43a..349e0ac 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -138,7 +138,7 @@ def test_get_keyword_arguments_rf32(): @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', 'value')] + assert args('keyword_only_arguments') == ['*varargs', ('some', 111)] assert args('keyword_only_arguments_many') == ['*varargs', ('some', 'value'), ('other', None)] @@ -146,7 +146,7 @@ def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): @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=value'] + assert args('keyword_only_arguments') == ['*varargs', 'some=111'] assert args('keyword_only_arguments_many') == ['*varargs', 'some=value', 'other=None'] From 994851fc71c4d66dfbe310c28a7ce516de101ef9 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 Apr 2020 01:54:00 +0300 Subject: [PATCH 05/18] Fixed atest --- atest/tests_types.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atest/tests_types.robot b/atest/tests_types.robot index d3fb8cd..1ad4fcb 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -55,7 +55,7 @@ Keyword Annonations And Robot Types Defined Keyword Annonations And Keyword Only Arguments [Tags] py3 ${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments 1 ${1} some=222 - Should Match ${return} ('1', 1): , 222: + Should Match Regexp ${return} \\('1', 1\\): , 222: *** Keywords *** Import DynamicTypesAnnotationsLibrary In Python 3 Only From 2e2004d2d6e318c0c2ce6ce398e1cadc59fbf55e Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Tue, 7 Apr 2020 01:34:24 +0300 Subject: [PATCH 06/18] Added ArgumentSpec class for parsing keyword arguments --- src/robotlibcore.py | 36 ++++++++++++++++++++ utest/test_robotlibcore.py | 70 +++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index fc4aa81..9076126 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -247,3 +247,39 @@ class StaticCore(HybridCore): def __init__(self): HybridCore.__init__(self, []) + + +class ArgumentSpec(object): + + def __init__(self, positional=None, varargs=None, kwonlyargs=None, kwargs=None, defaults=None): + self.positional = positional or [] + self.varargs = varargs + self.kwonlyargs = kwonlyargs or [] + self.kwargs = kwargs + self.defaults = defaults or {} + + @classmethod + def from_function(cls, function): + spec = inspect.getfullargspec(function) + args = spec.args[1:] if inspect.ismethod(function) else spec.args # drop self + defaults = cls._get_defaults(spec) + positional = cls._remove_defaults_from_positional(args, defaults) + return cls(positional=positional, + defaults=defaults, + varargs=spec.varargs, + kwargs=spec.varkw) + + @classmethod + def _get_defaults(cls, spec): + if not spec.defaults: + return {} + names = spec.args[-len(spec.defaults):] + return dict(zip(names, spec.defaults)) + + @classmethod + def _remove_defaults_from_positional(cls, args, defaults): + positional = [] + for argument in args: + if argument not in defaults: + positional.append(argument) + return positional diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 349e0ac..6640bbc 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -3,13 +3,18 @@ import pytest from robot import __version__ as robot__version -from robotlibcore import HybridCore, PY2 +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(): expected = ['Custom name', 'Embedded arguments "${here}"', @@ -134,6 +139,69 @@ 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.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwargs is None + assert spec.defaults == {} + + +def test_argument_spec_mandatory(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.mandatory) + assert spec.positional == ['arg1', 'arg2'] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwargs is None + assert spec.defaults == {} + + +def test_argument_spec_defaults(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.defaults) + assert spec.positional == ['arg1'] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwargs is None + assert spec.defaults == {'arg2': 'default', 'arg3': 3} + + +def test_argument_spec_varargs_and_kwargs(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.varargs_and_kwargs) + assert spec.positional == [] + assert spec.varargs == 'args' + assert spec.kwonlyargs == [] + assert spec.kwargs == 'kws' + assert spec.defaults == {} + + +def test_argument_spec_kwargs_only(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.kwargs_only) + assert spec.positional == [] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwargs == 'kws' + assert spec.defaults == {} + + +def test_argument_spec_all_arguments(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.all_arguments) + assert spec.positional == ['mandatory'] + assert spec.varargs == 'varargs' + assert spec.kwonlyargs == [] + assert spec.kwargs == 'kwargs' + assert spec.defaults == {'default': 'value'} + + +def test_argument_spec_init(dyn_lib): + spec = ArgumentSpec.from_function(dyn_lib.__init__) + assert spec.positional == [] + assert spec.varargs is None + assert spec.kwonlyargs == [] + assert spec.kwargs is None + assert spec.defaults == {'arg': 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(): From 34441bb847d521fefc5a0dbfc12cbd33dde1bb6a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 8 Apr 2020 00:53:02 +0300 Subject: [PATCH 07/18] ArgumentSpec to support keyword only arguments --- atest/DynamicTypesAnnotationsLibrary.py | 4 +++ src/robotlibcore.py | 22 +++++++++--- utest/test_robotlibcore.py | 45 +++++++++++++++++++++---- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 75ef468..486e033 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -78,6 +78,10 @@ def keyword_exception_annotations(self, arg: 'NotHere'): 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_many(self, *varargs, some='value', other=None): return f'{some}: {type(some)}, {other}: {type(other)}, {varargs}: {type(varargs)}' diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 9076126..69c69c9 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -251,23 +251,30 @@ def __init__(self): class ArgumentSpec(object): - def __init__(self, positional=None, varargs=None, kwonlyargs=None, kwargs=None, defaults=None): + def __init__(self, positional=None, varargs=None, kwonlyargs=None, kwonlydefaults=None, kwargs=None, defaults=None): self.positional = positional or [] + self.defaults = defaults or {} self.varargs = varargs self.kwonlyargs = kwonlyargs or [] + self.kwonlydefaults = kwonlydefaults or {} self.kwargs = kwargs - self.defaults = defaults or {} @classmethod def from_function(cls, function): - spec = inspect.getfullargspec(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) positional = cls._remove_defaults_from_positional(args, defaults) + kwonlyargs, kwonlydefaults, kwargs = cls._get_kw_args(spec) return cls(positional=positional, defaults=defaults, varargs=spec.varargs, - kwargs=spec.varkw) + kwonlyargs=kwonlyargs, + kwonlydefaults=kwonlydefaults, + kwargs=kwargs) @classmethod def _get_defaults(cls, spec): @@ -283,3 +290,10 @@ def _remove_defaults_from_positional(cls, args, defaults): if argument not in defaults: positional.append(argument) return positional + + @classmethod + def _get_kw_args(cls, spec): + if PY2: + return None, None, spec.keywords + kwonlyargs = cls._remove_defaults_from_positional(spec.kwonlyargs, spec.kwonlydefaults or []) + return kwonlyargs, spec.kwonlydefaults, spec.varkw diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 6640bbc..18f79f2 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -142,64 +142,95 @@ def test_get_keyword_arguments_rf32(): 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 - assert spec.defaults == {} 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 - assert spec.defaults == {} def test_argument_spec_defaults(dyn_lib): spec = ArgumentSpec.from_function(dyn_lib.defaults) assert spec.positional == ['arg1'] + assert spec.defaults == {'arg2': 'default', 'arg3': 3} assert spec.varargs is None assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == {} assert spec.kwargs is None - assert spec.defaults == {'arg2': 'default', 'arg3': 3} 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' - assert spec.defaults == {} 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' - assert spec.defaults == {} def test_argument_spec_all_arguments(dyn_lib): spec = ArgumentSpec.from_function(dyn_lib.all_arguments) assert spec.positional == ['mandatory'] + assert spec.defaults == {'default': 'value'} assert spec.varargs == 'varargs' assert spec.kwonlyargs == [] + assert spec.kwonlydefaults == {} assert spec.kwargs == 'kwargs' - assert spec.defaults == {'default': 'value'} def test_argument_spec_init(dyn_lib): spec = ArgumentSpec.from_function(dyn_lib.__init__) assert spec.positional == [] + 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 == [] + 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 - assert spec.defaults == {'arg': None} @pytest.mark.skipif(PY2, reason='Only for Python 3') From 987ae574ed33856690cb8684cb8d9430b6eee4ac Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 8 Apr 2020 00:02:15 +0300 Subject: [PATCH 08/18] Pep fixes again --- src/robotlibcore.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 69c69c9..921cb15 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -22,12 +22,12 @@ 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 @@ -251,13 +251,14 @@ def __init__(self): class ArgumentSpec(object): - def __init__(self, positional=None, varargs=None, kwonlyargs=None, kwonlydefaults=None, kwargs=None, defaults=None): - self.positional = positional or [] - self.defaults = defaults or {} - self.varargs = varargs - self.kwonlyargs = kwonlyargs or [] - self.kwonlydefaults = kwonlydefaults or {} - self.kwargs = kwargs + 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 @classmethod def from_function(cls, function): @@ -295,5 +296,6 @@ def _remove_defaults_from_positional(cls, args, defaults): def _get_kw_args(cls, spec): if PY2: return None, None, spec.keywords - kwonlyargs = cls._remove_defaults_from_positional(spec.kwonlyargs, spec.kwonlydefaults or []) + kwonlydefaults = spec.kwonlydefaults or [] + kwonlyargs = cls._remove_defaults_from_positional(spec.kwonlyargs, kwonlydefaults) return kwonlyargs, spec.kwonlydefaults, spec.varkw From ab91d209bc15f4b4e1a8135d096e40fa27e5dc9a Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 8 Apr 2020 00:53:59 +0300 Subject: [PATCH 09/18] Move argument spec to ArgumentSpec --- atest/DynamicTypesAnnotationsLibrary.py | 8 ++++ src/robotlibcore.py | 56 +++++++++---------------- utest/test_robotlibcore.py | 22 ++++------ 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 486e033..968c434 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -82,6 +82,10 @@ def keyword_only_arguments(self, *varargs, some=111): def keyword_only_arguments_no_default(self, *varargs, other): return f'{varargs}, {other}' + @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)}' @@ -89,3 +93,7 @@ def keyword_only_arguments_many(self, *varargs, some='value', other=None): @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/src/robotlibcore.py b/src/robotlibcore.py index 921cb15..58b177e 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -32,6 +32,7 @@ from robot import __version__ as robot_version PY2 = sys.version_info < (3,) +RF31 = robot_version < '3.2' __version__ = '1.0.1.dev1' @@ -101,43 +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, kwonlydefaults = self.__get_arg_spec(kw_method) - if self.__rf_31: - args += self.__old_default_spec(defaults) - else: - args += self.__new_default_spec(defaults) - if varargs: - args.append('*%s' % varargs) - if kwargs: - args.append('**%s' % kwargs) - if kwonlydefaults: - args += self.__kwonlydefaults_spec(kwonlydefaults) - return args - - @property - def __rf_31(self): - return robot_version < '3.2' - - 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 __kwonlydefaults_spec(self, kwonlydefaults): - args = [] - for argument, default_value in kwonlydefaults.items(): - if self.__rf_31: - args.append(self.__old_kwonlydefaults_spec(argument, default_value)) - else: - args.append(self.__new_kwonlydefaults_spec(argument, default_value)) - return args - - def __new_kwonlydefaults_spec(self, argument, default_value): - return (argument, default_value) - - def __old_kwonlydefaults_spec(self, argument, default_value): - return '%s=%s' % (argument, default_value) + spec = ArgumentSpec.from_function(kw_method) + return spec.get_arguments() def __get_arg_spec(self, kw): if PY2: @@ -260,6 +226,22 @@ def __init__(self, positional=None, defaults=None, varargs=None, kwonlyargs=None self.kwonlydefaults = kwonlydefaults or {} self.kwargs = kwargs + def get_arguments(self): + args = self.positional + for arg, default in self.defaults.items(): + default_arg = '%s=%s' % (arg, default)if RF31 else (arg, default) + args.append(default_arg) + if self.varargs: + args.append('*%s' % self.varargs) + if self.kwonlyargs: + args += self.kwonlyargs + for kw_arg, kw_default in self.kwonlydefaults.items(): + kw_arg_default = '%s=%s' %(kw_arg, kw_default) if RF31 else (kw_arg, kw_default) + args.append(kw_arg_default) + if self.kwargs: + args.append('**%s' % self.kwargs) + return args + @classmethod def from_function(cls, function): if PY2: diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 18f79f2..4764bd4 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -44,12 +44,6 @@ def test_dir(): '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__get_typing_hints', '_DynamicCore__join_defaults_with_types', - '_DynamicCore__kwonlydefaults_spec', - '_DynamicCore__new_default_spec', - '_DynamicCore__new_kwonlydefaults_spec', - '_DynamicCore__old_default_spec', - '_DynamicCore__old_kwonlydefaults_spec', - '_DynamicCore__rf_31', '_HybridCore__get_members', '_HybridCore__get_members_from_instance', '_custom_name', @@ -87,12 +81,6 @@ def test_dir(): '_DynamicCore__get_keyword_path', '_DynamicCore__get_keyword_tags_supported', '_DynamicCore__join_defaults_with_types', - '_DynamicCore__kwonlydefaults_spec', - '_DynamicCore__new_default_spec', - '_DynamicCore__new_kwonlydefaults_spec', - '_DynamicCore__old_default_spec', - '_DynamicCore__old_kwonlydefaults_spec', - '_DynamicCore__rf_31', 'get_keyword_arguments', 'get_keyword_documentation', 'get_keyword_source', @@ -115,6 +103,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 @@ -239,6 +228,10 @@ 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') @@ -247,7 +240,10 @@ 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 From bf8535cea784c45d05ac9252e7baac816cc50d21 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 11 Apr 2020 23:42:27 +0300 Subject: [PATCH 10/18] Keyword only arguments no varags test --- atest/DynamicTypesAnnotationsLibrary.py | 4 ++++ utest/test_robotlibcore.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index 968c434..b36588a 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -82,6 +82,10 @@ def keyword_only_arguments(self, *varargs, some=111): 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}' + @keyword def keyword_only_arguments_default_and_no_default(self, *varargs, other, value=False): return f'{varargs}, {other}, {value}' diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 4764bd4..6042b76 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -222,6 +222,18 @@ def test_argument_spec_keyword_only_arguments_no_default(): 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_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') @pytest.mark.skipif(robot__version < '3.2', reason='For RF 3.2 or greater') def test_keyword_only_arguments_for_get_keyword_arguments_rf32(): From a1c7a15007d380863a02ca436199013a0663f99d Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sat, 11 Apr 2020 23:44:01 +0300 Subject: [PATCH 11/18] More pep fixes --- src/robotlibcore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 58b177e..ad51170 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -236,7 +236,7 @@ def get_arguments(self): if self.kwonlyargs: args += self.kwonlyargs for kw_arg, kw_default in self.kwonlydefaults.items(): - kw_arg_default = '%s=%s' %(kw_arg, kw_default) if RF31 else (kw_arg, kw_default) + kw_arg_default = '%s=%s' % (kw_arg, kw_default) if RF31 else (kw_arg, kw_default) args.append(kw_arg_default) if self.kwargs: args.append('**%s' % self.kwargs) From bd4b36d1b416c709273a553ac4fca72f50fa4284 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 00:02:20 +0300 Subject: [PATCH 12/18] More tests --- atest/DynamicTypesAnnotationsLibrary.py | 4 ++++ utest/test_robotlibcore.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index b36588a..c993473 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -86,6 +86,10 @@ def keyword_only_arguments_no_default(self, *varargs, other): def keyword_only_arguments_no_vararg(self, *, other): return f'{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}' diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index 6042b76..5f7dd76 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -234,6 +234,18 @@ def test_argument_spec_keyword_only_arguments_no_default(): 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'] + 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(): @@ -257,6 +269,7 @@ def test_keyword_only_arguments_for_get_keyword_arguments_rf31(): 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') == '' From bb191e9862ce7e2b41082c371b88daca55a31eba Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 00:21:19 +0300 Subject: [PATCH 13/18] Refactor keyword types --- src/robotlibcore.py | 27 +++++---------------------- utest/test_get_keyword_types.py | 6 ++++++ utest/test_robotlibcore.py | 2 -- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index ad51170..f677403 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -105,22 +105,6 @@ def get_keyword_arguments(self, name): spec = ArgumentSpec.from_function(kw_method) return spec.get_arguments() - def __get_arg_spec(self, kw): - if PY2: - spec = inspect.getargspec(kw) - keywords = spec.keywords - kwonlydefaults = {} - else: - spec = inspect.getfullargspec(kw) - keywords = spec.varkw - kwonlydefaults = spec.kwonlydefaults - 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, kwonlydefaults - def get_keyword_tags(self, name): self.__get_keyword_tags_supported = True return self.keywords[name].robot_tags @@ -170,14 +154,13 @@ def __get_typing_hints(self, method): return hints def __join_defaults_with_types(self, method, types): - _, defaults, _, _, kwonlydefaults = self.__get_arg_spec(method) - for name, value in defaults: + spec = ArgumentSpec.from_function(method) + for name, value in spec.defaults.items(): + if name not in types and isinstance(value, (bool, type(None))): + types[name] = type(value) + for name, value in spec.kwonlydefaults.items(): if name not in types and isinstance(value, (bool, type(None))): types[name] = type(value) - if kwonlydefaults: - for name, value in kwonlydefaults.items(): - if name not in types and isinstance(value, (bool, type(None))): - types[name] = type(value) return types def get_keyword_source(self, keyword_name): diff --git a/utest/test_get_keyword_types.py b/utest/test_get_keyword_types.py index 15ac1bd..b7875c6 100644 --- a/utest/test_get_keyword_types.py +++ b/utest/test_get_keyword_types.py @@ -187,3 +187,9 @@ def test_keyword_only_arguments_many(lib_types): 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 5f7dd76..d12a115 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -37,7 +37,6 @@ 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', @@ -75,7 +74,6 @@ 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', From 5238c2f712d52716ee4ea6f18634a9a92bcbce88 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 02:39:05 +0300 Subject: [PATCH 14/18] Refactor ArgumentSpec --- src/robotlibcore.py | 60 ++++++++++++++++++++------------------ utest/test_robotlibcore.py | 56 +++++++++++++++++------------------ 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index f677403..f32d7e7 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -155,10 +155,10 @@ def __get_typing_hints(self, method): def __join_defaults_with_types(self, method, types): spec = ArgumentSpec.from_function(method) - for name, value in spec.defaults.items(): + 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.items(): + for name, value in spec.kwonlydefaults: if name not in types and isinstance(value, (bool, type(None))): types[name] = type(value) return types @@ -203,28 +203,38 @@ 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.defaults = defaults or [] self.varargs = varargs self.kwonlyargs = kwonlyargs or [] - self.kwonlydefaults = kwonlydefaults or {} + self.kwonlydefaults = kwonlydefaults or [] self.kwargs = kwargs def get_arguments(self): - args = self.positional - for arg, default in self.defaults.items(): - default_arg = '%s=%s' % (arg, default)if RF31 else (arg, default) - args.append(default_arg) + args = self._get_positional(self.positional, self.defaults) + for default in self.defaults: + if RF31: + args.append('%s=%s' % (default[0], default[1])) + else: + args.append(default) if self.varargs: args.append('*%s' % self.varargs) - if self.kwonlyargs: - args += self.kwonlyargs - for kw_arg, kw_default in self.kwonlydefaults.items(): - kw_arg_default = '%s=%s' % (kw_arg, kw_default) if RF31 else (kw_arg, kw_default) - args.append(kw_arg_default) + kwonlyargs = self._get_positional(self.kwonlyargs, self.kwonlydefaults) + args += kwonlyargs + for kw_default in self.kwonlydefaults: + if RF31: + args.append('%s=%s' % (kw_default[0], kw_default[1])) + else: + args.append(kw_default) if self.kwargs: args.append('**%s' % self.kwargs) return args + def _get_positional(self, positional, defaults): + args = positional + for default in defaults: + args.remove(default[0]) + return args + @classmethod def from_function(cls, function): if PY2: @@ -233,9 +243,8 @@ def from_function(cls, function): spec = inspect.getfullargspec(function) args = spec.args[1:] if inspect.ismethod(function) else spec.args # drop self defaults = cls._get_defaults(spec) - positional = cls._remove_defaults_from_positional(args, defaults) kwonlyargs, kwonlydefaults, kwargs = cls._get_kw_args(spec) - return cls(positional=positional, + return cls(positional=args, defaults=defaults, varargs=spec.varargs, kwonlyargs=kwonlyargs, @@ -245,22 +254,15 @@ def from_function(cls, function): @classmethod def _get_defaults(cls, spec): if not spec.defaults: - return {} + return [] names = spec.args[-len(spec.defaults):] - return dict(zip(names, spec.defaults)) - - @classmethod - def _remove_defaults_from_positional(cls, args, defaults): - positional = [] - for argument in args: - if argument not in defaults: - positional.append(argument) - return positional + return list(zip(names, spec.defaults)) @classmethod def _get_kw_args(cls, spec): if PY2: - return None, None, spec.keywords - kwonlydefaults = spec.kwonlydefaults or [] - kwonlyargs = cls._remove_defaults_from_positional(spec.kwonlyargs, kwonlydefaults) - return kwonlyargs, spec.kwonlydefaults, spec.varkw + return [], [], spec.keywords + kwonlyargs = spec.kwonlyargs or [] + kwonlydefaults = spec.kwonlydefaults or {} + kwonlydefaults = [(arg, name) for arg, name in kwonlydefaults.items()] + return kwonlyargs, kwonlydefaults, spec.varkw diff --git a/utest/test_robotlibcore.py b/utest/test_robotlibcore.py index d12a115..1a23920 100644 --- a/utest/test_robotlibcore.py +++ b/utest/test_robotlibcore.py @@ -129,70 +129,70 @@ def test_get_keyword_arguments_rf32(): 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.defaults == [] assert spec.varargs is None assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + 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.defaults == [] assert spec.varargs is None assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + 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'] - assert spec.defaults == {'arg2': 'default', 'arg3': 3} + 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.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.defaults == [] assert spec.varargs == 'args' assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + 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.defaults == [] assert spec.varargs is None assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + 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'] - assert spec.defaults == {'default': 'value'} + assert spec.positional == ['mandatory', 'default'] + assert spec.defaults == [('default', 'value')] assert spec.varargs == 'varargs' assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + assert spec.kwonlydefaults == [] assert spec.kwargs == 'kwargs' def test_argument_spec_init(dyn_lib): spec = ArgumentSpec.from_function(dyn_lib.__init__) - assert spec.positional == [] - assert spec.defaults == {'arg': None} + assert spec.positional == ['arg'] + assert spec.defaults == [('arg', None)] assert spec.varargs is None assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {} + assert spec.kwonlydefaults == [] assert spec.kwargs is None @@ -201,10 +201,10 @@ 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.defaults == [] assert spec.varargs == 'varargs' - assert spec.kwonlyargs == [] - assert spec.kwonlydefaults == {'some': 111} + assert spec.kwonlyargs == ['some'] + assert spec.kwonlydefaults == [('some', 111)] assert spec.kwargs is None @@ -213,22 +213,22 @@ 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.defaults == [] assert spec.varargs == 'varargs' assert spec.kwonlyargs == ['other'] - assert spec.kwonlydefaults == {} + 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_default(): +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.defaults == [] assert spec.varargs is None assert spec.kwonlyargs == ['other'] - assert spec.kwonlydefaults == {} + assert spec.kwonlydefaults == [] assert spec.kwargs is None @@ -237,10 +237,10 @@ 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.defaults == [] assert spec.varargs == 'varargs' - assert spec.kwonlyargs == ['one', 'two', 'three'] - assert spec.kwonlydefaults == {'four': True, 'five': None, 'six': False} + assert spec.kwonlyargs == ['one', 'two', 'three', 'four', 'five', 'six'] + assert spec.kwonlydefaults == [('four', True), ('five', None), ('six', False)] assert spec.kwargs is None From bc09f36cc951c1e0a361684e48cc858fabc16e76 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 03:05:46 +0300 Subject: [PATCH 15/18] Refactor ArgumentSpec more --- src/robotlibcore.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index f32d7e7..37c3bc9 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -32,7 +32,7 @@ from robot import __version__ as robot_version PY2 = sys.version_info < (3,) -RF31 = robot_version < '3.2' +RF32 = robot_version > '3.2' __version__ = '1.0.1.dev1' @@ -210,30 +210,26 @@ def __init__(self, positional=None, defaults=None, varargs=None, kwonlyargs=None self.kwargs = kwargs def get_arguments(self): - args = self._get_positional(self.positional, self.defaults) - for default in self.defaults: - if RF31: - args.append('%s=%s' % (default[0], default[1])) - else: - args.append(default) + args = self._format_positional(self.positional, self.defaults) + args += self._format_default(self.defaults) if self.varargs: args.append('*%s' % self.varargs) - kwonlyargs = self._get_positional(self.kwonlyargs, self.kwonlydefaults) - args += kwonlyargs - for kw_default in self.kwonlydefaults: - if RF31: - args.append('%s=%s' % (kw_default[0], kw_default[1])) - else: - args.append(kw_default) + 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 _get_positional(self, positional, defaults): - args = positional + def _format_positional(self, positional, defaults): for default in defaults: - args.remove(default[0]) - return args + positional.remove(default[0]) + return positional + + def _format_default(self, defaults): + if RF32: + return [default for default in defaults] + return ['%s=%s' % (arg, default) for arg, default in defaults] + @classmethod def from_function(cls, function): From 22ca87ac70b08006be9470f5add8928bea615907 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 03:08:48 +0300 Subject: [PATCH 16/18] Pep fixes again --- src/robotlibcore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 37c3bc9..3a4a020 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -230,7 +230,6 @@ def _format_default(self, defaults): return [default for default in defaults] return ['%s=%s' % (arg, default) for arg, default in defaults] - @classmethod def from_function(cls, function): if PY2: From e99020aeec70c03adaed824a1467bfc584b4e97b Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 23:10:41 +0300 Subject: [PATCH 17/18] Readability improvemetns --- src/robotlibcore.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/robotlibcore.py b/src/robotlibcore.py index 3a4a020..ece0a40 100644 --- a/src/robotlibcore.py +++ b/src/robotlibcore.py @@ -221,14 +221,14 @@ def get_arguments(self): return args def _format_positional(self, positional, defaults): - for default in defaults: - positional.remove(default[0]) + 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' % (arg, default) for arg, default in defaults] + return ['%s=%s' % (argument, default) for argument, default in defaults] @classmethod def from_function(cls, function): @@ -258,6 +258,6 @@ def _get_kw_args(cls, spec): if PY2: return [], [], spec.keywords kwonlyargs = spec.kwonlyargs or [] - kwonlydefaults = spec.kwonlydefaults or {} - kwonlydefaults = [(arg, name) for arg, name in kwonlydefaults.items()] + defaults = spec.kwonlydefaults or {} + kwonlydefaults = [(arg, name) for arg, name in defaults.items()] return kwonlyargs, kwonlydefaults, spec.varkw From e5177d3bf702418058f7badabdf5163f6ba65b76 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 12 Apr 2020 23:18:55 +0300 Subject: [PATCH 18/18] Added atest --- atest/DynamicTypesAnnotationsLibrary.py | 2 +- atest/tests_types.robot | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/atest/DynamicTypesAnnotationsLibrary.py b/atest/DynamicTypesAnnotationsLibrary.py index c993473..398e495 100644 --- a/atest/DynamicTypesAnnotationsLibrary.py +++ b/atest/DynamicTypesAnnotationsLibrary.py @@ -84,7 +84,7 @@ def keyword_only_arguments_no_default(self, *varargs, other): @keyword def keyword_only_arguments_no_vararg(self, *, other): - return f'{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): diff --git a/atest/tests_types.robot b/atest/tests_types.robot index 1ad4fcb..27f5bd7 100644 --- a/atest/tests_types.robot +++ b/atest/tests_types.robot @@ -57,6 +57,11 @@ Keyword Annonations And Keyword Only Arguments ${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