Merge branch 'develop' into feature/http-frontend
This commit is contained in:
commit
7d047acc2e
@ -167,12 +167,19 @@ can install Mopidy from PyPI using Pip.
|
|||||||
|
|
||||||
sudo pacman -S base-devel python2-pip
|
sudo pacman -S base-devel python2-pip
|
||||||
|
|
||||||
|
And on Fedora Linux from the official repositories::
|
||||||
|
|
||||||
|
sudo yum install -y gcc python-devel python-pip
|
||||||
|
|
||||||
#. Then you'll need to install all of Mopidy's hard dependencies:
|
#. Then you'll need to install all of Mopidy's hard dependencies:
|
||||||
|
|
||||||
- Pykka >= 1.0::
|
- Pykka >= 1.0::
|
||||||
|
|
||||||
sudo pip install -U pykka
|
sudo pip install -U pykka
|
||||||
|
|
||||||
|
# On Fedora the binary is called pip-python:
|
||||||
|
sudo pip-python install -U pykka
|
||||||
|
|
||||||
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
|
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
|
||||||
popular Linux distributions. Search for GStreamer in your package manager,
|
popular Linux distributions. Search for GStreamer in your package manager,
|
||||||
and make sure to install the Python bindings, and the "good" and "ugly"
|
and make sure to install the Python bindings, and the "good" and "ugly"
|
||||||
@ -189,6 +196,11 @@ can install Mopidy from PyPI using Pip.
|
|||||||
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \
|
||||||
gstreamer0.10-ugly-plugins
|
gstreamer0.10-ugly-plugins
|
||||||
|
|
||||||
|
If you use Fedora you can install GStreamer like this::
|
||||||
|
|
||||||
|
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \
|
||||||
|
gstreamer0.10-plugins-ugly gstreamer0.10-tools
|
||||||
|
|
||||||
#. Optional: If you want Spotify support in Mopidy, you'll need to install
|
#. Optional: If you want Spotify support in Mopidy, you'll need to install
|
||||||
libspotify and the Python bindings, pyspotify.
|
libspotify and the Python bindings, pyspotify.
|
||||||
|
|
||||||
@ -212,15 +224,27 @@ can install Mopidy from PyPI using Pip.
|
|||||||
Remember to adjust the above example for the latest libspotify version
|
Remember to adjust the above example for the latest libspotify version
|
||||||
supported by pyspotify, your OS, and your CPU architecture.
|
supported by pyspotify, your OS, and your CPU architecture.
|
||||||
|
|
||||||
|
#. If you're on Fedora, you must add a configuration file so libspotify.so
|
||||||
|
can be found:
|
||||||
|
|
||||||
|
su -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/libspotify.conf'
|
||||||
|
sudo ldconfig
|
||||||
|
|
||||||
#. Then get, build, and install the latest release of pyspotify using Pip::
|
#. Then get, build, and install the latest release of pyspotify using Pip::
|
||||||
|
|
||||||
sudo pip install -U pyspotify
|
sudo pip install -U pyspotify
|
||||||
|
|
||||||
|
# Fedora:
|
||||||
|
sudo pip-python install -U pyspotify
|
||||||
|
|
||||||
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
#. Optional: If you want to scrobble your played tracks to Last.fm, you need
|
||||||
pylast::
|
pylast::
|
||||||
|
|
||||||
sudo pip install -U pylast
|
sudo pip install -U pylast
|
||||||
|
|
||||||
|
# Fedora:
|
||||||
|
sudo pip-python install -U pylast
|
||||||
|
|
||||||
#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
|
#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
|
||||||
Menu or from an UPnP client via Rygel, you need some additional
|
Menu or from an UPnP client via Rygel, you need some additional
|
||||||
dependencies: the Python bindings for libindicate, and the Python bindings
|
dependencies: the Python bindings for libindicate, and the Python bindings
|
||||||
@ -234,6 +258,9 @@ can install Mopidy from PyPI using Pip.
|
|||||||
|
|
||||||
sudo pip install -U mopidy
|
sudo pip install -U mopidy
|
||||||
|
|
||||||
|
# Fedora:
|
||||||
|
sudo pip-python install -U mopidy
|
||||||
|
|
||||||
To upgrade Mopidy to future releases, just rerun this command.
|
To upgrade Mopidy to future releases, just rerun this command.
|
||||||
|
|
||||||
Alternatively, if you want to track Mopidy development closer, you may
|
Alternatively, if you want to track Mopidy development closer, you may
|
||||||
|
|||||||
@ -6,32 +6,45 @@ from mopidy import settings
|
|||||||
from mopidy.models import Artist, Album, Track, Playlist
|
from mopidy.models import Artist, Album, Track, Playlist
|
||||||
|
|
||||||
|
|
||||||
|
artist_cache = {}
|
||||||
|
album_cache = {}
|
||||||
|
track_cache = {}
|
||||||
|
|
||||||
|
|
||||||
def to_mopidy_artist(spotify_artist):
|
def to_mopidy_artist(spotify_artist):
|
||||||
if spotify_artist is None:
|
if spotify_artist is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_artist(spotify_artist))
|
uri = str(Link.from_artist(spotify_artist))
|
||||||
|
if uri in artist_cache:
|
||||||
|
return artist_cache[uri]
|
||||||
if not spotify_artist.is_loaded():
|
if not spotify_artist.is_loaded():
|
||||||
return Artist(uri=uri, name='[loading...]')
|
return Artist(uri=uri, name='[loading...]')
|
||||||
return Artist(uri=uri, name=spotify_artist.name())
|
artist_cache[uri] = Artist(uri=uri, name=spotify_artist.name())
|
||||||
|
return artist_cache[uri]
|
||||||
|
|
||||||
|
|
||||||
def to_mopidy_album(spotify_album):
|
def to_mopidy_album(spotify_album):
|
||||||
if spotify_album is None:
|
if spotify_album is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_album(spotify_album))
|
uri = str(Link.from_album(spotify_album))
|
||||||
|
if uri in album_cache:
|
||||||
|
return album_cache[uri]
|
||||||
if not spotify_album.is_loaded():
|
if not spotify_album.is_loaded():
|
||||||
return Album(uri=uri, name='[loading...]')
|
return Album(uri=uri, name='[loading...]')
|
||||||
return Album(
|
album_cache[uri] = Album(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
name=spotify_album.name(),
|
name=spotify_album.name(),
|
||||||
artists=[to_mopidy_artist(spotify_album.artist())],
|
artists=[to_mopidy_artist(spotify_album.artist())],
|
||||||
date=spotify_album.year())
|
date=spotify_album.year())
|
||||||
|
return album_cache[uri]
|
||||||
|
|
||||||
|
|
||||||
def to_mopidy_track(spotify_track):
|
def to_mopidy_track(spotify_track):
|
||||||
if spotify_track is None:
|
if spotify_track is None:
|
||||||
return
|
return
|
||||||
uri = str(Link.from_track(spotify_track, 0))
|
uri = str(Link.from_track(spotify_track, 0))
|
||||||
|
if uri in track_cache:
|
||||||
|
return track_cache[uri]
|
||||||
if not spotify_track.is_loaded():
|
if not spotify_track.is_loaded():
|
||||||
return Track(uri=uri, name='[loading...]')
|
return Track(uri=uri, name='[loading...]')
|
||||||
spotify_album = spotify_track.album()
|
spotify_album = spotify_track.album()
|
||||||
@ -39,7 +52,7 @@ def to_mopidy_track(spotify_track):
|
|||||||
date = spotify_album.year()
|
date = spotify_album.year()
|
||||||
else:
|
else:
|
||||||
date = None
|
date = None
|
||||||
return Track(
|
track_cache[uri] = Track(
|
||||||
uri=uri,
|
uri=uri,
|
||||||
name=spotify_track.name(),
|
name=spotify_track.name(),
|
||||||
artists=[to_mopidy_artist(a) for a in spotify_track.artists()],
|
artists=[to_mopidy_artist(a) for a in spotify_track.artists()],
|
||||||
@ -48,6 +61,7 @@ def to_mopidy_track(spotify_track):
|
|||||||
date=date,
|
date=date,
|
||||||
length=spotify_track.duration(),
|
length=spotify_track.duration(),
|
||||||
bitrate=settings.SPOTIFY_BITRATE)
|
bitrate=settings.SPOTIFY_BITRATE)
|
||||||
|
return track_cache[uri]
|
||||||
|
|
||||||
|
|
||||||
def to_mopidy_playlist(spotify_playlist):
|
def to_mopidy_playlist(spotify_playlist):
|
||||||
|
|||||||
@ -15,32 +15,27 @@ class JsonRpcWrapper(object):
|
|||||||
processing of JSON-RPC 2.0 messages. The transport of the messages over
|
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.
|
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
|
The wrapper supports exporting the methods of one or more objects. Either
|
||||||
string as the key::
|
way, the objects must be exported with method name prefixes, called
|
||||||
|
"mounts".
|
||||||
|
|
||||||
jrw = JsonRpcWrapper(objects={'': my_object})
|
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::
|
||||||
|
|
||||||
To expose multiple objects, add them all to the objects mapping. The key in
|
jrw = JsonRpcWrapper(objects={
|
||||||
the mapping is used as the object's mounting point in the exposed API::
|
'foo': foo,
|
||||||
|
'hello': lambda: 'Hello, world!',
|
||||||
|
})
|
||||||
|
|
||||||
jrw = JsonRpcWrapper(objects={
|
This will export the Python callables on the left as the JSON-RPC 2.0
|
||||||
'': foo,
|
method names on the right::
|
||||||
'hello': lambda: 'Hello, world!',
|
|
||||||
'abc': abc,
|
|
||||||
})
|
|
||||||
|
|
||||||
This will create the following mapping between JSON-RPC 2.0 method names
|
foo.bar() -> foo.bar
|
||||||
and Python callables::
|
foo.baz() -> foo.baz
|
||||||
|
lambda -> hello
|
||||||
|
|
||||||
bar -> foo.bar()
|
Only the public methods of the mounted objects, or functions/methods
|
||||||
baz -> foo.baz()
|
included directly in the mapping, will be exposed.
|
||||||
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
|
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.
|
and its value unwrapped before the JSON-RPC wrapper returns the response.
|
||||||
@ -59,32 +54,13 @@ class JsonRpcWrapper(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, objects, decoders=None, encoders=None):
|
def __init__(self, objects, decoders=None, encoders=None):
|
||||||
self.obj = self._build_exported_object(objects)
|
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.decoder = get_combined_json_decoder(decoders or [])
|
||||||
self.encoder = get_combined_json_encoder(encoders 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):
|
def handle_json(self, request):
|
||||||
"""
|
"""
|
||||||
Handles an incoming request encoded as a JSON string.
|
Handles an incoming request encoded as a JSON string.
|
||||||
@ -151,8 +127,7 @@ class JsonRpcWrapper(object):
|
|||||||
if self._is_notification(request):
|
if self._is_notification(request):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._is_future(result):
|
result = self._unwrap_result(result)
|
||||||
result = result.get()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
@ -205,23 +180,44 @@ class JsonRpcWrapper(object):
|
|||||||
raise JsonRpcInvalidRequestError(
|
raise JsonRpcInvalidRequestError(
|
||||||
data='"params", if given, must be an array or an object')
|
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 '.' not in method_path:
|
||||||
|
raise JsonRpcMethodNotFoundError(
|
||||||
|
data='Could not find object mount in method name "%s"' % (
|
||||||
|
method_path))
|
||||||
|
|
||||||
|
mount, method_name = method_path.rsplit('.', 1)
|
||||||
|
|
||||||
|
if method_name.startswith('_'):
|
||||||
|
raise JsonRpcMethodNotFoundError(
|
||||||
|
data='Private methods are not exported')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path = name.split('.')
|
obj = self.objects[mount]
|
||||||
this = self.obj
|
except KeyError:
|
||||||
for part in path:
|
raise JsonRpcMethodNotFoundError(
|
||||||
if part.startswith('_'):
|
data='No object found at "%s"' % mount)
|
||||||
raise AttributeError
|
|
||||||
this = getattr(this, part)
|
try:
|
||||||
return this
|
return getattr(obj, method_name)
|
||||||
except (AttributeError, KeyError):
|
except AttributeError:
|
||||||
raise JsonRpcMethodNotFoundError()
|
raise JsonRpcMethodNotFoundError(
|
||||||
|
data='Object mounted at "%s" has no member "%s"' % (
|
||||||
|
mount, method_name))
|
||||||
|
|
||||||
def _is_notification(self, request):
|
def _is_notification(self, request):
|
||||||
return 'id' not in request
|
return 'id' not in request
|
||||||
|
|
||||||
def _is_future(self, result):
|
def _unwrap_result(self, result):
|
||||||
return isinstance(result, pykka.Future)
|
if isinstance(result, pykka.Future):
|
||||||
|
result = result.get()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
@ -295,33 +291,27 @@ class JsonRpcInspector(object):
|
|||||||
Inspects a group of classes and functions to create a description of what
|
Inspects a group of classes and functions to create a description of what
|
||||||
methods they can expose over JSON-RPC 2.0.
|
methods they can expose over JSON-RPC 2.0.
|
||||||
|
|
||||||
To inspect a single class, add it to the objects mapping using the empty
|
To inspect one or more classes, add them all to the objects mapping. The
|
||||||
string as the key::
|
key in the mapping is used as the classes' mounting point in the exposed
|
||||||
|
API::
|
||||||
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={
|
jri = JsonRpcInspector(objects={
|
||||||
'': Foo,
|
'foo': Foo,
|
||||||
'hello': lambda: 'Hello, world!',
|
'hello': lambda: 'Hello, world!',
|
||||||
'abc': Abc,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Since this inspector is based on inspecting classes and not instances, it
|
Since the inspector is based on inspecting classes and not instances, it
|
||||||
will not give you a complete picture of what is actually exported by
|
will not include methods added dynamically. The wrapper works with
|
||||||
:class:`JsonRpcWrapper`. In particular:
|
instances, and it will thus export dynamically added methods as well.
|
||||||
|
|
||||||
- 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
|
:param objects: mapping between mounts and exposed functions or classes
|
||||||
:type objects: dict
|
:type objects: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, objects):
|
def __init__(self, objects):
|
||||||
|
if '' in objects.keys():
|
||||||
|
raise AttributeError(
|
||||||
|
'The empty string is not allowed as an object mount')
|
||||||
self.objects = objects
|
self.objects = objects
|
||||||
|
|
||||||
def describe(self):
|
def describe(self):
|
||||||
|
|||||||
@ -25,9 +25,9 @@ XDG_DIRS = {
|
|||||||
'XDG_DATA_DIR': XDG_DATA_DIR,
|
'XDG_DATA_DIR': XDG_DATA_DIR,
|
||||||
'XDG_MUSIC_DIR': XDG_MUSIC_DIR,
|
'XDG_MUSIC_DIR': XDG_MUSIC_DIR,
|
||||||
}
|
}
|
||||||
DATA_PATH = os.path.join(XDG_DATA_DIR, 'mopidy')
|
DATA_PATH = os.path.join(unicode(XDG_DATA_DIR), 'mopidy')
|
||||||
SETTINGS_PATH = os.path.join(XDG_CONFIG_DIR, 'mopidy')
|
SETTINGS_PATH = os.path.join(unicode(XDG_CONFIG_DIR), 'mopidy')
|
||||||
SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py')
|
SETTINGS_FILE = os.path.join(unicode(SETTINGS_PATH), 'settings.py')
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_folder(folder):
|
def get_or_create_folder(folder):
|
||||||
|
|||||||
@ -44,8 +44,10 @@ class JsonRpcTestBase(unittest.TestCase):
|
|||||||
self.jrw = jsonrpc.JsonRpcWrapper(
|
self.jrw = jsonrpc.JsonRpcWrapper(
|
||||||
objects={
|
objects={
|
||||||
'hello': lambda: 'Hello, world!',
|
'hello': lambda: 'Hello, world!',
|
||||||
|
'calc': Calculator(),
|
||||||
'core': self.core,
|
'core': self.core,
|
||||||
'': Calculator(),
|
'core.playback': self.core.playback,
|
||||||
|
'core.tracklist': self.core.tracklist,
|
||||||
},
|
},
|
||||||
encoders=[models.ModelJSONEncoder],
|
encoders=[models.ModelJSONEncoder],
|
||||||
decoders=[models.model_json_decoder])
|
decoders=[models.model_json_decoder])
|
||||||
@ -54,6 +56,12 @@ class JsonRpcTestBase(unittest.TestCase):
|
|||||||
pykka.ActorRegistry.stop_all()
|
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):
|
class JsonRpcSerializationTest(JsonRpcTestBase):
|
||||||
def test_handle_json_converts_from_and_to_json(self):
|
def test_handle_json_converts_from_and_to_json(self):
|
||||||
self.jrw.handle_data = mock.Mock()
|
self.jrw.handle_data = mock.Mock()
|
||||||
@ -130,10 +138,10 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
|
|||||||
self.assertNotIn('error', response)
|
self.assertNotIn('error', response)
|
||||||
self.assertEqual(response['result'], 'Hello, world!')
|
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 = {
|
request = {
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'model',
|
'method': 'calc.model',
|
||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
response = self.jrw.handle_data(request)
|
response = self.jrw.handle_data(request)
|
||||||
@ -146,7 +154,7 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
|
|||||||
def test_call_method_which_returns_dict_from_plain_object(self):
|
def test_call_method_which_returns_dict_from_plain_object(self):
|
||||||
request = {
|
request = {
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'describe',
|
'method': 'calc.describe',
|
||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
response = self.jrw.handle_data(request)
|
response = self.jrw.handle_data(request)
|
||||||
@ -379,11 +387,10 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
error['data'], '"params", if given, must be an array or an object')
|
error['data'], '"params", if given, must be an array or an object')
|
||||||
|
|
||||||
def test_unknown_method_causes_unknown_method_error(self):
|
def test_method_on_without_object_causes_unknown_method_error(self):
|
||||||
request = {
|
request = {
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': 'bogus',
|
'method': 'bogus',
|
||||||
'params': ['bogus'],
|
|
||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
response = self.jrw.handle_data(request)
|
response = self.jrw.handle_data(request)
|
||||||
@ -391,11 +398,41 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
|
|||||||
error = response['error']
|
error = response['error']
|
||||||
self.assertEqual(error['code'], -32601)
|
self.assertEqual(error['code'], -32601)
|
||||||
self.assertEqual(error['message'], 'Method not found')
|
self.assertEqual(error['message'], 'Method not found')
|
||||||
|
self.assertEqual(
|
||||||
|
error['data'],
|
||||||
|
'Could not find object mount in method name "bogus"')
|
||||||
|
|
||||||
|
def test_method_on_unknown_object_causes_unknown_method_error(self):
|
||||||
|
request = {
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'bogus.bogus',
|
||||||
|
'id': 1,
|
||||||
|
}
|
||||||
|
response = self.jrw.handle_data(request)
|
||||||
|
|
||||||
|
error = response['error']
|
||||||
|
self.assertEqual(error['code'], -32601)
|
||||||
|
self.assertEqual(error['message'], 'Method not found')
|
||||||
|
self.assertEqual(error['data'], 'No object found at "bogus"')
|
||||||
|
|
||||||
|
def test_unknown_method_on_known_object_causes_unknown_method_error(self):
|
||||||
|
request = {
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'core.bogus',
|
||||||
|
'id': 1,
|
||||||
|
}
|
||||||
|
response = self.jrw.handle_data(request)
|
||||||
|
|
||||||
|
error = response['error']
|
||||||
|
self.assertEqual(error['code'], -32601)
|
||||||
|
self.assertEqual(error['message'], 'Method not found')
|
||||||
|
self.assertEqual(
|
||||||
|
error['data'], 'Object mounted at "core" has no member "bogus"')
|
||||||
|
|
||||||
def test_private_method_causes_unknown_method_error(self):
|
def test_private_method_causes_unknown_method_error(self):
|
||||||
request = {
|
request = {
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
'method': '_secret',
|
'method': 'core._secret',
|
||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
response = self.jrw.handle_data(request)
|
response = self.jrw.handle_data(request)
|
||||||
@ -403,6 +440,7 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
|
|||||||
error = response['error']
|
error = response['error']
|
||||||
self.assertEqual(error['code'], -32601)
|
self.assertEqual(error['code'], -32601)
|
||||||
self.assertEqual(error['message'], 'Method not found')
|
self.assertEqual(error['message'], 'Method not found')
|
||||||
|
self.assertEqual(error['data'], 'Private methods are not exported')
|
||||||
|
|
||||||
def test_invalid_params_causes_invalid_params_error(self):
|
def test_invalid_params_causes_invalid_params_error(self):
|
||||||
request = {
|
request = {
|
||||||
@ -493,6 +531,10 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase):
|
|||||||
|
|
||||||
|
|
||||||
class JsonRpcInspectorTest(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):
|
def test_can_describe_method_on_root(self):
|
||||||
inspector = jsonrpc.JsonRpcInspector({
|
inspector = jsonrpc.JsonRpcInspector({
|
||||||
'hello': lambda: 'Hello, world!',
|
'hello': lambda: 'Hello, world!',
|
||||||
@ -503,24 +545,24 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
|
|||||||
self.assertIn('hello', methods)
|
self.assertIn('hello', methods)
|
||||||
self.assertEqual(len(methods['hello']['params']), 0)
|
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({
|
inspector = jsonrpc.JsonRpcInspector({
|
||||||
'': Calculator,
|
'calc': Calculator,
|
||||||
})
|
})
|
||||||
|
|
||||||
methods = inspector.describe()
|
methods = inspector.describe()
|
||||||
|
|
||||||
self.assertIn('add', methods)
|
self.assertIn('calc.add', methods)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
methods['add']['description'],
|
methods['calc.add']['description'],
|
||||||
'Returns the sum of the given numbers')
|
'Returns the sum of the given numbers')
|
||||||
|
|
||||||
self.assertIn('sub', methods)
|
self.assertIn('calc.sub', methods)
|
||||||
self.assertIn('take_it_all', methods)
|
self.assertIn('calc.take_it_all', methods)
|
||||||
self.assertNotIn('_secret', methods)
|
self.assertNotIn('calc._secret', methods)
|
||||||
self.assertNotIn('__init__', methods)
|
self.assertNotIn('calc.__init__', methods)
|
||||||
|
|
||||||
method = methods['take_it_all']
|
method = methods['calc.take_it_all']
|
||||||
self.assertIn('params', method)
|
self.assertIn('params', method)
|
||||||
|
|
||||||
params = method['params']
|
params = method['params']
|
||||||
@ -542,16 +584,6 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
|
|||||||
self.assertNotIn('default', params[4])
|
self.assertNotIn('default', params[4])
|
||||||
self.assertEqual(params[4]['kwargs'], True)
|
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):
|
def test_inspector_can_describe_a_bunch_of_large_classes(self):
|
||||||
inspector = jsonrpc.JsonRpcInspector({
|
inspector = jsonrpc.JsonRpcInspector({
|
||||||
'core.library': core.LibraryController,
|
'core.library': core.LibraryController,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user