From 1cb797f048ae5131a01086a93d87952c03e8abe7 Mon Sep 17 00:00:00 2001 From: catchjosh Date: Sat, 4 Sep 2010 21:44:53 +0000 Subject: [PATCH] Added unit tests, renamed to follow absolute importing guidelines, and made minor corrections introduced by unit tests. git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@18 ae587032-bbab-11de-869a-473eb4776397 --- jsonrpclib/SimpleJSONRPCServer.py | 61 +++-- jsonrpclib/__init__.py | 9 +- jsonrpclib/config.py | 9 +- jsonrpclib/history.py | 9 +- jsonrpclib/jsonrpclib.py | 487 ------------------------------------- 5 files changed, 62 insertions(+), 513 deletions(-) delete mode 100644 jsonrpclib/jsonrpclib.py diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index f049f85..75193b2 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -8,20 +8,35 @@ import fcntl import sys def get_version(request): - if type(request) not in (types.ListType, types.DictType): - return None - if type(request) is types.ListType: - if len(request) == 0: - return None - if 'jsonrpc' not in request[0].keys(): - return None - return '2.0' # must be a dict if 'jsonrpc' in request.keys(): return 2.0 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) + ) + 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 + request.setdefault('params', []) + method = request.get('method', None) + params = request.get('params') + param_types = (types.ListType, types.DictType, types.TupleType) + if not method or type(method) not in types.StringTypes or \ + type(params) not in param_types: + fault = Fault( + -32600, 'Invalid request parameters or method.', rpcid=rpcid + ) + return fault + return True class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): @@ -34,34 +49,42 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): response = None try: request = jsonrpclib.loads(data) - except: - fault = Fault(-32600, 'Request %s invalid.' % data) - response = fault.response() - return response - version = get_version(request) - if not version: - fault = Fault(-32600, 'Request %s invalid.' % data) + except Exception, 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: # This SHOULD be a batch, by spec responses = [] for req_entry in request: + result = validate_request(req_entry) + if type(result) is Fault: + responses.append(result.response()) + continue resp_entry = self._marshaled_single_dispatch(req_entry) if resp_entry is not None: responses.append(resp_entry) - response = '[%s]' % ','.join(responses) - else: + if len(responses) > 0: + response = '[%s]' % ','.join(responses) + else: + response = '' + else: + result = validate_request(request) + if type(result) is Fault: + return result.response() response = self._marshaled_single_dispatch(request) return response def _marshaled_single_dispatch(self, request): # TODO - Use the multiprocessing and skip the response if # it is a notification - method = request['method'] - params = request['params'] # Put in support for custom dispatcher here # (See SimpleXMLRPCServer._marshaled_dispatch) + method = request.get('method') + params = request.get('params') try: response = self._dispatch(method, params) except: diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 1fd4dc0..6e884b8 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -1,3 +1,6 @@ -from jsonrpclib import * -from config import config -from history import history +from jsonrpclib.config import Config +config = Config.instance() +from jsonrpclib.history import History +history = History.instance() +from jsonrpclib.jsonrpc import Server, MultiCall, Fault +from jsonrpclib.jsonrpc import ProtocolError, loads, dumps diff --git a/jsonrpclib/config.py b/jsonrpclib/config.py index 1f9d7d8..4d28f1b 100644 --- a/jsonrpclib/config.py +++ b/jsonrpclib/config.py @@ -29,5 +29,10 @@ class Config(object): user_agent = 'jsonrpclib/0.1 (Python %s)' % \ '.'.join([str(ver) for ver in sys.version_info[0:3]]) # User agent to use for calls. - -config = Config + _instance = None + + @classmethod + def instance(cls): + if not cls._instance: + cls._instance = cls() + return cls._instance diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index ec53235..e6a01cf 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -8,6 +8,13 @@ class History(object): """ requests = [] responses = [] + _instance = None + + @classmethod + def instance(cls): + if not cls._instance: + cls._instance = cls() + return cls._instance def add_response(self, response_obj): self.responses.append(response_obj) @@ -32,5 +39,3 @@ class History(object): def clear(self): del self.requests[:] del self.responses[:] - -history = History() diff --git a/jsonrpclib/jsonrpclib.py b/jsonrpclib/jsonrpclib.py deleted file mode 100644 index 01d61b9..0000000 --- a/jsonrpclib/jsonrpclib.py +++ /dev/null @@ -1,487 +0,0 @@ -""" -Copyright 2009 Josh Marshall -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. - -============================ -JSONRPC Library (jsonrpclib) -============================ - -This library is a JSON-RPC v.2 (proposed) implementation which -follows the xmlrpclib API for portability between clients. It -uses the same Server / ServerProxy, loads, dumps, etc. syntax, -while providing features not present in XML-RPC like: - -* Keyword arguments -* Notifications -* Versioning -* Batches and batch notifications - -Eventually, I'll add a SimpleXMLRPCServer compatible library, -and other things to tie the thing off nicely. :) - -For a quick-start, just open a console and type the following, -replacing the server address, method, and parameters -appropriately. ->>> import jsonrpclib ->>> server = jsonrpclib.Server('http://localhost:8181') ->>> server.add(5, 6) -11 ->>> server._notify.add(5, 6) ->>> batch = jsonrpclib.MultiCall(server) ->>> batch.add(3, 50) ->>> batch.add(2, 3) ->>> batch._notify.add(3, 5) ->>> batch() -[53, 5] - -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 - -# Library includes -from config import config -from history import history - -# JSON library importing -cjson = None -json = None -try: - import cjson -except ImportError: - pass -if not cjson: - try: - import json - except ImportError: - pass -if not cjson and not json: - try: - import simplejson as json - except ImportError: - 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: - return json.dumps(obj, encoding=encoding) - -def jloads(json_string): - global cjson - if cjson: - return cjson.decode(json_string) - else: - return json.loads(json_string) - - -# XMLRPClib re-implemntations - -class ProtocolError(Exception): - pass - -class Transport(XMLTransport): - """ Just extends the XMLRPC transport where necessary. """ - user_agent = config.user_agent - - def send_content(self, connection, request_body): - connection.putheader("Content-Type", "application/json-rpc") - connection.putheader("Content-Length", str(len(request_body))) - connection.endheaders() - if request_body: - connection.send(request_body) - - def _parse_response(self, file_h, sock): - response_body = '' - while 1: - if sock: - response = sock.recv(1024) - else: - response = file_h.read(1024) - if not response: - break - response_body += response - if self.verbose: - print 'body: %s' % response - if response_body == '': - # Notification - return None - return_obj = loads(response_body) - return return_obj - -class SafeTransport(XMLSafeTransport): - """ Just extends for HTTPS calls """ - user_agent = Transport.user_agent - send_content = Transport.send_content - _parse_response = Transport._parse_response - -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, - verbose=0, version=None): - import urllib - if not version: - version = config.version - self.__version = version - schema, uri = urllib.splittype(uri) - if schema not in ('http', 'https'): - raise IOError('Unsupported JSON-RPC protocol.') - self.__host, self.__handler = urllib.splithost(uri) - if not self.__handler: - # Not sure if this is in the JSON spec? - self.__handler = '/RPC2' - if transport is None: - if schema == 'https': - transport = SafeTransport() - else: - transport = Transport() - self.__transport = transport - self.__encoding = encoding - self.__verbose = verbose - - def _request(self, methodname, params, rpcid=None): - request = dumps(params, methodname, encoding=self.__encoding, - rpcid=rpcid, version=self.__version) - 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) - response = self._run_request(request, notify=True) - check_for_errors(response) - return - - def _run_request(self, request, notify=None): - history.add_request(request) - - response = self.__transport.request( - self.__host, - self.__handler, - 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 - # outputting the response appropriately? - - history.add_response(response) - return response - - def __getattr__(self, name): - # Same as original, just with new _Method reference - return _Method(self._request, name) - - @property - def _notify(self): - # Just like __getattr__, but with notify namespace. - return _Notify(self._request_notify) - - -class _Method(XML_Method): - - def __call__(self, *args, **kwargs): - if len(args) > 0 and len(kwargs) > 0: - raise ProtocolError('Cannot use both positional ' + - 'and keyword arguments (according to JSON-RPC spec.)') - if len(args) > 0: - return self.__send(self.__name, args) - else: - return self.__send(self.__name, kwargs) - - def __getattr__(self, name): - # Even though this is verbatim, it doesn't support - # keyword arguments unless we rewrite it. - return _Method(self.__send, "%s.%s" % (self.__name, name)) - -class _Notify(object): - def __init__(self, request): - self._request = request - - def __getattr__(self, name): - return _Method(self._request, name) - -# Batch implementation - -class MultiCallMethod(object): - - def __init__(self, method, notify=False): - self.method = method - self.params = [] - self.notify = notify - - def __call__(self, *args, **kwargs): - if len(kwargs) > 0 and len(args) > 0: - raise ProtocolError('JSON-RPC does not support both ' + - 'positional and keyword arguments.') - if len(kwargs) > 0: - self.params = kwargs - else: - self.params = args - - def request(self, encoding=None, rpcid=None): - return dumps(self.params, self.method, version=2.0, - encoding=encoding, rpcid=rpcid, notify=self.notify) - - def __repr__(self): - return '%s' % self.request() - -class MultiCallNotify(object): - - def __init__(self, multicall): - self.multicall = multicall - - def __getattr__(self, name): - new_job = MultiCallMethod(name, notify=True) - self.multicall._job_list.append(new_job) - return new_job - -class MultiCallIterator(object): - - def __init__(self, results): - self.results = results - - def __iter__(self): - for i in range(0, len(self.results)): - yield self[i] - raise StopIteration - - def __getitem__(self, i): - item = self.results[i] - check_for_errors(item) - return item['result'] - - def __len__(self): - return len(self.results) - -class MultiCall(object): - - def __init__(self, server): - self._server = server - self._job_list = [] - - def _request(self): - if len(self._job_list) < 1: - # Should we alert? This /is/ pretty obvious. - return - request_body = '[ %s ]' % ','.join([job.request() for - job in self._job_list]) - responses = self._server._run_request(request_body) - del self._job_list[:] - return MultiCallIterator(responses) - - @property - def _notify(self): - return MultiCallNotify(self) - - def __getattr__(self, name): - new_job = MultiCallMethod(name) - self._job_list.append(new_job) - return new_job - - __call__ = _request - -# 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'): - self.faultCode = code - self.faultString = message - - def error(self): - return {'code':self.faultCode, 'message':self.faultString} - - def response(self, rpcid=None, version=None): - if not version: - version = config.version - return dumps(self, methodresponse=True, rpcid=rpcid, version=version) - - def __repr__(self): - return '' % (self.faultCode, self.faultString) - -def random_id(length=8): - import string - import random - random.seed() - choices = string.lowercase+string.digits - return_id = '' - for i in range(length): - return_id += random.choice(choices) - return return_id - -class Payload(dict): - def __init__(self, rpcid=None, version=None): - if not version: - version = config.version - self.id = rpcid - self.version = float(version) - - def request(self, method, params=[]): - if type(method) not in types.StringTypes: - raise ValueError('Method name must be a string.') - if not self.id: - self.id = random_id() - request = {'id':self.id, 'method':method, 'params':params} - if self.version >= 2: - request['jsonrpc'] = str(self.version) - return request - - def notify(self, method, params=[]): - request = self.request(method, params) - if self.version >= 2: - del request['id'] - else: - request['id'] = None - return request - - def response(self, result=None): - 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.'): - error = self.response() - if self.version >= 2: - del error['result'] - else: - error['result'] = None - error['error'] = {'code':code, 'message':message} - return 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 - 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 \ - 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. - """ - raise TypeError('Params must be a dict, list, tuple or Fault ' + - 'instance.') - # Begin parsing object - 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 types.StringTypes and methodresponse != True: - raise ValueError('Method name must be a string, or methodresponse '+ - 'must be set to True.') - if config.use_jsonclass == True: - import jsonclass - params = jsonclass.dump(params) - if methodresponse is True: - if rpcid is None: - 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) - else: - request = payload.request(methodname, params) - return jdumps(request, encoding=encoding) - -def loads(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. - """ - if data == '': - # 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: - import jsonclass - result = jsonclass.load(result) - return result - -def check_for_errors(result): - if not result: - # Notification - return result - if type(result) is not types.DictType: - raise TypeError('Response is not a dict.') - if 'jsonrpc' in result.keys() and float(result['jsonrpc']) > 2.0: - raise NotImplementedError('JSON-RPC version not yet supported.') - if 'result' not in result.keys() and 'error' not in result.keys(): - raise ValueError('Response does not have a result or error key.') - if 'error' in result.keys() and result['error'] != None: - code = result['error']['code'] - message = result['error']['message'] - raise ProtocolError((code, message)) - return result - -def isbatch(result): - if type(result) not in (types.ListType, types.TupleType): - return False - if len(result) < 1: - return False - if type(result[0]) is not types.DictType: - return False - if 'jsonrpc' not in result[0].keys(): - return False - try: - version = float(result[0]['jsonrpc']) - except ValueError: - raise ProtocolError('"jsonrpc" key must be a float(able) value.') - if version < 2: - return False - return True - -def isnotification(request): - if 'id' not in request.keys(): - # 2.0 notification - return True - if request['id'] == None: - # 1.0 notification - return True - return False -- 1.7.9.5