Merge branch 'feature/json-rpc' into feature/http-frontend

This commit is contained in:
Stein Magnus Jodal 2012-11-25 08:39:20 +01:00
commit 90663021a4
4 changed files with 999 additions and 6 deletions

View File

@ -4,6 +4,19 @@ Changes
This change log is used to track all major changes to Mopidy.
v0.10.0 (in development)
========================
**Changes**
- None yet
**Bug fixes**
- :issue:`256`: Fix crash caused by non-ASCII characters in paths returned from
``glib``. The bug can be worked around by overriding the settings that
includes offending `$XDG_` variables.
v0.9.0 (2012-11-21)
===================

393
mopidy/utils/jsonrpc.py Normal file
View File

@ -0,0 +1,393 @@
from __future__ import unicode_literals
import inspect
import json
import traceback
import pykka
class JsonRpcWrapper(object):
"""
Wrap objects and make them accessible through JSON-RPC 2.0 messaging.
This class takes responsibility of communicating with the objects and
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.
To expose a single object, add it to the objects mapping using the empty
string as the key::
jrw = JsonRpcWrapper(objects={'': my_object})
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 create the following mapping between JSON-RPC 2.0 method names
and Python callables::
bar -> foo.bar()
baz -> foo.baz()
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.
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.
For further details on the JSON-RPC 2.0 spec, see
http://www.jsonrpc.org/specification
:param objects: mapping between mounting points and exposed functions or
class instances
:type objects: dict
:param decoders: object builders to be used by :func`json.loads`
:type decoders: list of functions taking a dict and returning a dict
:param encoders: object serializers to be used by :func:`json.dumps`
:type encoders: list of :class:`json.JSONEncoder` subclasses with the
method :meth:`default` implemented
"""
def __init__(self, objects, decoders=None, encoders=None):
self.obj = self._build_exported_object(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.
Returns a response as a JSON string for commands, and :class:`None` for
notifications.
:param request: the serialized JSON-RPC request
:type request: string
:rtype: string or :class:`None`
"""
try:
request = json.loads(request, object_hook=self.decoder)
except ValueError:
response = JsonRpcParseError().get_response()
else:
response = self.handle_data(request)
if response is None:
return None
return json.dumps(response, cls=self.encoder)
def handle_data(self, request):
"""
Handles an incoming request in the form of a Python data structure.
Returns a Python data structure for commands, or a :class:`None` for
notifications.
:param request: the unserialized JSON-RPC request
:type request: dict
:rtype: dict, list, or :class:`None`
"""
if isinstance(request, list):
return self._handle_batch(request)
else:
return self._handle_single_request(request)
def _handle_batch(self, requests):
if not requests:
return JsonRpcInvalidRequestError(
data='Batch list cannot be empty').get_response()
responses = []
for request in requests:
response = self._handle_single_request(request)
if response:
responses.append(response)
return responses or None
def _handle_single_request(self, request):
try:
self._validate_request(request)
args, kwargs = self._get_params(request)
except JsonRpcInvalidRequestError as error:
return error.get_response()
try:
method = self._get_method(request['method'])
try:
result = method(*args, **kwargs)
if self._is_notification(request):
return None
if self._is_future(result):
result = result.get()
return {
'jsonrpc': '2.0',
'id': request['id'],
'result': result,
}
except TypeError as error:
raise JsonRpcInvalidParamsError(data={
'type': error.__class__.__name__,
'message': unicode(error),
'traceback': traceback.format_exc(),
})
except Exception as error:
raise JsonRpcApplicationError(data={
'type': error.__class__.__name__,
'message': unicode(error),
'traceback': traceback.format_exc(),
})
except JsonRpcError as error:
if self._is_notification(request):
return None
return error.get_response(request['id'])
def _validate_request(self, request):
if not isinstance(request, dict):
raise JsonRpcInvalidRequestError(
data='Request must be an object')
if not 'jsonrpc' in request:
raise JsonRpcInvalidRequestError(
data='"jsonrpc" member must be included')
if request['jsonrpc'] != '2.0':
raise JsonRpcInvalidRequestError(
data='"jsonrpc" value must be "2.0"')
if not 'method' in request:
raise JsonRpcInvalidRequestError(
data='"method" member must be included')
if not isinstance(request['method'], unicode):
raise JsonRpcInvalidRequestError(
data='"method" must be a string')
def _get_params(self, request):
if not 'params' in request:
return [], {}
params = request['params']
if isinstance(params, list):
return params, {}
elif isinstance(params, dict):
return [], params
else:
raise JsonRpcInvalidRequestError(
data='"params", if given, must be an array or an object')
def _get_method(self, name):
try:
path = name.split('.')
this = self.obj
for part in path:
if part.startswith('_'):
raise AttributeError
this = getattr(this, part)
return this
except (AttributeError, KeyError):
raise JsonRpcMethodNotFoundError()
def _is_notification(self, request):
return 'id' not in request
def _is_future(self, result):
return isinstance(result, pykka.Future)
class JsonRpcError(Exception):
code = -32000
message = 'Unspecified server error'
def __init__(self, data=None):
self.data = data
def get_response(self, request_id=None):
response = {
'jsonrpc': '2.0',
'id': request_id,
'error': {
'code': self.code,
'message': self.message,
},
}
if self.data:
response['error']['data'] = self.data
return response
class JsonRpcParseError(JsonRpcError):
code = -32700
message = 'Parse error'
class JsonRpcInvalidRequestError(JsonRpcError):
code = -32600
message = 'Invalid Request'
class JsonRpcMethodNotFoundError(JsonRpcError):
code = -32601
message = 'Method not found'
class JsonRpcInvalidParamsError(JsonRpcError):
code = -32602
message = 'Invalid params'
class JsonRpcApplicationError(JsonRpcError):
code = 0
message = 'Application error'
def get_combined_json_decoder(decoders):
def decode(dct):
for decoder in decoders:
dct = decoder(dct)
return dct
return decode
def get_combined_json_encoder(encoders):
class JsonRpcEncoder(json.JSONEncoder):
def default(self, obj):
for encoder in encoders:
try:
return encoder().default(obj)
except TypeError:
pass # Try next encoder
return json.JSONEncoder.default(self, obj)
return JsonRpcEncoder
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::
jri = JsonRpcInspector(objects={
'': Foo,
'hello': lambda: 'Hello, world!',
'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.
:param objects: mapping between mounts and exposed functions or classes
:type objects: dict
"""
def __init__(self, objects):
self.objects = objects
def describe(self):
"""
Inspects the object and returns a data structure which describes the
available properties and methods.
"""
methods = {}
for mount, obj in self.objects.iteritems():
if inspect.isroutine(obj):
methods[mount] = self._describe_method(obj)
else:
obj_methods = self._get_methods(obj)
for name, description in obj_methods.iteritems():
if mount:
name = '%s.%s' % (mount, name)
methods[name] = description
return methods
def _get_methods(self, obj):
methods = {}
for name, value in inspect.getmembers(obj):
if name.startswith('_'):
continue
if not inspect.isroutine(value):
continue
method = self._describe_method(value)
if method:
methods[name] = method
return methods
def _describe_method(self, method):
return {
'description': inspect.getdoc(method),
'params': self._describe_params(method),
}
def _describe_params(self, method):
argspec = inspect.getargspec(method)
defaults = argspec.defaults and list(argspec.defaults) or []
num_args_without_default = len(argspec.args) - len(defaults)
no_defaults = [None] * num_args_without_default
defaults = no_defaults + defaults
params = []
for arg, default in zip(argspec.args, defaults):
if arg == 'self':
continue
params.append({'name': arg})
if argspec.defaults:
for i, default in enumerate(reversed(argspec.defaults)):
params[len(params) - i - 1]['default'] = default
if argspec.varargs:
params.append({
'name': argspec.varargs,
'varargs': True,
})
if argspec.keywords:
params.append({
'name': argspec.keywords,
'kwargs': True,
})
return params

View File

@ -13,14 +13,21 @@ import glib
logger = logging.getLogger('mopidy.utils.path')
DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy')
SETTINGS_PATH = os.path.join(str(glib.get_user_config_dir()), 'mopidy')
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
XDG_CACHE_DIR = glib.get_user_cache_dir().decode('utf-8')
XDG_CONFIG_DIR = glib.get_user_config_dir().decode('utf-8')
XDG_DATA_DIR = glib.get_user_data_dir().decode('utf-8')
XDG_MUSIC_DIR = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC)
if XDG_MUSIC_DIR:
XDG_MUSIC_DIR = XDG_MUSIC_DIR.decode('utf-8')
XDG_DIRS = {
'XDG_CACHE_DIR': glib.get_user_cache_dir(),
'XDG_DATA_DIR': glib.get_user_data_dir(),
'XDG_MUSIC_DIR': glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC),
'XDG_CACHE_DIR': XDG_CACHE_DIR,
'XDG_CONFIG_DIR': XDG_CONFIG_DIR,
'XDG_DATA_DIR': XDG_DATA_DIR,
'XDG_MUSIC_DIR': XDG_MUSIC_DIR,
}
DATA_PATH = os.path.join(XDG_DATA_DIR, 'mopidy')
SETTINGS_PATH = os.path.join(XDG_CONFIG_DIR, 'mopidy')
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
def get_or_create_folder(folder):

580
tests/utils/jsonrpc_test.py Normal file
View File

@ -0,0 +1,580 @@
from __future__ import unicode_literals
import json
import pykka
import mock
from mopidy import core, models
from mopidy.backends import dummy
from mopidy.utils import jsonrpc
from tests import unittest
class Calculator(object):
def model(self):
return 'TI83'
def add(self, a, b):
"""Returns the sum of the given numbers"""
return a + b
def sub(self, a, b):
return a - b
def describe(self):
return {
'add': 'Returns the sum of the terms',
'sub': 'Returns the diff of the terms',
}
def take_it_all(self, a, b, c=True, *args, **kwargs):
pass
def _secret(self):
return 'Grand Unified Theory'
class JsonRpcTestBase(unittest.TestCase):
def setUp(self):
self.backend = dummy.DummyBackend.start(audio=None).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.jrw = jsonrpc.JsonRpcWrapper(
objects={
'hello': lambda: 'Hello, world!',
'core': self.core,
'': Calculator(),
},
encoders=[models.ModelJSONEncoder],
decoders=[models.model_json_decoder])
def tearDown(self):
pykka.ActorRegistry.stop_all()
class JsonRpcSerializationTest(JsonRpcTestBase):
def test_handle_json_converts_from_and_to_json(self):
self.jrw.handle_data = mock.Mock()
self.jrw.handle_data.return_value = {'foo': 'response'}
request = '{"foo": "request"}'
response = self.jrw.handle_json(request)
self.jrw.handle_data.assert_called_once_with({'foo': 'request'})
self.assertEqual(response, '{"foo": "response"}')
def test_handle_json_decodes_mopidy_models(self):
self.jrw.handle_data = mock.Mock()
self.jrw.handle_data.return_value = []
request = '{"foo": {"__model__": "Artist", "name": "bar"}}'
self.jrw.handle_json(request)
self.jrw.handle_data.assert_called_once_with(
{'foo': models.Artist(name='bar')})
def test_handle_json_encodes_mopidy_models(self):
self.jrw.handle_data = mock.Mock()
self.jrw.handle_data.return_value = {'foo': models.Artist(name='bar')}
request = '[]'
response = self.jrw.handle_json(request)
self.assertEqual(
response, '{"foo": {"__model__": "Artist", "name": "bar"}}')
def test_handle_json_returns_nothing_for_notices(self):
request = '{"jsonrpc": "2.0", "method": "core.get_uri_schemes"}'
response = self.jrw.handle_json(request)
self.assertEqual(response, None)
def test_invalid_json_command_causes_parse_error(self):
request = (
'{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]')
response = self.jrw.handle_json(request)
response = json.loads(response)
self.assertEqual(response['jsonrpc'], '2.0')
error = response['error']
self.assertEqual(error['code'], -32700)
self.assertEqual(error['message'], 'Parse error')
def test_invalid_json_batch_causes_parse_error(self):
request = """[
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method"
]"""
response = self.jrw.handle_json(request)
response = json.loads(response)
self.assertEqual(response['jsonrpc'], '2.0')
error = response['error']
self.assertEqual(error['code'], -32700)
self.assertEqual(error['message'], 'Parse error')
class JsonRpcSingleCommandTest(JsonRpcTestBase):
def test_call_method_on_root(self):
request = {
'jsonrpc': '2.0',
'method': 'hello',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['jsonrpc'], '2.0')
self.assertEqual(response['id'], 1)
self.assertNotIn('error', response)
self.assertEqual(response['result'], 'Hello, world!')
def test_call_method_on_plain_object_as_root(self):
request = {
'jsonrpc': '2.0',
'method': 'model',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['jsonrpc'], '2.0')
self.assertEqual(response['id'], 1)
self.assertNotIn('error', response)
self.assertEqual(response['result'], 'TI83')
def test_call_method_which_returns_dict_from_plain_object(self):
request = {
'jsonrpc': '2.0',
'method': 'describe',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['jsonrpc'], '2.0')
self.assertEqual(response['id'], 1)
self.assertNotIn('error', response)
self.assertIn('add', response['result'])
self.assertIn('sub', response['result'])
def test_call_method_on_actor_root(self):
request = {
'jsonrpc': '2.0',
'method': 'core.get_uri_schemes',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['jsonrpc'], '2.0')
self.assertEqual(response['id'], 1)
self.assertNotIn('error', response)
self.assertEqual(response['result'], ['dummy'])
def test_call_method_on_actor_member(self):
request = {
'jsonrpc': '2.0',
'method': 'core.playback.get_volume',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['result'], None)
def test_call_method_with_positional_params(self):
request = {
'jsonrpc': '2.0',
'method': 'core.playback.set_volume',
'params': [37],
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['result'], None)
self.assertEqual(self.core.playback.get_volume().get(), 37)
def test_call_methods_with_named_params(self):
request = {
'jsonrpc': '2.0',
'method': 'core.playback.set_volume',
'params': {'volume': 37},
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertEqual(response['result'], None)
self.assertEqual(self.core.playback.get_volume().get(), 37)
class JsonRpcSingleNotificationTest(JsonRpcTestBase):
def test_notification_does_not_return_a_result(self):
request = {
'jsonrpc': '2.0',
'method': 'core.get_uri_schemes',
}
response = self.jrw.handle_data(request)
self.assertIsNone(response)
def test_notification_makes_an_observable_change(self):
self.assertEqual(self.core.playback.get_volume().get(), None)
request = {
'jsonrpc': '2.0',
'method': 'core.playback.set_volume',
'params': [37],
}
response = self.jrw.handle_data(request)
self.assertIsNone(response)
self.assertEqual(self.core.playback.get_volume().get(), 37)
def test_notification_unknown_method_returns_nothing(self):
request = {
'jsonrpc': '2.0',
'method': 'bogus',
'params': ['bogus'],
}
response = self.jrw.handle_data(request)
self.assertIsNone(response)
class JsonRpcBatchTest(JsonRpcTestBase):
def test_batch_of_only_commands_returns_all(self):
self.core.playback.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat', 'id': 1},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single', 'id': 3},
]
response = self.jrw.handle_data(request)
self.assertEqual(len(response), 3)
response = {row['id']: row for row in response}
self.assertEqual(response[1]['result'], False)
self.assertEqual(response[2]['result'], True)
self.assertEqual(response[3]['result'], False)
def test_batch_of_commands_and_notifications_returns_some(self):
self.core.playback.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single', 'id': 3},
]
response = self.jrw.handle_data(request)
self.assertEqual(len(response), 2)
response = {row['id']: row for row in response}
self.assertNotIn(1, response)
self.assertEqual(response[2]['result'], True)
self.assertEqual(response[3]['result'], False)
def test_batch_of_only_notifications_returns_nothing(self):
self.core.playback.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single'},
]
response = self.jrw.handle_data(request)
self.assertIsNone(response)
class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
def test_application_error_response(self):
request = {
'jsonrpc': '2.0',
'method': 'core.tracklist.index',
'params': ['bogus'],
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertNotIn('result', response)
error = response['error']
self.assertEqual(error['code'], 0)
self.assertEqual(error['message'], 'Application error')
data = error['data']
self.assertEqual(data['type'], 'ValueError')
self.assertEqual(data['message'], "u'bogus' is not in list")
self.assertIn('traceback', data)
self.assertIn('Traceback (most recent call last):', data['traceback'])
def test_missing_jsonrpc_member_causes_invalid_request_error(self):
request = {
'method': 'core.get_uri_schemes',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], '"jsonrpc" member must be included')
def test_wrong_jsonrpc_version_causes_invalid_request_error(self):
request = {
'jsonrpc': '3.0',
'method': 'core.get_uri_schemes',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], '"jsonrpc" value must be "2.0"')
def test_missing_method_member_causes_invalid_request_error(self):
request = {
'jsonrpc': '2.0',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], '"method" member must be included')
def test_invalid_method_value_causes_invalid_request_error(self):
request = {
'jsonrpc': '2.0',
'method': 1,
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], '"method" must be a string')
def test_invalid_params_value_causes_invalid_request_error(self):
request = {
'jsonrpc': '2.0',
'method': 'core.get_uri_schemes',
'params': 'foobar',
'id': 1,
}
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(
error['data'], '"params", if given, must be an array or an object')
def test_unknown_method_causes_unknown_method_error(self):
request = {
'jsonrpc': '2.0',
'method': 'bogus',
'params': ['bogus'],
'id': 1,
}
response = self.jrw.handle_data(request)
error = response['error']
self.assertEqual(error['code'], -32601)
self.assertEqual(error['message'], 'Method not found')
def test_private_method_causes_unknown_method_error(self):
request = {
'jsonrpc': '2.0',
'method': '_secret',
'id': 1,
}
response = self.jrw.handle_data(request)
error = response['error']
self.assertEqual(error['code'], -32601)
self.assertEqual(error['message'], 'Method not found')
def test_invalid_params_causes_invalid_params_error(self):
request = {
'jsonrpc': '2.0',
'method': 'core.get_uri_schemes',
'params': ['bogus'],
'id': 1,
}
response = self.jrw.handle_data(request)
error = response['error']
self.assertEqual(error['code'], -32602)
self.assertEqual(error['message'], 'Invalid params')
data = error['data']
self.assertEqual(data['type'], 'TypeError')
self.assertEqual(
data['message'],
'get_uri_schemes() takes exactly 1 argument (2 given)')
self.assertIn('traceback', data)
self.assertIn('Traceback (most recent call last):', data['traceback'])
class JsonRpcBatchErrorTest(JsonRpcTestBase):
def test_empty_batch_list_causes_invalid_request_error(self):
request = []
response = self.jrw.handle_data(request)
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], 'Batch list cannot be empty')
def test_batch_with_invalid_command_causes_invalid_request_error(self):
request = [1]
response = self.jrw.handle_data(request)
self.assertEqual(len(response), 1)
response = response[0]
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], 'Request must be an object')
def test_batch_with_invalid_commands_causes_invalid_request_error(self):
request = [1, 2, 3]
response = self.jrw.handle_data(request)
self.assertEqual(len(response), 3)
response = response[2]
self.assertIsNone(response['id'])
error = response['error']
self.assertEqual(error['code'], -32600)
self.assertEqual(error['message'], 'Invalid Request')
self.assertEqual(error['data'], 'Request must be an object')
def test_batch_of_both_successfull_and_failing_requests(self):
request = [
# Call with positional params
{'jsonrpc': '2.0', 'method': 'core.playback.set_volume',
'params': [47], 'id': '1'},
# Notification
{'jsonrpc': '2.0', 'method': 'core.playback.set_consume',
'params': [True]},
# Call with positional params
{'jsonrpc': '2.0', 'method': 'core.playback.set_repeat',
'params': [False], 'id': '2'},
# Invalid request
{'foo': 'boo'},
# Unknown method
{'jsonrpc': '2.0', 'method': 'foo.get',
'params': {'name': 'myself'}, 'id': '5'},
# Call without params
{'jsonrpc': '2.0', 'method': 'core.playback.get_random',
'id': '9'},
]
response = self.jrw.handle_data(request)
self.assertEqual(len(response), 5)
response = {row['id']: row for row in response}
self.assertEqual(response['1']['result'], None)
self.assertEqual(response['2']['result'], None)
self.assertEqual(response[None]['error']['code'], -32600)
self.assertEqual(response['5']['error']['code'], -32601)
self.assertEqual(response['9']['result'], False)
class JsonRpcInspectorTest(JsonRpcTestBase):
def test_can_describe_method_on_root(self):
inspector = jsonrpc.JsonRpcInspector({
'hello': lambda: 'Hello, world!',
})
methods = inspector.describe()
self.assertIn('hello', methods)
self.assertEqual(len(methods['hello']['params']), 0)
def test_inspector_can_describe_methods_on_the_root_class(self):
inspector = jsonrpc.JsonRpcInspector({
'': Calculator,
})
methods = inspector.describe()
self.assertIn('add', methods)
self.assertEqual(
methods['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)
method = methods['take_it_all']
self.assertIn('params', method)
params = method['params']
self.assertEqual(params[0]['name'], 'a')
self.assertNotIn('default', params[0])
self.assertEqual(params[1]['name'], 'b')
self.assertNotIn('default', params[1])
self.assertEqual(params[2]['name'], 'c')
self.assertEqual(params[2]['default'], True)
self.assertEqual(params[3]['name'], 'args')
self.assertNotIn('default', params[3])
self.assertEqual(params[3]['varargs'], True)
self.assertEqual(params[4]['name'], 'kwargs')
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,
'core.playback': core.PlaybackController,
'core.playlists': core.PlaylistsController,
'core.tracklist': core.TracklistController,
})
methods = inspector.describe()
self.assertIn('core.library.lookup', methods.keys())
self.assertEquals(
methods['core.library.lookup']['params'][0]['name'], 'uri')
self.assertIn('core.playback.next', methods)
self.assertEquals(len(methods['core.playback.next']['params']), 0)
self.assertIn('core.playlists.get_playlists', methods)
self.assertEquals(
len(methods['core.playlists.get_playlists']['params']), 0)
self.assertIn('core.tracklist.filter', methods.keys())
self.assertEquals(
methods['core.tracklist.filter']['params'][0]['name'], 'criteria')
self.assertEquals(
methods['core.tracklist.filter']['params'][0]['kwargs'], True)