diff options
author | efokschaner <efokschaner@riotgames.com> | 2015-10-08 15:08:04 -0700 |
---|---|---|
committer | efokschaner <efokschaner@riotgames.com> | 2015-10-08 15:08:04 -0700 |
commit | b6b93baaed2cd49eb3e850655e5cc1cf6764cf11 (patch) | |
tree | eea385cc02ea5c9dd739dfc0a59b4d7b211b7e25 | |
parent | 812a6834eec8b8286f4312b3fb8f748da714329d (diff) | |
parent | 3058181be3481ad7b3e4ae9646d357e57420f933 (diff) | |
download | jsonrpclib-b6b93baaed2cd49eb3e850655e5cc1cf6764cf11.tar.gz |
Merge upstream master into master
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .travis.yml | 18 | ||||
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | dev-requirements.txt | 6 | ||||
-rw-r--r-- | jsonrpclib/SimpleJSONRPCServer.py | 47 | ||||
-rw-r--r-- | jsonrpclib/config.py | 8 | ||||
-rw-r--r-- | jsonrpclib/history.py | 6 | ||||
-rw-r--r-- | jsonrpclib/jsonclass.py | 31 | ||||
-rw-r--r-- | jsonrpclib/jsonrpc.py | 155 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | tests.py | 279 |
11 files changed, 321 insertions, 237 deletions
@@ -1,3 +1,4 @@ *.pyc build/* dist/* +.coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c26fcd2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +sudo: false +python: +- '2.7' +- '2.6' +install: +- pip install -r dev-requirements.txt +- pip install twine +script: nosetests tests.py --with-coverage --cover-package=jsonrpclib +deploy: + provider: pypi + user: joshmarshall + password: + secure: RbRcj7YdDXE9U2/a9yg4DW9UXFfrGWrM+S8uE8RgYu1D9njDDzUyNcFygaBXd8rxd8GpwRXHzSAO31B/Rk4NVbbM7JtcIA/52jx5j+4DdmEhffnzvahDkCZT6EV5iS3IxlShbuxgbzp3Qz14jF7Kl9iBSCOlIFItLCDlK7rfyJU= + on: + tags: true + repo: joshmarshall/jsonrpclib + branch: master @@ -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 ---- diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..33be765 --- /dev/null +++ b/dev-requirements.txt @@ -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 diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index d76da73..3a0a3bb 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -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): diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 4d28f1b..ca926ca 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -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: diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index d11863d..f052baa 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -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) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 1d86d5f..4326f28 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -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.') diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 3812b34..167bcd7 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -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,24 +153,31 @@ class JSONTarget(object): def close(self): return ''.join(self.data) + class Transport(TransportMixIn, XMLTransport): - pass + def __init__(self): + TransportMixIn.__init__(self) + XMLTransport.__init__(self) + class SafeTransport(TransportMixIn, XMLSafeTransport): - pass + def __init__(self): + TransportMixIn.__init__(self) + XMLSafeTransport.__init__(self) + from httplib import HTTP, HTTPConnection 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) @@ -174,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: @@ -205,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': @@ -241,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 @@ -265,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: @@ -287,17 +301,20 @@ class _Method(XML_Method): def __dir__(self): return self.__dict__.keys() + class _Notify(object): def __init__(self, request): self._request = request 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 +335,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 +352,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 +371,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 +382,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 +401,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 +429,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 +465,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 +478,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 +495,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 +508,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 +524,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 +541,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 +585,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 @@ -17,7 +17,7 @@ import distutils.core distutils.core.setup( name = "jsonrpclib", - version = "0.1.3", + version = "0.1.4", packages = ["jsonrpclib"], author = "Josh Marshall", author_email = "catchjosh@gmail.com", @@ -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() |