Skip to content

Commit f2c1779

Browse files
authored
Keyword only args (#35)
Fixes #9 add support for keyword only arguments in PythonLibCore.
1 parent 1b58d29 commit f2c1779

File tree

6 files changed

+300
-41
lines changed

6 files changed

+300
-41
lines changed

atest/DynamicTypesAnnotationsLibrary.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,35 @@ def keyword_robot_types_and_bool_defaults(self, arg1, arg2=False):
7373
@keyword
7474
def keyword_exception_annotations(self, arg: 'NotHere'):
7575
return arg
76+
77+
@keyword
78+
def keyword_only_arguments(self, *varargs, some=111):
79+
return f'{varargs}: {type(varargs)}, {some}: {type(some)}'
80+
81+
@keyword
82+
def keyword_only_arguments_no_default(self, *varargs, other):
83+
return f'{varargs}, {other}'
84+
85+
@keyword
86+
def keyword_only_arguments_no_vararg(self, *, other):
87+
return f'{other}: {type(other)}'
88+
89+
@keyword
90+
def keyword_only_arguments_many_positional_and_default(self, *varargs, one, two, three, four=True, five=None, six=False):
91+
return f'{varargs}, {one}, {two}, {three}, {four}, {five}, {six}'
92+
93+
@keyword
94+
def keyword_only_arguments_default_and_no_default(self, *varargs, other, value=False):
95+
return f'{varargs}, {other}, {value}'
96+
97+
@keyword
98+
def keyword_only_arguments_many(self, *varargs, some='value', other=None):
99+
return f'{some}: {type(some)}, {other}: {type(other)}, {varargs}: {type(varargs)}'
100+
101+
@keyword
102+
def keyword_mandatory_and_keyword_only_arguments(self, arg: int, *vararg, some=True):
103+
return f'{arg}, {vararg}, {some}'
104+
105+
@keyword
106+
def keyword_all_args(self, mandatory, positional=1, *varargs, other, value=False, **kwargs):
107+
return True

atest/DynamicTypesLibrary.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ def keyword_with_def_deco(self):
7575
@deco_wraps
7676
def keyword_wrapped(self, number=1, arg=''):
7777
return number, arg
78+
79+
@keyword
80+
def varargs_and_kwargs(self, *args, **kwargs):
81+
return '%s, %s' % (args, kwargs)

atest/tests_types.robot

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ Keyword Annonations And Robot Types Disbales Argument Conversion
4747
${return} = DynamicTypesAnnotationsLibrary.Keyword Robot Types Disabled And Annotations 111
4848
Should Match Regexp ${return} 111: <(class|type) 'str'>
4949

50-
5150
Keyword Annonations And Robot Types Defined
5251
[Tags] py3
5352
${return} = DynamicTypesAnnotationsLibrary.Keyword Robot Types And Bool Defaults tidii 111
5453
Should Match Regexp ${return} tidii: <(class|type) 'str'>, 111: <(class|type) 'str'>
5554

55+
Keyword Annonations And Keyword Only Arguments
56+
[Tags] py3
57+
${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments 1 ${1} some=222
58+
Should Match Regexp ${return} \\('1', 1\\): <class 'tuple'>, 222: <class '(int|str)'>
59+
60+
Keyword Only Arguments Without VarArg
61+
[Tags] py3
62+
${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments No Vararg other=tidii
63+
Should Match ${return} tidii: <class 'str'>
64+
5665
*** Keywords ***
5766
Import DynamicTypesAnnotationsLibrary In Python 3 Only
5867
${py3} = DynamicTypesLibrary.Is Python 3

src/robotlibcore.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@
2222
import inspect
2323
import os
2424
import sys
25+
2526
try:
2627
import typing
2728
except ImportError:
2829
typing = None
2930

30-
3131
from robot.api.deco import keyword # noqa F401
3232
from robot import __version__ as robot_version
3333

3434
PY2 = sys.version_info < (3,)
35+
RF32 = robot_version > '3.2'
3536

3637
__version__ = '1.0.1.dev1'
3738

@@ -101,36 +102,8 @@ def get_keyword_arguments(self, name):
101102
kw_method = self.__get_keyword(name)
102103
if kw_method is None:
103104
return None
104-
args, defaults, varargs, kwargs = self.__get_arg_spec(kw_method)
105-
if robot_version >= '3.2':
106-
args += self.__new_default_spec(defaults)
107-
else:
108-
args += self.__old_default_spec(defaults)
109-
if varargs:
110-
args.append('*%s' % varargs)
111-
if kwargs:
112-
args.append('**%s' % kwargs)
113-
return args
114-
115-
def __new_default_spec(self, defaults):
116-
return [(name, value) for name, value in defaults]
117-
118-
def __old_default_spec(self, defaults):
119-
return ['{}={}'.format(name, value) for name, value in defaults]
120-
121-
def __get_arg_spec(self, kw):
122-
if PY2:
123-
spec = inspect.getargspec(kw)
124-
keywords = spec.keywords
125-
else:
126-
spec = inspect.getfullargspec(kw)
127-
keywords = spec.varkw
128-
args = spec.args[1:] if inspect.ismethod(kw) else spec.args # drop self
129-
defaults = spec.defaults or ()
130-
nargs = len(args) - len(defaults)
131-
mandatory = args[:nargs]
132-
defaults = zip(args[nargs:], defaults)
133-
return mandatory, defaults, spec.varargs, keywords
105+
spec = ArgumentSpec.from_function(kw_method)
106+
return spec.get_arguments()
134107

135108
def get_keyword_tags(self, name):
136109
self.__get_keyword_tags_supported = True
@@ -181,8 +154,11 @@ def __get_typing_hints(self, method):
181154
return hints
182155

183156
def __join_defaults_with_types(self, method, types):
184-
_, defaults, _, _ = self.__get_arg_spec(method)
185-
for name, value in defaults:
157+
spec = ArgumentSpec.from_function(method)
158+
for name, value in spec.defaults:
159+
if name not in types and isinstance(value, (bool, type(None))):
160+
types[name] = type(value)
161+
for name, value in spec.kwonlydefaults:
186162
if name not in types and isinstance(value, (bool, type(None))):
187163
types[name] = type(value)
188164
return types
@@ -220,3 +196,68 @@ class StaticCore(HybridCore):
220196

221197
def __init__(self):
222198
HybridCore.__init__(self, [])
199+
200+
201+
class ArgumentSpec(object):
202+
203+
def __init__(self, positional=None, defaults=None, varargs=None, kwonlyargs=None,
204+
kwonlydefaults=None, kwargs=None):
205+
self.positional = positional or []
206+
self.defaults = defaults or []
207+
self.varargs = varargs
208+
self.kwonlyargs = kwonlyargs or []
209+
self.kwonlydefaults = kwonlydefaults or []
210+
self.kwargs = kwargs
211+
212+
def get_arguments(self):
213+
args = self._format_positional(self.positional, self.defaults)
214+
args += self._format_default(self.defaults)
215+
if self.varargs:
216+
args.append('*%s' % self.varargs)
217+
args += self._format_positional(self.kwonlyargs, self.kwonlydefaults)
218+
args += self._format_default(self.kwonlydefaults)
219+
if self.kwargs:
220+
args.append('**%s' % self.kwargs)
221+
return args
222+
223+
def _format_positional(self, positional, defaults):
224+
for argument, _ in defaults:
225+
positional.remove(argument)
226+
return positional
227+
228+
def _format_default(self, defaults):
229+
if RF32:
230+
return [default for default in defaults]
231+
return ['%s=%s' % (argument, default) for argument, default in defaults]
232+
233+
@classmethod
234+
def from_function(cls, function):
235+
if PY2:
236+
spec = inspect.getargspec(function)
237+
else:
238+
spec = inspect.getfullargspec(function)
239+
args = spec.args[1:] if inspect.ismethod(function) else spec.args # drop self
240+
defaults = cls._get_defaults(spec)
241+
kwonlyargs, kwonlydefaults, kwargs = cls._get_kw_args(spec)
242+
return cls(positional=args,
243+
defaults=defaults,
244+
varargs=spec.varargs,
245+
kwonlyargs=kwonlyargs,
246+
kwonlydefaults=kwonlydefaults,
247+
kwargs=kwargs)
248+
249+
@classmethod
250+
def _get_defaults(cls, spec):
251+
if not spec.defaults:
252+
return []
253+
names = spec.args[-len(spec.defaults):]
254+
return list(zip(names, spec.defaults))
255+
256+
@classmethod
257+
def _get_kw_args(cls, spec):
258+
if PY2:
259+
return [], [], spec.keywords
260+
kwonlyargs = spec.kwonlyargs or []
261+
defaults = spec.kwonlydefaults or {}
262+
kwonlydefaults = [(arg, name) for arg, name in defaults.items()]
263+
return kwonlyargs, kwonlydefaults, spec.varkw

utest/test_get_keyword_types.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ def test_dummy_magic_method(lib):
154154
assert types is None
155155

156156

157+
def test_varargs(lib):
158+
types = lib.get_keyword_types('varargs_and_kwargs')
159+
assert types == {}
160+
161+
157162
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
158163
def test_init_args_with_annotation(lib_types):
159164
types = lib_types.get_keyword_types('__init__')
@@ -164,3 +169,27 @@ def test_init_args_with_annotation(lib_types):
164169
def test_exception_in_annotations(lib_types):
165170
types = lib_types.get_keyword_types('keyword_exception_annotations')
166171
assert types == {'arg': 'NotHere'}
172+
173+
174+
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
175+
def test_keyword_only_arguments(lib_types):
176+
types = lib_types.get_keyword_types('keyword_only_arguments')
177+
assert types == {}
178+
179+
180+
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
181+
def test_keyword_only_arguments_many(lib_types):
182+
types = lib_types.get_keyword_types('keyword_only_arguments_many')
183+
assert types == {'other': type(None)}
184+
185+
186+
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
187+
def test_keyword_only_arguments_many(lib_types):
188+
types = lib_types.get_keyword_types('keyword_mandatory_and_keyword_only_arguments')
189+
assert types == {'arg': int, 'some': bool}
190+
191+
192+
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
193+
def test_keyword_only_arguments_many(lib_types):
194+
types = lib_types.get_keyword_types('keyword_only_arguments_many_positional_and_default')
195+
assert types == {'four': bool, 'five': type(None), 'six': bool}

0 commit comments

Comments
 (0)