Added unit tests, renamed to follow absolute importing guidelines, and made minor...
authorcatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>
Sat, 4 Sep 2010 21:44:53 +0000 (21:44 +0000)
committercatchjosh <catchjosh@ae587032-bbab-11de-869a-473eb4776397>
Sat, 4 Sep 2010 21:44:53 +0000 (21:44 +0000)
git-svn-id: http://jsonrpclib.googlecode.com/svn/trunk@18 ae587032-bbab-11de-869a-473eb4776397

jsonrpclib/SimpleJSONRPCServer.py
jsonrpclib/__init__.py
jsonrpclib/config.py
jsonrpclib/history.py
jsonrpclib/jsonrpclib.py [deleted file]

index f049f85..75193b2 100644 (file)
@@ -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:
index 1fd4dc0..6e884b8 100644 (file)
@@ -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
index 1f9d7d8..4d28f1b 100644 (file)
@@ -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
index ec53235..e6a01cf 100644 (file)
@@ -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 (file)
index 01d61b9..0000000
+++ /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 '<Fault %s: %s>' % (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