Skip to content

Commit 99a8d63

Browse files
committed
Implement get_library_information interface for speed-loading large libraries
Include support for the get_library_information API extention. Also includes a fix to deal with kwonly arguments and an extension to allow hosting multiple libraries on a single server.
1 parent d038453 commit 99a8d63

7 files changed

+158
-21
lines changed

README.rst

+13-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ accepts the following configuration parameters when it is initialized:
7272
===================== ================= ========================================
7373
Argument Default Explanation
7474
===================== ================= ========================================
75-
``library`` Test library instance or module to host. Mandatory argument.
75+
``libraries`` Test library instance or module or list thereof to host. Mandatory argument.
7676
``host`` ``'127.0.0.1'`` Address to listen. Use ``'0.0.0.0'`` to listen to all available interfaces.
7777
``port`` ``8270`` Port to listen. Use ``0`` to select a free port automatically. Can be given as an integer or as a string. The default port ``8270`` is `registered by IANA`__ for remote server usage.
7878
``port_file`` ``None`` File to write the port that is used. ``None`` (default) means no such file is written.
@@ -124,6 +124,18 @@ equivalent to the example above:
124124
port_file='/tmp/remote-port.txt', serve=False)
125125
server.serve()
126126

127+
When there are multiple libraries, they can be hosted using the same remote server.
128+
Simply pass a list of library instances or modules to it. Keyword names have to be
129+
unique over the libraries:
130+
131+
.. sourcecode:: python
132+
133+
from robotremoteserver import RobotRemoteServer
134+
from myFirstlibrary import MyFirstLibrary
135+
from mySecondlibrary import MySecondLibrary
136+
137+
RobotRemoteServer([MyFirstLibrary(), MySecondLibrary()])
138+
127139
Starting server on background
128140
-----------------------------
129141

src/robotremoteserver.py

+46-13
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@
4949

5050
class RobotRemoteServer(object):
5151

52-
def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
52+
def __init__(self, libraries, host='127.0.0.1', port=8270, port_file=None,
5353
allow_stop='DEPRECATED', serve=True, allow_remote_stop=True):
5454
"""Configure and start-up remote server.
5555
56-
:param library: Test library instance or module to host.
56+
:param libraries: A single, or list of test library instances or
57+
modules to host.
5758
:param host: Address to listen. Use ``'0.0.0.0'`` to listen
5859
to all available interfaces.
5960
:param port: Port to listen. Use ``0`` to select a free port
@@ -71,7 +72,9 @@ def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
7172
``Stop Remote Server`` keyword and
7273
``stop_remote_server`` XML-RPC method.
7374
"""
74-
self._library = RemoteLibraryFactory(library)
75+
if not isinstance(libraries, list):
76+
libraries = [libraries]
77+
self._library = [RemoteLibraryFactory(library_) for library_ in libraries]
7578
self._server = StoppableXMLRPCServer(host, int(port))
7679
self._register_functions(self._server)
7780
self._port_file = port_file
@@ -85,6 +88,9 @@ def _register_functions(self, server):
8588
server.register_function(self.run_keyword)
8689
server.register_function(self.get_keyword_arguments)
8790
server.register_function(self.get_keyword_documentation)
91+
server.register_function(self.get_keyword_tags)
92+
server.register_function(self.get_keyword_types)
93+
server.register_function(self.get_library_information)
8894
server.register_function(self.stop_remote_server)
8995

9096
@property
@@ -168,29 +174,54 @@ def stop_remote_server(self, log=True):
168174
return True
169175

170176
def get_keyword_names(self):
171-
return self._library.get_keyword_names() + ['stop_remote_server']
177+
keywords = ['stop_remote_server']
178+
for l in self._library:
179+
keywords += l.get_keyword_names()
180+
return keywords
172181

173182
def run_keyword(self, name, args, kwargs=None):
174183
if name == 'stop_remote_server':
175184
return KeywordRunner(self.stop_remote_server).run_keyword(args, kwargs)
176-
return self._library.run_keyword(name, args, kwargs)
185+
library_ = next((l for l in self._library if name in l._names),
186+
self._library[0])
187+
return library_.run_keyword(name, args, kwargs)
177188

178189
def get_keyword_arguments(self, name):
179190
if name == 'stop_remote_server':
180191
return []
181-
return self._library.get_keyword_arguments(name)
192+
library_ = next((l for l in self._library if name in l._names), #None)
193+
self._library[0])
194+
return library_.get_keyword_arguments(name) if library_ else []
182195

183196
def get_keyword_documentation(self, name):
184197
if name == 'stop_remote_server':
185198
return ('Stop the remote server unless stopping is disabled.\n\n'
186199
'Return ``True/False`` depending was server stopped or not.')
187-
return self._library.get_keyword_documentation(name)
200+
library_ = next((l for l in self._library if name in l._names), None)
201+
return library_.get_keyword_documentation(name) if library_ else ""
188202

189203
def get_keyword_tags(self, name):
190204
if name == 'stop_remote_server':
191205
return []
192-
return self._library.get_keyword_tags(name)
206+
library_ = next((l for l in self._library if name in l._names), None)
207+
return library_.get_keyword_tags(name) if library_ else []
193208

209+
def get_keyword_types(self, name):
210+
if name == 'stop_remote_server':
211+
return []
212+
library_ = next((l for l in self._library if name in l._names), None)
213+
return library_.get_keyword_types(name) if library_ and hasattr(library_, 'get_keyword_types') else []
214+
215+
def get_library_information(self):
216+
info_dict = dict()
217+
for kw in self.get_keyword_names():
218+
kw_dict = dict()
219+
kw_dict['args'] = self.get_keyword_arguments(kw)
220+
kw_dict['tags'] = self.get_keyword_tags(kw)
221+
kw_dict['doc'] = self.get_keyword_documentation(kw)
222+
kw_dict['types'] = self.get_keyword_types(kw)
223+
info_dict[kw] = kw_dict
224+
return info_dict
194225

195226
class StoppableXMLRPCServer(SimpleXMLRPCServer):
196227
allow_reuse_address = True
@@ -308,16 +339,18 @@ def get_keyword_arguments(self, name):
308339
if __name__ == '__init__':
309340
return []
310341
kw = self._get_keyword(name)
311-
args, varargs, kwargs, defaults = inspect.getargspec(kw)
342+
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = inspect.getfullargspec(kw)
312343
if inspect.ismethod(kw):
313344
args = args[1:] # drop 'self'
314345
if defaults:
315346
args, names = args[:-len(defaults)], args[-len(defaults):]
316347
args += ['%s=%s' % (n, d) for n, d in zip(names, defaults)]
317348
if varargs:
318349
args.append('*%s' % varargs)
319-
if kwargs:
320-
args.append('**%s' % kwargs)
350+
if kwonlyargs:
351+
args += ['%s=%s' % (a, kwonlydefaults[a]) if a in kwonlydefaults else a for a in kwonlyargs]
352+
if varkw:
353+
args.append('**%s' % varkw)
321354
return args
322355

323356
def get_keyword_documentation(self, name):
@@ -371,8 +404,8 @@ def __init__(self, library, get_keyword_names, run_keyword):
371404
= dynamic_method(library, 'get_keyword_tags')
372405

373406
def _get_kwargs_support(self, run_keyword):
374-
spec = inspect.getargspec(run_keyword)
375-
return len(spec.args) > 3 # self, name, args, kwargs=None
407+
spec = inspect.getfullargspec(run_keyword)
408+
return spec.varkw or spec.kwonlyargs
376409

377410
def run_keyword(self, name, args, kwargs=None):
378411
args = [name, args, kwargs] if kwargs else [name, args]

test/atest/instantiation.robot

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
*** Settings ***
2+
Documentation Testing the feature where instantiation of both a
3+
... single library or a list of libraries must be
4+
... possible.
5+
Resource resource.robot
6+
7+
*** Test Cases ***
8+
A single library can be loaded
9+
[Setup] Start And Import Remote Library Basics.py Remote1
10+
[Teardown] Remote1.Stop Remote Server
11+
Passing
12+
13+
Multiple libraries can be loaded
14+
[Setup] Start And Import Remote Library MultiLib.py Remote3
15+
[Teardown] Remote3.Stop Remote Server
16+
Keyword from first library
17+
Keyword from second library
18+
Keyword from third library
19+
20+
Libraries can be bulk-loaded
21+
[Setup] Start And Import Remote Library Loading.py Bulk BulkMode
22+
[Teardown] Bulk.Stop Remote Server
23+
Bulk.Basic
24+
Bulk.Complex positional named=Monty free=Python
25+
26+
Libraries can be loaded per keyword
27+
[Setup] Start And Import Remote Library Loading.py Single SingleMode
28+
[Teardown] Single.Stop Remote Server
29+
Single.Basic
30+
Single.Complex positional named=Monty free=Python

test/libs/Loading.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import sys
2+
from robot.api.deco import keyword
3+
from robotremoteserver import RobotRemoteServer
4+
5+
class KwLibrary:
6+
def basic(self):
7+
pass
8+
9+
@keyword('Complex', tags=['tag1', 'tag2'])
10+
def complex_kw(self, arg1, *, named, namedWithDefault='something', **kwargs):
11+
pass
12+
13+
class OneByOneRemoteServer(RobotRemoteServer):
14+
15+
def _register_functions(self, server):
16+
"""
17+
Do not register get_library_information. This removes the bulk load feature
18+
and checks the fallback to loading individual keywords.
19+
"""
20+
server.register_function(self.get_keyword_names)
21+
server.register_function(self.run_keyword)
22+
server.register_function(self.get_keyword_arguments)
23+
server.register_function(self.get_keyword_documentation)
24+
server.register_function(self.get_keyword_tags)
25+
server.register_function(self.get_keyword_types)
26+
server.register_function(self.stop_remote_server)
27+
28+
class BulkLoadRemoteServer(RobotRemoteServer):
29+
30+
def _register_functions(self, server):
31+
"""
32+
Individual get_keyword_* methods are not registered.
33+
This removes the fall back scenario should get_library_information fail.
34+
"""
35+
server.register_function(self.get_library_information)
36+
server.register_function(self.run_keyword)
37+
server.register_function(self.stop_remote_server)
38+
39+
if __name__ == '__main__':
40+
if 'BulkMode' in sys.argv:
41+
BulkLoadRemoteServer(KwLibrary(), '127.0.0.1', *sys.argv[1:])
42+
elif 'SingleMode' in sys.argv:
43+
OneByOneRemoteServer(KwLibrary(), '127.0.0.1', *sys.argv[1:])
44+
else:
45+
raise ValueError("Pass either BulkMode or SingleMode to run this library")

test/libs/MultiLib.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class FirstLib:
2+
def keyword_from_first_library(self):
3+
pass
4+
5+
class SecondLib:
6+
def keyword_from_second_library(self):
7+
pass
8+
9+
class ThirdLib:
10+
def keyword_from_third_library(self):
11+
pass
12+
13+
if __name__ == '__main__':
14+
import sys
15+
from robotremoteserver import RobotRemoteServer
16+
17+
RobotRemoteServer([FirstLib(), SecondLib(), ThirdLib()], '127.0.0.1', *sys.argv[1:])

test/utest/test_dynamicargsdoctags.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class NoArgsDocTags(object):
4444
def get_keyword_names(self):
4545
return ['keyword']
4646

47-
def run_keyword(self, name, args, kwargs=None):
47+
def run_keyword(self, name, args, *, kwargs=None):
4848
pass
4949

5050

test/utest/test_robotremoteserver.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
class NonServingRemoteServer(RobotRemoteServer):
1010

11-
def __init__(self, library):
12-
self._library = RemoteLibraryFactory(library)
13-
11+
def __init__(self, libraries):
12+
if not isinstance(libraries, list):
13+
libraries = [libraries]
14+
self._library = [RemoteLibraryFactory(library_) for library_ in libraries]
1415

1516
class StaticLibrary:
1617
streams = ()
@@ -96,9 +97,8 @@ def setUp(self):
9697

9798
def test_get_keyword_names(self):
9899
self.assertEquals(self.server.get_keyword_names(),
99-
['failing_keyword', 'logging_keyword',
100-
'passing_keyword', 'returning_keyword',
101-
'stop_remote_server'])
100+
['stop_remote_server', 'failing_keyword', 'logging_keyword',
101+
'passing_keyword', 'returning_keyword'])
102102

103103
def test_run_passing_keyword(self):
104104
self.assertEquals(self._run('passing_keyword'), {'status': 'PASS'})

0 commit comments

Comments
 (0)