Skip to content

Commit 86f2ecb

Browse files
committed
Type conversion: Support passing type info to @Keyword as list
Also moved type validation logic into a separate module.
1 parent b94c63e commit 86f2ecb

6 files changed

Lines changed: 188 additions & 19 deletions

File tree

atest/robot/keywords/type_conversion/keyword_decorator.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,5 @@ Invalid type spec causes error
173173
Check Test Case ${TESTNAME}
174174
${error} = Catenate
175175
... Adding keyword 'invalid_type_spec' to library 'KeywordDecorator' failed:
176-
... Type information must be given as a dictionary, got string.
176+
... Type information must be given as a dictionary or a list, got string.
177177
Check Log Message ${ERRORS[0]} ${error} ERROR
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
*** Settings ***
2+
Suite Setup Run Tests ${EMPTY} keywords/type_conversion/keyword_decorator_with_list.robot
3+
Resource atest_resource.robot
4+
5+
*** Test Cases ***
6+
Basics
7+
Check Test Case ${TESTNAME}
8+
9+
None means no type
10+
Check Test Case ${TESTNAME}
11+
12+
Less types than arguments is ok
13+
Check Test Case ${TESTNAME}
14+
15+
More types than arguments causes error
16+
Check Test Case ${TESTNAME}
17+
${error} = Catenate
18+
... Adding keyword 'too_many_types' to library 'KeywordDecoratorWithList' failed:
19+
... Type information given to 2 arguments but keyword has only 1 argument.
20+
Check Log Message ${ERRORS[0]} ${error} ERROR
21+
22+
Varargs and kwargs
23+
Check Test Case ${TESTNAME}
24+
25+
Kwonly
26+
[Tags] require-py3
27+
Check Test Case ${TESTNAME}
28+
29+
Kwonly with kwargs
30+
[Tags] require-py3
31+
Check Test Case ${TESTNAME}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from datetime import date
2+
from decimal import Decimal
3+
4+
from robot.api.deco import keyword
5+
6+
7+
@keyword(types=[int, Decimal, bool, date, list])
8+
def basics(integer, decimal, boolean, date_, list_=None):
9+
_validate_type(integer, 42)
10+
_validate_type(decimal, Decimal('3.14'))
11+
_validate_type(boolean, True)
12+
_validate_type(date_, date(2018, 8, 30))
13+
_validate_type(list_, ['foo'])
14+
15+
16+
@keyword(types=[int, None, float])
17+
def none_means_no_type(foo, bar, zap):
18+
_validate_type(foo, 1)
19+
_validate_type(bar, u'2')
20+
_validate_type(zap, 3.0)
21+
22+
23+
@keyword(types=[int, float])
24+
def less_types_than_arguments_is_ok(foo, bar, zap):
25+
_validate_type(foo, 1)
26+
_validate_type(bar, 2.0)
27+
_validate_type(zap, u'3')
28+
29+
30+
@keyword(types=[int, int])
31+
def too_many_types(argument):
32+
raise RuntimeError('Should not be executed!')
33+
34+
35+
@keyword(types=[int, int, int])
36+
def varargs_and_kwargs(arg, *varargs, **kwargs):
37+
_validate_type(arg, 1)
38+
_validate_type(varargs, (2, 3, 4))
39+
_validate_type(kwargs, {'kw': 5})
40+
41+
42+
try:
43+
exec('''
44+
@keyword(types=[None, int, float])
45+
def kwonly(*, foo, bar=None, zap):
46+
_validate_type(foo, u'1')
47+
_validate_type(bar, 2)
48+
_validate_type(zap, 3.0)
49+
50+
51+
@keyword(types=[None, None, int, float, Decimal])
52+
def kwonly_with_varargs_and_kwargs(*varargs, foo, bar=None, zap, **kwargs):
53+
_validate_type(varargs, ('0',))
54+
_validate_type(foo, u'1')
55+
_validate_type(bar, 2)
56+
_validate_type(zap, 3.0)
57+
_validate_type(kwargs, {'quux': Decimal(4)})
58+
''')
59+
except SyntaxError:
60+
pass
61+
62+
63+
def _validate_type(argument, expected):
64+
if argument != expected or type(argument) != type(expected):
65+
raise AssertionError('%r (%s) != %r (%s)'
66+
% (argument, type(argument).__name__,
67+
expected, type(expected).__name__))
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
*** Settings ***
2+
Library KeywordDecoratorWithList.py
3+
Resource conversion.resource
4+
5+
*** Test Cases ***
6+
Basics
7+
Basics 42 3.14 True 2018-08-30 ['foo']
8+
9+
None means no type
10+
None means no type 1 2 3
11+
12+
Less types than arguments is ok
13+
Less types than arguments is ok 1 2 3
14+
15+
More types than arguments causes error
16+
[Documentation] FAIL No keyword with name 'Too many types' found.
17+
Too many types
18+
19+
Varargs and kwargs
20+
Varargs and kwargs 1 2 3 4 kw=5
21+
22+
Kwonly
23+
[Tags] require-py3
24+
Kwonly foo=1 zap=3 bar=2
25+
26+
Kwonly with kwargs
27+
[Tags] require-py3
28+
Kwonly with varargs and kwargs 0 foo=1 zap=3 bar=2 quux=4
29+

src/robot/running/arguments/argumentspec.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import sys
1717

1818
from robot.errors import DataError
19-
from robot.utils import is_dict_like, plural_or_not as s, seq2str, type_name
19+
from robot.utils import (is_dict_like, is_list_like, plural_or_not as s,
20+
seq2str, type_name)
2021

2122
from .argumentconverter import ArgumentConverter
2223
from .argumentmapper import ArgumentMapper
2324
from .argumentresolver import ArgumentResolver
25+
from .typevalidator import TypeValidator
2426

2527

2628
class ArgumentSpec(object):
@@ -35,24 +37,8 @@ def __init__(self, name=None, type='Keyword', positional=None,
3537
self.kwonlyargs = kwonlyargs or []
3638
self.kwargs = kwargs
3739
self.defaults = defaults or {}
38-
self.types = types or {}
40+
self.types = TypeValidator(self).validate(types) if types else {}
3941
self.supports_named = supports_named
40-
if types:
41-
self._validate_types(types)
42-
43-
def _validate_types(self, types):
44-
if not is_dict_like(types):
45-
raise DataError('Type information must be given as a dictionary, '
46-
'got %s.' % type_name(types))
47-
names = set(self.positional + self.kwonlyargs + ['return'])
48-
if self.varargs:
49-
names.add(self.varargs)
50-
if self.kwargs:
51-
names.add(self.kwargs)
52-
extra = sorted(t for t in types if t not in names)
53-
if extra:
54-
raise DataError('Type information given to non-existing '
55-
'argument%s %s.' % (s(extra), seq2str(extra)))
5642

5743
@property
5844
def minargs(self):
@@ -63,6 +49,11 @@ def minargs(self):
6349
def maxargs(self):
6450
return len(self.positional) if not self.varargs else sys.maxsize
6551

52+
@property
53+
def argument_names(self):
54+
return (self.positional + ([self.varargs] if self.varargs else []) +
55+
self.kwonlyargs + ([self.kwargs] if self.kwargs else []))
56+
6657
def resolve(self, arguments, variables=None, resolve_named=True,
6758
resolve_variables_until=None, dict_to_kwargs=False):
6859
resolver = ArgumentResolver(self, resolve_named,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2008-2015 Nokia Networks
2+
# Copyright 2016- Robot Framework Foundation
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from robot.errors import DataError
17+
from robot.utils import (is_dict_like, is_list_like, plural_or_not as s,
18+
seq2str, type_name)
19+
20+
21+
class TypeValidator(object):
22+
23+
def __init__(self, argspec):
24+
""":type argspec: :py:class:`robot.running.arguments.ArgumentSpec`"""
25+
self._argspec = argspec
26+
27+
def validate(self, types):
28+
if is_dict_like(types):
29+
return self.validate_type_dict(types)
30+
if is_list_like(types):
31+
return self.convert_type_list_to_dict(types)
32+
raise DataError('Type information must be given as a dictionary or '
33+
'a list, got %s.' % type_name(types))
34+
35+
def validate_type_dict(self, types):
36+
names = set(self._argspec.argument_names + ['return'])
37+
extra = [t for t in types if t not in names]
38+
if extra:
39+
raise DataError('Type information given to non-existing '
40+
'argument%s %s.'
41+
% (s(extra), seq2str(sorted(extra))))
42+
return types
43+
44+
def convert_type_list_to_dict(self, types):
45+
names = self._argspec.argument_names
46+
if len(types) > len(names):
47+
raise DataError('Type information given to %d argument%s but '
48+
'keyword has only %d argument%s.'
49+
% (len(types), s(types), len(names), s(names)))
50+
return {name: value for name, value in zip(names, types)
51+
if value is not None}

0 commit comments

Comments
 (0)