jsonrpc: Add inspector that describes the available API

This commit is contained in:
Stein Magnus Jodal 2012-11-24 00:46:20 +01:00
parent 29e2178a79
commit 569ee6c5f3
2 changed files with 174 additions and 0 deletions

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import inspect
import json
import traceback
@ -245,3 +246,84 @@ def get_combined_json_encoder(encoders):
pass # Try next encoder
return json.JSONEncoder.default(self, obj)
return JsonRpcEncoder
class JsonRpcInspector(object):
"""
Inspects a group of objects to create a description of what methods they
can expose over JSON-RPC 2.0.
:param objects: mapping between mounts and exposed 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

@ -21,6 +21,7 @@ class Calculator(object):
return 'TI83'
def add(self, a, b):
"""Returns the sum of the given numbers"""
return a + b
def sub(self, a, b):
@ -32,6 +33,9 @@ class Calculator(object):
'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'
@ -491,3 +495,91 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase):
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)