Skip to content

Commit e7e7e74

Browse files
authored
initial builder implementation (robotframework#65)
Rewrite keyword argument, types and doc building.
1 parent 905a668 commit e7e7e74

9 files changed

+292
-269
lines changed

atest/DynamicTypesLibrary.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ def keyword_default_types(self, arg=None):
5353
def keyword_many_default_types(self, arg1=1, arg2='Foobar'):
5454
return arg1, arg2
5555

56-
@keyword
57-
def keyword_booleans(self, arg1=True, arg2=False):
58-
return '%s: %s, %s: %s' % (arg1, type(arg1), arg2, type(arg2))
59-
6056
@keyword
6157
def keyword_none(self, arg=None):
6258
return '%s: %s' % (arg, type(arg))
@@ -79,3 +75,7 @@ def keyword_wrapped(self, number=1, arg=''):
7975
@keyword
8076
def varargs_and_kwargs(self, *args, **kwargs):
8177
return '%s, %s' % (args, kwargs)
78+
79+
@keyword
80+
def keyword_booleans(self, arg1=True, arg2=False):
81+
return '%s: %s, %s: %s' % (arg1, type(arg1), arg2, type(arg2))

atest/moc_library.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from robot.api.deco import keyword
2+
3+
4+
class MockLibrary(object):
5+
6+
def no_args(self):
7+
pass
8+
9+
@keyword(types={'arg1': str, 'arg2': int})
10+
def positional_args(self, arg1, arg2):
11+
"""Some documentation
12+
13+
Multi line docs
14+
"""
15+
pass
16+
17+
@keyword(types=None)
18+
def types_disabled(self, arg=False):
19+
pass
20+
21+
@keyword
22+
def positional_and_default(self, arg1, arg2, named1='string1', named2=123):
23+
pass
24+
25+
def default_only(self, named1='string1', named2=123):
26+
pass
27+
28+
def varargs_kwargs(self, *vargs, **kwargs):
29+
pass

atest/moc_library_py3.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class MockLibraryPy3:
2+
3+
def named_only(self, *varargs, key1, key2):
4+
pass
5+
6+
def named_only_with_defaults(self, *varargs, key1, key2, key3='default1', key4=True):
7+
pass
8+
9+
def args_with_type_hints(self, arg1, arg2, arg3: str, arg4: None) -> bool:
10+
pass
11+
12+
def self_and_keyword_only_types(x: 'MockLibraryPy3', mandatory, *varargs: int, other: bool, **kwargs: int):
13+
pass

atest/tests_types.robot

-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ Keyword Default As Booleans With Defaults
1919
${return} DynamicTypesLibrary.Keyword Booleans
2020
Should Match Regexp ${return} True: <(class|type) 'bool'>, False: <(class|type) 'bool'>
2121

22-
Keyword Default As Booleans With Strings
23-
${return} = DynamicTypesLibrary.Keyword Booleans False True
24-
Should Match Regexp ${return} False: <(class|type) 'bool'>, True: <(class|type) 'bool'>
25-
2622
Keyword Default As Booleans With Objects
2723
${return} = DynamicTypesLibrary.Keyword Booleans ${False} ${True}
2824
Should Match Regexp ${return} False: <(class|type) 'bool'>, True: <(class|type) 'bool'>

src/robotlibcore.py

+127-117
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,20 @@ class HybridCore(object):
4141

4242
def __init__(self, library_components):
4343
self.keywords = {}
44+
self.keywords_spec = {}
4445
self.attributes = {}
4546
self.add_library_components(library_components)
4647
self.add_library_components([self])
4748

4849
def add_library_components(self, library_components):
50+
self.keywords_spec['__init__'] = KeywordBuilder.build(self.__init__)
4951
for component in library_components:
5052
for name, func in self.__get_members(component):
5153
if callable(func) and hasattr(func, 'robot_name'):
5254
kw = getattr(component, name)
5355
kw_name = func.robot_name or name
5456
self.keywords[kw_name] = kw
57+
self.keywords_spec[kw_name] = KeywordBuilder.build(kw)
5558
# Expose keywords as attributes both using original
5659
# method names as well as possible custom names.
5760
self.attributes[name] = self.attributes[kw_name] = kw
@@ -98,37 +101,23 @@ def run_keyword(self, name, args, kwargs=None):
98101
return self.keywords[name](*args, **(kwargs or {}))
99102

100103
def get_keyword_arguments(self, name):
101-
kw_method = self.__get_keyword(name)
102-
if kw_method is None:
103-
return None
104-
spec = ArgumentSpec.from_function(kw_method)
105-
return spec.get_arguments()
104+
spec = self.keywords_spec.get(name)
105+
return spec.argument_specification
106106

107107
def get_keyword_tags(self, name):
108108
return self.keywords[name].robot_tags
109109

110110
def get_keyword_documentation(self, name):
111111
if name == '__intro__':
112112
return inspect.getdoc(self) or ''
113-
if name == '__init__':
114-
return inspect.getdoc(self.__init__) or ''
115-
kw = self.keywords[name]
116-
return inspect.getdoc(kw) or ''
113+
spec = self.keywords_spec.get(name)
114+
return spec.documentation
117115

118-
def get_keyword_types(self, keyword_name):
119-
method = self.__get_keyword(keyword_name)
120-
if method is None:
121-
return method
122-
types = getattr(method, 'robot_types', ())
123-
if types is None:
124-
return types
125-
if not types:
126-
types = self.__get_typing_hints(method)
127-
if RF31:
128-
types = self.__join_defaults_with_types(method, types)
129-
else:
130-
types.pop('return', None)
131-
return types
116+
def get_keyword_types(self, name):
117+
spec = self.keywords_spec.get(name)
118+
if spec is None:
119+
raise ValueError('Keyword "%s" not found.' % name)
120+
return spec.argument_types
132121

133122
def __get_keyword(self, keyword_name):
134123
if keyword_name == '__init__':
@@ -140,22 +129,6 @@ def __get_keyword(self, keyword_name):
140129
raise ValueError('Keyword "%s" not found.' % keyword_name)
141130
return method
142131

143-
def __get_typing_hints(self, method):
144-
if PY2:
145-
return {}
146-
spec = ArgumentSpec.from_function(method)
147-
return spec.get_typing_hints()
148-
149-
def __join_defaults_with_types(self, method, types):
150-
spec = ArgumentSpec.from_function(method)
151-
for name, value in spec.defaults:
152-
if name not in types and isinstance(value, (bool, type(None))):
153-
types[name] = type(value)
154-
for name, value in spec.kwonlydefaults:
155-
if name not in types and isinstance(value, (bool, type(None))):
156-
types[name] = type(value)
157-
return types
158-
159132
def get_keyword_source(self, keyword_name):
160133
method = self.__get_keyword(keyword_name)
161134
path = self.__get_keyword_path(method)
@@ -185,91 +158,128 @@ def __get_keyword_path(self, method):
185158
return None
186159

187160

188-
class ArgumentSpec(object):
189-
190-
_function = None
191-
192-
def __init__(self, positional=None, defaults=None, varargs=None, kwonlyargs=None,
193-
kwonlydefaults=None, kwargs=None):
194-
self.positional = positional or []
195-
self.defaults = defaults or []
196-
self.varargs = varargs
197-
self.kwonlyargs = kwonlyargs or []
198-
self.kwonlydefaults = kwonlydefaults or []
199-
self.kwargs = kwargs
200-
201-
def __contains__(self, item):
202-
if item in self.positional:
203-
return True
204-
if self.varargs and item in self.varargs:
205-
return True
206-
if item in self.kwonlyargs:
207-
return True
208-
if self.kwargs and item in self.kwargs:
209-
return True
210-
return False
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 get_typing_hints(self):
224-
try:
225-
hints = typing.get_type_hints(self._function)
226-
except Exception:
227-
hints = self._function.__annotations__
228-
for arg in list(hints):
229-
# remove return and self statements
230-
if arg not in self:
231-
hints.pop(arg)
232-
return hints
161+
class KeywordBuilder(object):
233162

234-
def _format_positional(self, positional, defaults):
235-
for argument, _ in defaults:
236-
positional.remove(argument)
237-
return positional
163+
@classmethod
164+
def build(cls, function):
165+
return KeywordSpecification(
166+
argument_specification=cls._get_arguments(function),
167+
documentation=inspect.getdoc(function) or '',
168+
argument_types=cls._get_types(function)
169+
)
238170

239-
def _format_default(self, defaults):
240-
if not RF31:
241-
return [default for default in defaults]
242-
return ['%s=%s' % (argument, default) for argument, default in defaults]
171+
@classmethod
172+
def _get_arguments(cls, function):
173+
arg_spec = cls._get_arg_spec(function)
174+
argument_specification = cls._get_default_and_named_args(
175+
arg_spec, function
176+
)
177+
argument_specification.extend(cls._get_var_args(arg_spec))
178+
kw_only_args = cls._get_kw_only(arg_spec)
179+
if kw_only_args:
180+
argument_specification.extend(kw_only_args)
181+
argument_specification.extend(cls._get_kwargs(arg_spec))
182+
return argument_specification
243183

244184
@classmethod
245-
def from_function(cls, function):
246-
cls._function = function
185+
def _get_arg_spec(cls, function):
247186
if PY2:
248-
spec = inspect.getargspec(function)
249-
else:
250-
spec = inspect.getfullargspec(function)
251-
args = spec.args[1:] if inspect.ismethod(function) else spec.args # drop self
252-
defaults = cls._get_defaults(spec)
253-
kwonlyargs, kwonlydefaults, kwargs = cls._get_kw_args(spec)
254-
return cls(positional=args,
255-
defaults=defaults,
256-
varargs=spec.varargs,
257-
kwonlyargs=kwonlyargs,
258-
kwonlydefaults=kwonlydefaults,
259-
kwargs=kwargs)
187+
return inspect.getargspec(function)
188+
return inspect.getfullargspec(function)
189+
190+
@classmethod
191+
def _get_default_and_named_args(cls, arg_spec, function):
192+
args = cls._drop_self_from_args(function, arg_spec)
193+
args.reverse()
194+
defaults = list(arg_spec.defaults) if arg_spec.defaults else []
195+
formated_args = []
196+
for arg in args:
197+
if defaults:
198+
formated_args.append(
199+
cls._format_defaults(arg, defaults.pop())
200+
)
201+
else:
202+
formated_args.append(arg)
203+
formated_args.reverse()
204+
return formated_args
205+
206+
@classmethod
207+
def _drop_self_from_args(cls, function, arg_spec):
208+
return arg_spec.args[1:] if inspect.ismethod(function) else arg_spec.args
260209

261210
@classmethod
262-
def _get_defaults(cls, spec):
263-
if not spec.defaults:
264-
return []
265-
names = spec.args[-len(spec.defaults):]
266-
return list(zip(names, spec.defaults))
211+
def _get_var_args(cls, arg_spec):
212+
if arg_spec.varargs:
213+
return ['*%s' % arg_spec.varargs]
214+
return []
267215

268216
@classmethod
269-
def _get_kw_args(cls, spec):
217+
def _get_kwargs(cls, arg_spec):
270218
if PY2:
271-
return [], [], spec.keywords
272-
kwonlyargs = spec.kwonlyargs or []
273-
defaults = spec.kwonlydefaults or {}
274-
kwonlydefaults = [(arg, name) for arg, name in defaults.items()]
275-
return kwonlyargs, kwonlydefaults, spec.varkw
219+
return ['**%s' % arg_spec.keywords] if arg_spec.keywords else []
220+
return ['**%s' % arg_spec.varkw] if arg_spec.varkw else []
221+
222+
@classmethod
223+
def _get_kw_only(cls, arg_spec):
224+
kw_only_args = []
225+
if PY2:
226+
return kw_only_args
227+
for arg in arg_spec.kwonlyargs:
228+
if not arg_spec.kwonlydefaults or arg not in arg_spec.kwonlydefaults:
229+
kw_only_args.append(arg)
230+
else:
231+
value = arg_spec.kwonlydefaults.get(arg, '')
232+
kw_only_args.append(cls._format_defaults(arg, value))
233+
return kw_only_args
234+
235+
@classmethod
236+
def _format_defaults(cls, arg, value):
237+
if RF31:
238+
return '%s=%s' % (arg, value)
239+
return arg, value
240+
241+
@classmethod
242+
def _get_types(cls, function):
243+
if function is None:
244+
return function
245+
types = getattr(function, 'robot_types', ())
246+
if types is None or types:
247+
return types
248+
if not types:
249+
types = cls._get_typing_hints(function)
250+
return types
251+
252+
@classmethod
253+
def _get_typing_hints(cls, function):
254+
if PY2:
255+
return {}
256+
try:
257+
hints = typing.get_type_hints(function)
258+
except Exception:
259+
hints = function.__annotations__
260+
all_args = cls._args_as_list(function)
261+
for arg_with_hint in list(hints):
262+
# remove return and self statements
263+
if arg_with_hint not in all_args:
264+
hints.pop(arg_with_hint)
265+
return hints
266+
267+
@classmethod
268+
def _args_as_list(cls, function):
269+
arg_spec = cls._get_arg_spec(function)
270+
function_args = []
271+
function_args.extend(cls._drop_self_from_args(function, arg_spec))
272+
if arg_spec.varargs:
273+
function_args.append(arg_spec.varargs)
274+
function_args.extend(arg_spec.kwonlyargs or [])
275+
if arg_spec.varkw:
276+
function_args.append(arg_spec.varkw)
277+
return function_args
278+
279+
280+
class KeywordSpecification(object):
281+
282+
def __init__(self, argument_specification=None, documentation=None, argument_types=None):
283+
self.argument_specification = argument_specification
284+
self.documentation = documentation
285+
self.argument_types = argument_types

utest/test_get_keyword_source.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def test_location_in_class(lib, lib_path_components):
5252
@pytest.mark.skipif(PY2, reason='Only applicable on Python 3')
5353
def test_decorator_wrapper(lib_types, lib_path_types):
5454
source = lib_types.get_keyword_source('keyword_wrapped')
55-
assert source == '%s:76' % lib_path_types
55+
assert source == '%s:72' % lib_path_types
5656

5757

5858
def test_location_in_class_custom_keyword_name(lib, lib_path_components):
@@ -81,7 +81,7 @@ def test_no_path_and_no_line_number(lib, when):
8181

8282
def test_def_in_decorator(lib_types, lib_path_types):
8383
source = lib_types.get_keyword_source('keyword_with_def_deco')
84-
assert source == '%s:70' % lib_path_types
84+
assert source == '%s:66' % lib_path_types
8585

8686

8787
def test_error_in_getfile(lib, when):

0 commit comments

Comments
 (0)