diff --git a/mopidy/utils/jsonrpc.py b/mopidy/utils/jsonrpc.py index d31cccbd..47b660d3 100644 --- a/mopidy/utils/jsonrpc.py +++ b/mopidy/utils/jsonrpc.py @@ -15,43 +15,24 @@ class JsonRpcWrapper(object): processing of JSON-RPC 2.0 messages. The transport of the messages over HTTP, WebSocket, TCP, or whatever is of no concern to this class. - The wrapper supports exporting the methods of multiple objects. If so, they - must be exported with different prefixes, called "mounts". + The wrapper supports exporting the methods of one or more objects. Either + way, the objects must be exported with method name prefixes, called + "mounts". - - To expose a single object, add it to the objects mapping using the empty - string as the mount:: + To expose objects, add them all to the objects mapping. The key in the + mapping is used as the object's mounting point in the exposed API:: - jrw = JsonRpcWrapper(objects={'': my_object}) + jrw = JsonRpcWrapper(objects={ + 'foo': foo, + 'hello': lambda: 'Hello, world!', + }) - If ``my_object`` has a method named ``my_method()`` will be exported as - the JSON-RPC 2.0 method name ``my_method``. + This will export the Python callables on the left as the JSON-RPC 2.0 + method names on the right:: - - To expose multiple objects, add them all to the objects mapping. The key - in the mapping is used as the object's mounting point in the exposed - API:: - - jrw = JsonRpcWrapper(objects={ - '': foo, - 'hello': lambda: 'Hello, world!', - 'abc': abc, - }) - - This will export the Python callables on the left as the JSON-RPC 2.0 - method names on the right:: - - foo.bar() -> bar - foo.baz() -> baz - lambda -> hello - abc.def() -> abc.def - - If the ``foo`` object mounted at the root also got a method named - ``hello``, there will be a name collision between ``foo.hello()`` and the - lambda function mounted at ``hello``. In that case, the JSON-RPC 2.0 - method name ``hello`` will refer to the lambda function, because it was - mounted explicitly as ``hello``. - - It is recommended to avoid name collisions entirely by using non-empty - mounts for all objects. + foo.bar() -> foo.bar + foo.baz() -> foo.baz + lambda -> hello Only the public methods of the mounted objects, or functions/methods included directly in the mapping, will be exposed. @@ -73,6 +54,9 @@ class JsonRpcWrapper(object): """ def __init__(self, objects, decoders=None, encoders=None): + if '' in objects.keys(): + raise AttributeError( + 'The empty string is not allowed as an object mount') self.objects = objects self.decoder = get_combined_json_decoder(decoders or []) self.encoder = get_combined_json_encoder(encoders or []) @@ -305,18 +289,13 @@ class JsonRpcInspector(object): Inspects a group of classes and functions to create a description of what methods they can expose over JSON-RPC 2.0. - To inspect a single class, add it to the objects mapping using the empty - string as the key:: - - jri = JsonRpcInspector(objects={'': MyClass}) - - To inspect multiple classes, add them all to the objects mapping. The key - in the mapping is used as the classes' mounting point in the exposed API:: + To inspect one or more classes, add them all to the objects mapping. The + key in the mapping is used as the classes' mounting point in the exposed + API:: jri = JsonRpcInspector(objects={ - '': Foo, + 'foo': Foo, 'hello': lambda: 'Hello, world!', - 'abc': Abc, }) Since the inspector is based on inspecting classes and not instances, it @@ -328,6 +307,9 @@ class JsonRpcInspector(object): """ def __init__(self, objects): + if '' in objects.keys(): + raise AttributeError( + 'The empty string is not allowed as an object mount') self.objects = objects def describe(self): diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index 8c487e87..d4730be2 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -44,10 +44,10 @@ class JsonRpcTestBase(unittest.TestCase): self.jrw = jsonrpc.JsonRpcWrapper( objects={ 'hello': lambda: 'Hello, world!', + 'calc': Calculator(), 'core': self.core, 'core.playback': self.core.playback, 'core.tracklist': self.core.tracklist, - '': Calculator(), }, encoders=[models.ModelJSONEncoder], decoders=[models.model_json_decoder]) @@ -56,6 +56,12 @@ class JsonRpcTestBase(unittest.TestCase): pykka.ActorRegistry.stop_all() +class JsonRpcSetupTest(JsonRpcTestBase): + def test_empty_object_mounts_is_not_allowed(self): + test = lambda: jsonrpc.JsonRpcWrapper(objects={'': Calculator()}) + self.assertRaises(AttributeError, test) + + class JsonRpcSerializationTest(JsonRpcTestBase): def test_handle_json_converts_from_and_to_json(self): self.jrw.handle_data = mock.Mock() @@ -132,10 +138,10 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase): self.assertNotIn('error', response) self.assertEqual(response['result'], 'Hello, world!') - def test_call_method_on_plain_object_as_root(self): + def test_call_method_on_plain_object(self): request = { 'jsonrpc': '2.0', - 'method': 'model', + 'method': 'calc.model', 'id': 1, } response = self.jrw.handle_data(request) @@ -148,7 +154,7 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase): def test_call_method_which_returns_dict_from_plain_object(self): request = { 'jsonrpc': '2.0', - 'method': 'describe', + 'method': 'calc.describe', 'id': 1, } response = self.jrw.handle_data(request) @@ -510,6 +516,10 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase): class JsonRpcInspectorTest(JsonRpcTestBase): + def test_empty_object_mounts_is_not_allowed(self): + test = lambda: jsonrpc.JsonRpcInspector(objects={'': Calculator}) + self.assertRaises(AttributeError, test) + def test_can_describe_method_on_root(self): inspector = jsonrpc.JsonRpcInspector({ 'hello': lambda: 'Hello, world!', @@ -520,24 +530,24 @@ class JsonRpcInspectorTest(JsonRpcTestBase): self.assertIn('hello', methods) self.assertEqual(len(methods['hello']['params']), 0) - def test_inspector_can_describe_methods_on_the_root_class(self): + def test_inspector_can_describe_an_object_with_methods(self): inspector = jsonrpc.JsonRpcInspector({ - '': Calculator, + 'calc': Calculator, }) methods = inspector.describe() - self.assertIn('add', methods) + self.assertIn('calc.add', methods) self.assertEqual( - methods['add']['description'], + methods['calc.add']['description'], 'Returns the sum of the given numbers') - self.assertIn('sub', methods) - self.assertIn('take_it_all', methods) - self.assertNotIn('_secret', methods) - self.assertNotIn('__init__', methods) + self.assertIn('calc.sub', methods) + self.assertIn('calc.take_it_all', methods) + self.assertNotIn('calc._secret', methods) + self.assertNotIn('calc.__init__', methods) - method = methods['take_it_all'] + method = methods['calc.take_it_all'] self.assertIn('params', method) params = method['params'] @@ -559,16 +569,6 @@ class JsonRpcInspectorTest(JsonRpcTestBase): self.assertNotIn('default', params[4]) self.assertEqual(params[4]['kwargs'], True) - def test_inspector_can_describe_methods_not_on_the_root(self): - inspector = jsonrpc.JsonRpcInspector({ - 'calc': Calculator, - }) - - methods = inspector.describe() - - self.assertIn('calc.add', methods) - self.assertIn('calc.sub', methods) - def test_inspector_can_describe_a_bunch_of_large_classes(self): inspector = jsonrpc.JsonRpcInspector({ 'core.library': core.LibraryController,