Merge pull request #40 from joshmarshall/travis-and-pip

Adding Travis for testing, fixing minor PIP violations.
This commit is contained in:
Josh Marshall 2015-10-08 15:51:17 -05:00
commit 10bea44bbe
10 changed files with 306 additions and 235 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.pyc
build/*
dist/*
.coverage

10
.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: python
sudo: false
python:
- '2.7'
- '2.6'
install:
- pip install -r dev-requirements.txt
# will need this when we automatically push to pypi
# - pip install twine
script: nosetests tests.py --with-coverage --cover-package=jsonrpclib

View File

@ -1,3 +1,5 @@
[![Build Status](https://travis-ci.org/joshmarshall/jsonrpclib.svg)](https://travis-ci.org/joshmarshall/jsonrpclib)
JSONRPClib
==========
This library is an implementation of the JSON-RPC specification.
@ -211,7 +213,8 @@ TESTS
I've dropped almost-verbatim tests from the JSON-RPC spec 2.0 page.
You can run it with:
python tests.py
pip install -r dev-requirements.txt
nosetests tests.py
TODO
----

6
dev-requirements.txt Normal file
View File

@ -0,0 +1,6 @@
coverage==4.0
linecache2==1.0.0
nose==1.3.7
six==1.9.0
traceback2==1.4.0
unittest2==1.1.0

View File

@ -15,6 +15,7 @@ except ImportError:
# For Windows
fcntl = None
def get_version(request):
# must be a dict
if 'jsonrpc' in request.keys():
@ -22,9 +23,10 @@ def get_version(request):
if 'id' in request.keys():
return 1.0
return None
def validate_request(request):
if type(request) is not types.DictType:
if not isinstance(request, dict):
fault = Fault(
-32600, 'Request must be {}, not %s.' % type(request)
)
@ -33,27 +35,27 @@ def validate_request(request):
version = get_version(request)
if not version:
fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid)
return fault
return fault
request.setdefault('params', [])
method = request.get('method', None)
params = request.get('params')
param_types = (types.ListType, types.DictType, types.TupleType)
if not method or type(method) not in types.StringTypes or \
type(params) not in param_types:
type(params) not in param_types:
fault = Fault(
-32600, 'Invalid request parameters or method.', rpcid=rpcid
)
return fault
return True
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
def __init__(self, encoding=None):
SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self,
allow_none=True,
encoding=encoding)
SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(
self, allow_none=True, encoding=encoding)
def _marshaled_dispatch(self, data, dispatch_method = None):
def _marshaled_dispatch(self, data, dispatch_method=None):
response = None
try:
request = jsonrpclib.loads(data)
@ -64,7 +66,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
if not request:
fault = Fault(-32600, 'Request invalid -- no request data.')
return fault.response()
if type(request) is types.ListType:
if isinstance(request, list):
# This SHOULD be a batch, by spec
responses = []
for req_entry in request:
@ -79,7 +81,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
response = '[%s]' % ','.join(responses)
else:
response = ''
else:
else:
result = validate_request(request)
if type(result) is Fault:
return result.response()
@ -99,7 +101,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
exc_type, exc_value, exc_tb = sys.exc_info()
fault = Fault(-32603, '%s:%s' % (exc_type, exc_value))
return fault.response()
if 'id' not in request.keys() or request['id'] == None:
if 'id' not in request.keys() or request['id'] is None:
# It's a notification
return None
try:
@ -132,25 +134,26 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
pass
if func is not None:
try:
if type(params) is types.ListType:
if isinstance(params, types.ListType):
response = func(*params)
else:
response = func(**params)
return response
except TypeError:
return Fault(-32602, 'Invalid parameters.')
# except TypeError:
# return Fault(-32602, 'Invalid parameters.')
except:
err_lines = traceback.format_exc().splitlines()
trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
fault = jsonrpclib.Fault(-32603, 'Server error: %s' %
fault = jsonrpclib.Fault(-32603, 'Server error: %s' %
trace_string)
return fault
else:
return Fault(-32601, 'Method %s not supported.' % method)
class SimpleJSONRPCRequestHandler(
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
def do_POST(self):
if not self.is_rpc_path_valid():
self.report_404()
@ -166,13 +169,13 @@ class SimpleJSONRPCRequestHandler(
data = ''.join(L)
response = self.server._marshaled_dispatch(data)
self.send_response(200)
except Exception, e:
except Exception:
self.send_response(500)
err_lines = traceback.format_exc().splitlines()
trace_string = '%s | %s' % (err_lines[-3], err_lines[-1])
fault = jsonrpclib.Fault(-32603, 'Server error: %s' % trace_string)
response = fault.response()
if response == None:
if response is None:
response = ''
self.send_header("Content-type", "application/json-rpc")
self.send_header("Content-length", str(len(response)))
@ -181,6 +184,7 @@ class SimpleJSONRPCRequestHandler(
self.wfile.flush()
self.connection.shutdown(1)
class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher):
allow_reuse_address = True
@ -198,7 +202,7 @@ class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher):
# Unix sockets can't be bound if they already exist in the
# filesystem. The convention of e.g. X11 is to unlink
# before binding again.
if os.path.exists(addr):
if os.path.exists(addr):
try:
os.unlink(addr)
except OSError:
@ -207,13 +211,14 @@ class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher):
if vi[0] < 3 and vi[1] < 6:
SocketServer.TCPServer.__init__(self, addr, requestHandler)
else:
SocketServer.TCPServer.__init__(self, addr, requestHandler,
bind_and_activate)
SocketServer.TCPServer.__init__(
self, addr, requestHandler, bind_and_activate)
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher):
def __init__(self, encoding=None):

View File

@ -1,12 +1,14 @@
import sys
class LocalClasses(dict):
def add(self, cls):
self[cls.__name__] = cls
class Config(object):
"""
This is pretty much used exclusively for the 'jsonclass'
This is pretty much used exclusively for the 'jsonclass'
functionality... set use_jsonclass to False to turn it off.
You can change serialize_method and ignore_attribute, or use
the local_classes.add(class) to include "local" classes.
@ -15,7 +17,7 @@ class Config(object):
# Change to False to keep __jsonclass__ entries raw.
serialize_method = '_serialize'
# The serialize_method should be a string that references the
# method on a custom class object which is responsible for
# method on a custom class object which is responsible for
# returning a tuple of the constructor arguments and a dict of
# attributes.
ignore_attribute = '_ignore'
@ -30,7 +32,7 @@ class Config(object):
'.'.join([str(ver) for ver in sys.version_info[0:3]])
# User agent to use for calls.
_instance = None
@classmethod
def instance(cls):
if not cls._instance:

View File

@ -2,13 +2,13 @@ class History(object):
"""
This holds all the response and request objects for a
session. A server using this should call "clear" after
each request cycle in order to keep it from clogging
each request cycle in order to keep it from clogging
memory.
"""
requests = []
responses = []
_instance = None
@classmethod
def instance(cls):
if not cls._instance:
@ -17,7 +17,7 @@ class History(object):
def add_response(self, response_obj):
self.responses.append(response_obj)
def add_request(self, request_obj):
self.requests.append(request_obj)

View File

@ -1,7 +1,6 @@
import types
import inspect
import re
import traceback
from jsonrpclib import config
@ -30,9 +29,11 @@ value_types = [
supported_types = iter_types+string_types+numeric_types+value_types
invalid_module_chars = r'[^a-zA-Z0-9\_\.]'
class TranslationError(Exception):
pass
def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]):
if not serialize_method:
serialize_method = config.serialize_method
@ -46,17 +47,17 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]):
if obj_type in (types.ListType, types.TupleType):
new_obj = []
for item in obj:
new_obj.append(dump(item, serialize_method,
ignore_attribute, ignore))
if obj_type is types.TupleType:
new_obj.append(
dump(item, serialize_method, ignore_attribute, ignore))
if isinstance(obj_type, types.TupleType):
new_obj = tuple(new_obj)
return new_obj
# It's a dict...
else:
new_obj = {}
for key, value in obj.iteritems():
new_obj[key] = dump(value, serialize_method,
ignore_attribute, ignore)
new_obj[key] = dump(
value, serialize_method, ignore_attribute, ignore)
return new_obj
# It's not a standard type, so it needs __jsonclass__
module_name = inspect.getmodule(obj).__name__
@ -64,7 +65,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]):
json_class = class_name
if module_name not in ['', '__main__']:
json_class = '%s.%s' % (module_name, json_class)
return_obj = {"__jsonclass__":[json_class,]}
return_obj = {"__jsonclass__": [json_class]}
# If a serialization method is defined..
if serialize_method in dir(obj):
# Params can be a dict (keyword) or list (positional)
@ -84,21 +85,23 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]):
if type(attr_value) in supported_types and \
attr_name not in ignore_list and \
attr_value not in ignore_list:
attrs[attr_name] = dump(attr_value, serialize_method,
ignore_attribute, ignore)
attrs[attr_name] = dump(
attr_value, serialize_method, ignore_attribute, ignore)
return_obj.update(attrs)
return return_obj
def load(obj):
if type(obj) in string_types+numeric_types+value_types:
if type(obj) in string_types + numeric_types + value_types:
return obj
if type(obj) is types.ListType:
if isinstance(obj, list):
return_list = []
for entry in obj:
return_list.append(load(entry))
return return_list
# Othewise, it's a dict type
if '__jsonclass__' not in obj.keys():
if '__jsonclass__' not in obj:
return_dict = {}
for key, value in obj.iteritems():
new_value = load(value)
@ -139,9 +142,9 @@ def load(obj):
json_class = getattr(temp_module, json_class_name)
# Creating the object...
new_obj = None
if type(params) is types.ListType:
if isinstance(params, list):
new_obj = json_class(*params)
elif type(params) is types.DictType:
elif isinstance(params, dict):
new_obj = json_class(**params)
else:
raise TranslationError('Constructor args must be a dict or list.')

View File

@ -1,15 +1,15 @@
"""
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
============================
JSONRPC Library (jsonrpclib)
@ -29,7 +29,7 @@ Eventually, I'll add a SimpleXMLRPCServer compatible library,
and other things to tie the thing off nicely. :)
For a quick-start, just open a console and type the following,
replacing the server address, method, and parameters
replacing the server address, method, and parameters
appropriately.
>>> import jsonrpclib
>>> server = jsonrpclib.Server('http://localhost:8181')
@ -47,17 +47,14 @@ See http://code.google.com/p/jsonrpclib/ for more info.
"""
import types
import sys
from xmlrpclib import Transport as XMLTransport
from xmlrpclib import SafeTransport as XMLSafeTransport
from xmlrpclib import ServerProxy as XMLServerProxy
from xmlrpclib import _Method as XML_Method
import time
import string
import random
# Library includes
import jsonrpclib
from jsonrpclib import config
from jsonrpclib import history
@ -80,14 +77,17 @@ except ImportError:
IDCHARS = string.ascii_lowercase+string.digits
class UnixSocketMissing(Exception):
"""
Just a properly named Exception if Unix Sockets usage is
"""
Just a properly named Exception if Unix Sockets usage is
attempted on a platform that doesn't support them (Windows)
"""
pass
#JSON Abstractions
# JSON Abstractions
def jdumps(obj, encoding='utf-8'):
# Do 'serialize' test at some point for other classes
@ -97,6 +97,7 @@ def jdumps(obj, encoding='utf-8'):
else:
return json.dumps(obj, encoding=encoding)
def jloads(json_string):
global cjson
if cjson:
@ -107,14 +108,17 @@ def jloads(json_string):
# XMLRPClib re-implementations
class ProtocolError(Exception):
pass
class TransportMixIn(object):
""" Just extends the XMLRPC transport where necessary. """
user_agent = config.user_agent
# for Python 2.7 support
_connection = None
_connection = (None, None)
_extra_headers = []
def send_content(self, connection, request_body):
connection.putheader("Content-Type", "application/json-rpc")
@ -127,6 +131,7 @@ class TransportMixIn(object):
target = JSONTarget()
return JSONParser(target), target
class JSONParser(object):
def __init__(self, target):
self.target = target
@ -137,6 +142,7 @@ class JSONParser(object):
def close(self):
pass
class JSONTarget(object):
def __init__(self):
self.data = []
@ -147,11 +153,13 @@ class JSONTarget(object):
def close(self):
return ''.join(self.data)
class Transport(TransportMixIn, XMLTransport):
def __init__(self):
TransportMixIn.__init__(self)
XMLTransport.__init__(self)
class SafeTransport(TransportMixIn, XMLSafeTransport):
def __init__(self):
TransportMixIn.__init__(self)
@ -162,14 +170,14 @@ from socket import socket
USE_UNIX_SOCKETS = False
try:
try:
from socket import AF_UNIX, SOCK_STREAM
USE_UNIX_SOCKETS = True
except ImportError:
pass
if (USE_UNIX_SOCKETS):
class UnixHTTPConnection(HTTPConnection):
def connect(self):
self.sock = socket(AF_UNIX, SOCK_STREAM)
@ -179,19 +187,19 @@ if (USE_UNIX_SOCKETS):
_connection_class = UnixHTTPConnection
class UnixTransport(TransportMixIn, XMLTransport):
def make_connection(self, host):
import httplib
host, extra_headers, x509 = self.get_host_info(host)
return UnixHTTP(host)
class ServerProxy(XMLServerProxy):
"""
Unfortunately, much more of this class has to be copied since
so much of it does the serialization.
"""
def __init__(self, uri, transport=None, encoding=None,
def __init__(self, uri, transport=None, encoding=None,
verbose=0, version=None):
import urllib
if not version:
@ -210,7 +218,7 @@ class ServerProxy(XMLServerProxy):
self.__host, self.__handler = urllib.splithost(uri)
if not self.__handler:
# Not sure if this is in the JSON spec?
#self.__handler = '/'
# self.__handler = '/'
self.__handler == '/'
if transport is None:
if schema == 'unix':
@ -246,13 +254,13 @@ class ServerProxy(XMLServerProxy):
request,
verbose=self.__verbose
)
# Here, the XMLRPC library translates a single list
# response to the single value -- should we do the
# same, and require a tuple / list to be passed to
# the response object, or expect the Server to be
# the response object, or expect the Server to be
# outputting the response appropriately?
history.add_response(response)
if not response:
return None
@ -270,11 +278,12 @@ class ServerProxy(XMLServerProxy):
class _Method(XML_Method):
def __call__(self, *args, **kwargs):
if len(args) > 0 and len(kwargs) > 0:
raise ProtocolError('Cannot use both positional ' +
'and keyword arguments (according to JSON-RPC spec.)')
raise ProtocolError(
'Cannot use both positional and keyword arguments '
'(according to JSON-RPC spec.)')
if len(args) > 0:
return self.__send(self.__name, args)
else:
@ -285,7 +294,8 @@ class _Method(XML_Method):
return self
# The old method returned a new instance, but this seemed wasteful.
# The only thing that changes is the name.
#return _Method(self.__send, "%s.%s" % (self.__name, name))
# return _Method(self.__send, "%s.%s" % (self.__name, name))
class _Notify(object):
def __init__(self, request):
@ -293,11 +303,13 @@ class _Notify(object):
def __getattr__(self, name):
return _Method(self._request, name)
# Batch implementation
class MultiCallMethod(object):
def __init__(self, method, notify=False):
self.method = method
self.params = []
@ -318,14 +330,15 @@ class MultiCallMethod(object):
def __repr__(self):
return '%s' % self.request()
def __getattr__(self, method):
new_method = '%s.%s' % (self.method, method)
self.method = new_method
return self
class MultiCallNotify(object):
def __init__(self, multicall):
self.multicall = multicall
@ -334,8 +347,9 @@ class MultiCallNotify(object):
self.multicall._job_list.append(new_job)
return new_job
class MultiCallIterator(object):
def __init__(self, results):
self.results = results
@ -352,8 +366,9 @@ class MultiCallIterator(object):
def __len__(self):
return len(self.results)
class MultiCall(object):
def __init__(self, server):
self._server = server
self._job_list = []
@ -362,8 +377,8 @@ class MultiCall(object):
if len(self._job_list) < 1:
# Should we alert? This /is/ pretty obvious.
return
request_body = '[ %s ]' % ','.join([job.request() for
job in self._job_list])
request_body = '[ {0} ]'.format(
','.join([job.request() for job in self._job_list]))
responses = self._server._run_request(request_body)
del self._job_list[:]
if not responses:
@ -381,19 +396,21 @@ class MultiCall(object):
__call__ = _request
# These lines conform to xmlrpclib's "compatibility" line.
# These lines conform to xmlrpclib's "compatibility" line.
# Not really sure if we should include these, but oh well.
Server = ServerProxy
class Fault(object):
# JSON-RPC error class
def __init__(self, code=-32000, message='Server error', rpcid=None):
self.faultCode = code
self.faultString = message
self.rpcid = rpcid
def error(self):
return {'code':self.faultCode, 'message':self.faultString}
return {'code': self.faultCode, 'message': self.faultString}
def response(self, rpcid=None, version=None):
if not version:
@ -407,25 +424,27 @@ class Fault(object):
def __repr__(self):
return '<Fault %s: %s>' % (self.faultCode, self.faultString)
def random_id(length=8):
return_id = ''
for i in range(length):
return_id += random.choice(IDCHARS)
return return_id
class Payload(dict):
def __init__(self, rpcid=None, version=None):
if not version:
version = config.version
self.id = rpcid
self.version = float(version)
def request(self, method, params=[]):
if type(method) not in types.StringTypes:
raise ValueError('Method name must be a string.')
if not self.id:
self.id = random_id()
request = { 'id':self.id, 'method':method }
request = {'id': self.id, 'method': method}
if params:
request['params'] = params
if self.version >= 2:
@ -441,7 +460,7 @@ class Payload(dict):
return request
def response(self, result=None):
response = {'result':result, 'id':self.id}
response = {'result': result, 'id': self.id}
if self.version >= 2:
response['jsonrpc'] = str(self.version)
else:
@ -454,13 +473,15 @@ class Payload(dict):
del error['result']
else:
error['result'] = None
error['error'] = {'code':code, 'message':message}
error['error'] = {'code': code, 'message': message}
return error
def dumps(params=[], methodname=None, methodresponse=None,
def dumps(
params=[], methodname=None, methodresponse=None,
encoding=None, rpcid=None, version=None, notify=None):
"""
This differs from the Python implementation in that it implements
This differs from the Python implementation in that it implements
the rpcid argument since the 2.0 spec requires it for responses.
"""
if not version:
@ -469,7 +490,7 @@ def dumps(params=[], methodname=None, methodresponse=None,
if methodname in types.StringTypes and \
type(params) not in valid_params and \
not isinstance(params, Fault):
"""
"""
If a method, and params are not in a listish or a Fault,
error out.
"""
@ -482,10 +503,14 @@ def dumps(params=[], methodname=None, methodresponse=None,
if type(params) is Fault:
response = payload.error(params.faultCode, params.faultString)
return jdumps(response, encoding=encoding)
if type(methodname) not in types.StringTypes and methodresponse != True:
raise ValueError('Method name must be a string, or methodresponse '+
'must be set to True.')
if config.use_jsonclass == True:
if type(methodname) not in types.StringTypes and \
methodresponse is not True:
raise ValueError(
'Method name must be a string, or methodresponse must '
'be set to True.')
if config.use_jsonclass is True:
from jsonrpclib import jsonclass
params = jsonclass.dump(params)
if methodresponse is True:
@ -494,12 +519,13 @@ def dumps(params=[], methodname=None, methodresponse=None,
response = payload.response(params)
return jdumps(response, encoding=encoding)
request = None
if notify == True:
if notify is True:
request = payload.notify(methodname, params)
else:
request = payload.request(methodname, params)
return jdumps(request, encoding=encoding)
def loads(data):
"""
This differs from the Python implementation, in that it returns
@ -510,36 +536,39 @@ def loads(data):
# notification
return None
result = jloads(data)
# if the above raises an error, the implementing server code
# if the above raises an error, the implementing server code
# should return something like the following:
# { 'jsonrpc':'2.0', 'error': fault.error(), id: None }
if config.use_jsonclass == True:
if config.use_jsonclass is True:
from jsonrpclib import jsonclass
result = jsonclass.load(result)
return result
def check_for_errors(result):
if not result:
# Notification
return result
if type(result) is not types.DictType:
if not isinstance(result, dict):
raise TypeError('Response is not a dict.')
if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0:
raise NotImplementedError('JSON-RPC version not yet supported.')
if 'result' not in result.keys() and 'error' not in result.keys():
raise ValueError('Response does not have a result or error key.')
if 'error' in result.keys() and result['error'] != None:
if 'error' in result.keys() and result['error'] is not None:
code = result['error']['code']
message = result['error']['message']
raise ProtocolError((code, message))
return result
def isbatch(result):
if type(result) not in (types.ListType, types.TupleType):
return False
if len(result) < 1:
return False
if type(result[0]) is not types.DictType:
if not isinstance(result[0], dict):
return False
if 'jsonrpc' not in result[0].keys():
return False
@ -551,11 +580,12 @@ def isbatch(result):
return False
return True
def isnotification(request):
if 'id' not in request.keys():
# 2.0 notification
return True
if request['id'] == None:
if request['id'] is None:
# 1.0 notification
return True
return False

279
tests.py
View File

@ -1,16 +1,11 @@
"""
The tests in this file compare the request and response objects
to the JSON-RPC 2.0 specification document, as well as testing
several internal components of the jsonrpclib library. Run this
several internal components of the jsonrpclib library. Run this
module without any parameters to run the tests.
Currently, this is not easily tested with a framework like
nosetests because we spin up a daemon thread running the
the Server, and nosetests (at least in my tests) does not
ever "kill" the thread.
If you are testing jsonrpclib and the module doesn't return to
the command prompt after running the tests, you can hit
the command prompt after running the tests, you can hit
"Ctrl-C" (or "Ctrl-Break" on Windows) and that should kill it.
TODO:
@ -19,36 +14,47 @@ TODO:
* Implement JSONClass, History, Config tests
"""
from jsonrpclib import Server, MultiCall, history, config, ProtocolError
from jsonrpclib import jsonrpc
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler
import socket
import tempfile
import unittest
import os
import time
try:
import json
except ImportError:
import simplejson as json
import os
import socket
import sys
import tempfile
from threading import Thread
PORTS = range(8000, 8999)
if sys.version_info < (2, 7):
import unittest2 as unittest
else:
import unittest
from jsonrpclib import Server, MultiCall, history, ProtocolError
from jsonrpclib import jsonrpc
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler
def get_port(family=socket.AF_INET):
sock = socket.socket(family, socket.SOCK_STREAM)
sock.bind(("localhost", 0))
return sock.getsockname()[1]
class TestCompatibility(unittest.TestCase):
client = None
port = None
server = None
def setUp(self):
self.port = PORTS.pop()
self.port = get_port()
self.server = server_set_up(addr=('', self.port))
self.client = Server('http://localhost:%d' % self.port)
# v1 tests forthcoming
# Version 2.0 Tests
def test_positional(self):
""" Positional arguments in a single call """
@ -59,7 +65,7 @@ class TestCompatibility(unittest.TestCase):
request = json.loads(history.request)
response = json.loads(history.response)
verify_request = {
"jsonrpc": "2.0", "method": "subtract",
"jsonrpc": "2.0", "method": "subtract",
"params": [42, 23], "id": request['id']
}
verify_response = {
@ -67,7 +73,7 @@ class TestCompatibility(unittest.TestCase):
}
self.assertTrue(request == verify_request)
self.assertTrue(response == verify_response)
def test_named(self):
""" Named arguments in a single call """
result = self.client.subtract(subtrahend=23, minuend=42)
@ -77,8 +83,8 @@ class TestCompatibility(unittest.TestCase):
request = json.loads(history.request)
response = json.loads(history.response)
verify_request = {
"jsonrpc": "2.0", "method": "subtract",
"params": {"subtrahend": 23, "minuend": 42},
"jsonrpc": "2.0", "method": "subtract",
"params": {"subtrahend": 23, "minuend": 42},
"id": request['id']
}
verify_response = {
@ -86,101 +92,103 @@ class TestCompatibility(unittest.TestCase):
}
self.assertTrue(request == verify_request)
self.assertTrue(response == verify_response)
def test_notification(self):
""" Testing a notification (response should be null) """
result = self.client._notify.update(1, 2, 3, 4, 5)
self.assertTrue(result == None)
self.assertTrue(result is None)
request = json.loads(history.request)
response = history.response
verify_request = {
"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]
"jsonrpc": "2.0", "method": "update", "params": [1, 2, 3, 4, 5]
}
verify_response = ''
self.assertTrue(request == verify_request)
self.assertTrue(response == verify_response)
def test_non_existent_method(self):
self.assertRaises(ProtocolError, self.client.foobar)
with self.assertRaises(ProtocolError):
self.client.foobar()
request = json.loads(history.request)
response = json.loads(history.response)
verify_request = {
"jsonrpc": "2.0", "method": "foobar", "id": request['id']
}
verify_response = {
"jsonrpc": "2.0",
"error":
{"code": -32601, "message": response['error']['message']},
"jsonrpc": "2.0",
"error":
{"code": -32601, "message": response['error']['message']},
"id": request['id']
}
self.assertTrue(request == verify_request)
self.assertTrue(response == verify_response)
def test_invalid_json(self):
invalid_json = '{"jsonrpc": "2.0", "method": "foobar, '+ \
invalid_json = '{"jsonrpc": "2.0", "method": "foobar, ' + \
'"params": "bar", "baz]'
response = self.client._run_request(invalid_json)
response = json.loads(history.response)
verify_response = json.loads(
'{"jsonrpc": "2.0", "error": {"code": -32700,'+
'{"jsonrpc": "2.0", "error": {"code": -32700,' +
' "message": "Parse error."}, "id": null}'
)
verify_response['error']['message'] = response['error']['message']
self.assertTrue(response == verify_response)
def test_invalid_request(self):
invalid_request = '{"jsonrpc": "2.0", "method": 1, "params": "bar"}'
response = self.client._run_request(invalid_request)
response = json.loads(history.response)
verify_response = json.loads(
'{"jsonrpc": "2.0", "error": {"code": -32600, '+
'{"jsonrpc": "2.0", "error": {"code": -32600, ' +
'"message": "Invalid Request."}, "id": null}'
)
verify_response['error']['message'] = response['error']['message']
self.assertTrue(response == verify_response)
def test_batch_invalid_json(self):
invalid_request = '[ {"jsonrpc": "2.0", "method": "sum", '+ \
'"params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]'
invalid_request = '[ {"jsonrpc": "2.0", "method": "sum", ' + \
'"params": [1, 2, 4], "id": "1"},{"jsonrpc": "2.0", "method" ]'
response = self.client._run_request(invalid_request)
response = json.loads(history.response)
verify_response = json.loads(
'{"jsonrpc": "2.0", "error": {"code": -32700,'+
'{"jsonrpc": "2.0", "error": {"code": -32700,' +
'"message": "Parse error."}, "id": null}'
)
verify_response['error']['message'] = response['error']['message']
self.assertTrue(response == verify_response)
def test_empty_array(self):
invalid_request = '[]'
response = self.client._run_request(invalid_request)
response = json.loads(history.response)
verify_response = json.loads(
'{"jsonrpc": "2.0", "error": {"code": -32600, '+
'{"jsonrpc": "2.0", "error": {"code": -32600, ' +
'"message": "Invalid Request."}, "id": null}'
)
verify_response['error']['message'] = response['error']['message']
self.assertTrue(response == verify_response)
def test_nonempty_array(self):
invalid_request = '[1,2]'
invalid_request = '[1, 2]'
request_obj = json.loads(invalid_request)
response = self.client._run_request(invalid_request)
response = json.loads(history.response)
self.assertTrue(len(response) == len(request_obj))
for resp in response:
verify_resp = json.loads(
'{"jsonrpc": "2.0", "error": {"code": -32600, '+
'{"jsonrpc": "2.0", "error": {"code": -32600, ' +
'"message": "Invalid Request."}, "id": null}'
)
verify_resp['error']['message'] = resp['error']['message']
self.assertTrue(resp == verify_resp)
def test_batch(self):
multicall = MultiCall(self.client)
multicall.sum(1,2,4)
multicall.sum(1, 2, 4)
multicall._notify.notify_hello(7)
multicall.subtract(42,23)
multicall.subtract(42, 23)
multicall.foo.get(name='myself')
multicall.get_data()
job_requests = [j.request() for j in multicall._job_list]
@ -188,38 +196,44 @@ class TestCompatibility(unittest.TestCase):
json_requests = '[%s]' % ','.join(job_requests)
requests = json.loads(json_requests)
responses = self.client._run_request(json_requests)
verify_requests = json.loads("""[
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "sum",
"params": [1, 2, 4], "id": "1"},
{"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
{"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
{"foo": "boo"},
{"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
{"jsonrpc": "2.0", "method": "get_data", "id": "9"}
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23],
"id": "2"}, {"foo": "boo"},
{"jsonrpc": "2.0", "method": "foo.get",
"params": {"name": "myself"}, "id": "5"},
{"jsonrpc": "2.0", "method": "get_data", "id": "9"}
]""")
# Thankfully, these are in order so testing is pretty simple.
verify_responses = json.loads("""[
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "result": 19, "id": "2"},
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "5"},
{"jsonrpc": "2.0",
"error": {"code": -32600, "message": "Invalid Request."},
"id": null},
{"jsonrpc": "2.0",
"error": {"code": -32601, "message": "Method not found."},
"id": "5"},
{"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
]""")
self.assertTrue(len(requests) == len(verify_requests))
self.assertTrue(len(responses) == len(verify_responses))
responses_by_id = {}
response_i = 0
for i in range(len(requests)):
verify_request = verify_requests[i]
request = requests[i]
response = None
if request.get('method') != 'notify_hello':
req_id = request.get('id')
if verify_request.has_key('id'):
if "id" in verify_request:
verify_request['id'] = req_id
verify_response = verify_responses[response_i]
verify_response['id'] = req_id
@ -227,23 +241,23 @@ class TestCompatibility(unittest.TestCase):
response_i += 1
response = verify_response
self.assertTrue(request == verify_request)
for response in responses:
verify_response = responses_by_id.get(response.get('id'))
if verify_response.has_key('error'):
if "error" in verify_response:
verify_response['error']['message'] = \
response['error']['message']
self.assertTrue(response == verify_response)
def test_batch_notifications(self):
def test_batch_notifications(self):
multicall = MultiCall(self.client)
multicall._notify.notify_sum(1, 2, 4)
multicall._notify.notify_hello(7)
result = multicall()
self.assertTrue(len(result) == 0)
valid_request = json.loads(
'[{"jsonrpc": "2.0", "method": "notify_sum", '+
'"params": [1,2,4]},{"jsonrpc": "2.0", '+
'[{"jsonrpc": "2.0", "method": "notify_sum", ' +
'"params": [1, 2, 4]},{"jsonrpc": "2.0", ' +
'"method": "notify_hello", "params": [7]}]'
)
request = json.loads(history.request)
@ -253,23 +267,24 @@ class TestCompatibility(unittest.TestCase):
valid_req = valid_request[i]
self.assertTrue(req == valid_req)
self.assertTrue(history.response == '')
class InternalTests(unittest.TestCase):
"""
These tests verify that the client and server portions of
"""
These tests verify that the client and server portions of
jsonrpclib talk to each other properly.
"""
"""
client = None
server = None
port = None
def setUp(self):
self.port = PORTS.pop()
self.port = get_port()
self.server = server_set_up(addr=('', self.port))
def get_client(self):
return Server('http://localhost:%d' % self.port)
def get_multicall_client(self):
server = self.get_client()
return MultiCall(server)
@ -278,33 +293,34 @@ class InternalTests(unittest.TestCase):
client = self.get_client()
result = client.ping()
self.assertTrue(result)
def test_single_args(self):
client = self.get_client()
result = client.add(5, 10)
self.assertTrue(result == 15)
def test_single_kwargs(self):
client = self.get_client()
result = client.add(x=5, y=10)
self.assertTrue(result == 15)
def test_single_kwargs_and_args(self):
client = self.get_client()
self.assertRaises(ProtocolError, client.add, (5,), {'y':10})
with self.assertRaises(ProtocolError):
client.add(5, y=10)
def test_single_notify(self):
client = self.get_client()
result = client._notify.add(5, 10)
self.assertTrue(result == None)
self.assertTrue(result is None)
def test_single_namespace(self):
client = self.get_client()
response = client.namespace.sum(1,2,4)
response = client.namespace.sum(1, 2, 4)
request = json.loads(history.request)
response = json.loads(history.response)
verify_request = {
"jsonrpc": "2.0", "params": [1, 2, 4],
"jsonrpc": "2.0", "params": [1, 2, 4],
"id": "5", "method": "namespace.sum"
}
verify_response = {
@ -314,25 +330,18 @@ class InternalTests(unittest.TestCase):
verify_response['id'] = request['id']
self.assertTrue(verify_request == request)
self.assertTrue(verify_response == response)
def test_multicall_success(self):
multicall = self.get_multicall_client()
multicall.ping()
multicall.add(5, 10)
multicall.namespace.sum([5, 10, 15])
multicall.namespace.sum(5, 10, 15)
correct = [True, 15, 30]
i = 0
for result in multicall():
self.assertTrue(result == correct[i])
i += 1
def test_multicall_success(self):
multicall = self.get_multicall_client()
for i in range(3):
multicall.add(5, i)
result = multicall()
self.assertTrue(result[2] == 7)
def test_multicall_failure(self):
multicall = self.get_multicall_client()
multicall.ping()
@ -345,87 +354,96 @@ class InternalTests(unittest.TestCase):
else:
def func():
return result[i]
self.assertRaises(raises[i], func)
with self.assertRaises(raises[i]):
func()
if jsonrpc.USE_UNIX_SOCKETS:
# We won't do these tests unless Unix Sockets are supported
@unittest.skip("Skipping Unix socket tests right now.")
class UnixSocketInternalTests(InternalTests):
"""
These tests run the same internal communication tests,
These tests run the same internal communication tests,
but over a Unix socket instead of a TCP socket.
"""
def setUp(self):
suffix = "%d.sock" % PORTS.pop()
# Open to safer, alternative processes
suffix = "%d.sock" % get_port()
# Open to safer, alternative processes
# for getting a temp file name...
temp = tempfile.NamedTemporaryFile(
suffix=suffix
)
self.port = temp.name
temp.close()
self.server = server_set_up(
addr=self.port,
addr=self.port,
address_family=socket.AF_UNIX
)
def get_client(self):
return Server('unix:/%s' % self.port)
def tearDown(self):
""" Removes the tempory socket file """
os.unlink(self.port)
class UnixSocketErrorTests(unittest.TestCase):
"""
Simply tests that the proper exceptions fire if
"""
Simply tests that the proper exceptions fire if
Unix sockets are attempted to be used on a platform
that doesn't support them.
"""
def setUp(self):
self.original_value = jsonrpc.USE_UNIX_SOCKETS
if (jsonrpc.USE_UNIX_SOCKETS):
jsonrpc.USE_UNIX_SOCKETS = False
def test_client(self):
address = "unix://shouldnt/work.sock"
self.assertRaises(
jsonrpc.UnixSocketMissing,
Server,
address
)
with self.assertRaises(jsonrpc.UnixSocketMissing):
Server(address)
def tearDown(self):
jsonrpc.USE_UNIX_SOCKETS = self.original_value
""" Test Methods """
def subtract(minuend, subtrahend):
""" Using the keywords from the JSON-RPC v2 doc """
return minuend-subtrahend
def add(x, y):
return x + y
def update(*args):
return args
def summation(*args):
return sum(args)
def notify_hello(*args):
return args
def get_data():
return ['hello', 5]
def ping():
return True
def server_set_up(addr, address_family=socket.AF_INET):
# Not sure this is a good idea to spin up a new server thread
# for each test... but it seems to work fine.
@ -447,10 +465,3 @@ def server_set_up(addr, address_family=socket.AF_INET):
server_proc.daemon = True
server_proc.start()
return server_proc
if __name__ == '__main__':
print "==============================================================="
print " NOTE: There may be threading exceptions after tests finish. "
print "==============================================================="
time.sleep(2)
unittest.main()