Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions atest/DynamicTypesAnnotationsLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions atest/DynamicTypesLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
11 changes: 10 additions & 1 deletion atest/tests_types.robot
Original file line number Diff line number Diff line change
Expand Up @@ -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\\): <class 'tuple'>, 222: <class '(int|str)'>

Keyword Only Arguments Without VarArg
[Tags] py3
${return} = DynamicTypesAnnotationsLibrary.Keyword Only Arguments No Vararg other=tidii
Should Match ${return} tidii: <class 'str'>

*** Keywords ***
Import DynamicTypesAnnotationsLibrary In Python 3 Only
${py3} = DynamicTypesLibrary.Is Python 3
Expand Down
107 changes: 74 additions & 33 deletions src/robotlibcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
29 changes: 29 additions & 0 deletions utest/test_get_keyword_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__')
Expand All @@ -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}
Loading