diff --git a/README.rst b/README.rst index 476f0ed..4b68dc7 100644 --- a/README.rst +++ b/README.rst @@ -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. @@ -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 ----------------------------- diff --git a/src/robotremoteserver.py b/src/robotremoteserver.py index 62fc5ed..a5d1877 100644 --- a/src/robotremoteserver.py +++ b/src/robotremoteserver.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -308,7 +340,7 @@ 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: @@ -316,8 +348,10 @@ def get_keyword_arguments(self, name): 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): @@ -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] diff --git a/test/atest/instantiation.robot b/test/atest/instantiation.robot new file mode 100644 index 0000000..cbc8650 --- /dev/null +++ b/test/atest/instantiation.robot @@ -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 diff --git a/test/libs/Loading.py b/test/libs/Loading.py new file mode 100644 index 0000000..3488ed9 --- /dev/null +++ b/test/libs/Loading.py @@ -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") diff --git a/test/libs/MultiLib.py b/test/libs/MultiLib.py new file mode 100644 index 0000000..37ce685 --- /dev/null +++ b/test/libs/MultiLib.py @@ -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:]) diff --git a/test/utest/test_dynamicargsdoctags.py b/test/utest/test_dynamicargsdoctags.py index 760d45b..f514c45 100644 --- a/test/utest/test_dynamicargsdoctags.py +++ b/test/utest/test_dynamicargsdoctags.py @@ -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 diff --git a/test/utest/test_robotremoteserver.py b/test/utest/test_robotremoteserver.py index dbd5cb0..4ad42be 100755 --- a/test/utest/test_robotremoteserver.py +++ b/test/utest/test_robotremoteserver.py @@ -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 = () @@ -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'})