diff --git a/mopidy/utils/jsonrpc.py b/mopidy/utils/jsonrpc.py index 23f50580..c280fc27 100644 --- a/mopidy/utils/jsonrpc.py +++ b/mopidy/utils/jsonrpc.py @@ -37,10 +37,7 @@ class JsonRpcWrapper(object): hello -> lambda abc.def -> abc.def() - Only the public methods of the objects will be exposed. Attributes are not - exposed by themself, but public methods on public attributes are exposed, - using dotted paths from the exposed object to the method at the end of the - path. + Only the public methods of the mounted objects will be exposed. If a method returns a :class:`pykka.Future`, the future will be completed and its value unwrapped before the JSON-RPC wrapper returns the response. @@ -59,32 +56,10 @@ class JsonRpcWrapper(object): """ def __init__(self, objects, decoders=None, encoders=None): - self.obj = self._build_exported_object(objects) + self.objects = objects self.decoder = get_combined_json_decoder(decoders or []) self.encoder = get_combined_json_encoder(encoders or []) - def _build_exported_object(self, objects): - class EmptyObject(object): - pass - - if '' in objects: - exported_object = objects[''] - else: - exported_object = EmptyObject() - - mounts = sorted(objects.keys(), key=lambda x: len(x)) - for mount in mounts: - parent = exported_object - path = mount.split('.') - for part in path[:-1]: - if not hasattr(parent, part): - setattr(parent, part, EmptyObject()) - parent = getattr(parent, part) - if path[-1]: - setattr(parent, path[-1], objects[mount]) - - return exported_object - def handle_json(self, request): """ Handles an incoming request encoded as a JSON string. @@ -205,15 +180,21 @@ class JsonRpcWrapper(object): raise JsonRpcInvalidRequestError( data='"params", if given, must be an array or an object') - def _get_method(self, name): + def _get_method(self, method_path): + if inspect.isroutine(self.objects.get(method_path, None)): + # The mounted object is the callable + return self.objects[method_path] + + # The mounted object contains the callable + if '.' in method_path: + mount, method_name = method_path.rsplit('.', 1) + else: + mount, method_name = '', method_path try: - path = name.split('.') - this = self.obj - for part in path: - if part.startswith('_'): - raise AttributeError - this = getattr(this, part) - return this + if method_name.startswith('_'): + raise AttributeError + obj = self.objects[mount] + return getattr(obj, method_name) except (AttributeError, KeyError): raise JsonRpcMethodNotFoundError() @@ -309,13 +290,9 @@ class JsonRpcInspector(object): 'abc': Abc, }) - Since this inspector is based on inspecting classes and not instances, it - will not give you a complete picture of what is actually exported by - :class:`JsonRpcWrapper`. In particular: - - - it will not include methods added dynamically, and - - it will not include public methods on attributes on the instances that - are to be exposed. + Since the inspector is based on inspecting classes and not instances, it + will not include methods added dynamically. The wrapper works with + instances, and it will thus export dynamically added methods as well. :param objects: mapping between mounts and exposed functions or classes :type objects: dict diff --git a/tests/utils/jsonrpc_test.py b/tests/utils/jsonrpc_test.py index 39ad8a81..ea99a8f3 100644 --- a/tests/utils/jsonrpc_test.py +++ b/tests/utils/jsonrpc_test.py @@ -45,6 +45,8 @@ class JsonRpcTestBase(unittest.TestCase): objects={ 'hello': lambda: 'Hello, world!', 'core': self.core, + 'core.playback': self.core.playback, + 'core.tracklist': self.core.tracklist, '': Calculator(), }, encoders=[models.ModelJSONEncoder],