Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed-loading large libraries with get_library_information #74

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 13 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ accepts the following configuration parameters when it is initialized:
===================== ================= ========================================
Argument Default Explanation
===================== ================= ========================================
``library`` Test library instance or module to host. Mandatory argument.
``libraries`` Test library instance or module or list thereof to host. Mandatory argument.
``host`` ``'127.0.0.1'`` Address to listen. Use ``'0.0.0.0'`` to listen to all available interfaces.
``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.
``port_file`` ``None`` File to write the port that is used. ``None`` (default) means no such file is written.
Expand Down Expand Up @@ -124,6 +124,18 @@ equivalent to the example above:
port_file='/tmp/remote-port.txt', serve=False)
server.serve()

When there are multiple libraries, they can be hosted using the same remote server.
Simply pass a list of library instances or modules to it. Keyword names have to be
unique over the libraries:

.. sourcecode:: python

from robotremoteserver import RobotRemoteServer
from myFirstlibrary import MyFirstLibrary
from mySecondlibrary import MySecondLibrary

RobotRemoteServer([MyFirstLibrary(), MySecondLibrary()])

Starting server on background
-----------------------------

Expand Down
60 changes: 47 additions & 13 deletions src/robotremoteserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@

class RobotRemoteServer(object):

def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
def __init__(self, libraries, host='127.0.0.1', port=8270, port_file=None,
allow_stop='DEPRECATED', serve=True, allow_remote_stop=True):
"""Configure and start-up remote server.

:param library: Test library instance or module to host.
:param libraries: A single, or list of test library instances or
modules to host.
:param host: Address to listen. Use ``'0.0.0.0'`` to listen
to all available interfaces.
:param port: Port to listen. Use ``0`` to select a free port
Expand All @@ -71,7 +72,9 @@ def __init__(self, library, host='127.0.0.1', port=8270, port_file=None,
``Stop Remote Server`` keyword and
``stop_remote_server`` XML-RPC method.
"""
self._library = RemoteLibraryFactory(library)
if not isinstance(libraries, list):
libraries = [libraries]
self._library = [RemoteLibraryFactory(library_) for library_ in libraries]
self._server = StoppableXMLRPCServer(host, int(port))
self._register_functions(self._server)
self._port_file = port_file
Expand All @@ -85,6 +88,9 @@ def _register_functions(self, server):
server.register_function(self.run_keyword)
server.register_function(self.get_keyword_arguments)
server.register_function(self.get_keyword_documentation)
server.register_function(self.get_keyword_tags)
server.register_function(self.get_keyword_types)
server.register_function(self.get_library_information)
server.register_function(self.stop_remote_server)

@property
Expand Down Expand Up @@ -168,29 +174,55 @@ def stop_remote_server(self, log=True):
return True

def get_keyword_names(self):
return self._library.get_keyword_names() + ['stop_remote_server']
keywords = ['stop_remote_server']
for l in self._library:
keywords += l.get_keyword_names()
return keywords

def run_keyword(self, name, args, kwargs=None):
if name == 'stop_remote_server':
return KeywordRunner(self.stop_remote_server).run_keyword(args, kwargs)
return self._library.run_keyword(name, args, kwargs)
library_ = next((l for l in self._library if name in l.get_keyword_names()),
self._library[0])
return library_.run_keyword(name, args, kwargs)

def get_keyword_arguments(self, name):
if name == 'stop_remote_server':
return []
return self._library.get_keyword_arguments(name)
library_ = next((l for l in self._library if name in l.get_keyword_names()), None)
return library_.get_keyword_arguments(name) if library_ else []

def get_keyword_documentation(self, name):
if name == 'stop_remote_server':
return ('Stop the remote server unless stopping is disabled.\n\n'
'Return ``True/False`` depending was server stopped or not.')
return self._library.get_keyword_documentation(name)
library_ = next((l for l in self._library if name in l.get_keyword_names()), None)
return library_.get_keyword_documentation(name) if library_ else ""

def get_keyword_tags(self, name):
if name == 'stop_remote_server':
return []
return self._library.get_keyword_tags(name)
library_ = next((l for l in self._library if name in l.get_keyword_names()), None)
return library_.get_keyword_tags(name) if library_ else []

def get_keyword_types(self, name):
if name == 'stop_remote_server':
return []
library_ = next((l for l in self._library if name in l.get_keyword_names()), None)
return library_.get_keyword_types(name) if library_ and hasattr(library_, 'get_keyword_types') else []

def get_library_information(self):
info_dict = dict()
for kw in self.get_keyword_names():
info_dict[kw] = dict(args=self.get_keyword_arguments(kw),
tags=self.get_keyword_tags(kw),
doc=self.get_keyword_documentation(kw),
types=self.get_keyword_types(kw),
)
if len(self._library) == 1:
info_dict['__intro__'] = dict(doc=self._library[0].get_keyword_documentation('__intro__'))
info_dict['__init__'] = dict(doc=self._library[0].get_keyword_documentation('__init__'))
return info_dict

class StoppableXMLRPCServer(SimpleXMLRPCServer):
allow_reuse_address = True
Expand Down Expand Up @@ -308,16 +340,18 @@ def get_keyword_arguments(self, name):
if __name__ == '__init__':
return []
kw = self._get_keyword(name)
args, varargs, kwargs, defaults = inspect.getargspec(kw)
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations = inspect.getfullargspec(kw)
if inspect.ismethod(kw):
args = args[1:] # drop 'self'
if defaults:
args, names = args[:-len(defaults)], args[-len(defaults):]
args += ['%s=%s' % (n, d) for n, d in zip(names, defaults)]
if varargs:
args.append('*%s' % varargs)
if kwargs:
args.append('**%s' % kwargs)
if kwonlyargs:
args += ['%s=%s' % (a, kwonlydefaults[a]) if a in kwonlydefaults else a for a in kwonlyargs]
if varkw:
args.append('**%s' % varkw)
return args

def get_keyword_documentation(self, name):
Expand Down Expand Up @@ -371,8 +405,8 @@ def __init__(self, library, get_keyword_names, run_keyword):
= dynamic_method(library, 'get_keyword_tags')

def _get_kwargs_support(self, run_keyword):
spec = inspect.getargspec(run_keyword)
return len(spec.args) > 3 # self, name, args, kwargs=None
spec = inspect.getfullargspec(run_keyword)
return spec.varkw or spec.kwonlyargs

def run_keyword(self, name, args, kwargs=None):
args = [name, args, kwargs] if kwargs else [name, args]
Expand Down
30 changes: 30 additions & 0 deletions test/atest/instantiation.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
*** Settings ***
Documentation Testing the feature where instantiation of both a
... single library or a list of libraries must be
... possible.
Resource resource.robot

*** Test Cases ***
A single library can be loaded
[Setup] Start And Import Remote Library Basics.py Remote1
[Teardown] Remote1.Stop Remote Server
Passing

Multiple libraries can be loaded
[Setup] Start And Import Remote Library MultiLib.py Remote3
[Teardown] Remote3.Stop Remote Server
Keyword from first library
Keyword from second library
Keyword from third library

Libraries can be bulk-loaded
[Setup] Start And Import Remote Library Loading.py Bulk BulkMode
[Teardown] Bulk.Stop Remote Server
Bulk.Basic
Bulk.Complex positional named=Monty free=Python

Libraries can be loaded per keyword
[Setup] Start And Import Remote Library Loading.py Single SingleMode
[Teardown] Single.Stop Remote Server
Single.Basic
Single.Complex positional named=Monty free=Python
45 changes: 45 additions & 0 deletions test/libs/Loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sys
from robot.api.deco import keyword
from robotremoteserver import RobotRemoteServer

class KwLibrary:
def basic(self):
pass

@keyword('Complex', tags=['tag1', 'tag2'])
def complex_kw(self, arg1, *, named, namedWithDefault='something', **kwargs):
pass

class OneByOneRemoteServer(RobotRemoteServer):

def _register_functions(self, server):
"""
Do not register get_library_information. This removes the bulk load feature
and checks the fallback to loading individual keywords.
"""
server.register_function(self.get_keyword_names)
server.register_function(self.run_keyword)
server.register_function(self.get_keyword_arguments)
server.register_function(self.get_keyword_documentation)
server.register_function(self.get_keyword_tags)
server.register_function(self.get_keyword_types)
server.register_function(self.stop_remote_server)

class BulkLoadRemoteServer(RobotRemoteServer):

def _register_functions(self, server):
"""
Individual get_keyword_* methods are not registered.
This removes the fall back scenario should get_library_information fail.
"""
server.register_function(self.get_library_information)
server.register_function(self.run_keyword)
server.register_function(self.stop_remote_server)

if __name__ == '__main__':
if 'BulkMode' in sys.argv:
BulkLoadRemoteServer(KwLibrary(), '127.0.0.1', *sys.argv[1:])
elif 'SingleMode' in sys.argv:
OneByOneRemoteServer(KwLibrary(), '127.0.0.1', *sys.argv[1:])
else:
raise ValueError("Pass either BulkMode or SingleMode to run this library")
17 changes: 17 additions & 0 deletions test/libs/MultiLib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class FirstLib:
def keyword_from_first_library(self):
pass

class SecondLib:
def keyword_from_second_library(self):
pass

class ThirdLib:
def keyword_from_third_library(self):
pass

if __name__ == '__main__':
import sys
from robotremoteserver import RobotRemoteServer

RobotRemoteServer([FirstLib(), SecondLib(), ThirdLib()], '127.0.0.1', *sys.argv[1:])
2 changes: 1 addition & 1 deletion test/utest/test_dynamicargsdoctags.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class NoArgsDocTags(object):
def get_keyword_names(self):
return ['keyword']

def run_keyword(self, name, args, kwargs=None):
def run_keyword(self, name, args, *, kwargs=None):
pass


Expand Down
12 changes: 6 additions & 6 deletions test/utest/test_robotremoteserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

class NonServingRemoteServer(RobotRemoteServer):

def __init__(self, library):
self._library = RemoteLibraryFactory(library)

def __init__(self, libraries):
if not isinstance(libraries, list):
libraries = [libraries]
self._library = [RemoteLibraryFactory(library_) for library_ in libraries]

class StaticLibrary:
streams = ()
Expand Down Expand Up @@ -96,9 +97,8 @@ def setUp(self):

def test_get_keyword_names(self):
self.assertEquals(self.server.get_keyword_names(),
['failing_keyword', 'logging_keyword',
'passing_keyword', 'returning_keyword',
'stop_remote_server'])
['stop_remote_server', 'failing_keyword', 'logging_keyword',
'passing_keyword', 'returning_keyword'])

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