From 22bd092c767b971b559d3527e0147534690f4acd Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 09:43:57 +0200 Subject: [PATCH 01/58] Code style update - Remove spaces, ... --- jsonrpclib/SimpleJSONRPCServer.py | 20 ++++++------ jsonrpclib/config.py | 4 +-- jsonrpclib/history.py | 4 +-- jsonrpclib/jsonclass.py | 10 +++--- jsonrpclib/jsonrpc.py | 52 +++++++++++++++---------------- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index d76da73..da0f9eb 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -22,18 +22,18 @@ 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: fault = Fault( - -32600, 'Request must be {}, not %s.' % type(request) + -32600, 'Request must be {}, not %s.' % type(request) ) return fault rpcid = request.get('id', None) 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') @@ -41,7 +41,7 @@ def validate_request(request): if not method or type(method) not in types.StringTypes or \ type(params) not in param_types: fault = Fault( - -32600, 'Invalid request parameters or method.', rpcid=rpcid + -32600, 'Invalid request parameters or method.', rpcid=rpcid ) return fault return True @@ -53,7 +53,7 @@ def __init__(self, encoding=None): 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) @@ -79,7 +79,7 @@ def _marshaled_dispatch(self, data, dispatch_method = None): response = '[%s]' % ','.join(responses) else: response = '' - else: + else: result = validate_request(request) if type(result) is Fault: return result.response() @@ -142,7 +142,7 @@ def _dispatch(self, method, params): 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: @@ -150,13 +150,13 @@ def _dispatch(self, method, params): class SimpleJSONRPCRequestHandler( SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): - + def do_POST(self): if not self.is_rpc_path_valid(): self.report_404() return try: - max_chunk_size = 10*1024*1024 + max_chunk_size = 10 * 1024 * 1024 size_remaining = int(self.headers["content-length"]) L = [] while size_remaining: @@ -198,7 +198,7 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, # 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: diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 4d28f1b..ec0c7da 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -15,7 +15,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 +30,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..8862100 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -8,7 +8,7 @@ class History(object): requests = [] responses = [] _instance = None - + @classmethod def instance(cls): if not cls._instance: @@ -17,7 +17,7 @@ def instance(cls): 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 298c3da..dc89434 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -27,7 +27,7 @@ types.NoneType ] -supported_types = iter_types+string_types+numeric_types+value_types +supported_types = iter_types + string_types + numeric_types + value_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' class TranslationError(Exception): @@ -40,7 +40,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): ignore_attribute = config.ignore_attribute obj_type = type(obj) # Parse / return default "types"... - if obj_type in numeric_types+string_types+value_types: + if obj_type in numeric_types + string_types + value_types: return obj if obj_type in iter_types: if obj_type in (types.ListType, types.TupleType): @@ -64,7 +64,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) @@ -79,7 +79,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): # parameters passed to __init__ return_obj['__jsonclass__'].append([]) attrs = {} - ignore_list = getattr(obj, ignore_attribute, [])+ignore + ignore_list = getattr(obj, ignore_attribute, []) + ignore for attr_name, attr_value in obj.__dict__.iteritems(): if type(attr_value) in supported_types and \ attr_name not in ignore_list and \ @@ -90,7 +90,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): 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: return_list = [] diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index e11939a..72e86be 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -78,7 +78,7 @@ 'module(s) available.' ) -IDCHARS = string.ascii_lowercase+string.digits +IDCHARS = string.ascii_lowercase + string.digits class UnixSocketMissing(Exception): """ @@ -87,7 +87,7 @@ class UnixSocketMissing(Exception): """ pass -#JSON Abstractions +# JSON Abstractions def jdumps(obj, encoding='utf-8'): # Do 'serialize' test at some point for other classes @@ -157,14 +157,14 @@ class SafeTransport(TransportMixIn, XMLSafeTransport): 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,14 +179,14 @@ def make_connection(self, host): 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 +205,7 @@ def __init__(self, uri, transport=None, encoding=None, 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 +241,13 @@ def _run_request(self, request, notify=None): 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,7 +265,7 @@ def _notify(self): class _Method(XML_Method): - + def __call__(self, *args, **kwargs): if len(args) > 0 and len(kwargs) > 0: raise ProtocolError('Cannot use both positional ' + @@ -280,7 +280,7 @@ def __getattr__(self, name): 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): @@ -288,11 +288,11 @@ def __init__(self, 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 = [] @@ -313,14 +313,14 @@ def request(self, encoding=None, rpcid=None): 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 @@ -330,7 +330,7 @@ def __getattr__(self, name): return new_job class MultiCallIterator(object): - + def __init__(self, results): self.results = results @@ -348,7 +348,7 @@ def __len__(self): return len(self.results) class MultiCall(object): - + def __init__(self, server): self._server = server self._job_list = [] @@ -376,13 +376,13 @@ def __getattr__(self, name): __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): + def __init__(self, code= -32000, message='Server error', rpcid=None): self.faultCode = code self.faultString = message self.rpcid = rpcid @@ -414,7 +414,7 @@ def __init__(self, rpcid=None, version=None): 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.') @@ -443,7 +443,7 @@ def response(self, result=None): response['error'] = None return response - def error(self, code=-32000, message='Server error.'): + def error(self, code= -32000, message='Server error.'): error = self.response() if self.version >= 2: del error['result'] @@ -452,7 +452,7 @@ def error(self, code=-32000, message='Server error.'): 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 @@ -478,7 +478,7 @@ def dumps(params=[], methodname=None, methodresponse=None, 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 '+ + raise ValueError('Method name must be a string, or methodresponse ' + 'must be set to True.') if config.use_jsonclass == True: from jsonrpclib import jsonclass @@ -505,7 +505,7 @@ 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: From 6092d1c0fc498ee7efadd163b1c2bb18db81f460 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 09:47:24 +0200 Subject: [PATCH 02/58] SimpleJSONRPCDispatcher accepts a custom dispatch_method The dispatch_method given to _marshaled_dispatch and _marshaled_single_dispatch is now used instead of _dispatch, if given. --- jsonrpclib/SimpleJSONRPCServer.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index da0f9eb..93261cb 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -72,7 +72,8 @@ def _marshaled_dispatch(self, data, dispatch_method=None): if type(result) is Fault: responses.append(result.response()) continue - resp_entry = self._marshaled_single_dispatch(req_entry) + resp_entry = self._marshaled_single_dispatch(req_entry, + dispatch_method) if resp_entry is not None: responses.append(resp_entry) if len(responses) > 0: @@ -83,10 +84,10 @@ def _marshaled_dispatch(self, data, dispatch_method=None): result = validate_request(request) if type(result) is Fault: return result.response() - response = self._marshaled_single_dispatch(request) + response = self._marshaled_single_dispatch(request, dispatch_method) return response - def _marshaled_single_dispatch(self, request): + def _marshaled_single_dispatch(self, request, dispatch_method=None): # TODO - Use the multiprocessing and skip the response if # it is a notification # Put in support for custom dispatcher here @@ -94,7 +95,10 @@ def _marshaled_single_dispatch(self, request): method = request.get('method') params = request.get('params') try: - response = self._dispatch(method, params) + if dispatch_method is not None: + response = dispatch_method(method, params) + else: + response = self._dispatch(method, params) except: exc_type, exc_value, exc_tb = sys.exc_info() fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) From e8d076250147f5485b8f13f6ac1b8553da30f297 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 09:49:12 +0200 Subject: [PATCH 03/58] Eclipse project files added --- .project | 17 +++++++++++++++++ .pydevproject | 10 ++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .project create mode 100644 .pydevproject diff --git a/.project b/.project new file mode 100644 index 0000000..ed51de6 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + jsonrpclib-patched + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..c0f34ac --- /dev/null +++ b/.pydevproject @@ -0,0 +1,10 @@ + + + + +Default +python 2.7 + +/jsonrpclib-patched + + From db7a7dcf733a57cbfec5f1bbaefd2d2e32217a1f Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 10:03:20 +0200 Subject: [PATCH 04/58] Python 3 compatibility --- jsonrpclib/SimpleJSONRPCServer.py | 115 ++++++++++++++++++++------ jsonrpclib/__init__.py | 2 + jsonrpclib/config.py | 3 + jsonrpclib/history.py | 3 + jsonrpclib/jsonclass.py | 96 +++++++++++++++------- jsonrpclib/jsonrpc.py | 132 +++++++++++++++++++----------- 6 files changed, 251 insertions(+), 100 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 93261cb..755b5f7 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -1,20 +1,85 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- + +# Local modules import jsonrpclib from jsonrpclib import Fault from jsonrpclib.jsonrpc import USE_UNIX_SOCKETS -import SimpleXMLRPCServer -import SocketServer + +# Standard library import socket import logging import os -import types import traceback import sys + try: import fcntl except ImportError: # For Windows fcntl = None +# ------------------------------------------------------------------------------ + +if sys.version_info[0] < 3: + # Python 2 + import SimpleXMLRPCServer as xmlrpcserver + import SocketServer as socketserver + + import types + StringTypes = types.StringTypes + TupleType = types.TupleType + ListType = types.ListType + DictType = types.DictType + + def _to_bytes(string): + """ + Converts the given string into bytes + """ + if type(string) is unicode: + return str(string) + + return string + + def _from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + + return str(data) + +else: + # Python 3 + import xmlrpc.server as xmlrpcserver + import socketserver + + StringTypes = (str,) + TupleType = tuple + ListType = list + DictType = dict + + def _to_bytes(string): + """ + Converts the given string into bytes + """ + if type(string) is bytes: + return string + + return bytes(string, "UTF-8") + + def _from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + + return str(data, "UTF-8") + +# ------------------------------------------------------------------------------ + def get_version(request): # must be a dict if 'jsonrpc' in request.keys(): @@ -24,7 +89,7 @@ def get_version(request): return None def validate_request(request): - if type(request) is not types.DictType: + if type(request) is not DictType: fault = Fault( -32600, 'Request must be {}, not %s.' % type(request) ) @@ -37,8 +102,8 @@ def validate_request(request): 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 \ + param_types = (ListType, DictType, TupleType) + if not method or type(method) not in StringTypes or \ type(params) not in param_types: fault = Fault( -32600, 'Invalid request parameters or method.', rpcid=rpcid @@ -46,10 +111,10 @@ def validate_request(request): return fault return True -class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): +class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher): def __init__(self, encoding=None): - SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, + xmlrpcserver.SimpleXMLRPCDispatcher.__init__(self, allow_none=True, encoding=encoding) @@ -57,14 +122,14 @@ def _marshaled_dispatch(self, data, dispatch_method=None): response = None try: request = jsonrpclib.loads(data) - except Exception, e: + except Exception as e: fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e)) response = fault.response() return response if not request: fault = Fault(-32600, 'Request invalid -- no request data.') return fault.response() - if type(request) is types.ListType: + if type(request) is ListType: # This SHOULD be a batch, by spec responses = [] for req_entry in request: @@ -91,7 +156,7 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): # TODO - Use the multiprocessing and skip the response if # it is a notification # Put in support for custom dispatcher here - # (See SimpleXMLRPCServer._marshaled_dispatch) + # (See xmlrpcserver._marshaled_dispatch) method = request.get('method') params = request.get('params') try: @@ -100,7 +165,7 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): else: response = self._dispatch(method, params) except: - exc_type, exc_value, exc_tb = sys.exc_info() + exc_type, exc_value, _ = 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: @@ -113,7 +178,7 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): ) return response except: - exc_type, exc_value, exc_tb = sys.exc_info() + exc_type, exc_value, _ = sys.exc_info() fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) return fault.response() @@ -127,7 +192,7 @@ def _dispatch(self, method, params): return self.instance._dispatch(method, params) else: try: - func = SimpleXMLRPCServer.resolve_dotted_attribute( + func = xmlrpcserver.resolve_dotted_attribute( self.instance, method, True @@ -136,7 +201,7 @@ def _dispatch(self, method, params): pass if func is not None: try: - if type(params) is types.ListType: + if type(params) is ListType: response = func(*params) else: response = func(**params) @@ -153,7 +218,7 @@ def _dispatch(self, method, params): return Fault(-32601, 'Method %s not supported.' % method) class SimpleJSONRPCRequestHandler( - SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + xmlrpcserver.SimpleXMLRPCRequestHandler): def do_POST(self): if not self.is_rpc_path_valid(): @@ -165,12 +230,12 @@ def do_POST(self): L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) + L.append(_from_bytes(self.rfile.read(chunk_size))) size_remaining -= len(L[-1]) 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]) @@ -181,11 +246,11 @@ def do_POST(self): self.send_header("Content-type", "application/json-rpc") self.send_header("Content-length", str(len(response))) self.end_headers() - self.wfile.write(response) + self.wfile.write(_to_bytes(response)) self.wfile.flush() self.connection.shutdown(1) -class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher): +class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): allow_reuse_address = True @@ -209,9 +274,9 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, logging.warning("Could not unlink socket %s", addr) # if python 2.5 and lower if vi[0] < 3 and vi[1] < 6: - SocketServer.TCPServer.__init__(self, addr, requestHandler) + socketserver.TCPServer.__init__(self, addr, requestHandler) else: - SocketServer.TCPServer.__init__(self, addr, requestHandler, + 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) @@ -225,9 +290,9 @@ def __init__(self, encoding=None): def handle_jsonrpc(self, request_text): response = self._marshaled_dispatch(request_text) - print 'Content-Type: application/json-rpc' - print 'Content-Length: %d' % len(response) - print + sys.stdout.write('Content-Type: application/json-rpc\r\n') + sys.stdout.write('Content-Length: %d\r\n' % len(response)) + sys.stdout.write('\r\n') sys.stdout.write(response) handle_xmlrpc = handle_jsonrpc diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 6e884b8..dd22f82 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -1,3 +1,5 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- from jsonrpclib.config import Config config = Config.instance() from jsonrpclib.history import History diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index ec0c7da..b61b716 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -1,3 +1,6 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- + import sys class LocalClasses(dict): diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index 8862100..58f2fb0 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -1,3 +1,6 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- + class History(object): """ This holds all the response and request objects for a diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index dc89434..87b8fbb 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -1,31 +1,69 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- import types import inspect import re -import traceback +import sys from jsonrpclib import config -iter_types = [ - types.DictType, - types.ListType, - types.TupleType -] +if sys.version_info < 3 : + # Python 2 + StringTypes = types.StringTypes + TupleType = types.TupleType + ListType = types.ListType + DictType = types.DictType -string_types = [ - types.StringType, - types.UnicodeType -] + iter_types = [ + types.DictType, + types.ListType, + types.TupleType + ] -numeric_types = [ - types.IntType, - types.LongType, - types.FloatType -] + string_types = [ + types.StringType, + types.UnicodeType + ] + + numeric_types = [ + types.IntType, + types.LongType, + types.FloatType + ] + + value_types = [ + types.BooleanType, + types.NoneType + ] + +else: + # Python 3 + StringTypes = (str,) + TupleType = tuple + ListType = list + DictType = dict + + iter_types = [ + dict, + list, + tuple + ] + + string_types = [ + bytes, + str + ] + + numeric_types = [ + int, + float + ] + + value_types = [ + bool, + type(None) + ] -value_types = [ - types.BooleanType, - types.NoneType -] supported_types = iter_types + string_types + numeric_types + value_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' @@ -43,18 +81,18 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): if obj_type in numeric_types + string_types + value_types: return obj if obj_type in iter_types: - if obj_type in (types.ListType, types.TupleType): + if obj_type in (ListType, TupleType): new_obj = [] for item in obj: new_obj.append(dump(item, serialize_method, ignore_attribute, ignore)) - if obj_type is types.TupleType: + if obj_type is TupleType: new_obj = tuple(new_obj) return new_obj # It's a dict... else: new_obj = {} - for key, value in obj.iteritems(): + for key, value in obj.items(): new_obj[key] = dump(value, serialize_method, ignore_attribute, ignore) return new_obj @@ -80,7 +118,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return_obj['__jsonclass__'].append([]) attrs = {} ignore_list = getattr(obj, ignore_attribute, []) + ignore - for attr_name, attr_value in obj.__dict__.iteritems(): + for attr_name, attr_value in obj.__dict__.items(): if type(attr_value) in supported_types and \ attr_name not in ignore_list and \ attr_value not in ignore_list: @@ -92,15 +130,15 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): def load(obj): if type(obj) in string_types + numeric_types + value_types: return obj - if type(obj) is types.ListType: + if type(obj) is ListType: return_list = [] for entry in obj: return_list.append(load(entry)) return return_list - # Othewise, it's a dict type + # Otherwise, it's a dict type if '__jsonclass__' not in obj.keys(): return_dict = {} - for key, value in obj.iteritems(): + for key, value in obj.items(): new_value = load(value) return_dict[key] = new_value return return_dict @@ -132,13 +170,13 @@ def load(obj): json_class = getattr(temp_module, json_class_name) # Creating the object... new_obj = None - if type(params) is types.ListType: + if type(params) is ListType: new_obj = json_class(*params) - elif type(params) is types.DictType: + elif type(params) is DictType: new_obj = json_class(**params) else: raise TranslationError('Constructor args must be a dict or list.') - for key, value in obj.iteritems(): + for key, value in obj.items(): if key == '__jsonclass__': continue setattr(new_obj, key, value) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 72e86be..8a5e6a1 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -1,15 +1,17 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- """ -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 +31,7 @@ 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') @@ -46,21 +48,46 @@ 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 +# Standard library +import random +from socket import socket +import string +import sys + +if sys.version_info[0] < 3: + # Python 2 + from httplib import HTTPConnection + from urllib import splittype + from urllib import splithost + 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 types + StringTypes = types.StringTypes + TupleType = types.TupleType + ListType = types.ListType + DictType = types.DictType + +else: + # Python 3 + from http.client import HTTPConnection + from urllib.parse import splittype + from urllib.parse import splithost + from xmlrpc.client import Transport as XMLTransport + from xmlrpc.client import SafeTransport as XMLSafeTransport + from xmlrpc.client import ServerProxy as XMLServerProxy + from xmlrpc.client import _Method as XML_Method + + StringTypes = (str,) + TupleType = tuple + ListType = list + # JSON library importing cjson = None json = None @@ -81,8 +108,8 @@ 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 @@ -95,7 +122,13 @@ def jdumps(obj, encoding='utf-8'): if cjson: return cjson.encode(obj) else: - return json.dumps(obj, encoding=encoding) + try: + # Python 2 (explicit encoding) + return json.dumps(obj, encoding=encoding) + + except: + # Python 3 (no more encoding parameter) + return json.dumps(obj) def jloads(json_string): global cjson @@ -142,6 +175,12 @@ def __init__(self): self.data = [] def feed(self, data): + + try: + data = str(data, "UTF-8") + except: + pass + self.data.append(data) def close(self): @@ -152,8 +191,7 @@ class Transport(TransportMixIn, XMLTransport): class SafeTransport(TransportMixIn, XMLSafeTransport): pass -from httplib import HTTP, HTTPConnection -from socket import socket + USE_UNIX_SOCKETS = False @@ -170,14 +208,14 @@ def connect(self): self.sock = socket(AF_UNIX, SOCK_STREAM) self.sock.connect(self.host) - class UnixHTTP(HTTP): - _connection_class = UnixHTTPConnection + # Incompatible with Python 3.x + # class UnixHTTP(HTTP): + # _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) + host = self.get_host_info(host)[0] + return UnixHTTPConnection(host) class ServerProxy(XMLServerProxy): @@ -188,11 +226,10 @@ class ServerProxy(XMLServerProxy): def __init__(self, uri, transport=None, encoding=None, verbose=0, version=None): - import urllib if not version: version = config.version self.__version = version - schema, uri = urllib.splittype(uri) + schema, uri = splittype(uri) if schema not in ('http', 'https', 'unix'): raise IOError('Unsupported JSON-RPC protocol.') if schema == 'unix': @@ -202,11 +239,11 @@ def __init__(self, uri, transport=None, encoding=None, self.__host = uri self.__handler = '/' else: - self.__host, self.__handler = urllib.splithost(uri) + self.__host, self.__handler = 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': transport = UnixTransport() @@ -276,6 +313,9 @@ def __call__(self, *args, **kwargs): return self.__send(self.__name, kwargs) def __getattr__(self, name): + if name == "__name__": + return self.__name + self.__name = '%s.%s' % (self.__name, name) return self # The old method returned a new instance, but this seemed wasteful. @@ -404,7 +444,7 @@ def __repr__(self): def random_id(length=8): return_id = '' - for i in range(length): + for _ in range(length): return_id += random.choice(IDCHARS) return return_id @@ -416,7 +456,7 @@ def __init__(self, rpcid=None, version=None): self.version = float(version) def request(self, method, params=[]): - if type(method) not in types.StringTypes: + if type(method) not in StringTypes: raise ValueError('Method name must be a string.') if not self.id: self.id = random_id() @@ -455,16 +495,16 @@ def error(self, code= -32000, message='Server error.'): 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: version = config.version - valid_params = (types.TupleType, types.ListType, types.DictType) - if methodname in types.StringTypes and \ + valid_params = (TupleType, ListType, DictType) + if methodname in 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. """ @@ -477,7 +517,7 @@ 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: + if type(methodname) not in StringTypes and methodresponse != True: raise ValueError('Method name must be a string, or methodresponse ' + 'must be set to True.') if config.use_jsonclass == True: @@ -517,7 +557,7 @@ def check_for_errors(result): if not result: # Notification return result - if type(result) is not types.DictType: + if type(result) is not DictType: 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.') @@ -530,11 +570,11 @@ def check_for_errors(result): return result def isbatch(result): - if type(result) not in (types.ListType, types.TupleType): + if type(result) not in (ListType, TupleType): return False if len(result) < 1: return False - if type(result[0]) is not types.DictType: + if type(result[0]) is not DictType: return False if 'jsonrpc' not in result[0].keys(): return False From 6ce7a4a8dc2efd15fabdf124e9f2b42641349daf Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 10:57:51 +0200 Subject: [PATCH 05/58] Removed UNIX sockets code + type correction - No more reference to Unix sockets (maybe they'll come back later) - Fixed a typo in jsonclass, in the Python version comparison --- jsonrpclib/SimpleJSONRPCServer.py | 13 +------ jsonrpclib/__init__.py | 6 +++ jsonrpclib/jsonclass.py | 55 +++++++++++++++------------- jsonrpclib/jsonrpc.py | 61 +++++++------------------------ 4 files changed, 50 insertions(+), 85 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 755b5f7..9d14d27 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -4,12 +4,9 @@ # Local modules import jsonrpclib from jsonrpclib import Fault -from jsonrpclib.jsonrpc import USE_UNIX_SOCKETS # Standard library import socket -import logging -import os import traceback import sys @@ -263,15 +260,7 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, # check Python version and decide on how to call it vi = sys.version_info self.address_family = address_family - if USE_UNIX_SOCKETS and address_family == socket.AF_UNIX: - # 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): - try: - os.unlink(addr) - except OSError: - logging.warning("Could not unlink socket %s", addr) + # if python 2.5 and lower if vi[0] < 3 and vi[1] < 6: socketserver.TCPServer.__init__(self, addr, requestHandler) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index dd22f82..42f216e 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -1,8 +1,14 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- + +# Create a configuration instance from jsonrpclib.config import Config config = Config.instance() + +# Create a history instance from jsonrpclib.history import History history = History.instance() + +# Easy access to utility methods from jsonrpclib.jsonrpc import Server, MultiCall, Fault from jsonrpclib.jsonrpc import ProtocolError, loads, dumps diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 87b8fbb..4cb4cc4 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -1,69 +1,69 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- -import types + +# Local package +from jsonrpclib import config + +# Standard library import inspect import re import sys -from jsonrpclib import config - -if sys.version_info < 3 : +if sys.version_info[0] < 3 : # Python 2 - StringTypes = types.StringTypes + import types TupleType = types.TupleType ListType = types.ListType DictType = types.DictType - iter_types = [ + iter_types = ( types.DictType, types.ListType, types.TupleType - ] + ) - string_types = [ + string_types = ( types.StringType, types.UnicodeType - ] + ) - numeric_types = [ + numeric_types = ( types.IntType, types.LongType, types.FloatType - ] + ) - value_types = [ + value_types = ( types.BooleanType, types.NoneType - ] + ) else: # Python 3 - StringTypes = (str,) TupleType = tuple ListType = list DictType = dict - iter_types = [ + iter_types = ( dict, list, tuple - ] + ) - string_types = [ + string_types = ( bytes, str - ] + ) - numeric_types = [ + numeric_types = ( int, float - ] + ) - value_types = [ + value_types = ( bool, type(None) - ] - + ) supported_types = iter_types + string_types + numeric_types + value_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' @@ -128,20 +128,25 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return return_obj def load(obj): + # Primtive if type(obj) in string_types + numeric_types + value_types: return obj - if type(obj) is ListType: + + # List + elif type(obj) in (ListType, TupleType): return_list = [] for entry in obj: return_list.append(load(entry)) return return_list + # Otherwise, it's a dict type - if '__jsonclass__' not in obj.keys(): + elif '__jsonclass__' not in obj.keys(): return_dict = {} for key, value in obj.items(): new_value = load(value) return_dict[key] = new_value return return_dict + # It's a dict, and it's a __jsonclass__ orig_module_name = obj['__jsonclass__'][0] params = obj['__jsonclass__'][1] diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 8a5e6a1..f91e3b1 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -54,7 +54,6 @@ # Standard library import random -from socket import socket import string import sys @@ -107,13 +106,7 @@ IDCHARS = string.ascii_lowercase + string.digits -class UnixSocketMissing(Exception): - """ - Just a properly named Exception if Unix Sockets usage is - attempted on a platform that doesn't support them (Windows) - """ - pass - +# ------------------------------------------------------------------------------ # JSON Abstractions def jdumps(obj, encoding='utf-8'): @@ -137,7 +130,7 @@ def jloads(json_string): else: return json.loads(json_string) - +# ------------------------------------------------------------------------------ # XMLRPClib re-implementations class ProtocolError(Exception): @@ -192,31 +185,7 @@ class Transport(TransportMixIn, XMLTransport): class SafeTransport(TransportMixIn, XMLSafeTransport): pass - -USE_UNIX_SOCKETS = False - -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) - self.sock.connect(self.host) - - # Incompatible with Python 3.x - # class UnixHTTP(HTTP): - # _connection_class = UnixHTTPConnection - - class UnixTransport(TransportMixIn, XMLTransport): - def make_connection(self, host): - host = self.get_host_info(host)[0] - return UnixHTTPConnection(host) - +# ------------------------------------------------------------------------------ class ServerProxy(XMLServerProxy): """ @@ -230,24 +199,16 @@ def __init__(self, uri, transport=None, encoding=None, version = config.version self.__version = version schema, uri = splittype(uri) - if schema not in ('http', 'https', 'unix'): + if schema not in ('http', 'https'): raise IOError('Unsupported JSON-RPC protocol.') - if schema == 'unix': - if not USE_UNIX_SOCKETS: - # Don't like the "generic" Exception... - raise UnixSocketMissing("Unix sockets not available.") - self.__host = uri + + self.__host, self.__handler = splithost(uri) + if not self.__handler: + # Not sure if this is in the JSON spec? self.__handler = '/' - else: - self.__host, self.__handler = splithost(uri) - if not self.__handler: - # Not sure if this is in the JSON spec? - self.__handler = '/' if transport is None: - if schema == 'unix': - transport = UnixTransport() - elif schema == 'https': + if schema == 'https': transport = SafeTransport() else: transport = Transport() @@ -300,6 +261,7 @@ def _notify(self): # Just like __getattr__, but with notify namespace. return _Notify(self._request_notify) +# ------------------------------------------------------------------------------ class _Method(XML_Method): @@ -329,6 +291,7 @@ def __init__(self, request): def __getattr__(self, name): return _Method(self._request, name) +# ------------------------------------------------------------------------------ # Batch implementation class MultiCallMethod(object): @@ -420,6 +383,8 @@ def __getattr__(self, name): # 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): From 1d510403a318740520441e9b92e98ef0a3d9a753 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 10:59:23 +0200 Subject: [PATCH 06/58] Test package updated - Removed tests about Unix sockets - Corrected a test using bad parameters --- tests.py | 193 +++++++++++++++++++------------------------------------ 1 file changed, 67 insertions(+), 126 deletions(-) diff --git a/tests.py b/tests.py index 3ce1009..418dfc6 100644 --- a/tests.py +++ b/tests.py @@ -19,14 +19,11 @@ * Implement JSONClass, History, Config tests """ -from jsonrpclib import Server, MultiCall, history, config, ProtocolError -from jsonrpclib import jsonrpc +from jsonrpclib import Server, MultiCall, history, ProtocolError from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler import socket -import tempfile import unittest -import os import time try: import json @@ -37,18 +34,18 @@ PORTS = range(8000, 8999) class TestCompatibility(unittest.TestCase): - + client = None port = None server = None - + def setUp(self): self.port = PORTS.pop() 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 +56,7 @@ def test_positional(self): 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 +64,7 @@ def test_positional(self): } 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 +74,8 @@ def test_named(self): 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,7 +83,7 @@ def test_named(self): } 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) @@ -94,12 +91,12 @@ def test_notification(self): 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) request = json.loads(history.request) @@ -108,60 +105,60 @@ def test_non_existent_method(self): "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", '+ \ + 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]' request_obj = json.loads(invalid_request) @@ -170,17 +167,17 @@ def test_nonempty_array(self): 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,7 +185,7 @@ def test_batch(self): 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": "notify_hello", "params": [7]}, @@ -197,7 +194,7 @@ def test_batch(self): {"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"}, @@ -206,13 +203,13 @@ def test_batch(self): {"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] @@ -227,23 +224,23 @@ def test_batch(self): 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'): 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 +250,23 @@ def test_batch_notifications(self): 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 jsonrpclib talk to each other properly. - """ + """ client = None server = None port = None - + def setUp(self): self.port = PORTS.pop() 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 +275,33 @@ def test_connect(self): 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}) - + def test_single_notify(self): client = self.get_client() result = client._notify.add(5, 10) self.assertTrue(result == 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 +311,24 @@ def test_single_namespace(self): 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(): + + for i, result in enumerate(multicall()): self.assertTrue(result == correct[i]) - i += 1 - - def test_multicall_success(self): + + def test_multicall_success_2(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() @@ -346,86 +342,31 @@ def test_multicall_failure(self): def func(): return result[i] self.assertRaises(raises[i], func) - - -if jsonrpc.USE_UNIX_SOCKETS: - # We won't do these tests unless Unix Sockets are supported - - class UnixSocketInternalTests(InternalTests): - """ - 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 - # 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, - 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 - 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 - ) - - 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 - + 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. From d34d523eaad271bda10b5ae5c1aa9792f83c33d3 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 12:08:13 +0200 Subject: [PATCH 07/58] Python 3 compatibility - Second pass Passes all the tests from tests.py in both Python 2 and 3 --- jsonrpclib/SimpleJSONRPCServer.py | 69 +++--------------- jsonrpclib/__init__.py | 1 + jsonrpclib/jsonclass.py | 84 +++++----------------- jsonrpclib/jsonrpc.py | 33 ++++----- jsonrpclib/utils.py | 112 ++++++++++++++++++++++++++++++ tests.py | 12 ++-- 6 files changed, 158 insertions(+), 153 deletions(-) create mode 100644 jsonrpclib/utils.py diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 9d14d27..40ef445 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -3,7 +3,7 @@ # Local modules import jsonrpclib -from jsonrpclib import Fault +from jsonrpclib import Fault, utils # Standard library import socket @@ -23,58 +23,11 @@ import SimpleXMLRPCServer as xmlrpcserver import SocketServer as socketserver - import types - StringTypes = types.StringTypes - TupleType = types.TupleType - ListType = types.ListType - DictType = types.DictType - - def _to_bytes(string): - """ - Converts the given string into bytes - """ - if type(string) is unicode: - return str(string) - - return string - - def _from_bytes(data): - """ - Converts the given bytes into a string - """ - if type(data) is str: - return data - - return str(data) - else: # Python 3 import xmlrpc.server as xmlrpcserver import socketserver - StringTypes = (str,) - TupleType = tuple - ListType = list - DictType = dict - - def _to_bytes(string): - """ - Converts the given string into bytes - """ - if type(string) is bytes: - return string - - return bytes(string, "UTF-8") - - def _from_bytes(data): - """ - Converts the given bytes into a string - """ - if type(data) is str: - return data - - return str(data, "UTF-8") - # ------------------------------------------------------------------------------ def get_version(request): @@ -86,7 +39,7 @@ def get_version(request): return None def validate_request(request): - if type(request) is not DictType: + if type(request) is not utils.DictType: fault = Fault( -32600, 'Request must be {}, not %s.' % type(request) ) @@ -99,12 +52,12 @@ def validate_request(request): request.setdefault('params', []) method = request.get('method', None) params = request.get('params') - param_types = (ListType, DictType, TupleType) - if not method or type(method) not in StringTypes or \ + param_types = (utils.ListType, utils.DictType, utils.TupleType) + if not method or type(method) not in utils.StringTypes or \ type(params) not in param_types: - fault = Fault( - -32600, 'Invalid request parameters or method.', rpcid=rpcid - ) + fault = Fault(-32600, + 'Invalid request parameters or method.', + rpcid=rpcid) return fault return True @@ -126,7 +79,7 @@ def _marshaled_dispatch(self, data, dispatch_method=None): if not request: fault = Fault(-32600, 'Request invalid -- no request data.') return fault.response() - if type(request) is ListType: + if type(request) is utils.ListType: # This SHOULD be a batch, by spec responses = [] for req_entry in request: @@ -198,7 +151,7 @@ def _dispatch(self, method, params): pass if func is not None: try: - if type(params) is ListType: + if type(params) is utils.ListType: response = func(*params) else: response = func(**params) @@ -227,7 +180,7 @@ def do_POST(self): L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(_from_bytes(self.rfile.read(chunk_size))) + L.append(utils.from_bytes(self.rfile.read(chunk_size))) size_remaining -= len(L[-1]) data = ''.join(L) response = self.server._marshaled_dispatch(data) @@ -243,7 +196,7 @@ def do_POST(self): self.send_header("Content-type", "application/json-rpc") self.send_header("Content-length", str(len(response))) self.end_headers() - self.wfile.write(_to_bytes(response)) + self.wfile.write(utils.to_bytes(response)) self.wfile.flush() self.connection.shutdown(1) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 42f216e..247fab1 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -12,3 +12,4 @@ # Easy access to utility methods from jsonrpclib.jsonrpc import Server, MultiCall, Fault from jsonrpclib.jsonrpc import ProtocolError, loads, dumps +import jsonrpclib.utils as utils diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 4cb4cc4..778da6f 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -2,70 +2,13 @@ # -- Content-Encoding: UTF-8 -- # Local package -from jsonrpclib import config +from jsonrpclib import config, utils # Standard library import inspect import re -import sys -if sys.version_info[0] < 3 : - # Python 2 - import types - TupleType = types.TupleType - ListType = types.ListType - DictType = types.DictType - - iter_types = ( - types.DictType, - types.ListType, - types.TupleType - ) - - string_types = ( - types.StringType, - types.UnicodeType - ) - - numeric_types = ( - types.IntType, - types.LongType, - types.FloatType - ) - - value_types = ( - types.BooleanType, - types.NoneType - ) - -else: - # Python 3 - TupleType = tuple - ListType = list - DictType = dict - - iter_types = ( - dict, - list, - tuple - ) - - string_types = ( - bytes, - str - ) - - numeric_types = ( - int, - float - ) - - value_types = ( - bool, - type(None) - ) - -supported_types = iter_types + string_types + numeric_types + value_types +supported_types = utils.iter_types + utils.primitive_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' class TranslationError(Exception): @@ -78,17 +21,21 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): ignore_attribute = config.ignore_attribute obj_type = type(obj) # Parse / return default "types"... - if obj_type in numeric_types + string_types + value_types: + # Primitive + if obj_type in utils.primitive_types: return obj - if obj_type in iter_types: - if obj_type in (ListType, TupleType): + + # Iterative + if obj_type in utils.iter_types: + if obj_type in (utils.ListType, utils.TupleType): new_obj = [] for item in obj: new_obj.append(dump(item, serialize_method, ignore_attribute, ignore)) - if obj_type is TupleType: + if obj_type is utils.TupleType: new_obj = tuple(new_obj) return new_obj + # It's a dict... else: new_obj = {} @@ -96,6 +43,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, 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__ class_name = obj.__class__.__name__ @@ -128,12 +76,12 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return return_obj def load(obj): - # Primtive - if type(obj) in string_types + numeric_types + value_types: + # Primitive + if type(obj) in utils.primitive_types: return obj # List - elif type(obj) in (ListType, TupleType): + elif type(obj) in (utils.ListType, utils.TupleType): return_list = [] for entry in obj: return_list.append(load(entry)) @@ -175,9 +123,9 @@ def load(obj): json_class = getattr(temp_module, json_class_name) # Creating the object... new_obj = None - if type(params) is ListType: + if type(params) is utils.ListType: new_obj = json_class(*params) - elif type(params) is DictType: + elif type(params) is utils.DictType: 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 f91e3b1..08897f7 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -49,8 +49,7 @@ """ # Library includes -from jsonrpclib import config -from jsonrpclib import history +from jsonrpclib import config, history, utils # Standard library import random @@ -67,12 +66,6 @@ from xmlrpclib import ServerProxy as XMLServerProxy from xmlrpclib import _Method as XML_Method - import types - StringTypes = types.StringTypes - TupleType = types.TupleType - ListType = types.ListType - DictType = types.DictType - else: # Python 3 from http.client import HTTPConnection @@ -83,10 +76,6 @@ from xmlrpc.client import ServerProxy as XMLServerProxy from xmlrpc.client import _Method as XML_Method - StringTypes = (str,) - TupleType = tuple - ListType = list - # JSON library importing cjson = None json = None @@ -143,6 +132,9 @@ class TransportMixIn(object): _connection = None def send_content(self, connection, request_body): + # Convert the body first + request_body = utils.to_bytes(request_body) + connection.putheader("Content-Type", "application/json-rpc") connection.putheader("Content-Length", str(len(request_body))) connection.endheaders() @@ -168,9 +160,8 @@ def __init__(self): self.data = [] def feed(self, data): - try: - data = str(data, "UTF-8") + data = utils.from_bytes(data) except: pass @@ -421,7 +412,7 @@ def __init__(self, rpcid=None, version=None): self.version = float(version) def request(self, method, params=[]): - if type(method) not in StringTypes: + if type(method) not in utils.StringTypes: raise ValueError('Method name must be a string.') if not self.id: self.id = random_id() @@ -465,8 +456,8 @@ def dumps(params=[], methodname=None, methodresponse=None, """ if not version: version = config.version - valid_params = (TupleType, ListType, DictType) - if methodname in StringTypes and \ + valid_params = (utils.TupleType, utils.ListType, utils.DictType) + if methodname in utils.StringTypes and \ type(params) not in valid_params and \ not isinstance(params, Fault): """ @@ -482,7 +473,7 @@ 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 StringTypes and methodresponse != True: + if type(methodname) not in utils.StringTypes and methodresponse != True: raise ValueError('Method name must be a string, or methodresponse ' + 'must be set to True.') if config.use_jsonclass == True: @@ -522,7 +513,7 @@ def check_for_errors(result): if not result: # Notification return result - if type(result) is not DictType: + if type(result) is not utils.DictType: 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.') @@ -535,11 +526,11 @@ def check_for_errors(result): return result def isbatch(result): - if type(result) not in (ListType, TupleType): + if type(result) not in (utils.ListType, utils.TupleType): return False if len(result) < 1: return False - if type(result[0]) is not DictType: + if type(result[0]) is not utils.DictType: return False if 'jsonrpc' not in result[0].keys(): return False diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py new file mode 100644 index 0000000..6ef65a4 --- /dev/null +++ b/jsonrpclib/utils.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +Utility methods, for compatibility between Python version + +:author: Thomas Calmant +:version: 1.0.0 +""" + +import sys + +# ------------------------------------------------------------------------------ + +if sys.version_info[0] < 3: + # Python 2 + import types + StringTypes = types.StringTypes + TupleType = types.TupleType + ListType = types.ListType + DictType = types.DictType + + iter_types = ( + types.DictType, + types.ListType, + types.TupleType + ) + + string_types = ( + types.StringType, + types.UnicodeType + ) + + numeric_types = ( + types.IntType, + types.LongType, + types.FloatType + ) + + value_types = ( + types.BooleanType, + types.NoneType + ) + + def to_bytes(string): + """ + Converts the given string into bytes + """ + if type(string) is unicode: + return str(string) + + return string + + def from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + + return str(data) + +# ------------------------------------------------------------------------------ + +else: + # Python 3 + StringTypes = (str,) + TupleType = tuple + ListType = list + DictType = dict + + iter_types = ( + dict, + list, + tuple + ) + + string_types = ( + bytes, + str + ) + + numeric_types = ( + int, + float + ) + + value_types = ( + bool, + type(None) + ) + + def to_bytes(string): + """ + Converts the given string into bytes + """ + if type(string) is bytes: + return string + + return bytes(string, "UTF-8") + + def from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + + return str(data, "UTF-8") + +# ------------------------------------------------------------------------------ +# Common +primitive_types = string_types + numeric_types + value_types diff --git a/tests.py b/tests.py index 418dfc6..d559b1d 100644 --- a/tests.py +++ b/tests.py @@ -31,7 +31,7 @@ import simplejson as json from threading import Thread -PORTS = range(8000, 8999) +PORTS = list(range(8000, 8999)) class TestCompatibility(unittest.TestCase): @@ -216,7 +216,7 @@ def test_batch(self): 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,7 +227,7 @@ def test_batch(self): 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) @@ -390,8 +390,8 @@ def log_request(self, *args, **kwargs): return server_proc if __name__ == '__main__': - print "===============================================================" - print " NOTE: There may be threading exceptions after tests finish. " - print "===============================================================" + print("===============================================================") + print(" NOTE: There may be threading exceptions after tests finish. ") + print("===============================================================") time.sleep(2) unittest.main() From 3ddaca6dcfe02af484d29c0987df224254a428b8 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 12:48:59 +0200 Subject: [PATCH 08/58] Request IDs generated by UUID + code enhancement - random_id() method replaced by str(uuid.uuid4()) => ensures ID uniqueness - JSON utility methods (jdumps, jloads) are defined according to what can be imported, instead of doing a test inside a single definition --- jsonrpclib/SimpleJSONRPCServer.py | 16 ++++---- jsonrpclib/jsonrpc.py | 67 +++++++++++++------------------ 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 40ef445..d235ed8 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -65,15 +65,15 @@ class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher): def __init__(self, encoding=None): xmlrpcserver.SimpleXMLRPCDispatcher.__init__(self, - allow_none=True, - encoding=encoding) + allow_none=True, + encoding=encoding) def _marshaled_dispatch(self, data, dispatch_method=None): response = None try: request = jsonrpclib.loads(data) - except Exception as e: - fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e)) + except Exception as ex: + fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, ex)) response = fault.response() return response if not request: @@ -177,12 +177,12 @@ def do_POST(self): try: max_chunk_size = 10 * 1024 * 1024 size_remaining = int(self.headers["content-length"]) - L = [] + chunks = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(utils.from_bytes(self.rfile.read(chunk_size))) - size_remaining -= len(L[-1]) - data = ''.join(L) + chunks.append(utils.from_bytes(self.rfile.read(chunk_size))) + size_remaining -= len(chunks[-1]) + data = ''.join(chunks) response = self.server._marshaled_dispatch(data) self.send_response(200) except Exception: diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 08897f7..0140c2a 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -52,13 +52,11 @@ from jsonrpclib import config, history, utils # Standard library -import random -import string import sys +import uuid if sys.version_info[0] < 3: # Python 2 - from httplib import HTTPConnection from urllib import splittype from urllib import splithost from xmlrpclib import Transport as XMLTransport @@ -68,7 +66,6 @@ else: # Python 3 - from http.client import HTTPConnection from urllib.parse import splittype from urllib.parse import splithost from xmlrpc.client import Transport as XMLTransport @@ -76,47 +73,43 @@ from xmlrpc.client import ServerProxy as XMLServerProxy from xmlrpc.client import _Method as XML_Method -# JSON library importing -cjson = None -json = None +# ------------------------------------------------------------------------------ +# JSON library import try: + # Using cjson import cjson + + # Declare cjson methods + def jdumps(obj, encoding='utf-8'): + return cjson.encode(obj) + + def jloads(json_string): + return cjson.decode(json_string) + except ImportError: + # Use json or simplejson try: import json except ImportError: try: import simplejson as json except ImportError: - raise ImportError( - 'You must have the cjson, json, or simplejson ' + - 'module(s) available.' - ) - -IDCHARS = string.ascii_lowercase + string.digits + raise ImportError('You must have the cjson, json, or simplejson ' \ + 'module(s) available.') -# ------------------------------------------------------------------------------ -# JSON Abstractions - -def jdumps(obj, encoding='utf-8'): - # Do 'serialize' test at some point for other classes - global cjson - if cjson: - return cjson.encode(obj) - else: - try: + # Declare json methods + if sys.version_info[0] < 3: + def jdumps(obj, encoding='utf-8'): # Python 2 (explicit encoding) return json.dumps(obj, encoding=encoding) - except: - # Python 3 (no more encoding parameter) + else: + # Python 3 + def jdumps(obj, encoding='utf-8'): + # Python 3 (the encoding parameter has been removed) return json.dumps(obj) -def jloads(json_string): - global cjson - if cjson: - return cjson.decode(json_string) - else: + def jloads(json_string): return json.loads(json_string) # ------------------------------------------------------------------------------ @@ -398,14 +391,9 @@ def response(self, rpcid=None, version=None): def __repr__(self): return '' % (self.faultCode, self.faultString) -def random_id(length=8): - return_id = '' - for _ in range(length): - return_id += random.choice(IDCHARS) - return return_id - class Payload(dict): def __init__(self, rpcid=None, version=None): + dict.__init__(self) if not version: version = config.version self.id = rpcid @@ -414,8 +402,11 @@ def __init__(self, rpcid=None, version=None): def request(self, method, params=[]): if type(method) not in utils.StringTypes: raise ValueError('Method name must be a string.') + if not self.id: - self.id = random_id() + # Generate a request ID + self.id = str(uuid.uuid4()) + request = { 'id':self.id, 'method':method } if params: request['params'] = params @@ -449,7 +440,7 @@ def error(self, code= -32000, message='Server error.'): return error def dumps(params=[], methodname=None, methodresponse=None, - encoding=None, rpcid=None, version=None, notify=None): + encoding=None, rpcid=None, version=None, notify=None): """ This differs from the Python implementation in that it implements the rpcid argument since the 2.0 spec requires it for responses. From 653a1f8ebaa544840b0e4cc7c4788c2b8fe42393 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 13:06:52 +0200 Subject: [PATCH 09/58] setup.py file updated - New project name: jsonrpclib-pelix (not yet registered) --- setup.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 72f8b35..d33f321 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -- Content-Encoding: UTF-8 -- """ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,17 +14,24 @@ limitations under the License. """ -import distutils.core +try: + from setuptools import setup +except ImportError: + from distutils.core import setup -distutils.core.setup( - name = "jsonrpclib", - version = "0.1.3", - packages = ["jsonrpclib"], - author = "Josh Marshall", - author_email = "catchjosh@gmail.com", - url = "http://github.com/joshmarshall/jsonrpclib/", - license = "http://www.apache.org/licenses/LICENSE-2.0", - description = "This project is an implementation of the JSON-RPC v2.0 " + - "specification (backwards-compatible) as a client library.", - long_description = open("README.md").read() +# ------------------------------------------------------------------------------ + +setup( + name="jsonrpclib-pelix", + version="0.1.4", + packages=["jsonrpclib"], + author="Thomas Calmant", + author_email="thomas.calmant@gmail.com", + url="http://github.com/tcalmant/jsonrpclib/", + license="http://www.apache.org/licenses/LICENSE-2.0", + description="Fork of jsonrpclib by Josh Marshall, usable with Pelix " \ + "remote services." \ + "This project is an implementation of the JSON-RPC v2.0 " \ + "specification (backwards-compatible) as a client library.", + long_description=open("README.md").read() ) From a7993291e63597ae426c1d19608c96fdf9f9c7e2 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 14:44:42 +0200 Subject: [PATCH 10/58] README file updated --- README.md | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 85a157e..09b0733 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,30 @@ -JSONRPClib -========== +JSONRPClib (patched for Pelix) +============================== This library is an implementation of the JSON-RPC specification. It supports both the original 1.0 specification, as well as the -new (proposed) 2.0 spec, which includes batch submission, keyword +new (proposed) 2.0 specification, which includes batch submission, keyword arguments, etc. It is licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html). -Communication -------------- -Feel free to send any questions, comments, or patches to our Google Group -mailing list (you'll need to join to send a message): -http://groups.google.com/group/jsonrpclib +About this version +------------------ +This is a patched version of the original jsonrpclib project by Josh Marshall, +available at https://github.com/joshmarshall/jsonrpclib. + +The suffix *-pelix* only indicates that this version works with Pelix Remote +Services, but it is **not** a Pelix specific implementation. + +* This version adds support for Python 3, staying compatible with Python 2. +* It is now possible to use the dispatch_method argument while extending + the SimpleJSONRPCDispatcher, to use a custom dispatcher. + This allows to use this package by Pelix Remote Services. + +* The support for Unix sockets has been removed, as it is not trivial to convert + to Python 3 (and I don't use them) +* This version cannot be installed with the original jsonrpclib, as it uses the + same package name. Summary ------- @@ -47,14 +59,14 @@ Installation You can install this from PyPI with one of the following commands (sudo may be required): - easy_install jsonrpclib - pip install jsonrpclib + easy_install jsonrpclib-pelix + pip install jsonrpclib-pelix Alternatively, you can download the source from the github repository -at http://github.com/joshmarshall/jsonrpclib and manually install it +at http://github.com/tcalmant/jsonrpclib and manually install it with the following commands: - git clone git://github.com/joshmarshall/jsonrpclib.git + git clone git://github.com/tcalmant/jsonrpclib.git cd jsonrpclib python setup.py install @@ -115,7 +127,12 @@ b.) returns the entire structure of the request / response for manual parsing. SimpleJSONRPCServer ------------------- -This is identical in usage (or should be) to the SimpleXMLRPCServer in the default Python install. Some of the differences in features are that it obviously supports notification, batch calls, class translation (if left on), etc. Note: The import line is slightly different from the regular SimpleXMLRPCServer, since the SimpleJSONRPCServer is distributed within the jsonrpclib library. +This is identical in usage (or should be) to the SimpleXMLRPCServer in the +default Python install. Some of the differences in features are that it +obviously supports notification, batch calls, class translation (if left on), +etc. +Note: The import line is slightly different from the regular SimpleXMLRPCServer, +since the SimpleJSONRPCServer is distributed within the jsonrpclib library. from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer @@ -213,7 +230,3 @@ You can run it with: python tests.py -TODO ----- -* Use HTTP error codes on SimpleJSONRPCServer -* Test, test, test and optimize From 45a10c26e5d77b71e71a18826162e4b39d46a37b Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 15:13:20 +0200 Subject: [PATCH 11/58] Replace Readme.md by Readme.rst + setup.py update --- README.md | 232 ----------------------------------------------- README.rst | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 15 ++- 3 files changed, 273 insertions(+), 235 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index 09b0733..0000000 --- a/README.md +++ /dev/null @@ -1,232 +0,0 @@ -JSONRPClib (patched for Pelix) -============================== -This library is an implementation of the JSON-RPC specification. -It supports both the original 1.0 specification, as well as the -new (proposed) 2.0 specification, which includes batch submission, keyword -arguments, etc. - -It is licensed under the Apache License, Version 2.0 -(http://www.apache.org/licenses/LICENSE-2.0.html). - -About this version ------------------- -This is a patched version of the original jsonrpclib project by Josh Marshall, -available at https://github.com/joshmarshall/jsonrpclib. - -The suffix *-pelix* only indicates that this version works with Pelix Remote -Services, but it is **not** a Pelix specific implementation. - -* This version adds support for Python 3, staying compatible with Python 2. -* It is now possible to use the dispatch_method argument while extending - the SimpleJSONRPCDispatcher, to use a custom dispatcher. - This allows to use this package by Pelix Remote Services. - -* The support for Unix sockets has been removed, as it is not trivial to convert - to Python 3 (and I don't use them) -* This version cannot be installed with the original jsonrpclib, as it uses the - same package name. - -Summary -------- -This library implements the JSON-RPC 2.0 proposed specification in pure Python. -It is designed to be as compatible with the syntax of xmlrpclib as possible -(it extends where possible), so that projects using xmlrpclib could easily be -modified to use JSON and experiment with the differences. - -It is backwards-compatible with the 1.0 specification, and supports all of the -new proposed features of 2.0, including: - -* Batch submission (via MultiCall) -* Keyword arguments -* Notifications (both in a batch and 'normal') -* Class translation using the 'jsonclass' key. - -I've added a "SimpleJSONRPCServer", which is intended to emulate the -"SimpleXMLRPCServer" from the default Python distribution. - -Requirements ------------- -It supports cjson and simplejson, and looks for the parsers in that order -(searching first for cjson, then for the "built-in" simplejson as json in 2.6+, -and then the simplejson external library). One of these must be installed to -use this library, although if you have a standard distribution of 2.6+, you -should already have one. Keep in mind that cjson is supposed to be the -quickest, I believe, so if you are going for full-on optimization you may -want to pick it up. - -Installation ------------- -You can install this from PyPI with one of the following commands (sudo -may be required): - - easy_install jsonrpclib-pelix - pip install jsonrpclib-pelix - -Alternatively, you can download the source from the github repository -at http://github.com/tcalmant/jsonrpclib and manually install it -with the following commands: - - git clone git://github.com/tcalmant/jsonrpclib.git - cd jsonrpclib - python setup.py install - -Client Usage ------------- - -This is (obviously) taken from a console session. - - >>> import jsonrpclib - >>> server = jsonrpclib.Server('http://localhost:8080') - >>> server.add(5,6) - 11 - >>> print jsonrpclib.history.request - {"jsonrpc": "2.0", "params": [5, 6], "id": "gb3c9g37", "method": "add"} - >>> print jsonrpclib.history.response - {'jsonrpc': '2.0', 'result': 11, 'id': 'gb3c9g37'} - >>> server.add(x=5, y=10) - 15 - >>> server._notify.add(5,6) - # No result returned... - >>> batch = jsonrpclib.MultiCall(server) - >>> batch.add(5, 6) - >>> batch.ping({'key':'value'}) - >>> batch._notify.add(4, 30) - >>> results = batch() - >>> for result in results: - >>> ... print result - 11 - {'key': 'value'} - # Note that there are only two responses -- this is according to spec. - -If you need 1.0 functionality, there are a bunch of places you can pass that -in, although the best is just to change the value on -jsonrpclib.config.version: - - >>> import jsonrpclib - >>> jsonrpclib.config.version - 2.0 - >>> jsonrpclib.config.version = 1.0 - >>> server = jsonrpclib.Server('http://localhost:8080') - >>> server.add(7, 10) - 17 - >>> print jsonrpclib..history.request - {"params": [7, 10], "id": "thes7tl2", "method": "add"} - >>> print jsonrpclib.history.response - {'id': 'thes7tl2', 'result': 17, 'error': None} - >>> - -The equivalent loads and dumps functions also exist, although with minor -modifications. The dumps arguments are almost identical, but it adds three -arguments: rpcid for the 'id' key, version to specify the JSON-RPC -compatibility, and notify if it's a request that you want to be a -notification. - -Additionally, the loads method does not return the params and method like -xmlrpclib, but instead a.) parses for errors, raising ProtocolErrors, and -b.) returns the entire structure of the request / response for manual parsing. - -SimpleJSONRPCServer -------------------- -This is identical in usage (or should be) to the SimpleXMLRPCServer in the -default Python install. Some of the differences in features are that it -obviously supports notification, batch calls, class translation (if left on), -etc. -Note: The import line is slightly different from the regular SimpleXMLRPCServer, -since the SimpleJSONRPCServer is distributed within the jsonrpclib library. - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - server = SimpleJSONRPCServer(('localhost', 8080)) - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - server.serve_forever() - -Class Translation ------------------ -I've recently added "automatic" class translation support, although it is -turned off by default. This can be devastatingly slow if improperly used, so -the following is just a short list of things to keep in mind when using it. - -* Keep It (the object) Simple Stupid. (for exceptions, keep reading.) -* Do not require init params (for exceptions, keep reading) -* Getter properties without setters could be dangerous (read: not tested) - -If any of the above are issues, use the _serialize method. (see usage below) -The server and client must BOTH have use_jsonclass configuration item on and -they must both have access to the same libraries used by the objects for -this to work. - -If you have excessively nested arguments, it would be better to turn off the -translation and manually invoke it on specific objects using -jsonrpclib.jsonclass.dump / jsonrpclib.jsonclass.load (since the default -behavior recursively goes through attributes and lists / dicts / tuples). - -[test_obj.py] - - # This object is /very/ simple, and the system will look through the - # attributes and serialize what it can. - class TestObj(object): - foo = 'bar' - - # This object requires __init__ params, so it uses the _serialize method - # and returns a tuple of init params and attribute values (the init params - # can be a dict or a list, but the attribute values must be a dict.) - class TestSerial(object): - foo = 'bar' - def __init__(self, *args): - self.args = args - def _serialize(self): - return (self.args, {'foo':self.foo,}) - -[usage] - - import jsonrpclib - import test_obj - - jsonrpclib.config.use_jsonclass = True - - testobj1 = test_obj.TestObj() - testobj2 = test_obj.TestSerial() - server = jsonrpclib.Server('http://localhost:8080') - # The 'ping' just returns whatever is sent - ping1 = server.ping(testobj1) - ping2 = server.ping(testobj2) - print jsonrpclib.history.request - # {"jsonrpc": "2.0", "params": [{"__jsonclass__": ["test_obj.TestSerial", ["foo"]]}], "id": "a0l976iv", "method": "ping"} - print jsonrpclib.history.result - # {'jsonrpc': '2.0', 'result': , 'id': 'a0l976iv'} - -To turn on this behaviour, just set jsonrpclib.config.use_jsonclass to True. -If you want to use a different method for serialization, just set -jsonrpclib.config.serialize_method to the method name. Finally, if you are -using classes that you have defined in the implementation (as in, not a -separate library), you'll need to add those (on BOTH the server and the -client) using the jsonrpclib.config.classes.add() method. -(Examples forthcoming.) - -Feedback on this "feature" is very, VERY much appreciated. - -Why JSON-RPC? -------------- -In my opinion, there are several reasons to choose JSON over XML for RPC: - -* Much simpler to read (I suppose this is opinion, but I know I'm right. :) -* Size / Bandwidth - Main reason, a JSON object representation is just much smaller. -* Parsing - JSON should be much quicker to parse than XML. -* Easy class passing with jsonclass (when enabled) - -In the interest of being fair, there are also a few reasons to choose XML -over JSON: - -* Your server doesn't do JSON (rather obvious) -* Wider XML-RPC support across APIs (can we change this? :)) -* Libraries are more established, i.e. more stable (Let's change this too.) - -TESTS ------ -I've dropped almost-verbatim tests from the JSON-RPC spec 2.0 page. -You can run it with: - - python tests.py - diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7aceff5 --- /dev/null +++ b/README.rst @@ -0,0 +1,261 @@ +JSONRPClib (patched for Pelix) +############################## + +This library is an implementation of the JSON-RPC specification. +It supports both the original 1.0 specification, as well as the +new (proposed) 2.0 specification, which includes batch submission, keyword +arguments, etc. + +It is licensed under the Apache License, Version 2.0 +(http://www.apache.org/licenses/LICENSE-2.0.html). + + +About this version +****************** + +This is a patched version of the original ``jsonrpclib`` project by +Josh Marshall, available at https://github.com/joshmarshall/jsonrpclib. + +The suffix *-pelix* only indicates that this version works with Pelix Remote +Services, but it is **not** a Pelix specific implementation. + +* This version adds support for Python 3, staying compatible with Python 2. +* It is now possible to use the dispatch_method argument while extending + the SimpleJSONRPCDispatcher, to use a custom dispatcher. + This allows to use this package by Pelix Remote Services. + +* The support for Unix sockets has been removed, as it is not trivial to convert + to Python 3 (and I don't use them) +* This version cannot be installed with the original ``jsonrpclib``, as it uses + the same package name. + + +Summary +******* + +This library implements the JSON-RPC 2.0 proposed specification in pure Python. +It is designed to be as compatible with the syntax of ``xmlrpclib`` as possible +(it extends where possible), so that projects using ``xmlrpclib`` could easily +be modified to use JSON and experiment with the differences. + +It is backwards-compatible with the 1.0 specification, and supports all of the +new proposed features of 2.0, including: + +* Batch submission (via MultiCall) +* Keyword arguments +* Notifications (both in a batch and 'normal') +* Class translation using the ``__jsonclass__`` key. + +I've added a "SimpleJSONRPCServer", which is intended to emulate the +"SimpleXMLRPCServer" from the default Python distribution. + +Requirements +************ + +It supports ``cjson`` and ``simplejson``, and looks for the parsers in that +order (searching first for ``cjson``, then for the *built-in* ``simplejson`` as +``json`` in 2.6+, and then the ``simplejson`` external library). +One of these must be installed to use this library, although if you have a +standard distribution of 2.6+, you should already have one. +Keep in mind that ``cjson`` is supposed to be the quickest, I believe, so if +you are going for full-on optimization you may want to pick it up. + + +Installation +************ + +You can install this from PyPI with one of the following commands (sudo +may be required): + +.. code-block:: console + + easy_install jsonrpclib-pelix + pip install jsonrpclib-pelix + +Alternatively, you can download the source from the github repository +at http://github.com/tcalmant/jsonrpclib and manually install it +with the following commands: + +.. code-block:: console + + git clone git://github.com/tcalmant/jsonrpclib.git + cd jsonrpclib + python setup.py install + + +Client Usage +************ + +This is (obviously) taken from a console session. + +.. code-block:: python + + >>> import jsonrpclib + >>> server = jsonrpclib.Server('http://localhost:8080') + >>> server.add(5,6) + 11 + >>> print jsonrpclib.history.request + {"jsonrpc": "2.0", "params": [5, 6], "id": "gb3c9g37", "method": "add"} + >>> print jsonrpclib.history.response + {'jsonrpc': '2.0', 'result': 11, 'id': 'gb3c9g37'} + >>> server.add(x=5, y=10) + 15 + >>> server._notify.add(5,6) + # No result returned... + >>> batch = jsonrpclib.MultiCall(server) + >>> batch.add(5, 6) + >>> batch.ping({'key':'value'}) + >>> batch._notify.add(4, 30) + >>> results = batch() + >>> for result in results: + >>> ... print result + 11 + {'key': 'value'} + # Note that there are only two responses -- this is according to spec. + +If you need 1.0 functionality, there are a bunch of places you can pass that +in, although the best is just to change the value on +``jsonrpclib``.config.version: + +.. code-block:: python + + >>> import jsonrpclib + >>> jsonrpclib.config.version + 2.0 + >>> jsonrpclib.config.version = 1.0 + >>> server = jsonrpclib.Server('http://localhost:8080') + >>> server.add(7, 10) + 17 + >>> print jsonrpclib..history.request + {"params": [7, 10], "id": "thes7tl2", "method": "add"} + >>> print jsonrpclib.history.response + {'id': 'thes7tl2', 'result': 17, 'error': None} + >>> + +The equivalent loads and dumps functions also exist, although with minor +modifications. The dumps arguments are almost identical, but it adds three +arguments: rpcid for the 'id' key, version to specify the JSON-RPC +compatibility, and notify if it's a request that you want to be a +notification. + +Additionally, the loads method does not return the params and method like +``xmlrpclib``, but instead a.) parses for errors, raising ProtocolErrors, and +b.) returns the entire structure of the request / response for manual parsing. + +SimpleJSONRPCServer +******************* + +This is identical in usage (or should be) to the SimpleXMLRPCServer in the +default Python install. Some of the differences in features are that it +obviously supports notification, batch calls, class translation (if left on), +etc. +Note: The import line is slightly different from the regular SimpleXMLRPCServer, +since the SimpleJSONRPCServer is distributed within the ``jsonrpclib`` library. + +.. code-block:: python + + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + + server = SimpleJSONRPCServer(('localhost', 8080)) + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + server.serve_forever() + + +Class Translation +***************** + +I've recently added "automatic" class translation support, although it is +turned off by default. This can be devastatingly slow if improperly used, so +the following is just a short list of things to keep in mind when using it. + +* Keep It (the object) Simple Stupid. (for exceptions, keep reading.) +* Do not require init params (for exceptions, keep reading) +* Getter properties without setters could be dangerous (read: not tested) + +If any of the above are issues, use the _serialize method. (see usage below) +The server and client must BOTH have use_jsonclass configuration item on and +they must both have access to the same libraries used by the objects for +this to work. + +If you have excessively nested arguments, it would be better to turn off the +translation and manually invoke it on specific objects using +``jsonrpclib.jsonclass.dump`` / ``jsonrpclib.jsonclass.load`` (since the default +behavior recursively goes through attributes and lists / dicts / tuples). + + Sample file: *test_obj.py* + +.. code-block:: python + + # This object is /very/ simple, and the system will look through the + # attributes and serialize what it can. + class TestObj(object): + foo = 'bar' + + # This object requires __init__ params, so it uses the _serialize method + # and returns a tuple of init params and attribute values (the init params + # can be a dict or a list, but the attribute values must be a dict.) + class TestSerial(object): + foo = 'bar' + def __init__(self, *args): + self.args = args + def _serialize(self): + return (self.args, {'foo':self.foo,}) + +* Sample usage + +.. code-block:: python + + import jsonrpclib + import test_obj + + jsonrpclib.config.use_jsonclass = True + + testobj1 = test_obj.TestObj() + testobj2 = test_obj.TestSerial() + server = jsonrpclib.Server('http://localhost:8080') + # The 'ping' just returns whatever is sent + ping1 = server.ping(testobj1) + ping2 = server.ping(testobj2) + print jsonrpclib.history.request + # {"jsonrpc": "2.0", "params": [{"__jsonclass__": ["test_obj.TestSerial", ["foo"]]}], "id": "a0l976iv", "method": "ping"} + print jsonrpclib.history.result + # {'jsonrpc': '2.0', 'result': , 'id': 'a0l976iv'} + +To turn on this behaviour, just set ``jsonrpclib``.config.use_jsonclass to True. +If you want to use a different method for serialization, just set +``jsonrpclib``.config.serialize_method to the method name. Finally, if you are +using classes that you have defined in the implementation (as in, not a +separate library), you'll need to add those (on BOTH the server and the +client) using the ``jsonrpclib``.config.classes.add() method. +(Examples forthcoming.) + +Feedback on this "feature" is very, VERY much appreciated. + +Why JSON-RPC? +************* + +In my opinion, there are several reasons to choose JSON over XML for RPC: + +* Much simpler to read (I suppose this is opinion, but I know I'm right. :) +* Size / Bandwidth - Main reason, a JSON object representation is just much smaller. +* Parsing - JSON should be much quicker to parse than XML. +* Easy class passing with ``jsonclass`` (when enabled) + +In the interest of being fair, there are also a few reasons to choose XML +over JSON: + +* Your server doesn't do JSON (rather obvious) +* Wider XML-RPC support across APIs (can we change this? :)) +* Libraries are more established, i.e. more stable (Let's change this too.) + +TESTS +***** + +I've dropped almost-verbatim tests from the JSON-RPC spec 2.0 page. +You can run it with: + +.. code-block:: console + + python tests.py diff --git a/setup.py b/setup.py index d33f321..5d216a2 100755 --- a/setup.py +++ b/setup.py @@ -24,14 +24,23 @@ setup( name="jsonrpclib-pelix", version="0.1.4", - packages=["jsonrpclib"], + license="http://www.apache.org/licenses/LICENSE-2.0", author="Thomas Calmant", author_email="thomas.calmant@gmail.com", url="http://github.com/tcalmant/jsonrpclib/", - license="http://www.apache.org/licenses/LICENSE-2.0", + download_url='https://github.com/tcalmant/jsonrpclib/archive/master.zip', description="Fork of jsonrpclib by Josh Marshall, usable with Pelix " \ "remote services." \ "This project is an implementation of the JSON-RPC v2.0 " \ "specification (backwards-compatible) as a client library.", - long_description=open("README.md").read() + long_description=open("README.rst").read(), + packages=["jsonrpclib"], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3' + ] ) From d075d88f5b8611d9db96614fa763ac640be3e9b7 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 22 May 2013 17:10:52 +0200 Subject: [PATCH 12/58] Class import from package fixed --- jsonrpclib/jsonclass.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 778da6f..5929dee 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -116,7 +116,8 @@ def load(obj): json_class_name = json_module_parts.pop() json_module_tree = '.'.join(json_module_parts) try: - temp_module = __import__(json_module_tree) + temp_module = __import__(json_module_tree, + fromlist=[json_class_name]) except ImportError: raise TranslationError('Could not import %s from module %s.' % (json_class_name, json_module_tree)) From ad83b6d88e698af8ab5371d0be744ca8231d9bf1 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 15:04:32 +0200 Subject: [PATCH 13/58] jsonclass: Replace "type() is" by isinstance() Allows to use custom dictionaries, ... --- jsonrpclib/jsonclass.py | 111 ++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 5929dee..770134b 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -11,28 +11,39 @@ supported_types = utils.iter_types + utils.primitive_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' + class TranslationError(Exception): pass + def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): + """ + Transforms the given object into a JSON-RPC compliant form. + Converts beans into dictionaries with a __jsonclass__ entry. + Doesn't change primitive types. + + :param obj: An object to convert + :return: A JSON-RPC compliant object + """ if not serialize_method: serialize_method = config.serialize_method + if not ignore_attribute: ignore_attribute = config.ignore_attribute - obj_type = type(obj) + # Parse / return default "types"... # Primitive - if obj_type in utils.primitive_types: + if isinstance(obj, utils.primitive_types): return obj # Iterative - if obj_type in utils.iter_types: - if obj_type in (utils.ListType, utils.TupleType): + if isinstance(obj, utils.iter_types): + if isinstance(obj, (utils.ListType, utils.TupleType)): new_obj = [] for item in obj: new_obj.append(dump(item, serialize_method, ignore_attribute, ignore)) - if obj_type is utils.TupleType: + if isinstance(obj, utils.TupleType): new_obj = tuple(new_obj) return new_obj @@ -48,9 +59,12 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): module_name = inspect.getmodule(obj).__name__ class_name = obj.__class__.__name__ json_class = class_name + if module_name not in ['', '__main__']: - json_class = '%s.%s' % (module_name, json_class) - return_obj = {"__jsonclass__":[json_class, ]} + json_class = '{0}.{1}'.format(module_name, 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) @@ -60,78 +74,99 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return_obj['__jsonclass__'].append(params) return_obj.update(attrs) return return_obj - # Otherwise, try to figure it out - # Obviously, we can't assume to know anything about the - # parameters passed to __init__ - return_obj['__jsonclass__'].append([]) - attrs = {} - ignore_list = getattr(obj, ignore_attribute, []) + ignore - for attr_name, attr_value in obj.__dict__.items(): - 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) - return_obj.update(attrs) - return return_obj + + else: + # Otherwise, try to figure it out + # Obviously, we can't assume to know anything about the + # parameters passed to __init__ + return_obj['__jsonclass__'].append([]) + attrs = {} + ignore_list = getattr(obj, ignore_attribute, []) + ignore + for attr_name, attr_value in obj.__dict__.items(): + 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) + return_obj.update(attrs) + return return_obj + def load(obj): + """ + If 'obj' is a dictionary containing a __jsonclass__ entry, converts the + dictionary item into a bean of this class. + + :param obj: An object from a JSON-RPC dictionary + :return: The loaded object + """ # Primitive - if type(obj) in utils.primitive_types: + if isinstance(obj, utils.primitive_types): return obj # List - elif type(obj) in (utils.ListType, utils.TupleType): - return_list = [] - for entry in obj: - return_list.append(load(entry)) - return return_list + elif isinstance(obj, (utils.ListType, utils.TupleType)): + return [load(entry) for entry in obj] # Otherwise, it's a dict type elif '__jsonclass__' not in obj.keys(): return_dict = {} for key, value in obj.items(): - new_value = load(value) - return_dict[key] = new_value + return_dict[key] = load(value) return return_dict - # It's a dict, and it's a __jsonclass__ + # It's a dict, and it has a __jsonclass__ orig_module_name = obj['__jsonclass__'][0] params = obj['__jsonclass__'][1] - if orig_module_name == '': + + # Validate the module name + if not orig_module_name: raise TranslationError('Module name empty.') + json_module_clean = re.sub(invalid_module_chars, '', orig_module_name) if json_module_clean != orig_module_name: - raise TranslationError('Module name %s has invalid characters.' % - orig_module_name) + raise TranslationError('Module name {0} has invalid characters.' \ + .format(orig_module_name)) + + # Load the class json_module_parts = json_module_clean.split('.') json_class = None if len(json_module_parts) == 1: # Local class name -- probably means it won't work if json_module_parts[0] not in config.classes.keys(): - raise TranslationError('Unknown class or module %s.' % - json_module_parts[0]) + raise TranslationError('Unknown class or module {0}.' \ + .format(json_module_parts[0])) json_class = config.classes[json_module_parts[0]] + else: + # Module + class json_class_name = json_module_parts.pop() json_module_tree = '.'.join(json_module_parts) try: + # Use fromlist to load the module itself, not the package temp_module = __import__(json_module_tree, fromlist=[json_class_name]) except ImportError: raise TranslationError('Could not import %s from module %s.' % (json_class_name, json_module_tree)) json_class = getattr(temp_module, json_class_name) - # Creating the object... + + # Create the object new_obj = None - if type(params) is utils.ListType: + if isinstance(params, utils.ListType): new_obj = json_class(*params) - elif type(params) is utils.DictType: + + if isinstance(params, utils.DictType): new_obj = json_class(**params) + else: raise TranslationError('Constructor args must be a dict or list.') + for key, value in obj.items(): if key == '__jsonclass__': + # Ignore the __jsonclass__ member continue + setattr(new_obj, key, value) + return new_obj From eacf4355b4761e8fa0db61ef2f6621ea5051995a Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 15:05:30 +0200 Subject: [PATCH 14/58] New methods: dump(), load(), Fault.dump() - dump() returns the JSON-RPC dictionary instead of its string representation - dumps() calls dump() - same thing with load() and loads() - Fault.dump() is the dictionary equivalent of response() --- jsonrpclib/jsonrpc.py | 294 +++++++++++++++++++++++++++++++++++------- 1 file changed, 245 insertions(+), 49 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 0140c2a..6b969c6 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -75,6 +75,10 @@ # ------------------------------------------------------------------------------ # JSON library import + +# JSON class serialization +from jsonrpclib import jsonclass + try: # Using cjson import cjson @@ -370,36 +374,96 @@ def __getattr__(self, name): # ------------------------------------------------------------------------------ class Fault(object): - # JSON-RPC error class + """ + JSON-RPC error class + """ def __init__(self, code= -32000, message='Server error', rpcid=None): + """ + Sets up the error description + + :param code: Fault code + :param message: Associated message + :param rpcid: Request ID + """ self.faultCode = code self.faultString = message self.rpcid = rpcid def error(self): + """ + Returns the error as a dictionary + + :returns: A {'code', 'message'} dictionary + """ return {'code':self.faultCode, 'message':self.faultString} def response(self, rpcid=None, version=None): + """ + Returns the error as a JSON-RPC response string + + :param rpcid: Forced request ID + :param version: JSON-RPC version + :return: A JSON-RPC response string + """ if not version: version = config.version + if rpcid: self.rpcid = rpcid - return dumps( - self, methodresponse=True, rpcid=self.rpcid, version=version - ) + + return dumps(self, methodresponse=True, rpcid=self.rpcid, + version=version) + + def dump(self, rpcid=None, version=None): + """ + Returns the error as a JSON-RPC response dictionary + + :param rpcid: Forced request ID + :param version: JSON-RPC version + :return: A JSON-RPC response dictionary + """ + if not version: + version = config.version + + if rpcid: + self.rpcid = rpcid + + return dump(self, is_response=True, rpcid=self.rpcid, + version=version) def __repr__(self): - return '' % (self.faultCode, self.faultString) + """ + String representation + """ + return ''.format(self.faultCode, self.faultString) -class Payload(dict): + +class Payload(object): + """ + JSON-RPC content handler + """ def __init__(self, rpcid=None, version=None): - dict.__init__(self) + """ + Sets up the JSON-RPC handler + + :param rpcid: Request ID + :param version: JSON-RPC version + """ if not version: version = config.version + self.id = rpcid self.version = float(version) + def request(self, method, params=[]): + """ + Prepares a method call request + + :param method: Method name + :param params: Method parameters + :return: A JSON-RPC request dictionary + """ if type(method) not in utils.StringTypes: raise ValueError('Method name must be a string.') @@ -410,27 +474,58 @@ def request(self, method, params=[]): request = { 'id':self.id, 'method':method } if params: request['params'] = params + if self.version >= 2: request['jsonrpc'] = str(self.version) + return request + def notify(self, method, params=[]): + """ + Prepares a notification request + + :param method: Notification name + :param params: Notification parameters + :return: A JSON-RPC notification dictionary + """ + # Prepare the request dictionary request = self.request(method, params) + + # Remove the request ID, as it's a notification if self.version >= 2: del request['id'] else: request['id'] = None + return request + def response(self, result=None): + """ + Prepares a response dictionary + + :param result: The result of method call + :return: A JSON-RPC response dictionary + """ response = {'result':result, 'id':self.id} + if self.version >= 2: response['jsonrpc'] = str(self.version) else: response['error'] = None + return response + def error(self, code= -32000, message='Server error.'): + """ + Prepares an error dictionary + + :param code: Error code + :param message: Error message + :return: A JSON-RPC error dictionary + """ error = self.response() if self.version >= 2: del error['result'] @@ -439,83 +534,175 @@ def error(self, code= -32000, message='Server error.'): error['error'] = {'code':code, 'message':message} return error -def dumps(params=[], methodname=None, methodresponse=None, - encoding=None, rpcid=None, version=None, notify=None): +# ------------------------------------------------------------------------------ + +def dump(params=[], methodname=None, rpcid=None, version=None, + is_response=None, is_notify=None): """ - This differs from the Python implementation in that it implements - the rpcid argument since the 2.0 spec requires it for responses. + Prepares a JSON-RPC dictionary (request, notification, response or error) + + :param params: Method parameters (if a method name is given) or a Fault + :param methodname: Method name + :param rpcid: Request ID + :param version: JSON-RPC version + :param is_response: If True, this is a response dictionary + :param is_notify: If True, this is a notification request + :return: A JSON-RPC dictionary """ + # Default version if not version: version = config.version - valid_params = (utils.TupleType, utils.ListType, utils.DictType) + + # Validate method name and parameters + valid_params = (utils.TupleType, utils.ListType, utils.DictType, Fault) if methodname in utils.StringTypes and \ - type(params) not in valid_params and \ - not isinstance(params, Fault): + not isinstance(params, valid_params): """ If a method, and params are not in a listish or a Fault, error out. """ - raise TypeError('Params must be a dict, list, tuple or Fault ' + - 'instance.') - # Begin parsing object + raise TypeError('Params must be a dict, list, tuple or Fault instance.') + + # Prepares the JSON-RPC content payload = Payload(rpcid=rpcid, version=version) - if not encoding: - encoding = 'utf-8' + if type(params) is Fault: - response = payload.error(params.faultCode, params.faultString) - return jdumps(response, encoding=encoding) - if type(methodname) not in utils.StringTypes and methodresponse != True: - raise ValueError('Method name must be a string, or methodresponse ' + + # Prepare an error dictionary + return payload.error(params.faultCode, params.faultString) + + if type(methodname) not in utils.StringTypes and not is_response: + # Neither a request nor a response + raise ValueError('Method name must be a string, or is_response ' \ 'must be set to True.') - if config.use_jsonclass == True: - from jsonrpclib import jsonclass + + if config.use_jsonclass: + # Use jsonclass to convert the parameters params = jsonclass.dump(params) - if methodresponse is True: + + if is_response: + # Prepare a response dictionary if rpcid is None: + # A response must have a request ID raise ValueError('A method response must have an rpcid.') - response = payload.response(params) - return jdumps(response, encoding=encoding) - request = None - if notify == True: - request = payload.notify(methodname, params) + return payload.response(params) + + if is_notify: + # Prepare a notification dictionary + return payload.notify(methodname, params) + else: - request = payload.request(methodname, params) + # Prepare a method call dictionary + return payload.request(methodname, params) + + +def dumps(params=[], methodname=None, methodresponse=None, + encoding=None, rpcid=None, version=None, notify=None): + """ + Prepares a JSON-RPC request/response string + + :param params: Method parameters (if a method name is given) or a Fault + :param methodname: Method name + :param methodresponse: If True, this is a response dictionary + :param encoding: Result string encoding + :param rpcid: Request ID + :param version: JSON-RPC version + :param notify: If True, this is a notification request + :return: A JSON-RPC dictionary + """ + # Prepare the dictionary + request = dump(params, methodname, rpcid, version, methodresponse, notify) + + # Set the default encoding + if not encoding: + encoding = "UTF-8" + + # Returns it as a JSON string return jdumps(request, encoding=encoding) -def loads(data): + +def load(data): """ - This differs from the Python implementation, in that it returns - the request structure in Dict format instead of the method, params. - It will return a list in the case of a batch request / response. + Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans + + :param data: A JSON-RPC dictionary + :return: A parsed dictionary or None """ - if data == '': - # notification + if data is None: + # Notification return None - result = jloads(data) + # 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: - from jsonrpclib import jsonclass - result = jsonclass.load(result) - return result + if config.use_jsonclass: + # Convert beans + data = jsonclass.load(data) + + return data + + +def loads(data): + """ + Loads a JSON-RPC request/response string. Calls jsonclass to load beans + + :param data: A JSON-RPC string + :return: A parsed dictionary or None + """ + if data == '': + # Notification + return None + + # Parse the JSON dictionary + result = jloads(data) + + # Load the beans + return load(result) + +# ------------------------------------------------------------------------------ def check_for_errors(result): + """ + Checks if a result dictionary signals an error + + :param result: A result dictionary + :raise TypeError: Invalid parameter + :raise NotImplementedError: Unknown JSON-RPC version + :raise ValueError: Invalid dictionary content + :raise ProtocolError: An error occurred on the server side + :return: The result parameter + """ if not result: # Notification return result + if type(result) is not utils.DictType: + # Invalid argument raise TypeError('Response is not a dict.') - if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0: + + if 'jsonrpc' in result and float(result['jsonrpc']) > 2.0: + # Unknown JSON-RPC version raise NotImplementedError('JSON-RPC version not yet supported.') - if 'result' not in result.keys() and 'error' not in result.keys(): + + if 'result' not in result and 'error' not in result: + # Invalid dictionary content raise ValueError('Response does not have a result or error key.') - if 'error' in result.keys() and result['error'] != None: + + if 'error' in result and result['error']: + # Server-side error code = result['error']['code'] - message = result['error']['message'] + try: + # Get the message (jsonrpclib) + message = result['error']['message'] + + except KeyError: + # Get the trace (jabsorb) + message = result['error'].get('trace', '') + raise ProtocolError((code, message)) + return result + def isbatch(result): if type(result) not in (utils.ListType, utils.TupleType): return False @@ -533,11 +720,20 @@ def isbatch(result): return False return True + def isnotification(request): - if 'id' not in request.keys(): + """ + Tests if the given request is a notification + + :param request: A request dictionary + :return: True if the request is a notification + """ + if 'id' not in request: # 2.0 notification return True - if request['id'] == None: + + if request['id'] is None: # 1.0 notification return True + return False From 2b5941f9ab45cab2be1af7238fdfd858e4f92b50 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 15:09:24 +0200 Subject: [PATCH 15/58] New method: SimpleJSONRPCDispatcher._unmarshaled_dispatch - Takes a loaded dictionary as a parameter (jsonrpclib.load/s) - Returns a JSON-RPC dictionary (jsonrpclib.dump) - _marshaled_dispatch wraps _unmarshaled_dispatch (keeps its behaviour) --- jsonrpclib/SimpleJSONRPCServer.py | 311 +++++++++++++++++++++++------- jsonrpclib/__init__.py | 5 +- 2 files changed, 239 insertions(+), 77 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index d235ed8..c1a4b30 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -7,8 +7,8 @@ # Standard library import socket -import traceback import sys +import traceback try: import fcntl @@ -31,150 +31,270 @@ # ------------------------------------------------------------------------------ def get_version(request): - # must be a dict - if 'jsonrpc' in request.keys(): + """ + Computes the JSON-RPC version + + :param request: A request dictionary + :return: The JSON-RPC version or None + """ + if 'jsonrpc' in request: return 2.0 - if 'id' in request.keys(): + + elif 'id' in request: return 1.0 + return None + def validate_request(request): + """ + Validates the format of a request dictionary + + :param request: A request dictionary + :return: True if the dictionary is valid, else a Fault object + """ if type(request) is not utils.DictType: - fault = Fault( - -32600, 'Request must be {}, not %s.' % type(request) - ) - return fault + # Invalid request type + return Fault(-32600, 'Request must be dict, not {0}' \ + .format(type(request).__name__)) + + # Get the request ID rpcid = request.get('id', None) + + # Check request version version = get_version(request) if not version: - fault = Fault(-32600, 'Request %s invalid.' % request, rpcid=rpcid) - return fault + return Fault(-32600, 'Request {0} invalid.'.format(request), + rpcid=rpcid) + + # Default parameters: empty list request.setdefault('params', []) + + # Check parameters method = request.get('method', None) params = request.get('params') param_types = (utils.ListType, utils.DictType, utils.TupleType) + if not method or type(method) not in utils.StringTypes or \ type(params) not in param_types: - fault = Fault(-32600, - 'Invalid request parameters or method.', + # Invalid type of method name or parameters + return Fault(-32600, 'Invalid request parameters or method.', rpcid=rpcid) - return fault + + # Valid request return True +# ------------------------------------------------------------------------------ + class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher): def __init__(self, encoding=None): + """ + Sets up the dispatcher with the given encoding. + None values are allowed. + """ + if not encoding: + # Default encoding + encoding = "UTF-8" + xmlrpcserver.SimpleXMLRPCDispatcher.__init__(self, allow_none=True, encoding=encoding) - def _marshaled_dispatch(self, data, dispatch_method=None): - response = None - try: - request = jsonrpclib.loads(data) - except Exception as ex: - fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, ex)) - response = fault.response() - return response + + def _unmarshaled_dispatch(self, request, dispatch_method=None): + """ + Loads the request dictionary (unmarshaled), calls the method(s) + accordingly and returns a JSON-RPC dictionary (not marshaled) + + :param request: JSON-RPC request dictionary (or list of) + :param dispatch_method: Custom dispatch method (for method resolution) + :return: A JSON-RPC dictionary (or an array of) or an empty string + """ if not request: + # Invalid request dictionary fault = Fault(-32600, 'Request invalid -- no request data.') - return fault.response() + return fault.dump() + + response = None + if type(request) is utils.ListType: # This SHOULD be a batch, by spec - responses = [] + response = [] for req_entry in request: + # Validate the request result = validate_request(req_entry) if type(result) is Fault: - responses.append(result.response()) + response.append(result.dump()) continue + + # Call the method resp_entry = self._marshaled_single_dispatch(req_entry, dispatch_method) - if resp_entry is not None: - responses.append(resp_entry) - if len(responses) > 0: - response = '[%s]' % ','.join(responses) - else: - response = '' + + # Store its result + if isinstance(resp_entry, Fault): + response.append(resp_entry.dump()) + + elif resp_entry is not None: + response.append(resp_entry) + + if len(response) == 0: + # Return an empty string (jsonrpclib internal behaviour) + return '' + else: + # Single call result = validate_request(request) if type(result) is Fault: - return result.response() + return result.dump() + + # Call the method response = self._marshaled_single_dispatch(request, dispatch_method) + + if isinstance(response, Fault): + return response.dump() + return response + + def _marshaled_dispatch(self, data, dispatch_method=None): + """ + Parses the request data (marshaled), calls method(s) and returns a + JSON string (marshaled) + + :param data: A JSON request string + :param dispatch_method: Custom dispatch method (for method resolution) + :return: A JSON-RPC response string (marshaled) + """ + # Parse the request + try: + request = jsonrpclib.loads(data) + + except Exception as ex: + # Parsing/loading error + fault = Fault(-32700, 'Request {0} invalid. ({1})'.format(data, ex)) + return fault.response() + + # Get the response dictionary + response = self._unmarshaled_dispatch(request, dispatch_method) + if response not in (None, ''): + # Returns its string form + return jsonrpclib.jdumps(response, self.encoding) + + else: + return response + + def _marshaled_single_dispatch(self, request, dispatch_method=None): + """ + Dispatches a single method call + + :param request: A validated request dictionary + :param dispatch_method: Custom dispatch method (for method resolution) + :return: A JSON-RPC response dictionary + """ # TODO - Use the multiprocessing and skip the response if # it is a notification - # Put in support for custom dispatcher here - # (See xmlrpcserver._marshaled_dispatch) method = request.get('method') params = request.get('params') try: + # Call the method if dispatch_method is not None: response = dispatch_method(method, params) else: response = self._dispatch(method, params) + except: + # Return a fault exc_type, exc_value, _ = 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: - # It's a notification + fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value)) + return fault.dump() + + if 'id' not in request or not request['id']: + # It's a notification, no result needed return None + + # Prepare a JSON-RPC dictionary try: - response = jsonrpclib.dumps(response, - methodresponse=True, - rpcid=request['id'] - ) - return response + return jsonrpclib.dump(response, rpcid=request['id'], + is_response=True) + except: + # JSON conversion exception exc_type, exc_value, _ = sys.exc_info() - fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) - return fault.response() + fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value)) + return fault.dump() + def _dispatch(self, method, params): + """ + Default method resolver and caller + + :param method: Name of the method to call + :param params: List of arguments to give to the method + :return: The result of the method + """ func = None try: + # Try with registered methods func = self.funcs[method] + except KeyError: if self.instance is not None: + # Try with the registered instance if hasattr(self.instance, '_dispatch'): + # Instance has a custom dispatcher return self.instance._dispatch(method, params) + else: + # Resolve the method name in the instance try: - func = xmlrpcserver.resolve_dotted_attribute( - self.instance, - method, - True - ) + func = xmlrpcserver.resolve_dotted_attribute(\ + self.instance, method, True) except AttributeError: + # Unknown method pass + if func is not None: try: + # Call the method if type(params) is utils.ListType: - response = func(*params) + return func(*params) + else: - response = func(**params) - return response + return func(**params) + except TypeError: + # Maybe the parameters are wrong return Fault(-32602, 'Invalid parameters.') + except: + # Method exception 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) - return fault + trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1]) + return Fault(-32603, 'Server error: {0}'.format(trace_string)) + else: - return Fault(-32601, 'Method %s not supported.' % method) + # Unknown method + return Fault(-32601, 'Method {0} not supported.'.format(method)) -class SimpleJSONRPCRequestHandler( - xmlrpcserver.SimpleXMLRPCRequestHandler): +# ------------------------------------------------------------------------------ +class SimpleJSONRPCRequestHandler(xmlrpcserver.SimpleXMLRPCRequestHandler): + """ + HTTP server request handler + """ def do_POST(self): + """ + Handles POST requests + """ if not self.is_rpc_path_valid(): self.report_404() return + try: + # Read the request body max_chunk_size = 10 * 1024 * 1024 size_remaining = int(self.headers["content-length"]) chunks = [] @@ -183,58 +303,99 @@ def do_POST(self): chunks.append(utils.from_bytes(self.rfile.read(chunk_size))) size_remaining -= len(chunks[-1]) data = ''.join(chunks) + + # Execute the method response = self.server._marshaled_dispatch(data) + + # No exception: send a 200 OK self.send_response(200) + except Exception: + # Exception: send 500 Server Error 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) + trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1]) + fault = jsonrpclib.Fault(-32603, 'Server error: {0}'\ + .format(trace_string)) response = fault.response() - if response == None: + + if response is None: + # Avoid to send None response = '' + + # Convert the response to the valid string format + response = utils.to_bytes(response) + + # Send it self.send_header("Content-type", "application/json-rpc") self.send_header("Content-length", str(len(response))) self.end_headers() - self.wfile.write(utils.to_bytes(response)) + self.wfile.write(response) self.wfile.flush() self.connection.shutdown(1) -class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): +# ------------------------------------------------------------------------------ +class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): + """ + JSON-RPC server (and dispatcher) + """ + # This simplifies server restart after error allow_reuse_address = True def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, logRequests=True, encoding=None, bind_and_activate=True, address_family=socket.AF_INET): - self.logRequests = logRequests + """ + Sets up the server and the dispatcher + + :param addr: The server listening address + :param requestHandler: Custom request handler + :param logRequests: Flag to(de)activate requests logging + :param encoding: The dispatcher request encoding + :param bind_and_activate: If True, starts the server immediately + :param address_family: The server listening address family + """ + # Set up the dispatcher fields SimpleJSONRPCDispatcher.__init__(self, encoding) - # TCPServer.__init__ has an extra parameter on 2.6+, so - # check Python version and decide on how to call it - vi = sys.version_info + + # Prepare the server configuration + self.logRequests = logRequests self.address_family = address_family - # if python 2.5 and lower - if vi[0] < 3 and vi[1] < 6: - socketserver.TCPServer.__init__(self, addr, requestHandler) - else: - socketserver.TCPServer.__init__(self, addr, requestHandler, - bind_and_activate) + # Set up the server + socketserver.TCPServer.__init__(self, addr, requestHandler, + bind_and_activate) + + # Windows-specific 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): +# ------------------------------------------------------------------------------ +class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): + """ + JSON-RPC CGI handler (and dispatcher) + """ def __init__(self, encoding=None): + """ + Sets up the dispatcher + + :param encoding: Dispatcher encoding + """ SimpleJSONRPCDispatcher.__init__(self, encoding) def handle_jsonrpc(self, request_text): + """ + Handle a JSON-RPC request + """ response = self._marshaled_dispatch(request_text) sys.stdout.write('Content-Type: application/json-rpc\r\n') sys.stdout.write('Content-Length: %d\r\n' % len(response)) sys.stdout.write('\r\n') sys.stdout.write(response) + # XML-RPC alias handle_xmlrpc = handle_jsonrpc diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 247fab1..5b83289 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -10,6 +10,7 @@ history = History.instance() # Easy access to utility methods -from jsonrpclib.jsonrpc import Server, MultiCall, Fault -from jsonrpclib.jsonrpc import ProtocolError, loads, dumps +from jsonrpclib.jsonrpc import Server, MultiCall, Fault, ProtocolError +from jsonrpclib.jsonrpc import loads, dumps, load, dump +from jsonrpclib.jsonrpc import jloads, jdumps import jsonrpclib.utils as utils From 77b0a04422fc85218202c1a3ede4a05d13b781a0 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 16:42:58 +0200 Subject: [PATCH 16/58] Replacement of a "type() is" by a "isinstance()" --- jsonrpclib/SimpleJSONRPCServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index c1a4b30..3b686b1 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -53,7 +53,7 @@ def validate_request(request): :param request: A request dictionary :return: True if the dictionary is valid, else a Fault object """ - if type(request) is not utils.DictType: + if not isinstance(request, utils.DictType): # Invalid request type return Fault(-32600, 'Request must be dict, not {0}' \ .format(type(request).__name__)) From 31253a554743373d109b31ac07a34d7ac4132698 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 16:43:20 +0200 Subject: [PATCH 17/58] Ignore Python 3 __pycache__ folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f7d6c1a..2143145 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc +__pycache__ build/* dist/* From 8a28b9be5d9d5129bdbf3f838235c602dfb2f275 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 24 May 2013 17:32:02 +0200 Subject: [PATCH 18/58] Requests with ID 0 (int) are not notifications - Replaced a 'not id' by 'id not in (None, "")' - _unmarshaled_dispatch now throws a NoMulticallResult exception instead of returning '' --- jsonrpclib/SimpleJSONRPCServer.py | 56 ++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 3b686b1..5400e82 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -86,6 +86,13 @@ def validate_request(request): # ------------------------------------------------------------------------------ +class NoMulticallResult(Exception): + """ + No result in multicall + """ + pass + + class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher): def __init__(self, encoding=None): @@ -109,23 +116,23 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): :param request: JSON-RPC request dictionary (or list of) :param dispatch_method: Custom dispatch method (for method resolution) - :return: A JSON-RPC dictionary (or an array of) or an empty string + :return: A JSON-RPC dictionary (or an array of) or None if the request + was a notification + :raise NoMulticallResult: No result in batch """ if not request: # Invalid request dictionary fault = Fault(-32600, 'Request invalid -- no request data.') return fault.dump() - response = None - if type(request) is utils.ListType: # This SHOULD be a batch, by spec - response = [] + responses = [] for req_entry in request: # Validate the request result = validate_request(req_entry) if type(result) is Fault: - response.append(result.dump()) + responses.append(result.dump()) continue # Call the method @@ -134,14 +141,16 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): # Store its result if isinstance(resp_entry, Fault): - response.append(resp_entry.dump()) + responses.append(resp_entry.dump()) elif resp_entry is not None: - response.append(resp_entry) + responses.append(resp_entry) - if len(response) == 0: - # Return an empty string (jsonrpclib internal behaviour) - return '' + if len(responses) == 0: + # No non-None result + raise NoMulticallResult("No result") + + return responses else: # Single call @@ -155,7 +164,7 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): if isinstance(response, Fault): return response.dump() - return response + return response def _marshaled_dispatch(self, data, dispatch_method=None): @@ -177,13 +186,20 @@ def _marshaled_dispatch(self, data, dispatch_method=None): return fault.response() # Get the response dictionary - response = self._unmarshaled_dispatch(request, dispatch_method) - if response not in (None, ''): - # Returns its string form - return jsonrpclib.jdumps(response, self.encoding) + try: + response = self._unmarshaled_dispatch(request, dispatch_method) - else: - return response + if response is not None: + # Compute the string representation of the dictionary/list + return jsonrpclib.jdumps(response, self.encoding) + + else: + # No result (notification) + return '' + + except NoMulticallResult: + # Return an empty string (jsonrpclib internal behaviour) + return '' def _marshaled_single_dispatch(self, request, dispatch_method=None): @@ -192,7 +208,8 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): :param request: A validated request dictionary :param dispatch_method: Custom dispatch method (for method resolution) - :return: A JSON-RPC response dictionary + :return: A JSON-RPC response dictionary, or None if it was a + notification request """ # TODO - Use the multiprocessing and skip the response if # it is a notification @@ -211,8 +228,9 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value)) return fault.dump() - if 'id' not in request or not request['id']: + if 'id' not in request or request['id'] in (None, ''): # It's a notification, no result needed + # Do not use 'not id' as it might be the integer 0 return None # Prepare a JSON-RPC dictionary From 155aaa4fa7c064b32b302aa76bc218b786b8a9ee Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 30 May 2013 16:04:32 +0200 Subject: [PATCH 19/58] Updated gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 2143145..6cff226 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ +# Bytecoded form *.pyc __pycache__ + +# When installed in develop mode +*.egg-info/ + +# setup.py folders build/* dist/* From 0ac1d705b43fc0d6301bf65798fecaddaf174a83 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Tue, 4 Jun 2013 20:08:31 +0200 Subject: [PATCH 20/58] Small code review (added some comments) --- jsonrpclib/SimpleJSONRPCServer.py | 15 ++++---- jsonrpclib/config.py | 25 +++++++++---- jsonrpclib/history.py | 52 ++++++++++++++++++++++----- jsonrpclib/jsonclass.py | 59 ++++++++++++++++++++++--------- 4 files changed, 112 insertions(+), 39 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 5400e82..8e2b7a0 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -55,7 +55,7 @@ def validate_request(request): """ if not isinstance(request, utils.DictType): # Invalid request type - return Fault(-32600, 'Request must be dict, not {0}' \ + return Fault(-32600, 'Request must be a dict, not {0}' \ .format(type(request).__name__)) # Get the request ID @@ -75,11 +75,11 @@ def validate_request(request): params = request.get('params') param_types = (utils.ListType, utils.DictType, utils.TupleType) - if not method or type(method) not in utils.StringTypes or \ - type(params) not in param_types: + if not method or not isinstance(method, utils.StringTypes) or \ + not isinstance(params, param_types): # Invalid type of method name or parameters return Fault(-32600, 'Invalid request parameters or method.', - rpcid=rpcid) + rpcid=rpcid) # Valid request return True @@ -182,7 +182,8 @@ def _marshaled_dispatch(self, data, dispatch_method=None): except Exception as ex: # Parsing/loading error - fault = Fault(-32700, 'Request {0} invalid. ({1})'.format(data, ex)) + fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})' \ + .format(data, type(ex).__name__, ex)) return fault.response() # Get the response dictionary @@ -283,9 +284,9 @@ def _dispatch(self, method, params): else: return func(**params) - except TypeError: + except TypeError as ex: # Maybe the parameters are wrong - return Fault(-32602, 'Invalid parameters.') + return Fault(-32602, 'Invalid parameters: {0}'.format(ex)) except: # Method exception diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index b61b716..59ac0cf 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -14,28 +14,39 @@ class Config(object): You can change serialize_method and ignore_attribute, or use the local_classes.add(class) to include "local" classes. """ - use_jsonclass = True # Change to False to keep __jsonclass__ entries raw. - serialize_method = '_serialize' + use_jsonclass = True + # The serialize_method should be a string that references the # 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' + serialize_method = '_serialize' + # The ignore attribute should be a string that references the # attribute on a custom class object which holds strings and / or # references of the attributes the class translator should ignore. - classes = LocalClasses() + ignore_attribute = '_ignore' + # The list of classes to use for jsonclass translation. - version = 2.0 + classes = LocalClasses() + # Version of the JSON-RPC spec to support - user_agent = 'jsonrpclib/0.1 (Python %s)' % \ - '.'.join([str(ver) for ver in sys.version_info[0:3]]) + version = 2.0 + # User agent to use for calls. + user_agent = 'jsonrpclib-pelix/0.1 (Python {0})' \ + .format('.'.join(str(ver) for ver in sys.version_info[0:3])) + + # "Singleton" of Config _instance = None @classmethod def instance(cls): + """ + Returns/Creates the instance of Config + """ if not cls._instance: cls._instance = cls() + return cls._instance diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index 58f2fb0..0a96605 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -8,36 +8,72 @@ class History(object): each request cycle in order to keep it from clogging memory. """ - requests = [] - responses = [] + # The singleton _instance = None + def __init__(self): + """ + Sets up members + """ + self.requests = [] + self.responses = [] + @classmethod def instance(cls): + """ + Returns/Creates the instance of History + """ if not cls._instance: cls._instance = cls() + return cls._instance def add_response(self, response_obj): + """ + Adds a response to the history + + :param response_obj: Response content + """ self.responses.append(response_obj) def add_request(self, request_obj): + """ + Adds a request to the history + + :param request_obj: A request object + """ self.requests.append(request_obj) @property def request(self): - if len(self.requests) == 0: - return None - else: + """ + Returns the latest stored request or None + + :return: The latest stored request + """ + try: return self.requests[-1] + except IndexError: + return None + @property def response(self): - if len(self.responses) == 0: - return None - else: + """ + Returns the latest stored response or None + + :return: The latest stored response + """ + try: return self.responses[-1] + except IndexError: + return None + + def clear(self): + """ + Clears the history lists + """ del self.requests[:] del self.responses[:] diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 770134b..e60ffae 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -8,11 +8,17 @@ import inspect import re +# Supported transmitted code supported_types = utils.iter_types + utils.primitive_types + +# Regex of invalid module characters invalid_module_chars = r'[^a-zA-Z0-9\_\.]' class TranslationError(Exception): + """ + Unmarshaling exception + """ pass @@ -23,6 +29,10 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): Doesn't change primitive types. :param obj: An object to convert + :param serialize_method: Custom serialization method + :param ignore_attribute: Name of the object attribute containing the names + of members to ignore + :param ignore: A list of members to ignore :return: A JSON-RPC compliant object """ if not serialize_method: @@ -38,31 +48,36 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): # Iterative if isinstance(obj, utils.iter_types): - if isinstance(obj, (utils.ListType, utils.TupleType)): + + if isinstance(obj, utils.DictType): + # It's a dictionary + new_obj = {} + for key, value in obj.items(): + new_obj[key] = dump(value, serialize_method, + ignore_attribute, ignore) + return new_obj + + else: + # ... list or tuple new_obj = [] for item in obj: new_obj.append(dump(item, serialize_method, ignore_attribute, ignore)) + if isinstance(obj, utils.TupleType): + # Keep the "tuple" type new_obj = tuple(new_obj) - return new_obj - # It's a dict... - else: - new_obj = {} - for key, value in obj.items(): - 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__ - class_name = obj.__class__.__name__ - json_class = class_name + json_class = obj.__class__.__name__ - if module_name not in ['', '__main__']: + if module_name not in ('', '__main__'): json_class = '{0}.{1}'.format(module_name, json_class) + # Keep the class name in the returned object return_obj = {"__jsonclass__": [json_class, ]} # If a serialization method is defined.. @@ -83,11 +98,11 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): attrs = {} ignore_list = getattr(obj, ignore_attribute, []) + ignore for attr_name, attr_value in obj.__dict__.items(): - if type(attr_value) in supported_types and \ + if isinstance(attr_value, 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) + ignore_attribute, ignore) return_obj.update(attrs) return return_obj @@ -106,7 +121,11 @@ def load(obj): # List elif isinstance(obj, (utils.ListType, utils.TupleType)): - return [load(entry) for entry in obj] + return_obj = [load(entry) for entry in obj] + if isinstance(obj, utils.TupleType): + return_obj = tuple(return_obj) + + return return_obj # Otherwise, it's a dict type elif '__jsonclass__' not in obj.keys(): @@ -147,9 +166,15 @@ def load(obj): temp_module = __import__(json_module_tree, fromlist=[json_class_name]) except ImportError: - raise TranslationError('Could not import %s from module %s.' % - (json_class_name, json_module_tree)) - json_class = getattr(temp_module, json_class_name) + raise TranslationError('Could not import {0} from module {1}.' \ + .format(json_class_name, json_module_tree)) + + try: + json_class = getattr(temp_module, json_class_name) + + except AttributeError: + raise TranslationError("Unknown class {0}.{1}." \ + .format(json_module_tree, json_class_name)) # Create the object new_obj = None From 65becf851fb1f54cfdbfe6d5a3408fc3cc66ea0a Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Tue, 4 Jun 2013 20:20:08 +0200 Subject: [PATCH 21/58] Enhanced JSON parser feeding JSONTarget: raw data is stored in the feeding list, the conversion to string (if required) is done only once all data has been received. This avoids errors decoding splitted wide-characters. --- jsonrpclib/jsonrpc.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 6b969c6..fd006d7 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -157,15 +157,23 @@ def __init__(self): self.data = [] def feed(self, data): - try: - data = utils.from_bytes(data) - except: - pass - + # Store raw data: it might not contain whole wide-character self.data.append(data) def close(self): - return ''.join(self.data) + if not self.data: + return '' + + else: + data = type(self.data[0])().join(self.data) + try: + # Convert the whole final string + data = utils.from_bytes(data) + except: + # Try a pass-through + pass + + return data class Transport(TransportMixIn, XMLTransport): pass From 0ed2bb85ced2c0076a3ee7db7c5d51ed01084df3 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Tue, 4 Jun 2013 20:43:56 +0200 Subject: [PATCH 22/58] History becomes an optional parameter of ServerProxy * Removes the potential memory hog (history was never cleared) * Allows to have different instances of History for each ServerProxy * Tests updated --- jsonrpclib/__init__.py | 4 ---- jsonrpclib/history.py | 13 ------------ jsonrpclib/jsonrpc.py | 24 ++++++++++++++++++---- tests.py | 45 +++++++++++++++++++++++------------------- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 5b83289..f803787 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -5,10 +5,6 @@ from jsonrpclib.config import Config config = Config.instance() -# Create a history instance -from jsonrpclib.history import History -history = History.instance() - # Easy access to utility methods from jsonrpclib.jsonrpc import Server, MultiCall, Fault, ProtocolError from jsonrpclib.jsonrpc import loads, dumps, load, dump diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index 0a96605..f578c6c 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -8,9 +8,6 @@ class History(object): each request cycle in order to keep it from clogging memory. """ - # The singleton - _instance = None - def __init__(self): """ Sets up members @@ -18,16 +15,6 @@ def __init__(self): self.requests = [] self.responses = [] - @classmethod - def instance(cls): - """ - Returns/Creates the instance of History - """ - if not cls._instance: - cls._instance = cls() - - return cls._instance - def add_response(self, response_obj): """ Adds a response to the history diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index fd006d7..84334ee 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -49,7 +49,7 @@ """ # Library includes -from jsonrpclib import config, history, utils +from jsonrpclib import config, utils # Standard library import sys @@ -190,10 +190,21 @@ class ServerProxy(XMLServerProxy): """ def __init__(self, uri, transport=None, encoding=None, - verbose=0, version=None): + verbose=0, version=None, history=None): + """ + Sets up the server proxy + + :param uri: Request URI + :param transport: Custom transport handler + :param encoding: Specified encoding + :param verbose: Log verbosity level + :param version: JSON-RPC specification version + :param history: History object (for tests) + """ if not version: version = config.version self.__version = version + schema, uri = splittype(uri) if schema not in ('http', 'https'): raise IOError('Unsupported JSON-RPC protocol.') @@ -209,8 +220,10 @@ def __init__(self, uri, transport=None, encoding=None, else: transport = Transport() self.__transport = transport + self.__encoding = encoding self.__verbose = verbose + self.__history = history def _request(self, methodname, params, rpcid=None): request = dumps(params, methodname, encoding=self.__encoding, @@ -227,7 +240,8 @@ def _request_notify(self, methodname, params, rpcid=None): return def _run_request(self, request, notify=None): - history.add_request(request) + if self.__history is not None: + self.__history.add_request(request) response = self.__transport.request( self.__host, @@ -242,7 +256,9 @@ def _run_request(self, request, notify=None): # the response object, or expect the Server to be # outputting the response appropriately? - history.add_response(response) + if self.__history is not None: + self.__history.add_response(response) + if not response: return None return_obj = loads(response) diff --git a/tests.py b/tests.py index d559b1d..f8e3fe1 100644 --- a/tests.py +++ b/tests.py @@ -19,12 +19,13 @@ * Implement JSONClass, History, Config tests """ -from jsonrpclib import Server, MultiCall, history, ProtocolError +from jsonrpclib import Server, MultiCall, ProtocolError from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler import socket import unittest import time +import jsonrpclib.history try: import json except ImportError: @@ -41,8 +42,10 @@ class TestCompatibility(unittest.TestCase): def setUp(self): self.port = PORTS.pop() + self.history = jsonrpclib.history.History() self.server = server_set_up(addr=('', self.port)) - self.client = Server('http://localhost:%d' % self.port) + self.client = Server('http://localhost:{0}'.format(self.port), + history=self.history) # v1 tests forthcoming @@ -53,8 +56,8 @@ def test_positional(self): self.assertTrue(result == -19) result = self.client.subtract(42, 23) self.assertTrue(result == 19) - request = json.loads(history.request) - response = json.loads(history.response) + request = json.loads(self.history.request) + response = json.loads(self.history.response) verify_request = { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": request['id'] @@ -71,8 +74,8 @@ def test_named(self): self.assertTrue(result == 19) result = self.client.subtract(minuend=42, subtrahend=23) self.assertTrue(result == 19) - request = json.loads(history.request) - response = json.loads(history.response) + request = json.loads(self.history.request) + response = json.loads(self.history.response) verify_request = { "jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, @@ -88,8 +91,8 @@ 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) - request = json.loads(history.request) - response = history.response + request = json.loads(self.history.request) + response = self.history.response verify_request = { "jsonrpc": "2.0", "method": "update", "params": [1, 2, 3, 4, 5] } @@ -99,8 +102,8 @@ def test_notification(self): def test_non_existent_method(self): self.assertRaises(ProtocolError, self.client.foobar) - request = json.loads(history.request) - response = json.loads(history.response) + request = json.loads(self.history.request) + response = json.loads(self.history.response) verify_request = { "jsonrpc": "2.0", "method": "foobar", "id": request['id'] } @@ -117,7 +120,7 @@ def test_invalid_json(self): invalid_json = '{"jsonrpc": "2.0", "method": "foobar, ' + \ '"params": "bar", "baz]' response = self.client._run_request(invalid_json) - response = json.loads(history.response) + response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32700,' + ' "message": "Parse error."}, "id": null}' @@ -128,7 +131,7 @@ def test_invalid_json(self): 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) + response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32600, ' + '"message": "Invalid Request."}, "id": null}' @@ -140,7 +143,7 @@ def test_batch_invalid_json(self): 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) + response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32700,' + '"message": "Parse error."}, "id": null}' @@ -151,7 +154,7 @@ def test_batch_invalid_json(self): def test_empty_array(self): invalid_request = '[]' response = self.client._run_request(invalid_request) - response = json.loads(history.response) + response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32600, ' + '"message": "Invalid Request."}, "id": null}' @@ -163,7 +166,7 @@ def test_nonempty_array(self): invalid_request = '[1,2]' request_obj = json.loads(invalid_request) response = self.client._run_request(invalid_request) - response = json.loads(history.response) + response = json.loads(self.history.response) self.assertTrue(len(response) == len(request_obj)) for resp in response: verify_resp = json.loads( @@ -243,13 +246,13 @@ def test_batch_notifications(self): '"params": [1,2,4]},{"jsonrpc": "2.0", ' + '"method": "notify_hello", "params": [7]}]' ) - request = json.loads(history.request) + request = json.loads(self.history.request) self.assertTrue(len(request) == len(valid_request)) for i in range(len(request)): req = request[i] valid_req = valid_request[i] self.assertTrue(req == valid_req) - self.assertTrue(history.response == '') + self.assertTrue(self.history.response == '') class InternalTests(unittest.TestCase): """ @@ -262,10 +265,12 @@ class InternalTests(unittest.TestCase): def setUp(self): self.port = PORTS.pop() + self.history = jsonrpclib.history.History() self.server = server_set_up(addr=('', self.port)) def get_client(self): - return Server('http://localhost:%d' % self.port) + return Server('http://localhost:{0}'.format(self.port), + history=self.history) def get_multicall_client(self): server = self.get_client() @@ -298,8 +303,8 @@ def test_single_notify(self): def test_single_namespace(self): client = self.get_client() response = client.namespace.sum(1, 2, 4) - request = json.loads(history.request) - response = json.loads(history.response) + request = json.loads(self.history.request) + response = json.loads(self.history.response) verify_request = { "jsonrpc": "2.0", "params": [1, 2, 4], "id": "5", "method": "namespace.sum" From 2f857f5612c92e16b225855525ee7bc7d5e4105d Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 19 Jun 2013 11:49:02 +0200 Subject: [PATCH 23/58] Applied: Improved 1.0 protocol compliance (error handling and required params) Commit 0327b55e0ab48b0aacbc6817082cf589f7b8f5a9 from https://github.com/drdaeman/jsonrpclib --- jsonrpclib/jsonrpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 84334ee..548326d 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -401,7 +401,7 @@ class Fault(object): """ JSON-RPC error class """ - def __init__(self, code= -32000, message='Server error', rpcid=None): + def __init__(self, code=-32000, message='Server error', rpcid=None): """ Sets up the error description @@ -496,7 +496,7 @@ def request(self, method, params=[]): self.id = str(uuid.uuid4()) request = { 'id':self.id, 'method':method } - if params: + if params or self.version < 1.1: request['params'] = params if self.version >= 2: @@ -542,7 +542,7 @@ def response(self, result=None): return response - def error(self, code= -32000, message='Server error.'): + def error(self, code=-32000, message='Server error.'): """ Prepares an error dictionary From 4e6cd15bf821406f2a7e1b020d077fe103cc9644 Mon Sep 17 00:00:00 2001 From: Aleksey Zhukov Date: Tue, 13 Mar 2012 16:46:16 +0400 Subject: [PATCH 24/58] Less strict error response handling. This should support any types of error responses, including: - `{..., "error": {"code": -42, "message": "spam"}}`, - `{..., "error": {"reason": "spam"}}`, - `{..., "error": {"what_happened": "spam"}}`, - or even `{..., "error": "spam"}}` Conflicts: jsonrpclib/jsonrpc.py --- jsonrpclib/jsonrpc.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 548326d..ad6925d 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -713,16 +713,27 @@ def check_for_errors(result): if 'error' in result and result['error']: # Server-side error - code = result['error']['code'] - try: - # Get the message (jsonrpclib) - message = result['error']['message'] + if 'code' in result['error']: + # Code + Message + code = result['error']['code'] + try: + # Get the message (jsonrpclib) + message = result['error']['message'] + + except KeyError: + # Get the trace (jabsorb) + message = result['error'].get('trace', '') - except KeyError: - # Get the trace (jabsorb) - message = result['error'].get('trace', '') + raise ProtocolError((code, message)) - raise ProtocolError((code, message)) + elif isinstance(result['error'], dict) and len(result['error']) == 1: + # Error with a single entry ('reason', ...): use its content + error_key = result['error'].keys()[0] + raise ProtocolError(result['error'][error_key]) + + else: + # Use the raw error content + raise ProtocolError(result['error']) return result From fd0468de5baec50c47357d3c724fd93d8b139b4f Mon Sep 17 00:00:00 2001 From: Tuomas Salo Date: Thu, 11 Apr 2013 17:01:43 +0300 Subject: [PATCH 25/58] In case of a non-pre-defined error, raise an AppError and give access to error.data Conflicts: jsonrpclib/__init__.py jsonrpclib/jsonrpc.py --- jsonrpclib/__init__.py | 2 +- jsonrpclib/jsonrpc.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index f803787..c465b3c 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -6,7 +6,7 @@ config = Config.instance() # Easy access to utility methods -from jsonrpclib.jsonrpc import Server, MultiCall, Fault, ProtocolError +from jsonrpclib.jsonrpc import Server, MultiCall, Fault, ProtocolError, AppError from jsonrpclib.jsonrpc import loads, dumps, load, dump from jsonrpclib.jsonrpc import jloads, jdumps import jsonrpclib.utils as utils diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index ad6925d..aafe0b1 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -122,6 +122,10 @@ def jloads(json_string): class ProtocolError(Exception): pass +class AppError(ProtocolError): + def data(self): + return self[0][2] + class TransportMixIn(object): """ Just extends the XMLRPC transport where necessary. """ user_agent = config.user_agent @@ -724,6 +728,16 @@ def check_for_errors(result): # Get the trace (jabsorb) message = result['error'].get('trace', '') + if -32700 <= code <= -32000: + # Pre-defined errors + # See http://www.jsonrpc.org/specification#error_object + raise ProtocolError((code, message)) + + else: + # Application error + data = result['error'].get('data', None) + raise AppError((code, message, data)) + raise ProtocolError((code, message)) elif isinstance(result['error'], dict) and len(result['error']) == 1: From c02bf57bbeb20708221574dc7ed858dc54a2e6ad Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 19 Jun 2013 12:08:38 +0200 Subject: [PATCH 26/58] Added some documentation to ProtocolError and AppError --- jsonrpclib/jsonrpc.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index aafe0b1..2b5384f 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -120,12 +120,32 @@ def jloads(json_string): # XMLRPClib re-implementations class ProtocolError(Exception): + """ + JSON-RPC error + + ProtocolError[0] can be: + * an error message (string) + * a (code, message) tuple + """ pass class AppError(ProtocolError): + """ + Application error: the error code is not in the pre-defined ones + + AppError[0][0]: Error code + AppError[0][1]: Error message or trace + AppError[0][2]: Associated data + """ def data(self): + """ + Retrieves the value found in the 'data' entry of the error, or None + + :return: The data associated to the error, or None + """ return self[0][2] + class TransportMixIn(object): """ Just extends the XMLRPC transport where necessary. """ user_agent = config.user_agent From f9920b50cfcdf7f9d4a0ebca0eb38e400bcfc519 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 19 Jun 2013 12:40:47 +0200 Subject: [PATCH 27/58] Content-Type can now be configured Modifying jsonrpclib.config.content_type (string) By default, it stays to "application/json-rpc", as the JSON-RPC specification indicates. --- jsonrpclib/SimpleJSONRPCServer.py | 8 ++++---- jsonrpclib/config.py | 5 +++++ jsonrpclib/jsonrpc.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 8e2b7a0..ddcbd2e 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -3,7 +3,7 @@ # Local modules import jsonrpclib -from jsonrpclib import Fault, utils +from jsonrpclib import Fault, utils, config # Standard library import socket @@ -346,7 +346,7 @@ def do_POST(self): response = utils.to_bytes(response) # Send it - self.send_header("Content-type", "application/json-rpc") + self.send_header("Content-type", config.content_type) self.send_header("Content-length", str(len(response))) self.end_headers() self.wfile.write(response) @@ -411,8 +411,8 @@ def handle_jsonrpc(self, request_text): Handle a JSON-RPC request """ response = self._marshaled_dispatch(request_text) - sys.stdout.write('Content-Type: application/json-rpc\r\n') - sys.stdout.write('Content-Length: %d\r\n' % len(response)) + sys.stdout.write('Content-Type: {0}\r\n'.format(config.content_type)) + sys.stdout.write('Content-Length: {0:d}\r\n'.format(len(response))) sys.stdout.write('\r\n') sys.stdout.write(response) diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 59ac0cf..954f976 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -38,6 +38,11 @@ class Config(object): user_agent = 'jsonrpclib-pelix/0.1 (Python {0})' \ .format('.'.join(str(ver) for ver in sys.version_info[0:3])) + # Content-type to use. According to the JSON-RPC specification, + # it SHOULD be 'application/json-rpc' + # but MAY be 'application/json' or 'application/jsonrequest' + content_type = "application/json-rpc" + # "Singleton" of Config _instance = None diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 2b5384f..fca18ba 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -156,7 +156,7 @@ def send_content(self, connection, request_body): # Convert the body first request_body = utils.to_bytes(request_body) - connection.putheader("Content-Type", "application/json-rpc") + connection.putheader("Content-Type", config.content_type) connection.putheader("Content-Length", str(len(request_body))) connection.endheaders() if request_body: From 28d8d81c971a33a042ccabb9d692f6f3d2aa6bc2 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Wed, 19 Jun 2013 12:41:54 +0200 Subject: [PATCH 28/58] Changed the tests starting delay from 2 to .5 seconds --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index f8e3fe1..0ee721a 100644 --- a/tests.py +++ b/tests.py @@ -398,5 +398,5 @@ def log_request(self, *args, **kwargs): print("===============================================================") print(" NOTE: There may be threading exceptions after tests finish. ") print("===============================================================") - time.sleep(2) + time.sleep(.5) unittest.main() From c0778558c72c9a3549856fbfa3692436a80f6265 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 20 Jun 2013 17:08:25 +0200 Subject: [PATCH 29/58] Applied "Custom headers can be sent with request." Commit 3dd69f9ae38c9b88c4bf84d4b67e60054c567cdc from https://github.com/dejw/jsonrpclib --- jsonrpclib/jsonrpc.py | 80 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index fca18ba..73a4a10 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -52,6 +52,7 @@ from jsonrpclib import config, utils # Standard library +import contextlib import sys import uuid @@ -152,12 +153,70 @@ class TransportMixIn(object): # for Python 2.7 support _connection = None + # Additional headers: list of dictionaries + additional_headers = [] + + # List of non-overridable headers + # Use the configuration to change the content-type + readonly_headers = ('content-length', 'content-type') + + def push_headers(self, headers): + """ + Adds a dictionary of headers to the additional headers list + + :param headers: A dictionary + """ + self.additional_headers.append(headers) + + def pop_headers(self, headers): + """ + Removes the given dictionary from the additional headers list. + Also validates that given headers are on top of the stack + + :param headers: Headers to remove + :raise AssertionError: The given dictionary is not on the latest stored + in the additional headers list + """ + assert self.additional_headers[-1] == headers + self.additional_headers.pop() + + + def emit_additional_headers(self, connection): + """ + Puts headers as is in the request, filtered read only headers + + :param connection: The request connection + """ + additional_headers = {} + + # Prepare the merged dictionary + for headers in self.additional_headers: + additional_headers.update(headers) + + # Remove forbidden keys + for forbidden in self.readonly_headers: + additional_headers.pop(forbidden, None) + + # Reversed order: in the case of multiple headers value definition, + # the latest pushed has priority + for key, value in additional_headers.items(): + key = str(key) + if key.lower() not in self.readonly_headers: + # Only accept replaceable headers + connection.putheader(str(key), str(value)) + + def send_content(self, connection, request_body): # Convert the body first request_body = utils.to_bytes(request_body) + # "static" headers connection.putheader("Content-Type", config.content_type) connection.putheader("Content-Length", str(len(request_body))) + + # Emit additional headers here in order not to override content-length + self.emit_additional_headers(connection) + connection.endheaders() if request_body: connection.send(request_body) @@ -214,7 +273,7 @@ class ServerProxy(XMLServerProxy): """ def __init__(self, uri, transport=None, encoding=None, - verbose=0, version=None, history=None): + verbose=0, version=None, headers=None, history=None): """ Sets up the server proxy @@ -223,6 +282,7 @@ def __init__(self, uri, transport=None, encoding=None, :param encoding: Specified encoding :param verbose: Log verbosity level :param version: JSON-RPC specification version + :param headers: Custom additional headers for each request :param history: History object (for tests) """ if not version: @@ -249,6 +309,9 @@ def __init__(self, uri, transport=None, encoding=None, self.__verbose = verbose self.__history = history + # Global custom headers are injected into Transport + self.__transport.push_headers(headers or {}) + def _request(self, methodname, params, rpcid=None): request = dumps(params, methodname, encoding=self.__encoding, rpcid=rpcid, version=self.__version) @@ -297,6 +360,21 @@ def _notify(self): # Just like __getattr__, but with notify namespace. return _Notify(self._request_notify) + @contextlib.contextmanager + def _additional_headers(self, headers): + """ + Allow to specify additional headers, to be added inside the with + block. Example of usage: + + >>> with client._additional_headers({'X-Test' : 'Test'}) as new_client: + ... new_client.method() + ... + >>> # Here old headers are restored + """ + self.__transport.push_headers(headers) + yield self + self.__transport.pop_headers(headers) + # ------------------------------------------------------------------------------ class _Method(XML_Method): From 20f5be4485e0a5e219e86d9c4d98a9be85ecc85c Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 20 Jun 2013 17:15:38 +0200 Subject: [PATCH 30/58] Added tests for the additional headers Applied commits from https://github.com/dejw/jsonrpclib: - 53d38c3f4cf853e241e442d696211fa54828c3ab: Added tests for additional request headers feature. - 9e63239d010e8bfb8c752a865b5c893c304400c3: Added test for notifications. - 0a3bbe68cebfaac72d0f799a3854627b5cea8468: Added test for nesting _additional_headers context. Tests have been modified to handle Python 2 / Python 3 compatibility --- tests.py | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/tests.py b/tests.py index 0ee721a..bbdea36 100644 --- a/tests.py +++ b/tests.py @@ -22,10 +22,23 @@ from jsonrpclib import Server, MultiCall, ProtocolError from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler +from jsonrpclib.utils import from_bytes + import socket import unittest import time import jsonrpclib.history + +import contextlib +import re +import sys + +if sys.version_info[0] < 3: + from StringIO import StringIO + +else: + from io import StringIO + try: import json except ImportError: @@ -349,6 +362,222 @@ def func(): self.assertRaises(raises[i], func) +class HeadersTests(unittest.TestCase): + """ + These tests verify functionality of additional headers. + """ + client = None + server = None + port = None + + REQUEST_LINE = "^send: POST" + + def setUp(self): + """ + Sets up the test + """ + self.port = PORTS.pop() + self.server = server_set_up(addr=('', self.port)) + + + @contextlib.contextmanager + def captured_headers(self): + """ + Captures the request headers. Yields the {header : value} dictionary, + where keys are in lower case. + """ + # Redirect the standard output, to catch jsonrpclib verbose messages + stdout = sys.stdout + sys.stdout = f = StringIO() + headers = {} + yield headers + sys.stdout = stdout + + # Extract the sent request content + request_lines = f.getvalue().splitlines() + request_lines = list(filter(lambda l: l.startswith("send:"), + request_lines)) + request_line = request_lines[0].split("send: ") [-1] + + # Convert it to a string + try: + # Use eval to convert the representation into a string + request_line = from_bytes(eval(request_line)) + except: + # Keep the received version + pass + + # Extract headers + raw_headers = request_line.splitlines()[1:-1] + raw_headers = map(lambda h: re.split(":\s?", h, 1), raw_headers) + for header, value in raw_headers: + headers[header.lower()] = value + + + def test_should_extract_headers(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1) + + # when + with self.captured_headers() as headers: + response = client.ping() + self.assertTrue(response) + + # then + self.assertTrue(len(headers) > 0) + self.assertTrue('content-type' in headers) + self.assertEqual(headers['content-type'], 'application/json-rpc') + + def test_should_add_additional_headers(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'X-My-Header' : 'Test'}) + + # when + with self.captured_headers() as headers: + response = client.ping() + self.assertTrue(response) + + # then + self.assertTrue('x-my-header' in headers) + self.assertEqual(headers['x-my-header'], 'Test') + + def test_should_add_additional_headers_to_notifications(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'X-My-Header' : 'Test'}) + + # when + with self.captured_headers() as headers: + client._notify.ping() + + # then + self.assertTrue('x-my-header' in headers) + self.assertEqual(headers['x-my-header'], 'Test') + + def test_should_override_headers(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={ + 'User-Agent' : 'jsonrpclib test', + 'Host' : 'example.com' + }) + + # when + with self.captured_headers() as headers: + response = client.ping() + self.assertTrue(response) + + # then + self.assertEqual(headers['user-agent'], 'jsonrpclib test') + self.assertEqual(headers['host'], 'example.com') + + def test_should_not_override_content_length(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'Content-Length' : 'invalid value'}) + + # when + with self.captured_headers() as headers: + response = client.ping() + self.assertTrue(response) + + # then + self.assertTrue('content-length' in headers) + self.assertNotEqual(headers['content-length'], 'invalid value') + + def test_should_convert_header_values_to_basestring(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'X-Test' : 123}) + + # when + with self.captured_headers() as headers: + response = client.ping() + self.assertTrue(response) + + # then + self.assertTrue('x-test' in headers) + self.assertEqual(headers['x-test'], '123') + + def test_should_add_custom_headers_to_methods(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1) + + # when + with self.captured_headers() as headers: + with client._additional_headers({'X-Method' : 'Method'}) as cl: + response = cl.ping() + + self.assertTrue(response) + + # then + self.assertTrue('x-method' in headers) + self.assertEqual(headers['x-method'], 'Method') + + def test_should_override_global_headers(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'X-Test' : 'Global'}) + + # when + with self.captured_headers() as headers: + with client._additional_headers({'X-Test' : 'Method'}) as cl: + response = cl.ping() + self.assertTrue(response) + + # then + self.assertTrue('x-test' in headers) + self.assertEqual(headers['x-test'], 'Method') + + def test_should_restore_global_headers(self): + # given + client = Server('http://localhost:{0}'.format(self.port), verbose=1, + headers={'X-Test' : 'Global'}) + + # when + with self.captured_headers() as headers: + with client._additional_headers({'X-Test' : 'Method'}) as cl: + response = cl.ping() + self.assertTrue(response) + + self.assertTrue('x-test' in headers) + self.assertEqual(headers['x-test'], 'Method') + + with self.captured_headers() as headers: + response = cl.ping() + self.assertTrue(response) + + # then + self.assertTrue('x-test' in headers) + self.assertEqual(headers['x-test'], 'Global') + + + def test_should_allow_to_nest_additional_header_blocks(self): + # given + client = Server('http://localhost:%d' % self.port, verbose=1) + + # when + with client._additional_headers({'X-Level-1' : '1'}) as cl_level1: + with self.captured_headers() as headers1: + response = cl_level1.ping() + self.assertTrue(response) + + with cl_level1._additional_headers({'X-Level-2' : '2'}) as cl: + with self.captured_headers() as headers2: + response = cl.ping() + self.assertTrue(response) + + # then + self.assertTrue('x-level-1' in headers1) + self.assertEqual(headers1['x-level-1'], '1') + + self.assertTrue('x-level-1' in headers2) + self.assertEqual(headers1['x-level-1'], '1') + self.assertTrue('x-level-2' in headers2) + self.assertEqual(headers2['x-level-2'], '2') + + """ Test Methods """ def subtract(minuend, subtrahend): """ Using the keywords from the JSON-RPC v2 doc """ From 589c38790279b35c2d3894d84ad14e3cc6002dd5 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 20 Jun 2013 17:28:24 +0200 Subject: [PATCH 31/58] Updated README file --- README.rst | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7aceff5..7f1478c 100644 --- a/README.rst +++ b/README.rst @@ -23,6 +23,21 @@ Services, but it is **not** a Pelix specific implementation. * It is now possible to use the dispatch_method argument while extending the SimpleJSONRPCDispatcher, to use a custom dispatcher. This allows to use this package by Pelix Remote Services. +* The modifications added in other forks of this project have been added: + + * From https://github.com/drdaeman/jsonrpclib: + + * Improved JSON-RPC 1.0 support + * Less strict error response handling + + * From https://github.com/tuomassalo/jsonrpclib: + + * In case of a non-pre-defined error, raise an AppError and give access to + *error.data* + + * From https://github.com/dejw/jsonrpclib: + + * Custom headers can be sent with request and associated tests * The support for Unix sockets has been removed, as it is not trivial to convert to Python 3 (and I don't use them) @@ -60,6 +75,9 @@ standard distribution of 2.6+, you should already have one. Keep in mind that ``cjson`` is supposed to be the quickest, I believe, so if you are going for full-on optimization you may want to pick it up. +Since library uses ``contextlib`` module, you should have at least Python 2.5 +installed. + Installation ************ @@ -142,6 +160,33 @@ Additionally, the loads method does not return the params and method like ``xmlrpclib``, but instead a.) parses for errors, raising ProtocolErrors, and b.) returns the entire structure of the request / response for manual parsing. + +Additional headers +****************** + +If your remote service requires custom headers in request, you can pass them +as as a ``headers`` keyword argument, when creating the ``ServerProxy``: + +.. code-block:: python + + >>> import jsonrpclib + >>> server = jsonrpclib.ServerProxy("http://localhost:8080", + headers={'X-Test' : 'Test'}) + +You can also put additional request headers only for certain method invocation: + +.. code-block:: python + + >>> import jsonrpclib + >>> server = jsonrpclib.Server("http://localhost:8080") + >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: + ... test_server.ping() + ... + >>> # X-Test header will be no longer sent in requests + +Of course ``_additional_headers`` contexts can be nested as well. + + SimpleJSONRPCServer ******************* @@ -250,7 +295,7 @@ over JSON: * Wider XML-RPC support across APIs (can we change this? :)) * Libraries are more established, i.e. more stable (Let's change this too.) -TESTS +Tests ***** I've dropped almost-verbatim tests from the JSON-RPC spec 2.0 page. @@ -259,3 +304,4 @@ You can run it with: .. code-block:: console python tests.py + python3 tests.py From 81da309e92bd5300e45c631fc2f675468d9e1f81 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 20 Jun 2013 17:49:07 +0200 Subject: [PATCH 32/58] Added __version__ to all modules Version set to 0.1.5 --- jsonrpclib/SimpleJSONRPCServer.py | 10 +++++++++- jsonrpclib/__init__.py | 5 +++-- jsonrpclib/config.py | 26 +++++++++++++++++++++++--- jsonrpclib/history.py | 7 +++++++ jsonrpclib/jsonclass.py | 14 +++++++++++++- jsonrpclib/jsonrpc.py | 7 ++++++- jsonrpclib/utils.py | 4 ++++ 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index ddcbd2e..864d4ca 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -1,9 +1,17 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- +""" +Defines a request dispatcher, a HTTP request handler, a HTTP server and a +CGI request handler. +""" +__version__ = "0.1.5" + +# ------------------------------------------------------------------------------ # Local modules +from jsonrpclib import Fault, config import jsonrpclib -from jsonrpclib import Fault, utils, config +import jsonrpclib.utils as utils # Standard library import socket diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index c465b3c..7377319 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -5,8 +5,9 @@ from jsonrpclib.config import Config config = Config.instance() -# Easy access to utility methods -from jsonrpclib.jsonrpc import Server, MultiCall, Fault, ProtocolError, AppError +# Easy access to utility methods and classes +from jsonrpclib.jsonrpc import Server, ServerProxy +from jsonrpclib.jsonrpc import MultiCall, Fault, ProtocolError, AppError from jsonrpclib.jsonrpc import loads, dumps, load, dump from jsonrpclib.jsonrpc import jloads, jdumps import jsonrpclib.utils as utils diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 954f976..edaa417 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -1,12 +1,31 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- +""" +The configuration module. +""" + +__version__ = "0.1.5" + +# ------------------------------------------------------------------------------ import sys +# ------------------------------------------------------------------------------ + class LocalClasses(dict): + """ + Associates local classes with their names (used in the jsonclass module) + """ def add(self, cls): + """ + Stores a local class + + :param cls: A class + """ self[cls.__name__] = cls +# ------------------------------------------------------------------------------ + class Config(object): """ This is pretty much used exclusively for the 'jsonclass' @@ -31,12 +50,13 @@ class Config(object): # The list of classes to use for jsonclass translation. classes = LocalClasses() - # Version of the JSON-RPC spec to support + # Version of the JSON-RPC specification to support version = 2.0 # User agent to use for calls. - user_agent = 'jsonrpclib-pelix/0.1 (Python {0})' \ - .format('.'.join(str(ver) for ver in sys.version_info[0:3])) + user_agent = 'jsonrpclib-pelix/{0} (Python {1})' \ + .format(__version__, + '.'.join(str(ver) for ver in sys.version_info[0:3])) # Content-type to use. According to the JSON-RPC specification, # it SHOULD be 'application/json-rpc' diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index f578c6c..a778bc7 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -1,5 +1,12 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- +""" +The history module. +""" + +__version__ = "0.1.5" + +# ------------------------------------------------------------------------------ class History(object): """ diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index e60ffae..fd4ee65 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -1,19 +1,30 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- +""" +The serialization module +""" + +__version__ = "0.1.5" + +# ------------------------------------------------------------------------------ # Local package -from jsonrpclib import config, utils +from jsonrpclib import config +import jsonrpclib.utils as utils # Standard library import inspect import re +# ------------------------------------------------------------------------------ + # Supported transmitted code supported_types = utils.iter_types + utils.primitive_types # Regex of invalid module characters invalid_module_chars = r'[^a-zA-Z0-9\_\.]' +# ------------------------------------------------------------------------------ class TranslationError(Exception): """ @@ -21,6 +32,7 @@ class TranslationError(Exception): """ pass +# ------------------------------------------------------------------------------ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): """ diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 73a4a10..ebc15e2 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -48,8 +48,13 @@ See http://code.google.com/p/jsonrpclib/ for more info. """ +__version__ = "0.1.5" + +# ------------------------------------------------------------------------------ + # Library includes -from jsonrpclib import config, utils +from jsonrpclib import config +import jsonrpclib.utils as utils # Standard library import contextlib diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py index 6ef65a4..cf7a121 100644 --- a/jsonrpclib/utils.py +++ b/jsonrpclib/utils.py @@ -7,6 +7,10 @@ :version: 1.0.0 """ +__version__ = "1.0.0" + +# ------------------------------------------------------------------------------ + import sys # ------------------------------------------------------------------------------ From 6d89019941ae64260fd496b583c2816623040eb6 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 20 Jun 2013 17:49:55 +0200 Subject: [PATCH 33/58] Updated version to 0.1.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5d216a2..cd5415b 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( name="jsonrpclib-pelix", - version="0.1.4", + version="0.1.5", license="http://www.apache.org/licenses/LICENSE-2.0", author="Thomas Calmant", author_email="thomas.calmant@gmail.com", From 3406fe24d2ed70cd5a39faa56c4833d1bc11b11d Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 22 Jul 2013 15:30:51 +0200 Subject: [PATCH 34/58] Added missing README.rst to packaging --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ab30e9a..bde859b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include *.txt +inclide README.rst From acc14f2d6099dc6947ea04ee6aec6c20a17d2cef Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Thu, 1 Aug 2013 15:31:45 +0200 Subject: [PATCH 35/58] Correction of a typo in the manifest --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index bde859b..eb0014a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.txt -inclide README.rst +include README.rst From 7f00bb12c266139fdce399a60e59f4f7a0cb94b3 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:08:24 +0200 Subject: [PATCH 36/58] Added support for sets and frozensets --- jsonrpclib/utils.py | 50 ++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py index cf7a121..5785ca5 100644 --- a/jsonrpclib/utils.py +++ b/jsonrpclib/utils.py @@ -4,10 +4,10 @@ Utility methods, for compatibility between Python version :author: Thomas Calmant -:version: 1.0.0 +:version: 1.0.1 """ -__version__ = "1.0.0" +__version__ = "1.0.1" # ------------------------------------------------------------------------------ @@ -19,15 +19,6 @@ # Python 2 import types StringTypes = types.StringTypes - TupleType = types.TupleType - ListType = types.ListType - DictType = types.DictType - - iter_types = ( - types.DictType, - types.ListType, - types.TupleType - ) string_types = ( types.StringType, @@ -40,11 +31,6 @@ types.FloatType ) - value_types = ( - types.BooleanType, - types.NoneType - ) - def to_bytes(string): """ Converts the given string into bytes @@ -68,15 +54,6 @@ def from_bytes(data): else: # Python 3 StringTypes = (str,) - TupleType = tuple - ListType = list - DictType = dict - - iter_types = ( - dict, - list, - tuple - ) string_types = ( bytes, @@ -88,11 +65,6 @@ def from_bytes(data): float ) - value_types = ( - bool, - type(None) - ) - def to_bytes(string): """ Converts the given string into bytes @@ -113,4 +85,22 @@ def from_bytes(data): # ------------------------------------------------------------------------------ # Common + +DictType = dict +ListType = list +SetTypes = (set, frozenset) +TupleType = tuple + +iter_types = ( + dict, + list, + set, frozenset, + tuple +) + +value_types = ( + bool, + type(None) +) + primitive_types = string_types + numeric_types + value_types From a5b3d6b0b31a32e86a2bc6abc1699f873e1f7685 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:14:48 +0200 Subject: [PATCH 37/58] Allow beans to be instantiated with a list of arguments --- jsonrpclib/jsonclass.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index fd4ee65..21147d1 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -39,7 +39,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): Transforms the given object into a JSON-RPC compliant form. Converts beans into dictionaries with a __jsonclass__ entry. Doesn't change primitive types. - + :param obj: An object to convert :param serialize_method: Custom serialization method :param ignore_attribute: Name of the object attribute containing the names @@ -123,7 +123,7 @@ def load(obj): """ If 'obj' is a dictionary containing a __jsonclass__ entry, converts the dictionary item into a bean of this class. - + :param obj: An object from a JSON-RPC dictionary :return: The loaded object """ @@ -193,11 +193,12 @@ def load(obj): if isinstance(params, utils.ListType): new_obj = json_class(*params) - if isinstance(params, utils.DictType): + elif isinstance(params, utils.DictType): new_obj = json_class(**params) else: - raise TranslationError('Constructor args must be a dict or list.') + raise TranslationError("Constructor args must be a dict or list, " + "not {0}".format(type(params).__name__)) for key, value in obj.items(): if key == '__jsonclass__': From 2afbfc37fe8c5e0121fc7d7ffd1a9087aa3f6128 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:16:49 +0200 Subject: [PATCH 38/58] Small improvement in bean reconstruction Instead of filtering the __jsonclass__ entry, it is removed before the setattr() loop. --- jsonrpclib/jsonclass.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 21147d1..76266ba 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -200,11 +200,11 @@ def load(obj): raise TranslationError("Constructor args must be a dict or list, " "not {0}".format(type(params).__name__)) - for key, value in obj.items(): - if key == '__jsonclass__': - # Ignore the __jsonclass__ member - continue + # Remove the class information, as it must be ignored during the + # reconstruction of the object + del obj['__jsonclass__'] + for key, value in obj.items(): setattr(new_obj, key, value) return new_obj From fa542fb0a315d8ae7403dd9c966f25c250e4d9e7 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:27:32 +0200 Subject: [PATCH 39/58] Differentiation between dict and other iterables --- jsonrpclib/jsonclass.py | 39 ++++++++++++++------------------------- jsonrpclib/utils.py | 4 ++-- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 76266ba..0ac2272 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -19,7 +19,8 @@ # ------------------------------------------------------------------------------ # Supported transmitted code -supported_types = utils.iter_types + utils.primitive_types +supported_types = (utils.DictType,) + utils.iterable_types \ + + utils.primitive_types # Regex of invalid module characters invalid_module_chars = r'[^a-zA-Z0-9\_\.]' @@ -59,28 +60,16 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return obj # Iterative - if isinstance(obj, utils.iter_types): + elif isinstance(obj, utils.iterable_types): + # List, set or tuple + return [dump(item, serialize_method, ignore_attribute, ignore) + for item in obj] - if isinstance(obj, utils.DictType): - # It's a dictionary - new_obj = {} - for key, value in obj.items(): - new_obj[key] = dump(value, serialize_method, - ignore_attribute, ignore) - return new_obj - - else: - # ... list or tuple - new_obj = [] - for item in obj: - new_obj.append(dump(item, serialize_method, - ignore_attribute, ignore)) - - if isinstance(obj, utils.TupleType): - # Keep the "tuple" type - new_obj = tuple(new_obj) - - return new_obj + elif isinstance(obj, utils.DictType): + # Dictionary + return dict((key, dump(value, serialize_method, + ignore_attribute, ignore)) + for key, value in obj.items()) # It's not a standard type, so it needs __jsonclass__ module_name = inspect.getmodule(obj).__name__ @@ -131,8 +120,8 @@ def load(obj): if isinstance(obj, utils.primitive_types): return obj - # List - elif isinstance(obj, (utils.ListType, utils.TupleType)): + # List, set or tuple + elif isinstance(obj, utils.iterable_types): return_obj = [load(entry) for entry in obj] if isinstance(obj, utils.TupleType): return_obj = tuple(return_obj) @@ -197,7 +186,7 @@ def load(obj): new_obj = json_class(**params) else: - raise TranslationError("Constructor args must be a dict or list, " + raise TranslationError("Constructor args must be a dict or a list, " "not {0}".format(type(params).__name__)) # Remove the class information, as it must be ignored during the diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py index 5785ca5..fc9b306 100644 --- a/jsonrpclib/utils.py +++ b/jsonrpclib/utils.py @@ -87,12 +87,12 @@ def from_bytes(data): # Common DictType = dict + ListType = list SetTypes = (set, frozenset) TupleType = tuple -iter_types = ( - dict, +iterable_types = ( list, set, frozenset, tuple From bb8b1c1a159ff8e0b4efc2d2b157cbe20e6b999e Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:36:19 +0200 Subject: [PATCH 40/58] Added Apache license tag to modules documentation --- jsonrpclib/SimpleJSONRPCServer.py | 26 +++++++++----- jsonrpclib/__init__.py | 5 +++ jsonrpclib/config.py | 14 ++++++-- jsonrpclib/history.py | 20 +++++++---- jsonrpclib/jsonclass.py | 10 +++++- jsonrpclib/jsonrpc.py | 58 ++++++++++++++++++------------- jsonrpclib/utils.py | 8 ++++- 7 files changed, 96 insertions(+), 45 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 864d4ca..2098d1e 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -3,9 +3,17 @@ """ Defines a request dispatcher, a HTTP request handler, a HTTP server and a CGI request handler. + +:license: Apache License 2.0 +:version: 0.1.5 """ -__version__ = "0.1.5" +# Module version +__version_info__ = (0, 1, 5) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ # Local modules @@ -41,7 +49,7 @@ def get_version(request): """ Computes the JSON-RPC version - + :param request: A request dictionary :return: The JSON-RPC version or None """ @@ -57,7 +65,7 @@ def get_version(request): def validate_request(request): """ Validates the format of a request dictionary - + :param request: A request dictionary :return: True if the dictionary is valid, else a Fault object """ @@ -121,7 +129,7 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): """ Loads the request dictionary (unmarshaled), calls the method(s) accordingly and returns a JSON-RPC dictionary (not marshaled) - + :param request: JSON-RPC request dictionary (or list of) :param dispatch_method: Custom dispatch method (for method resolution) :return: A JSON-RPC dictionary (or an array of) or None if the request @@ -179,7 +187,7 @@ def _marshaled_dispatch(self, data, dispatch_method=None): """ Parses the request data (marshaled), calls method(s) and returns a JSON string (marshaled) - + :param data: A JSON request string :param dispatch_method: Custom dispatch method (for method resolution) :return: A JSON-RPC response string (marshaled) @@ -214,7 +222,7 @@ def _marshaled_dispatch(self, data, dispatch_method=None): def _marshaled_single_dispatch(self, request, dispatch_method=None): """ Dispatches a single method call - + :param request: A validated request dictionary :param dispatch_method: Custom dispatch method (for method resolution) :return: A JSON-RPC response dictionary, or None if it was a @@ -257,7 +265,7 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): def _dispatch(self, method, params): """ Default method resolver and caller - + :param method: Name of the method to call :param params: List of arguments to give to the method :return: The result of the method @@ -375,7 +383,7 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, address_family=socket.AF_INET): """ Sets up the server and the dispatcher - + :param addr: The server listening address :param requestHandler: Custom request handler :param logRequests: Flag to(de)activate requests logging @@ -409,7 +417,7 @@ class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): def __init__(self, encoding=None): """ Sets up the dispatcher - + :param encoding: Dispatcher encoding """ SimpleJSONRPCDispatcher.__init__(self, encoding) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 7377319..223ee18 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -1,5 +1,10 @@ #!/usr/bin/python # -- Content-Encoding: UTF-8 -- +""" +Aliases to ease access to jsonrpclib classes + +:license: Apache License 2.0 +""" # Create a configuration instance from jsonrpclib.config import Config diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index edaa417..a4796f2 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -2,9 +2,17 @@ # -- Content-Encoding: UTF-8 -- """ The configuration module. + +:license: Apache License 2.0 +:version: 0.1.5 """ -__version__ = "0.1.5" +# Module version +__version_info__ = (0, 1, 5) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ @@ -19,7 +27,7 @@ class LocalClasses(dict): def add(self, cls): """ Stores a local class - + :param cls: A class """ self[cls.__name__] = cls @@ -28,7 +36,7 @@ def add(self, 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. diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index a778bc7..fdb57a7 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -2,9 +2,17 @@ # -- Content-Encoding: UTF-8 -- """ The history module. + +:license: Apache License 2.0 +:version: 0.1.5 """ -__version__ = "0.1.5" +# Module version +__version_info__ = (0, 1, 5) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ @@ -12,7 +20,7 @@ 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. """ def __init__(self): @@ -25,7 +33,7 @@ def __init__(self): def add_response(self, response_obj): """ Adds a response to the history - + :param response_obj: Response content """ self.responses.append(response_obj) @@ -33,7 +41,7 @@ def add_response(self, response_obj): def add_request(self, request_obj): """ Adds a request to the history - + :param request_obj: A request object """ self.requests.append(request_obj) @@ -42,7 +50,7 @@ def add_request(self, request_obj): def request(self): """ Returns the latest stored request or None - + :return: The latest stored request """ try: @@ -55,7 +63,7 @@ def request(self): def response(self): """ Returns the latest stored response or None - + :return: The latest stored response """ try: diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 0ac2272..89bbe75 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -2,9 +2,17 @@ # -- Content-Encoding: UTF-8 -- """ The serialization module + +:license: Apache License 2.0 +:version: 0.1.6 """ -__version__ = "0.1.5" +# Module version +__version_info__ = (0, 1, 6) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index ebc15e2..7370e34 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -45,10 +45,18 @@ >>> batch() [53, 5] -See http://code.google.com/p/jsonrpclib/ for more info. +See https://github.com/tcalmant/jsonrpclib for more info. + +:license: Apache License 2.0 +:version: 0.1.5 """ -__version__ = "0.1.5" +# Module version +__version_info__ = (0, 1, 5) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ @@ -128,7 +136,7 @@ def jloads(json_string): class ProtocolError(Exception): """ JSON-RPC error - + ProtocolError[0] can be: * an error message (string) * a (code, message) tuple @@ -138,7 +146,7 @@ class ProtocolError(Exception): class AppError(ProtocolError): """ Application error: the error code is not in the pre-defined ones - + AppError[0][0]: Error code AppError[0][1]: Error message or trace AppError[0][2]: Associated data @@ -146,7 +154,7 @@ class AppError(ProtocolError): def data(self): """ Retrieves the value found in the 'data' entry of the error, or None - + :return: The data associated to the error, or None """ return self[0][2] @@ -168,16 +176,16 @@ class TransportMixIn(object): def push_headers(self, headers): """ Adds a dictionary of headers to the additional headers list - + :param headers: A dictionary """ self.additional_headers.append(headers) def pop_headers(self, headers): """ - Removes the given dictionary from the additional headers list. + Removes the given dictionary from the additional headers list. Also validates that given headers are on top of the stack - + :param headers: Headers to remove :raise AssertionError: The given dictionary is not on the latest stored in the additional headers list @@ -189,7 +197,7 @@ def pop_headers(self, headers): def emit_additional_headers(self, connection): """ Puts headers as is in the request, filtered read only headers - + :param connection: The request connection """ additional_headers = {} @@ -281,7 +289,7 @@ def __init__(self, uri, transport=None, encoding=None, verbose=0, version=None, headers=None, history=None): """ Sets up the server proxy - + :param uri: Request URI :param transport: Custom transport handler :param encoding: Specified encoding @@ -511,7 +519,7 @@ class Fault(object): def __init__(self, code=-32000, message='Server error', rpcid=None): """ Sets up the error description - + :param code: Fault code :param message: Associated message :param rpcid: Request ID @@ -523,7 +531,7 @@ def __init__(self, code=-32000, message='Server error', rpcid=None): def error(self): """ Returns the error as a dictionary - + :returns: A {'code', 'message'} dictionary """ return {'code':self.faultCode, 'message':self.faultString} @@ -531,7 +539,7 @@ def error(self): def response(self, rpcid=None, version=None): """ Returns the error as a JSON-RPC response string - + :param rpcid: Forced request ID :param version: JSON-RPC version :return: A JSON-RPC response string @@ -548,7 +556,7 @@ def response(self, rpcid=None, version=None): def dump(self, rpcid=None, version=None): """ Returns the error as a JSON-RPC response dictionary - + :param rpcid: Forced request ID :param version: JSON-RPC version :return: A JSON-RPC response dictionary @@ -576,7 +584,7 @@ class Payload(object): def __init__(self, rpcid=None, version=None): """ Sets up the JSON-RPC handler - + :param rpcid: Request ID :param version: JSON-RPC version """ @@ -590,7 +598,7 @@ def __init__(self, rpcid=None, version=None): def request(self, method, params=[]): """ Prepares a method call request - + :param method: Method name :param params: Method parameters :return: A JSON-RPC request dictionary @@ -615,7 +623,7 @@ def request(self, method, params=[]): def notify(self, method, params=[]): """ Prepares a notification request - + :param method: Notification name :param params: Notification parameters :return: A JSON-RPC notification dictionary @@ -635,7 +643,7 @@ def notify(self, method, params=[]): def response(self, result=None): """ Prepares a response dictionary - + :param result: The result of method call :return: A JSON-RPC response dictionary """ @@ -652,7 +660,7 @@ def response(self, result=None): def error(self, code=-32000, message='Server error.'): """ Prepares an error dictionary - + :param code: Error code :param message: Error message :return: A JSON-RPC error dictionary @@ -671,7 +679,7 @@ def dump(params=[], methodname=None, rpcid=None, version=None, is_response=None, is_notify=None): """ Prepares a JSON-RPC dictionary (request, notification, response or error) - + :param params: Method parameters (if a method name is given) or a Fault :param methodname: Method name :param rpcid: Request ID @@ -730,7 +738,7 @@ def dumps(params=[], methodname=None, methodresponse=None, encoding=None, rpcid=None, version=None, notify=None): """ Prepares a JSON-RPC request/response string - + :param params: Method parameters (if a method name is given) or a Fault :param methodname: Method name :param methodresponse: If True, this is a response dictionary @@ -754,7 +762,7 @@ def dumps(params=[], methodname=None, methodresponse=None, def load(data): """ Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans - + :param data: A JSON-RPC dictionary :return: A parsed dictionary or None """ @@ -775,7 +783,7 @@ def load(data): def loads(data): """ Loads a JSON-RPC request/response string. Calls jsonclass to load beans - + :param data: A JSON-RPC string :return: A parsed dictionary or None """ @@ -794,7 +802,7 @@ def loads(data): def check_for_errors(result): """ Checks if a result dictionary signals an error - + :param result: A result dictionary :raise TypeError: Invalid parameter :raise NotImplementedError: Unknown JSON-RPC version @@ -876,7 +884,7 @@ def isbatch(result): def isnotification(request): """ Tests if the given request is a notification - + :param request: A request dictionary :return: True if the request is a notification """ diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py index fc9b306..71c95ac 100644 --- a/jsonrpclib/utils.py +++ b/jsonrpclib/utils.py @@ -4,10 +4,16 @@ Utility methods, for compatibility between Python version :author: Thomas Calmant +:license: Apache License 2.0 :version: 1.0.1 """ -__version__ = "1.0.1" +# Module version +__version_info__ = (1, 0, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" # ------------------------------------------------------------------------------ From 3fe6b2c0b7a76a1fe76983008dc3389cb6d20b97 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 10:39:10 +0200 Subject: [PATCH 41/58] Python 3 imports in a try-except block --- jsonrpclib/SimpleJSONRPCServer.py | 23 ++++++++++++----------- jsonrpclib/jsonrpc.py | 20 ++++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 2098d1e..880832f 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -27,22 +27,23 @@ import traceback try: - import fcntl -except ImportError: - # For Windows - fcntl = None - -# ------------------------------------------------------------------------------ + # Python 3 + import xmlrpc.server as xmlrpcserver + import socketserver -if sys.version_info[0] < 3: +except ImportError: # Python 2 import SimpleXMLRPCServer as xmlrpcserver import SocketServer as socketserver -else: - # Python 3 - import xmlrpc.server as xmlrpcserver - import socketserver + +try: + # Windows + import fcntl + +except ImportError: + # Others + fcntl = None # ------------------------------------------------------------------------------ diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 7370e34..f24d615 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -69,16 +69,7 @@ import sys import uuid -if sys.version_info[0] < 3: - # Python 2 - from urllib import splittype - from urllib import splithost - 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 - -else: +try: # Python 3 from urllib.parse import splittype from urllib.parse import splithost @@ -87,6 +78,15 @@ from xmlrpc.client import ServerProxy as XMLServerProxy from xmlrpc.client import _Method as XML_Method +except ImportError: + # Python 2 + from urllib import splittype + from urllib import splithost + 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 + # ------------------------------------------------------------------------------ # JSON library import From 2d37bb860ab3232236c35a2c3b3b4dd5924ebe72 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 11:46:15 +0200 Subject: [PATCH 42/58] Raise a TranslationError if failing to instantiate an object --- jsonrpclib/jsonclass.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 89bbe75..d2aed89 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -188,10 +188,20 @@ def load(obj): # Create the object new_obj = None if isinstance(params, utils.ListType): - new_obj = json_class(*params) + try: + new_obj = json_class(*params) + + except TypeError as ex: + raise TranslationError("Error instantiating {0}: {1}"\ + .format(json_class.__name__, ex)) elif isinstance(params, utils.DictType): - new_obj = json_class(**params) + try: + new_obj = json_class(**params) + + except TypeError as ex: + raise TranslationError("Error instantiating {0}: {1}"\ + .format(json_class.__name__, ex)) else: raise TranslationError("Constructor args must be a dict or a list, " From fc134541763f2c7a34aee0ec172daa40a5a43862 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 14:30:44 +0200 Subject: [PATCH 43/58] Corrected tests on Python 2 Python 2 has an io module, messing with StringIO --- tests.py | 241 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 171 insertions(+), 70 deletions(-) diff --git a/tests.py b/tests.py index bbdea36..155d20a 100644 --- a/tests.py +++ b/tests.py @@ -1,52 +1,157 @@ +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- """ 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 +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: * Finish implementing JSON-RPC 2.0 Spec tests * Implement JSON-RPC 1.0 tests * Implement JSONClass, History, Config tests + +:license: Apache License 2.0 +:version: 1.0.0 """ +# Module version +__version_info__ = (1, 0, 0) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + +# jsonrpclib from jsonrpclib import Server, MultiCall, ProtocolError from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer -from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler from jsonrpclib.utils import from_bytes - -import socket -import unittest -import time import jsonrpclib.history +# Standard library import contextlib import re import sys +import threading +import time +import unittest -if sys.version_info[0] < 3: +try: + # Python 2 from StringIO import StringIO -else: +except ImportError: + # Python 3 from io import StringIO try: import json + except ImportError: import simplejson as json -from threading import Thread + +# ------------------------------------------------------------------------------ PORTS = list(range(8000, 8999)) +# ------------------------------------------------------------------------------ +# 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 + +# ------------------------------------------------------------------------------ +# Server utility class + +class UtilityServer(object): + """ + Utility start/stop server + """ + def __init__(self): + """ + Sets up members + """ + self._server = None + self._thread = None + + + def start(self, addr, port): + """ + Starts the server + + :param addr: A binding address + :param port: A listening port + :return: This object (for in-line calls) + """ + # Create the server + self._server = server = SimpleJSONRPCServer((addr, port), + logRequests=False) + + # Register test methods + server.register_function(summation, 'sum') + server.register_function(summation, 'notify_sum') + server.register_function(notify_hello) + server.register_function(subtract) + server.register_function(update) + server.register_function(get_data) + server.register_function(add) + server.register_function(ping) + server.register_function(summation, 'namespace.sum') + + # Serve in a thread + self._thread = threading.Thread(target=server.serve_forever) + self._thread.daemon = True + self._thread.start() + + # Allow an in-line instantiation + return self + + + def stop(self): + """ + Stops the server and waits for its thread to finish + """ + self._server.shutdown() + self._server.server_close() + self._thread.join() + + self._server = None + self._thread = None + +# ------------------------------------------------------------------------------ + class TestCompatibility(unittest.TestCase): client = None @@ -54,12 +159,30 @@ class TestCompatibility(unittest.TestCase): server = None def setUp(self): + """ + Pre-test set up + """ + # Set up the server self.port = PORTS.pop() + self.server = UtilityServer().start('', self.port) + + # Set up the client self.history = jsonrpclib.history.History() - self.server = server_set_up(addr=('', self.port)) self.client = Server('http://localhost:{0}'.format(self.port), history=self.history) + + def tearDown(self): + """ + Post-test clean up + """ + # Close the client + self.client("close") + + # Stop the server + self.server.stop() + + # v1 tests forthcoming # Version 2.0 Tests @@ -132,7 +255,7 @@ def test_non_existent_method(self): def test_invalid_json(self): invalid_json = '{"jsonrpc": "2.0", "method": "foobar, ' + \ '"params": "bar", "baz]' - response = self.client._run_request(invalid_json) + self.client._run_request(invalid_json) response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32700,' + @@ -143,7 +266,7 @@ def test_invalid_json(self): def test_invalid_request(self): invalid_request = '{"jsonrpc": "2.0", "method": 1, "params": "bar"}' - response = self.client._run_request(invalid_request) + self.client._run_request(invalid_request) response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32600, ' + @@ -155,7 +278,7 @@ def test_invalid_request(self): def test_batch_invalid_json(self): invalid_request = '[ {"jsonrpc": "2.0", "method": "sum", ' + \ '"params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]' - response = self.client._run_request(invalid_request) + self.client._run_request(invalid_request) response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32700,' + @@ -166,7 +289,7 @@ def test_batch_invalid_json(self): def test_empty_array(self): invalid_request = '[]' - response = self.client._run_request(invalid_request) + self.client._run_request(invalid_request) response = json.loads(self.history.response) verify_response = json.loads( '{"jsonrpc": "2.0", "error": {"code": -32600, ' + @@ -178,7 +301,7 @@ def test_empty_array(self): def test_nonempty_array(self): invalid_request = '[1,2]' request_obj = json.loads(invalid_request) - response = self.client._run_request(invalid_request) + self.client._run_request(invalid_request) response = json.loads(self.history.response) self.assertTrue(len(response) == len(request_obj)) for resp in response: @@ -208,7 +331,7 @@ def test_batch(self): {"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": "get_data", "id": "9"} ]""") # Thankfully, these are in order so testing is pretty simple. @@ -267,19 +390,32 @@ def test_batch_notifications(self): self.assertTrue(req == valid_req) self.assertTrue(self.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): + # Set up the server self.port = PORTS.pop() + self.server = UtilityServer().start('', self.port) + + # Prepare the client self.history = jsonrpclib.history.History() - self.server = server_set_up(addr=('', self.port)) + + + def tearDown(self): + """ + Post-test clean up + """ + # Stop the server + self.server.stop() + def get_client(self): return Server('http://localhost:{0}'.format(self.port), @@ -315,7 +451,7 @@ def test_single_notify(self): def test_single_namespace(self): client = self.get_client() - response = client.namespace.sum(1, 2, 4) + client.namespace.sum(1, 2, 4) request = json.loads(self.history.request) response = json.loads(self.history.response) verify_request = { @@ -361,12 +497,12 @@ def func(): return result[i] self.assertRaises(raises[i], func) +# ------------------------------------------------------------------------------ class HeadersTests(unittest.TestCase): """ These tests verify functionality of additional headers. """ - client = None server = None port = None @@ -376,8 +512,17 @@ def setUp(self): """ Sets up the test """ + # Set up the server self.port = PORTS.pop() - self.server = server_set_up(addr=('', self.port)) + self.server = UtilityServer().start('', self.port) + + + def tearDown(self): + """ + Post-test clean up + """ + # Stop the server + self.server.stop() @contextlib.contextmanager @@ -577,51 +722,7 @@ def test_should_allow_to_nest_additional_header_blocks(self): self.assertTrue('x-level-2' in headers2) self.assertEqual(headers2['x-level-2'], '2') - -""" 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. - def log_request(self, *args, **kwargs): - """ Making the server output 'quiet' """ - pass - SimpleJSONRPCRequestHandler.log_request = log_request - server = SimpleJSONRPCServer(addr, address_family=address_family) - server.register_function(summation, 'sum') - server.register_function(summation, 'notify_sum') - server.register_function(notify_hello) - server.register_function(subtract) - server.register_function(update) - server.register_function(get_data) - server.register_function(add) - server.register_function(ping) - server.register_function(summation, 'namespace.sum') - server_proc = Thread(target=server.serve_forever) - server_proc.daemon = True - server_proc.start() - return server_proc +# ------------------------------------------------------------------------------ if __name__ == '__main__': print("===============================================================") From e00eae4a835cb7ef9d1af90c26fba2f1054937b8 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 14:43:15 +0200 Subject: [PATCH 44/58] Added tox configuration --- .gitignore | 3 +++ tox.ini | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 6cff226..e741c39 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.pyc __pycache__ +# Tox +.tox + # When installed in develop mode *.egg-info/ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6fa376d --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py26,py27,py32,py33,pypy + +[testenv] +commands = nosetests +deps = + nose From e48938d3956e4eee9624e6a0ee12a290905f8fe8 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 14:46:49 +0200 Subject: [PATCH 45/58] Updated setup.py file --- setup.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index cd5415b..d4110ba 100755 --- a/setup.py +++ b/setup.py @@ -14,8 +14,18 @@ limitations under the License. """ +# Module version +__version_info__ = (0, 1, 6) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + try: from setuptools import setup + except ImportError: from distutils.core import setup @@ -23,16 +33,16 @@ setup( name="jsonrpclib-pelix", - version="0.1.5", + version=__version__, license="http://www.apache.org/licenses/LICENSE-2.0", author="Thomas Calmant", - author_email="thomas.calmant@gmail.com", + author_email="thomas.calmant+github@gmail.com", url="http://github.com/tcalmant/jsonrpclib/", download_url='https://github.com/tcalmant/jsonrpclib/archive/master.zip', - description="Fork of jsonrpclib by Josh Marshall, usable with Pelix " \ - "remote services." \ - "This project is an implementation of the JSON-RPC v2.0 " \ - "specification (backwards-compatible) as a client library.", + description="This project is an implementation of the JSON-RPC v2.0 " \ + "specification (backwards-compatible) as a client library. " \ + "Fork of jsonrpclib by Josh Marshall, usable with Pelix " \ + "remote services.", long_description=open("README.rst").read(), packages=["jsonrpclib"], classifiers=[ From da75a402e9fbdae05c37129b2689a47ab963d508 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 14:50:35 +0200 Subject: [PATCH 46/58] Added the Apache License header to all files --- jsonrpclib/SimpleJSONRPCServer.py | 12 ++++++++++++ jsonrpclib/config.py | 12 ++++++++++++ jsonrpclib/history.py | 12 ++++++++++++ jsonrpclib/jsonclass.py | 12 ++++++++++++ jsonrpclib/utils.py | 12 ++++++++++++ tests.py | 13 +++++++++++++ 6 files changed, 73 insertions(+) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 880832f..876780f 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -4,6 +4,18 @@ Defines a request dispatcher, a HTTP request handler, a HTTP server and a CGI request handler. +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 + +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. + :license: Apache License 2.0 :version: 0.1.5 """ diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index a4796f2..1b9ed3f 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -3,6 +3,18 @@ """ The configuration module. +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 + +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. + :license: Apache License 2.0 :version: 0.1.5 """ diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index fdb57a7..3cfea05 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -3,6 +3,18 @@ """ The history module. +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 + +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. + :license: Apache License 2.0 :version: 0.1.5 """ diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index d2aed89..70457fb 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -3,6 +3,18 @@ """ The serialization module +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 + +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. + :license: Apache License 2.0 :version: 0.1.6 """ diff --git a/jsonrpclib/utils.py b/jsonrpclib/utils.py index 71c95ac..ef7c957 100644 --- a/jsonrpclib/utils.py +++ b/jsonrpclib/utils.py @@ -3,6 +3,18 @@ """ Utility methods, for compatibility between Python version +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 + +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. + :author: Thomas Calmant :license: Apache License 2.0 :version: 1.0.1 diff --git a/tests.py b/tests.py index 155d20a..ffc8f91 100644 --- a/tests.py +++ b/tests.py @@ -20,6 +20,19 @@ * Implement JSON-RPC 1.0 tests * Implement JSONClass, History, Config tests + +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 + +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. + :license: Apache License 2.0 :version: 1.0.0 """ From 5b00e4300c62e3fd8d20d413e6a6dff1af2fd5cf Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 16:43:26 +0200 Subject: [PATCH 47/58] Config is not a singleton anymore - Multiple clients/servers can have different configurations - Still a (shared) default configuration object: jsonrpclib.config.DEFAULT - Previous API should still work as-is: the new parameters have always been added in last position, with the DEFAULT instance by default. - The version all modified modules has been increased to 0.1.6 --- jsonrpclib/SimpleJSONRPCServer.py | 85 ++++++++++++++++++--------- jsonrpclib/__init__.py | 4 -- jsonrpclib/config.py | 81 ++++++++++++++------------ jsonrpclib/jsonclass.py | 16 ++++-- jsonrpclib/jsonrpc.py | 95 +++++++++++++++++++++---------- 5 files changed, 178 insertions(+), 103 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 876780f..6ded78f 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -17,11 +17,11 @@ limitations under the License. :license: Apache License 2.0 -:version: 0.1.5 +:version: 0.1.6 """ # Module version -__version_info__ = (0, 1, 5) +__version_info__ = (0, 1, 6) __version__ = ".".join(str(x) for x in __version_info__) # Documentation strings format @@ -29,7 +29,7 @@ # ------------------------------------------------------------------------------ # Local modules -from jsonrpclib import Fault, config +from jsonrpclib import Fault import jsonrpclib import jsonrpclib.utils as utils @@ -48,7 +48,6 @@ import SimpleXMLRPCServer as xmlrpcserver import SocketServer as socketserver - try: # Windows import fcntl @@ -75,17 +74,19 @@ def get_version(request): return None -def validate_request(request): +def validate_request(request, json_config): """ Validates the format of a request dictionary :param request: A request dictionary + :param json_config: A JSONRPClib Config instance :return: True if the dictionary is valid, else a Fault object """ if not isinstance(request, utils.DictType): # Invalid request type return Fault(-32600, 'Request must be a dict, not {0}' \ - .format(type(request).__name__)) + .format(type(request).__name__), + config=json_config) # Get the request ID rpcid = request.get('id', None) @@ -94,7 +95,7 @@ def validate_request(request): version = get_version(request) if not version: return Fault(-32600, 'Request {0} invalid.'.format(request), - rpcid=rpcid) + rpcid=rpcid, config=json_config) # Default parameters: empty list request.setdefault('params', []) @@ -108,7 +109,7 @@ def validate_request(request): not isinstance(params, param_types): # Invalid type of method name or parameters return Fault(-32600, 'Invalid request parameters or method.', - rpcid=rpcid) + rpcid=rpcid, config=json_config) # Valid request return True @@ -124,7 +125,7 @@ class NoMulticallResult(Exception): class SimpleJSONRPCDispatcher(xmlrpcserver.SimpleXMLRPCDispatcher): - def __init__(self, encoding=None): + def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): """ Sets up the dispatcher with the given encoding. None values are allowed. @@ -133,9 +134,11 @@ def __init__(self, encoding=None): # Default encoding encoding = "UTF-8" + xmlrpcserver.SimpleXMLRPCDispatcher.__init__(self, allow_none=True, encoding=encoding) + self.json_config = config def _unmarshaled_dispatch(self, request, dispatch_method=None): @@ -151,7 +154,8 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): """ if not request: # Invalid request dictionary - fault = Fault(-32600, 'Request invalid -- no request data.') + fault = Fault(-32600, 'Request invalid -- no request data.', + config=self.json_config) return fault.dump() if type(request) is utils.ListType: @@ -159,7 +163,7 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): responses = [] for req_entry in request: # Validate the request - result = validate_request(req_entry) + result = validate_request(req_entry, self.json_config) if type(result) is Fault: responses.append(result.dump()) continue @@ -183,7 +187,7 @@ def _unmarshaled_dispatch(self, request, dispatch_method=None): else: # Single call - result = validate_request(request) + result = validate_request(request, self.json_config) if type(result) is Fault: return result.dump() @@ -207,12 +211,13 @@ def _marshaled_dispatch(self, data, dispatch_method=None): """ # Parse the request try: - request = jsonrpclib.loads(data) + request = jsonrpclib.loads(data, self.json_config) except Exception as ex: # Parsing/loading error fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})' \ - .format(data, type(ex).__name__, ex)) + .format(data, type(ex).__name__, ex), + config=self.json_config) return fault.response() # Get the response dictionary @@ -255,7 +260,8 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): except: # Return a fault exc_type, exc_value, _ = sys.exc_info() - fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value)) + fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value), + config=self.json_config) return fault.dump() if 'id' not in request or request['id'] in (None, ''): @@ -266,12 +272,13 @@ def _marshaled_single_dispatch(self, request, dispatch_method=None): # Prepare a JSON-RPC dictionary try: return jsonrpclib.dump(response, rpcid=request['id'], - is_response=True) + is_response=True, config=self.json_config) except: # JSON conversion exception exc_type, exc_value, _ = sys.exc_info() - fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value)) + fault = Fault(-32603, '{0}:{1}'.format(exc_type, exc_value), + config=self.json_config) return fault.dump() @@ -315,23 +322,29 @@ def _dispatch(self, method, params): except TypeError as ex: # Maybe the parameters are wrong - return Fault(-32602, 'Invalid parameters: {0}'.format(ex)) + return Fault(-32602, 'Invalid parameters: {0}'.format(ex), + config=self.json_config) except: # Method exception err_lines = traceback.format_exc().splitlines() trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1]) - return Fault(-32603, 'Server error: {0}'.format(trace_string)) + return Fault(-32603, 'Server error: {0}'.format(trace_string), + config=self.json_config) else: # Unknown method - return Fault(-32601, 'Method {0} not supported.'.format(method)) + return Fault(-32601, 'Method {0} not supported.'.format(method), + config=self.json_config) # ------------------------------------------------------------------------------ class SimpleJSONRPCRequestHandler(xmlrpcserver.SimpleXMLRPCRequestHandler): """ - HTTP server request handler + HTTP request handler. + + The server that receives the requests must have a json_config member, + containing a JSONRPClib Config instance """ def do_POST(self): """ @@ -341,6 +354,9 @@ def do_POST(self): self.report_404() return + # Retrieve the configuration + config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT) + try: # Read the request body max_chunk_size = 10 * 1024 * 1024 @@ -364,7 +380,7 @@ def do_POST(self): err_lines = traceback.format_exc().splitlines() trace_string = '{0} | {1}'.format(err_lines[-3], err_lines[-1]) fault = jsonrpclib.Fault(-32603, 'Server error: {0}'\ - .format(trace_string)) + .format(trace_string), config=config) response = fault.response() if response is None: @@ -393,7 +409,8 @@ class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, logRequests=True, encoding=None, bind_and_activate=True, - address_family=socket.AF_INET): + address_family=socket.AF_INET, + config=jsonrpclib.config.DEFAULT): """ Sets up the server and the dispatcher @@ -403,13 +420,24 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, :param encoding: The dispatcher request encoding :param bind_and_activate: If True, starts the server immediately :param address_family: The server listening address family + :param config: A JSONRPClib Config instance """ # Set up the dispatcher fields - SimpleJSONRPCDispatcher.__init__(self, encoding) + SimpleJSONRPCDispatcher.__init__(self, encoding, config) # Prepare the server configuration self.logRequests = logRequests self.address_family = address_family + self.json_config = config + + # Work on the request handler + class RequestHandlerWrapper(requestHandler): + def __init__(self, *args, **kwargs): + """ + Constructs the wrapper after having stored the configuration + """ + self.config = config + super(RequestHandlerWrapper, self).__init__(*args, **kwargs) # Set up the server socketserver.TCPServer.__init__(self, addr, requestHandler, @@ -427,20 +455,23 @@ class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): """ JSON-RPC CGI handler (and dispatcher) """ - def __init__(self, encoding=None): + def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): """ Sets up the dispatcher :param encoding: Dispatcher encoding + :param config: A JSONRPClib Config instance """ - SimpleJSONRPCDispatcher.__init__(self, encoding) + SimpleJSONRPCDispatcher.__init__(self, encoding, config) + def handle_jsonrpc(self, request_text): """ Handle a JSON-RPC request """ response = self._marshaled_dispatch(request_text) - sys.stdout.write('Content-Type: {0}\r\n'.format(config.content_type)) + sys.stdout.write('Content-Type: {0}\r\n' \ + .format(self.json_config.content_type)) sys.stdout.write('Content-Length: {0:d}\r\n'.format(len(response))) sys.stdout.write('\r\n') sys.stdout.write(response) diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 223ee18..9c66eb2 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -6,10 +6,6 @@ :license: Apache License 2.0 """ -# Create a configuration instance -from jsonrpclib.config import Config -config = Config.instance() - # Easy access to utility methods and classes from jsonrpclib.jsonrpc import Server, ServerProxy from jsonrpclib.jsonrpc import MultiCall, Fault, ProtocolError, AppError diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 1b9ed3f..d095359 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -16,11 +16,11 @@ limitations under the License. :license: Apache License 2.0 -:version: 0.1.5 +:version: 0.1.6 """ # Module version -__version_info__ = (0, 1, 5) +__version_info__ = (0, 1, 6) __version__ = ".".join(str(x) for x in __version_info__) # Documentation strings format @@ -53,45 +53,56 @@ class Config(object): You can change serialize_method and ignore_attribute, or use the local_classes.add(class) to include "local" classes. """ - # Change to False to keep __jsonclass__ entries raw. - use_jsonclass = True - - # The serialize_method should be a string that references the - # method on a custom class object which is responsible for - # returning a tuple of the constructor arguments and a dict of - # attributes. - serialize_method = '_serialize' - - # The ignore attribute should be a string that references the - # attribute on a custom class object which holds strings and / or - # references of the attributes the class translator should ignore. - ignore_attribute = '_ignore' + def __init__(self, version=2.0, content_type="application/json-rpc", + user_agent=None, use_jsonclass=True, + serialize_method='_serialize', + ignore_attribute='_ignore'): + """ + Sets up a configuration of JSONRPClib + + :param version: JSON-RPC specification version + :param content_type: HTTP content type header value + :param user_agent: The HTTP request user agent + :param use_jsonclass: Allow bean marshalling + :param serialize_method: A string that references the method on a + custom class object which is responsible for + returning a tuple of the arguments and a dict + of attributes. + :param ignore_attribute: A string that references the attribute on a + custom class object which holds strings and/or + references of the attributes the class + translator should ignore. + """ + # JSON-RPC specification + self.version = version - # The list of classes to use for jsonclass translation. - classes = LocalClasses() + # Change to False to keep __jsonclass__ entries raw. + self.use_jsonclass = use_jsonclass - # Version of the JSON-RPC specification to support - version = 2.0 + # it SHOULD be 'application/json-rpc' + # but MAY be 'application/json' or 'application/jsonrequest' + self.content_type = content_type - # User agent to use for calls. - user_agent = 'jsonrpclib-pelix/{0} (Python {1})' \ + # Default user agent + if user_agent is None: + user_agent = 'jsonrpclib/{0} (Python {1})' \ .format(__version__, '.'.join(str(ver) for ver in sys.version_info[0:3])) + self.user_agent = user_agent - # Content-type to use. According to the JSON-RPC specification, - # it SHOULD be 'application/json-rpc' - # but MAY be 'application/json' or 'application/jsonrequest' - content_type = "application/json-rpc" + # The list of classes to use for jsonclass translation. + self.classes = LocalClasses() - # "Singleton" of Config - _instance = None + # The serialize_method should be a string that references the + # method on a custom class object which is responsible for + # returning a tuple of the constructor arguments and a dict of + # attributes. + self.serialize_method = serialize_method - @classmethod - def instance(cls): - """ - Returns/Creates the instance of Config - """ - if not cls._instance: - cls._instance = cls() + # The ignore attribute should be a string that references the + # attribute on a custom class object which holds strings and / or + # references of the attributes the class translator should ignore. + self.ignore_attribute = ignore_attribute - return cls._instance +# Default configuration +DEFAULT = Config() diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 70457fb..7ac179f 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -29,7 +29,7 @@ # ------------------------------------------------------------------------------ # Local package -from jsonrpclib import config +import jsonrpclib.config import jsonrpclib.utils as utils # Standard library @@ -55,7 +55,8 @@ class TranslationError(Exception): # ------------------------------------------------------------------------------ -def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): +def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[], + config=jsonrpclib.config.DEFAULT): """ Transforms the given object into a JSON-RPC compliant form. Converts beans into dictionaries with a __jsonclass__ entry. @@ -66,6 +67,7 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): :param ignore_attribute: Name of the object attribute containing the names of members to ignore :param ignore: A list of members to ignore + :param config: A JSONRPClib Config instance :return: A JSON-RPC compliant object """ if not serialize_method: @@ -128,12 +130,13 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): return return_obj -def load(obj): +def load(obj, classes=None): """ If 'obj' is a dictionary containing a __jsonclass__ entry, converts the dictionary item into a bean of this class. :param obj: An object from a JSON-RPC dictionary + :param classes: A custom {name: class} dictionary :return: The loaded object """ # Primitive @@ -171,12 +174,13 @@ def load(obj): # Load the class json_module_parts = json_module_clean.split('.') json_class = None - if len(json_module_parts) == 1: + if classes and len(json_module_parts) == 1: # Local class name -- probably means it won't work - if json_module_parts[0] not in config.classes.keys(): + try: + json_class = classes[json_module_parts[0]] + except KeyError: raise TranslationError('Unknown class or module {0}.' \ .format(json_module_parts[0])) - json_class = config.classes[json_module_parts[0]] else: # Module + class diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index f24d615..5cf1d82 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -48,11 +48,11 @@ See https://github.com/tcalmant/jsonrpclib for more info. :license: Apache License 2.0 -:version: 0.1.5 +:version: 0.1.6 """ # Module version -__version_info__ = (0, 1, 5) +__version_info__ = (0, 1, 6) __version__ = ".".join(str(x) for x in __version_info__) # Documentation strings format @@ -61,7 +61,7 @@ # ------------------------------------------------------------------------------ # Library includes -from jsonrpclib import config +import jsonrpclib.config import jsonrpclib.utils as utils # Standard library @@ -162,7 +162,6 @@ def data(self): class TransportMixIn(object): """ Just extends the XMLRPC transport where necessary. """ - user_agent = config.user_agent # for Python 2.7 support _connection = None @@ -173,6 +172,18 @@ class TransportMixIn(object): # Use the configuration to change the content-type readonly_headers = ('content-length', 'content-type') + def __init__(self, config=jsonrpclib.config.DEFAULT): + """ + Sets up the transport + + :param config: A JSONRPClib Config instance + """ + # Store the configuration + self._config = config + + # Set up the user agent + self.user_agent = config.user_agent + def push_headers(self, headers): """ Adds a dictionary of headers to the additional headers list @@ -224,7 +235,7 @@ def send_content(self, connection, request_body): request_body = utils.to_bytes(request_body) # "static" headers - connection.putheader("Content-Type", config.content_type) + connection.putheader("Content-Type", self._config.content_type) connection.putheader("Content-Length", str(len(request_body))) # Emit additional headers here in order not to override content-length @@ -286,7 +297,8 @@ class ServerProxy(XMLServerProxy): """ def __init__(self, uri, transport=None, encoding=None, - verbose=0, version=None, headers=None, history=None): + verbose=0, version=None, headers=None, history=None, + config=jsonrpclib.config.DEFAULT): """ Sets up the server proxy @@ -297,7 +309,11 @@ def __init__(self, uri, transport=None, encoding=None, :param version: JSON-RPC specification version :param headers: Custom additional headers for each request :param history: History object (for tests) + :param config: A JSONRPClib Config instance """ + # Store the configuration + self._config = config + if not version: version = config.version self.__version = version @@ -313,9 +329,9 @@ def __init__(self, uri, transport=None, encoding=None, if transport is None: if schema == 'https': - transport = SafeTransport() + transport = SafeTransport(config=config) else: - transport = Transport() + transport = Transport(config=config) self.__transport = transport self.__encoding = encoding @@ -327,14 +343,16 @@ def __init__(self, uri, transport=None, encoding=None, def _request(self, methodname, params, rpcid=None): request = dumps(params, methodname, encoding=self.__encoding, - rpcid=rpcid, version=self.__version) + rpcid=rpcid, version=self.__version, + config=self._config) response = self._run_request(request) check_for_errors(response) return response['result'] def _request_notify(self, methodname, params, rpcid=None): request = dumps(params, methodname, encoding=self.__encoding, - rpcid=rpcid, version=self.__version, notify=True) + rpcid=rpcid, version=self.__version, notify=True, + config=self._config) response = self._run_request(request, notify=True) check_for_errors(response) return @@ -361,7 +379,7 @@ def _run_request(self, request, notify=None): if not response: return None - return_obj = loads(response) + return_obj = loads(response, self._config) return return_obj def __getattr__(self, name): @@ -423,10 +441,11 @@ def __getattr__(self, name): class MultiCallMethod(object): - def __init__(self, method, notify=False): + def __init__(self, method, notify=False, config=jsonrpclib.config.DEFAULT): self.method = method self.params = [] self.notify = notify + self._config = config def __call__(self, *args, **kwargs): if len(kwargs) > 0 and len(args) > 0: @@ -439,7 +458,8 @@ def __call__(self, *args, **kwargs): def request(self, encoding=None, rpcid=None): return dumps(self.params, self.method, version=2.0, - encoding=encoding, rpcid=rpcid, notify=self.notify) + encoding=encoding, rpcid=rpcid, notify=self.notify, + config=self._config) def __repr__(self): return '%s' % self.request() @@ -451,11 +471,12 @@ def __getattr__(self, method): class MultiCallNotify(object): - def __init__(self, multicall): + def __init__(self, multicall, config=jsonrpclib.config.DEFAULT): self.multicall = multicall + self._config = config def __getattr__(self, name): - new_job = MultiCallMethod(name, notify=True) + new_job = MultiCallMethod(name, notify=True, config=self._config) self.multicall._job_list.append(new_job) return new_job @@ -479,9 +500,10 @@ def __len__(self): class MultiCall(object): - def __init__(self, server): + def __init__(self, server, config=jsonrpclib.config.DEFAULT): self._server = server self._job_list = [] + self._config = config def _request(self): if len(self._job_list) < 1: @@ -497,10 +519,10 @@ def _request(self): @property def _notify(self): - return MultiCallNotify(self) + return MultiCallNotify(self, self._config) def __getattr__(self, name): - new_job = MultiCallMethod(name) + new_job = MultiCallMethod(name, config=self._config) self._job_list.append(new_job) return new_job @@ -516,17 +538,20 @@ class Fault(object): """ JSON-RPC error class """ - def __init__(self, code=-32000, message='Server error', rpcid=None): + def __init__(self, code=-32000, message='Server error', rpcid=None, + config=jsonrpclib.config.DEFAULT): """ Sets up the error description :param code: Fault code :param message: Associated message :param rpcid: Request ID + :param config: A JSONRPClib Config instance """ self.faultCode = code self.faultString = message self.rpcid = rpcid + self.config = config def error(self): """ @@ -545,13 +570,13 @@ def response(self, rpcid=None, version=None): :return: A JSON-RPC response string """ if not version: - version = config.version + version = self.config.version if rpcid: self.rpcid = rpcid return dumps(self, methodresponse=True, rpcid=self.rpcid, - version=version) + version=version, config=self.config) def dump(self, rpcid=None, version=None): """ @@ -562,13 +587,13 @@ def dump(self, rpcid=None, version=None): :return: A JSON-RPC response dictionary """ if not version: - version = config.version + version = self.config.version if rpcid: self.rpcid = rpcid return dump(self, is_response=True, rpcid=self.rpcid, - version=version) + version=version, config=self.config) def __repr__(self): """ @@ -581,12 +606,14 @@ class Payload(object): """ JSON-RPC content handler """ - def __init__(self, rpcid=None, version=None): + def __init__(self, rpcid=None, version=None, + config=jsonrpclib.config.DEFAULT): """ Sets up the JSON-RPC handler :param rpcid: Request ID :param version: JSON-RPC version + :param config: A JSONRPClib Config instance """ if not version: version = config.version @@ -676,7 +703,7 @@ def error(self, code=-32000, message='Server error.'): # ------------------------------------------------------------------------------ def dump(params=[], methodname=None, rpcid=None, version=None, - is_response=None, is_notify=None): + is_response=None, is_notify=None, config=jsonrpclib.config.DEFAULT): """ Prepares a JSON-RPC dictionary (request, notification, response or error) @@ -686,6 +713,7 @@ def dump(params=[], methodname=None, rpcid=None, version=None, :param version: JSON-RPC version :param is_response: If True, this is a response dictionary :param is_notify: If True, this is a notification request + :param config: A JSONRPClib Config instance :return: A JSON-RPC dictionary """ # Default version @@ -735,7 +763,8 @@ def dump(params=[], methodname=None, rpcid=None, version=None, def dumps(params=[], methodname=None, methodresponse=None, - encoding=None, rpcid=None, version=None, notify=None): + encoding=None, rpcid=None, version=None, notify=None, + config=jsonrpclib.config.DEFAULT): """ Prepares a JSON-RPC request/response string @@ -746,10 +775,12 @@ def dumps(params=[], methodname=None, methodresponse=None, :param rpcid: Request ID :param version: JSON-RPC version :param notify: If True, this is a notification request + :param config: A JSONRPClib Config instance :return: A JSON-RPC dictionary """ # Prepare the dictionary - request = dump(params, methodname, rpcid, version, methodresponse, notify) + request = dump(params, methodname, rpcid, version, methodresponse, notify, + config) # Set the default encoding if not encoding: @@ -759,11 +790,12 @@ def dumps(params=[], methodname=None, methodresponse=None, return jdumps(request, encoding=encoding) -def load(data): +def load(data, config=jsonrpclib.config.DEFAULT): """ Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans :param data: A JSON-RPC dictionary + :param config: A JSONRPClib Config instance (or None for default values) :return: A parsed dictionary or None """ if data is None: @@ -775,16 +807,17 @@ def load(data): # { 'jsonrpc':'2.0', 'error': fault.error(), id: None } if config.use_jsonclass: # Convert beans - data = jsonclass.load(data) + data = jsonclass.load(data, config.classes) return data -def loads(data): +def loads(data, config=jsonrpclib.config.DEFAULT): """ Loads a JSON-RPC request/response string. Calls jsonclass to load beans :param data: A JSON-RPC string + :param config: A JSONRPClib Config instance (or None for default values) :return: A parsed dictionary or None """ if data == '': @@ -795,7 +828,7 @@ def loads(data): result = jloads(data) # Load the beans - return load(result) + return load(result, config) # ------------------------------------------------------------------------------ From 4fba6878c8e7e714ec4773b340c1fbaf69636835 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Mon, 14 Oct 2013 16:49:20 +0200 Subject: [PATCH 48/58] Renamed the Eclipse project: jsonrpclib-pelix --- .project | 2 +- .pydevproject | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.project b/.project index ed51de6..96b0cfe 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - jsonrpclib-patched + jsonrpclib-pelix diff --git a/.pydevproject b/.pydevproject index c0f34ac..270437d 100644 --- a/.pydevproject +++ b/.pydevproject @@ -1,10 +1,8 @@ - - - + Default python 2.7 -/jsonrpclib-patched +/jsonrpclib-pelix From e9902aed809df6f7458a00c3581f6c3d2195a72e Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Tue, 22 Oct 2013 12:00:05 +0200 Subject: [PATCH 49/58] Recursive loading of bean fields Fields of a bean loaded by jsonclass are also loaded through the same method. This allows to have beans containing beans. --- jsonrpclib/jsonclass.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 7ac179f..134498a 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -145,20 +145,14 @@ def load(obj, classes=None): # List, set or tuple elif isinstance(obj, utils.iterable_types): - return_obj = [load(entry) for entry in obj] - if isinstance(obj, utils.TupleType): - return_obj = tuple(return_obj) - - return return_obj + # This comes from a JSON parser, so it can only be a list... + return [load(entry) for entry in obj] # Otherwise, it's a dict type elif '__jsonclass__' not in obj.keys(): - return_dict = {} - for key, value in obj.items(): - return_dict[key] = load(value) - return return_dict + return dict((key, load(value)) for key, value in obj.items()) - # It's a dict, and it has a __jsonclass__ + # It's a dictionary, and it has a __jsonclass__ orig_module_name = obj['__jsonclass__'][0] params = obj['__jsonclass__'][1] @@ -228,6 +222,7 @@ def load(obj, classes=None): del obj['__jsonclass__'] for key, value in obj.items(): - setattr(new_obj, key, value) + # Recursive loading + setattr(new_obj, key, load(value, classes)) return new_obj From ad0ed6e4f9e67745ef96fc9d76620e15c5fa2c54 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 25 Oct 2013 11:30:30 +0200 Subject: [PATCH 50/58] Added Travis-CI configuration file Based on Tox, same as the iPOPO Travis configuration file. --- .travis.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..86594dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +# python: +# - "3.3" +# - "3.2" +# - "2.7" +# - "2.6" + +before_install: + - pip install tox + - sudo apt-get install python2.6-dev python2.7-dev python3.2-dev python3.3-dev + +script: tox -e py26,py27,py32,py33 + From c1508c93eb754f34bd19a1173c0a922291e50719 Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 25 Oct 2013 11:57:43 +0200 Subject: [PATCH 51/58] Added __call__ and __close methods to ServerProxy - Allows to use the 'client("close")()' method to close the transport layer of a ServerProxy, even on Python 2.6 => Fixes #2 - Updated tests to _really_ close the transport layer (and not just to retrieve the closing method) --- jsonrpclib/jsonrpc.py | 23 +++++++++++++++++++++++ tests.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 5cf1d82..d437d73 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -386,6 +386,29 @@ def __getattr__(self, name): # Same as original, just with new _Method reference return _Method(self._request, name) + def __close(self): + """ + Closes the transport layer + """ + self.__transport.close() + + + def __call__(self, attr): + """ + A workaround to get special attributes on the ServerProxy + without interfering with the magic __getattr__ + + (code from xmlrpclib in Python 2.7) + """ + if attr == "close": + return self.__close + + elif attr == "transport": + return self.__transport + + raise AttributeError("Attribute {0} not found".format(attr)) + + @property def _notify(self): # Just like __getattr__, but with notify namespace. diff --git a/tests.py b/tests.py index ffc8f91..14f2002 100644 --- a/tests.py +++ b/tests.py @@ -190,7 +190,7 @@ def tearDown(self): Post-test clean up """ # Close the client - self.client("close") + self.client("close")() # Stop the server self.server.stop() From d221c56e51d50c595aa077237897e949f59199fd Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 25 Oct 2013 12:08:59 +0200 Subject: [PATCH 52/58] XMLTransport.close() was also added in Python 2.7 Finally fixes #2 --- jsonrpclib/jsonrpc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index d437d73..3c25070 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -390,7 +390,12 @@ def __close(self): """ Closes the transport layer """ - self.__transport.close() + try: + self.__transport.close() + + except AttributeError: + # Not available in Python 2.6 + pass def __call__(self, attr): From 7ba842c915e58985b8e5c88fdbe6d651681ee33a Mon Sep 17 00:00:00 2001 From: Thomas Calmant Date: Fri, 25 Oct 2013 15:03:36 +0200 Subject: [PATCH 53/58] Version increment, related to issue #2 Also added supported Python versions to setup.py --- jsonrpclib/jsonrpc.py | 4 ++-- setup.py | 51 +++++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index 3c25070..172d7aa 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -48,11 +48,11 @@ See https://github.com/tcalmant/jsonrpclib for more info. :license: Apache License 2.0 -:version: 0.1.6 +:version: 0.1.6.1 """ # Module version -__version_info__ = (0, 1, 6) +__version_info__ = (0, 1, 6, 1) __version__ = ".".join(str(x) for x in __version_info__) # Documentation strings format diff --git a/setup.py b/setup.py index d4110ba..8eb86fa 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ """ # Module version -__version_info__ = (0, 1, 6) +__version_info__ = (0, 1, 6, 1) __version__ = ".".join(str(x) for x in __version_info__) # Documentation strings format @@ -31,26 +31,29 @@ # ------------------------------------------------------------------------------ -setup( - name="jsonrpclib-pelix", - version=__version__, - license="http://www.apache.org/licenses/LICENSE-2.0", - author="Thomas Calmant", - author_email="thomas.calmant+github@gmail.com", - url="http://github.com/tcalmant/jsonrpclib/", - download_url='https://github.com/tcalmant/jsonrpclib/archive/master.zip', - description="This project is an implementation of the JSON-RPC v2.0 " \ - "specification (backwards-compatible) as a client library. " \ - "Fork of jsonrpclib by Josh Marshall, usable with Pelix " \ - "remote services.", - long_description=open("README.rst").read(), - packages=["jsonrpclib"], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3' - ] -) +setup(name="jsonrpclib-pelix", + version=__version__, + license="http://www.apache.org/licenses/LICENSE-2.0", + author="Thomas Calmant", + author_email="thomas.calmant+github@gmail.com", + url="http://github.com/tcalmant/jsonrpclib/", + download_url='https://github.com/tcalmant/jsonrpclib/archive/master.zip', + description="This project is an implementation of the JSON-RPC v2.0 " \ + "specification (backwards-compatible) as a client library. " \ + "This version is a fork of jsonrpclib by Josh Marshall, " \ + "usable with Pelix remote services.", + long_description=open("README.rst").read(), + packages=["jsonrpclib"], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.0', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + ]) From cdb8f911e56417a99a2e75ff7e6ea512dc7d8fb7 Mon Sep 17 00:00:00 2001 From: David Gilman Date: Fri, 25 Oct 2013 13:26:16 -0500 Subject: [PATCH 54/58] initial WSGI support that works on 2.7 --- jsonrpclib/SimpleJSONRPCServer.py | 30 ++++++++++++++++++ tests.py | 51 ++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 6ded78f..dbd5fcf 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -478,3 +478,33 @@ def handle_jsonrpc(self, request_text): # XML-RPC alias handle_xmlrpc = handle_jsonrpc + +# ------------------------------------------------------------------------------ + +class WSGIJSONRPCApp(SimpleJSONRPCDispatcher): + """ + WSGI compatible JSON-RPC dispatcher + """ + def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): + """ + Sets up the dispatcher + + :param encoding: Dispatcher encoding + :param config: A JSONRPClib Config instance + """ + SimpleJSONRPCDispatcher.__init__(self, encoding, config) + + def __call__(self, environ, start_response): + """ + WSGI application callable. See the WSGI spec + """ + try: + msg_len = int(environ.get('CONTENT_LENGTH', 0)) + except ValueError: + msg_len = 0 + msg = environ['wsgi.input'].read(msg_len) + response = self._marshaled_dispatch(msg).encode(self.encoding) + headers = [('Content-Type', self.json_config.content_type), + ('Content-Length', str(len(response)))] + start_response('200 OK', headers) + return (response,) diff --git a/tests.py b/tests.py index 14f2002..760a2d1 100644 --- a/tests.py +++ b/tests.py @@ -48,7 +48,7 @@ # jsonrpclib from jsonrpclib import Server, MultiCall, ProtocolError -from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, WSGIJSONRPCApp from jsonrpclib.utils import from_bytes import jsonrpclib.history @@ -59,6 +59,8 @@ import threading import time import unittest +import wsgiref.validate +import wsgiref.simple_server try: # Python 2 @@ -105,6 +107,19 @@ def get_data(): def ping(): return True +@staticmethod +def TCPJSONRPCServer(addr, port): + return SimpleJSONRPCServer((addr, port), logRequests=False) + +@staticmethod +def WSGIJSONRPCServer(addr, port): + class NoLogHandler(wsgiref.simple_server.WSGIRequestHandler): + def log_message(self, format, *args): + pass + + return wsgiref.simple_server.make_server(addr, port, WSGIJSONRPCApp(), + handler_class=NoLogHandler) + # ------------------------------------------------------------------------------ # Server utility class @@ -120,17 +135,19 @@ def __init__(self): self._thread = None - def start(self, addr, port): + def start(self, server_cls, addr, port): """ Starts the server + :param server_cls: Callable that returns a subclass of XMLRPCServer :param addr: A binding address :param port: A listening port :return: This object (for in-line calls) """ # Create the server - self._server = server = SimpleJSONRPCServer((addr, port), - logRequests=False) + self._server = server = server_cls(addr, port) + if hasattr(server, 'get_app'): + server = server.get_app() # Register test methods server.register_function(summation, 'sum') @@ -144,7 +161,7 @@ def start(self, addr, port): server.register_function(summation, 'namespace.sum') # Serve in a thread - self._thread = threading.Thread(target=server.serve_forever) + self._thread = threading.Thread(target=self._server.serve_forever) self._thread.daemon = True self._thread.start() @@ -170,6 +187,7 @@ class TestCompatibility(unittest.TestCase): client = None port = None server = None + server_cls = TCPJSONRPCServer def setUp(self): """ @@ -177,7 +195,7 @@ def setUp(self): """ # Set up the server self.port = PORTS.pop() - self.server = UtilityServer().start('', self.port) + self.server = UtilityServer().start(self.server_cls, '', self.port) # Set up the client self.history = jsonrpclib.history.History() @@ -405,6 +423,11 @@ def test_batch_notifications(self): # ------------------------------------------------------------------------------ +class WSGITestCompatibility(TestCompatibility): + server_cls = WSGIJSONRPCServer + +# ------------------------------------------------------------------------------ + class InternalTests(unittest.TestCase): """ These tests verify that the client and server portions of @@ -412,11 +435,12 @@ class InternalTests(unittest.TestCase): """ server = None port = None + server_cls = TCPJSONRPCServer def setUp(self): # Set up the server self.port = PORTS.pop() - self.server = UtilityServer().start('', self.port) + self.server = UtilityServer().start(self.server_cls, '', self.port) # Prepare the client self.history = jsonrpclib.history.History() @@ -512,12 +536,18 @@ def func(): # ------------------------------------------------------------------------------ +class WSGIInternalTests(InternalTests): + server_cls = WSGIJSONRPCServer + +# ------------------------------------------------------------------------------ + class HeadersTests(unittest.TestCase): """ These tests verify functionality of additional headers. """ server = None port = None + server_cls = TCPJSONRPCServer REQUEST_LINE = "^send: POST" @@ -527,7 +557,7 @@ def setUp(self): """ # Set up the server self.port = PORTS.pop() - self.server = UtilityServer().start('', self.port) + self.server = UtilityServer().start(self.server_cls, '', self.port) def tearDown(self): @@ -737,6 +767,11 @@ def test_should_allow_to_nest_additional_header_blocks(self): # ------------------------------------------------------------------------------ +class WSGIHeadersTests(HeadersTests): + server_cls = WSGIJSONRPCServer + +# ------------------------------------------------------------------------------ + if __name__ == '__main__': print("===============================================================") print(" NOTE: There may be threading exceptions after tests finish. ") From 675d7372fb026b857f5f3abaa470dab4f4434dba Mon Sep 17 00:00:00 2001 From: David Gilman Date: Fri, 25 Oct 2013 14:07:01 -0500 Subject: [PATCH 55/58] WSGI should decode incoming JSON Note that it seems that people don't declare the encoding in Content-Type so I'll just assume people want to use the encoding that the server uses internally :) --- jsonrpclib/SimpleJSONRPCServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index dbd5fcf..dc14d97 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -502,7 +502,7 @@ def __call__(self, environ, start_response): msg_len = int(environ.get('CONTENT_LENGTH', 0)) except ValueError: msg_len = 0 - msg = environ['wsgi.input'].read(msg_len) + msg = environ['wsgi.input'].read(msg_len).decode(self.encoding) response = self._marshaled_dispatch(msg).encode(self.encoding) headers = [('Content-Type', self.json_config.content_type), ('Content-Length', str(len(response)))] From 4626d8034d8691f4088556dbe20fb73208a26e8a Mon Sep 17 00:00:00 2001 From: Alfred Morgan Date: Thu, 6 Feb 2014 11:54:20 -0800 Subject: [PATCH 56/58] Added CGIXMLRPCRequestHandler as a super class. I couldn't get CGI to work without this super class. --- jsonrpclib/SimpleJSONRPCServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index d76da73..57d5705 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -214,7 +214,7 @@ def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, flags |= fcntl.FD_CLOEXEC fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) -class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): +class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher, SimpleXMLRPCServer.CGIXMLRPCRequestHandler): def __init__(self, encoding=None): SimpleJSONRPCDispatcher.__init__(self, encoding) From 25f1db605f719ea339eb597582aa44bff6f505d3 Mon Sep 17 00:00:00 2001 From: Alfred Morgan Date: Mon, 10 Feb 2014 22:45:15 -0800 Subject: [PATCH 57/58] Added Accept http header for the client --- jsonrpclib/jsonrpc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index e11939a..abb7b6e 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -117,6 +117,7 @@ class TransportMixIn(object): _connection = None def send_content(self, connection, request_body): + connection.putheader("Accept", "application/json-rpc") connection.putheader("Content-Type", "application/json-rpc") connection.putheader("Content-Length", str(len(request_body))) connection.endheaders() From 3a54c3699bb9cd085586c61138b35b46a15ba325 Mon Sep 17 00:00:00 2001 From: Alfred Morgan Date: Tue, 11 Feb 2014 20:42:18 -0800 Subject: [PATCH 58/58] mime-type application/json for Accept/Content-Type I like Roland's answer "reason: transport and contents are independent. so all the transport here needs to know is the format of the contents, and that's just json." https://groups.google.com/forum/#!topic/json-rpc/6OXOOm4fcC8 --- jsonrpclib/jsonrpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonrpclib/jsonrpc.py b/jsonrpclib/jsonrpc.py index abb7b6e..42e9f79 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -117,8 +117,8 @@ class TransportMixIn(object): _connection = None def send_content(self, connection, request_body): - connection.putheader("Accept", "application/json-rpc") - connection.putheader("Content-Type", "application/json-rpc") + connection.putheader("Accept", "application/json") + connection.putheader("Content-Type", "application/json") connection.putheader("Content-Length", str(len(request_body))) connection.endheaders() if request_body: