diff options
-rw-r--r-- | .circleci/config.yml | 51 | ||||
-rw-r--r-- | .travis.yml | 17 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | dev-requirements.txt | 18 | ||||
-rw-r--r-- | jsonrpclib/SimpleJSONRPCServer.py | 77 | ||||
-rw-r--r-- | jsonrpclib/__init__.py | 10 | ||||
-rw-r--r-- | jsonrpclib/history.py | 9 | ||||
-rw-r--r-- | jsonrpclib/jsonclass.py | 46 | ||||
-rw-r--r-- | jsonrpclib/jsonrpc.py | 117 | ||||
-rw-r--r-- | pyproject.toml | 6 | ||||
-rw-r--r-- | setup.cfg | 13 | ||||
-rwxr-xr-x | setup.py | 35 | ||||
-rw-r--r-- | tests.py | 141 | ||||
-rw-r--r-- | tox.ini | 9 |
14 files changed, 316 insertions, 236 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..4c499eb --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,51 @@ +version: 2.1 + +jobs: + build-and-test: + docker: + - image: cimg/python:3.9.2 + steps: + - checkout + - restore_cache: + keys: + - dev-cache-{{ checksum "dev-requirements.txt" }}-{{ checksum "tox.ini" }} + - run: + name: Install dev dependencies + command: pip3 install -r dev-requirements.txt + - run: pyenv install --skip-existing 3.5-dev + - run: pyenv install --skip-existing 3.6-dev + - run: pyenv install --skip-existing 3.7-dev + - run: pyenv install --skip-existing 3.8-dev + - run: pyenv install --skip-existing 3.9-dev + - run: + name: Set local Python versions + command: pyenv local 3.5-dev 3.6-dev 3.7-dev 3.8-dev 3.9-dev 3.9.2 + - save_cache: + key: dev-cache-{{ checksum "dev-requirements.txt" }}-{{ checksum "tox.ini" }} + paths: + - /home/circleci/.cache/pip + - /home/circleci/.pyenv/versions/ + - /home/circleci/.local/lib/ + + - run: + command: flake8 + name: Flake8 / linting + - run: + command: tox + name: Run package tests across all python variants + - store_test_results: + path: reports + - run: + command: pip install build + name: Install build dependency + - run: + command: python3 -m build + name: Build source distribution + - store_artifacts: + path: ./dist + +workflows: + main: + jobs: + - build-and-test + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe1cd05..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -sudo: false -python: -- '2.7' -install: -- pip install -r dev-requirements.txt -- pip install twine -script: tox -deploy: - provider: pypi - user: joshmarshall - password: - secure: RbRcj7YdDXE9U2/a9yg4DW9UXFfrGWrM+S8uE8RgYu1D9njDDzUyNcFygaBXd8rxd8GpwRXHzSAO31B/Rk4NVbbM7JtcIA/52jx5j+4DdmEhffnzvahDkCZT6EV5iS3IxlShbuxgbzp3Qz14jF7Kl9iBSCOlIFItLCDlK7rfyJU= - on: - tags: true - repo: joshmarshall/jsonrpclib - branch: master @@ -1,4 +1,5 @@ -[](https://travis-ci.org/joshmarshall/jsonrpclib) +[](https://circleci.com/gh/joshmarshall/jsonrpclib) + JSONRPClib ========== diff --git a/dev-requirements.txt b/dev-requirements.txt index 95cb849..c85472b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,11 +1,7 @@ -coverage==4.0 -linecache2==1.0.0 -nose==1.3.7 -pluggy==0.3.1 -py==1.4.30 -six==1.9.0 -tox==2.1.1 -traceback2==1.4.0 -unittest2==1.1.0 -virtualenv==13.1.2 -wheel==0.24.0 +build +coverage +flake8 +pytest +tox +tox-pyenv +twine diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index a22ae7c..e6a46f3 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -1,20 +1,21 @@ -import jsonrpclib -from jsonrpclib import Fault -from jsonrpclib.jsonrpc import USE_UNIX_SOCKETS -import SimpleXMLRPCServer -import SocketServer -import socket import logging import os -import types -import traceback +import socket +import socketserver import sys +import traceback +import xmlrpc.server + try: import fcntl except ImportError: # For Windows fcntl = None +import jsonrpclib +from jsonrpclib import Fault +from jsonrpclib.jsonrpc import USE_UNIX_SOCKETS + def get_version(request): # must be a dict @@ -39,9 +40,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 \ - type(params) not in param_types: + if not method or not isinstance(method, str) or \ + not isinstance(params, (list, dict, tuple)): fault = Fault( -32600, 'Invalid request parameters or method.', rpcid=rpcid ) @@ -49,17 +49,17 @@ def validate_request(request): return True -class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): +class SimpleJSONRPCDispatcher(xmlrpc.server.SimpleXMLRPCDispatcher): def __init__(self, encoding=None): - SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__( + xmlrpc.server.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, e: + except Exception as e: fault = Fault(-32700, 'Request %s invalid. (%s)' % (data, e)) response = fault.response() return response @@ -97,7 +97,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): params = request.get('params') try: response = self._dispatch(method, params) - except: + except Exception: exc_type, exc_value, exc_tb = sys.exc_info() fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) return fault.response() @@ -111,7 +111,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): rpcid=request['id'] ) return response - except: + except Exception: exc_type, exc_value, exc_tb = sys.exc_info() fault = Fault(-32603, '%s:%s' % (exc_type, exc_value)) return fault.response() @@ -126,7 +126,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): return self.instance._dispatch(method, params) else: try: - func = SimpleXMLRPCServer.resolve_dotted_attribute( + func = xmlrpc.server.resolve_dotted_attribute( self.instance, method, True @@ -135,14 +135,14 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): pass if func is not None: try: - if isinstance(params, types.ListType): + if isinstance(params, list): response = func(*params) else: response = func(**params) return response # except TypeError: # return Fault(-32602, 'Invalid parameters.') - except: + except Exception: err_lines = traceback.format_exc().splitlines() trace_string = '%s | %s' % (err_lines[-3], err_lines[-1]) fault = jsonrpclib.Fault(-32603, 'Server error: %s' % @@ -153,7 +153,7 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): class SimpleJSONRPCRequestHandler( - SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + xmlrpc.server.SimpleXMLRPCRequestHandler): def do_POST(self): if not self.is_rpc_path_valid(): @@ -165,7 +165,8 @@ class SimpleJSONRPCRequestHandler( L = [] while size_remaining: chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) + chunk = self.rfile.read(chunk_size).decode() + L.append(chunk) size_remaining -= len(L[-1]) data = ''.join(L) response = self.server._marshaled_dispatch(data) @@ -181,12 +182,20 @@ class SimpleJSONRPCRequestHandler( self.send_header("Content-type", "application/json-rpc") self.send_header("Content-length", str(len(response))) self.end_headers() - self.wfile.write(response) + if isinstance(response, bytes): + self.wfile.write(response) + else: + self.wfile.write(response.encode()) self.wfile.flush() self.connection.shutdown(1) -class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher): +class SimpleJSONRPCUnixRequestHandler(SimpleJSONRPCRequestHandler): + + disable_nagle_algorithm = False + + +class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): allow_reuse_address = True @@ -195,10 +204,8 @@ class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher): address_family=socket.AF_INET): self.logRequests = logRequests 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 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 @@ -208,12 +215,12 @@ class SimpleJSONRPCServer(SocketServer.TCPServer, SimpleJSONRPCDispatcher): 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) - else: - SocketServer.TCPServer.__init__( - self, addr, requestHandler, bind_and_activate) + + if requestHandler == SimpleJSONRPCRequestHandler: + requestHandler = SimpleJSONRPCUnixRequestHandler + + socketserver.TCPServer.__init__( + self, addr, requestHandler, bind_and_activate) if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) flags |= fcntl.FD_CLOEXEC @@ -227,9 +234,9 @@ class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): 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 + print('Content-Type: application/json-rpc') + print('Content-Length: %d' % len(response)) + print() sys.stdout.write(response) handle_xmlrpc = handle_jsonrpc diff --git a/jsonrpclib/__init__.py b/jsonrpclib/__init__.py index 6e884b8..91dd5f9 100644 --- a/jsonrpclib/__init__.py +++ b/jsonrpclib/__init__.py @@ -1,6 +1,12 @@ 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 + +config = Config.instance() +history = History.instance() + +__all__ = [ + "config", "Config", "history", "History", "Server", "MultiCall", + "Fault", "ProtocolError", "loads", "dumps" +] diff --git a/jsonrpclib/history.py b/jsonrpclib/history.py index f052baa..090f996 100644 --- a/jsonrpclib/history.py +++ b/jsonrpclib/history.py @@ -5,6 +5,7 @@ class History(object): each request cycle in order to keep it from clogging memory. """ + size = 20 requests = [] responses = [] _instance = None @@ -16,10 +17,18 @@ class History(object): return cls._instance def add_response(self, response_obj): + if self.size == 0: + return self.responses.append(response_obj) + if self.size > 0: + self.responses = self.responses[0 - self.size:] def add_request(self, request_obj): + if self.size == 0: + return self.requests.append(request_obj) + if self.size > 0: + self.requests = self.requests[0 - self.size:] @property def request(self): diff --git a/jsonrpclib/jsonclass.py b/jsonrpclib/jsonclass.py index 4326f28..17e3653 100644 --- a/jsonrpclib/jsonclass.py +++ b/jsonrpclib/jsonclass.py @@ -1,30 +1,12 @@ -import types import inspect import re from jsonrpclib import config -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 -] +iter_types = (dict, list, tuple) +value_types = (bool, ) +string_types = (str, ) +numeric_types = (int, float) supported_types = iter_types+string_types+numeric_types+value_types invalid_module_chars = r'[^a-zA-Z0-9\_\.]' @@ -39,23 +21,22 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): serialize_method = config.serialize_method if not ignore_attribute: 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 is None or isinstance(obj, numeric_types+string_types+value_types): return obj - if obj_type in iter_types: - if obj_type in (types.ListType, types.TupleType): + if isinstance(obj, iter_types): + if isinstance(obj, (list, tuple)): new_obj = [] for item in obj: new_obj.append( dump(item, serialize_method, ignore_attribute, ignore)) - if isinstance(obj_type, types.TupleType): + if isinstance(obj, tuple): 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 @@ -81,7 +62,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,7 +73,8 @@ def dump(obj, serialize_method=None, ignore_attribute=None, ignore=[]): def load(obj): - if type(obj) in string_types + numeric_types + value_types: + if obj is None or isinstance( + obj, string_types + numeric_types + value_types): return obj if isinstance(obj, list): @@ -103,7 +85,7 @@ def load(obj): # Othewise, it's a dict type if '__jsonclass__' not in obj: 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 @@ -148,7 +130,7 @@ def load(obj): 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 167bcd7..9eeadcb 100644 --- a/jsonrpclib/jsonrpc.py +++ b/jsonrpclib/jsonrpc.py @@ -46,34 +46,28 @@ appropriately. See http://code.google.com/p/jsonrpclib/ for more info. """ -import types -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 +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 +import json import string import random -# Library includes -from jsonrpclib import config -from jsonrpclib import history +from jsonrpclib import Config +from jsonrpclib import History + +from http.client import HTTPConnection +from socket import socket + +USE_UNIX_SOCKETS = False -# JSON library importing -cjson = None -json = None try: - import cjson + from socket import AF_UNIX, SOCK_STREAM + USE_UNIX_SOCKETS = True except ImportError: - 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.' - ) + pass + IDCHARS = string.ascii_lowercase+string.digits @@ -91,19 +85,11 @@ class UnixSocketMissing(Exception): 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) + return json.dumps(obj) def jloads(json_string): - global cjson - if cjson: - return cjson.decode(json_string) - else: - return json.loads(json_string) + return json.loads(json_string) # XMLRPClib re-implementations @@ -115,16 +101,15 @@ class ProtocolError(Exception): class TransportMixIn(object): """ Just extends the XMLRPC transport where necessary. """ - user_agent = config.user_agent - # for Python 2.7 support - _connection = (None, None) - _extra_headers = [] + user_agent = Config.instance().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: + if type(request_body) == str: + request_body = request_body.encode("utf8") connection.send(request_body) def getparser(self): @@ -151,7 +136,7 @@ class JSONTarget(object): self.data.append(data) def close(self): - return ''.join(self.data) + return ''.join([d.decode() for d in self.data]) class Transport(TransportMixIn, XMLTransport): @@ -165,16 +150,6 @@ class SafeTransport(TransportMixIn, XMLSafeTransport): TransportMixIn.__init__(self) XMLSafeTransport.__init__(self) -from httplib import HTTP, HTTPConnection -from socket import socket - -USE_UNIX_SOCKETS = False - -try: - from socket import AF_UNIX, SOCK_STREAM - USE_UNIX_SOCKETS = True -except ImportError: - pass if (USE_UNIX_SOCKETS): @@ -183,14 +158,11 @@ if (USE_UNIX_SOCKETS): self.sock = socket(AF_UNIX, SOCK_STREAM) self.sock.connect(self.host) - class UnixHTTP(HTTP): - _connection_class = UnixHTTPConnection - class UnixTransport(TransportMixIn, XMLTransport): def make_connection(self, host): host, extra_headers, x509 = self.get_host_info(host) - return UnixHTTP(host) + return UnixHTTPConnection(host) class ServerProxy(XMLServerProxy): @@ -201,11 +173,14 @@ class ServerProxy(XMLServerProxy): def __init__(self, uri, transport=None, encoding=None, verbose=0, version=None): - import urllib + try: + from urllib.parse import splittype, splithost # python 3.x + except ImportError: + from urllib.parse import splittype, splithost if not version: - version = config.version + version = Config.instance().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': @@ -215,7 +190,7 @@ class ServerProxy(XMLServerProxy): 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 = '/' @@ -246,7 +221,7 @@ class ServerProxy(XMLServerProxy): return def _run_request(self, request, notify=None): - history.add_request(request) + History.instance().add_request(request) response = self.__transport.request( self.__host, @@ -261,7 +236,7 @@ class ServerProxy(XMLServerProxy): # the response object, or expect the Server to be # outputting the response appropriately? - history.add_response(response) + History.instance().add_response(response) if not response: return None return_obj = loads(response) @@ -361,7 +336,6 @@ class MultiCallIterator(object): def __iter__(self): for i in range(0, len(self.results)): yield self[i] - raise StopIteration def __getitem__(self, i): item = self.results[i] @@ -401,6 +375,7 @@ class MultiCall(object): __call__ = _request + # These lines conform to xmlrpclib's "compatibility" line. # Not really sure if we should include these, but oh well. Server = ServerProxy @@ -419,7 +394,7 @@ class Fault(object): def response(self, rpcid=None, version=None): if not version: - version = config.version + version = Config.instance().version if rpcid: self.rpcid = rpcid return dumps( @@ -440,12 +415,12 @@ def random_id(length=8): class Payload(dict): def __init__(self, rpcid=None, version=None): if not version: - version = config.version + version = Config.instance().version self.id = rpcid self.version = float(version) def request(self, method, params=[]): - if type(method) not in types.StringTypes: + if not isinstance(method, str): raise ValueError('Method name must be a string.') if not self.id: self.id = random_id() @@ -490,10 +465,9 @@ def dumps( 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 \ + version = Config.instance().version + if isinstance(methodname, str) and \ + not isinstance(params, (tuple, list, dict)) and \ not isinstance(params, Fault): """ If a method, and params are not in a listish or a Fault, @@ -505,17 +479,17 @@ def dumps( payload = Payload(rpcid=rpcid, version=version) if not encoding: encoding = 'utf-8' - if type(params) is Fault: + if isinstance(params, Fault): response = payload.error(params.faultCode, params.faultString) return jdumps(response, encoding=encoding) - if type(methodname) not in types.StringTypes and \ + if not isinstance(methodname, str) and \ methodresponse is not True: raise ValueError( 'Method name must be a string, or methodresponse must ' 'be set to True.') - if config.use_jsonclass is True: + if Config.instance().use_jsonclass is True: from jsonrpclib import jsonclass params = jsonclass.dump(params) if methodresponse is True: @@ -544,7 +518,7 @@ def loads(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 is True: + if Config.instance().use_jsonclass is True: from jsonrpclib import jsonclass result = jsonclass.load(result) return result @@ -559,7 +533,8 @@ def check_for_errors(result): 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(): + 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'] is not None: code = result['error']['code'] @@ -569,7 +544,7 @@ def check_for_errors(result): def isbatch(result): - if type(result) not in (types.ListType, types.TupleType): + if not isinstance(result, (list, tuple)): return False if len(result) < 1: return False diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4cadba8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..13d6b31 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +name = jsonrpclib +version = 0.2.1 +author = Josh Marshall +author_email = catchjosh@gmail.com +url = https://github.com/joshmarshall/jsonrpclib +long_description = file: README.md +long_description_content_type = text/markdown +description = Implementation of the JSON-RPC v2.0 specification (backwards-compatible) as a client library. + +[options] +packages = jsonrpclib +python_requires = >=3.5 diff --git a/setup.py b/setup.py deleted file mode 100755 index 1973208..0000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -""" -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. -""" - -import distutils.core -import os - -long_description = "Placeholder in case of missing README.md." - -if os.path.exists("README.md"): - with open("README.md") as readme_fp: - long_description = readme_fp.read() - -distutils.core.setup( - name="jsonrpclib", - version="0.1.7", - 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=long_description) @@ -36,6 +36,9 @@ from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCRequestHandler +ORIGINAL_HISTORY_SIZE = history.size + + def get_port(family=socket.AF_INET): sock = socket.socket(family, socket.SOCK_STREAM) sock.bind(("localhost", 0)) @@ -53,6 +56,10 @@ class TestCompatibility(unittest.TestCase): self.server = server_set_up(addr=('', self.port)) self.client = Server('http://localhost:%d' % self.port) + def tearDown(self): + self.server.stop() + self.server.join() + # v1 tests forthcoming # Version 2.0 Tests @@ -281,6 +288,13 @@ class InternalTests(unittest.TestCase): def setUp(self): self.port = get_port() self.server = server_set_up(addr=('', self.port)) + self.addCleanup(self.cleanup) + + def cleanup(self): + self.server.stop() + self.server.join() + history.size = ORIGINAL_HISTORY_SIZE + history.clear() def get_client(self): return Server('http://localhost:%d' % self.port) @@ -316,7 +330,10 @@ class InternalTests(unittest.TestCase): def test_single_namespace(self): client = self.get_client() + response = client.namespace.sum(1, 2, 4) + self.assertEqual(7, response) + request = json.loads(history.request) response = json.loads(history.response) verify_request = { @@ -328,8 +345,64 @@ class InternalTests(unittest.TestCase): } verify_request['id'] = request['id'] verify_response['id'] = request['id'] - self.assertTrue(verify_request == request) - self.assertTrue(verify_response == response) + self.assertEqual(verify_request, request) + self.assertEqual(verify_response, response) + + def test_history_defaults_to_20(self): + client = self.get_client() + self.assertEqual(20, history.size) + + for i in range(30): + client.namespace.sum(i, i) + + self.assertEqual(20, len(history.requests)) + self.assertEqual(20, len(history.responses)) + + verify_request = { + "jsonrpc": "2.0", "params": [29, 29], + "method": "namespace.sum" + } + verify_response = {"jsonrpc": "2.0", "result": 58} + + # it should truncate to the *last* 20 + request = json.loads(history.request) + response = json.loads(history.response) + + verify_request["id"] = request["id"] + self.assertEqual(request, verify_request) + + verify_response["id"] = request["id"] + self.assertEqual(response, verify_response) + + def test_history_allows_configurable_size(self): + client = self.get_client() + history.size = 10 + + for i in range(30): + client.namespace.sum(i, i) + + self.assertEqual(10, len(history.requests)) + self.assertEqual(10, len(history.responses)) + + def test_history_allows_unlimited_size(self): + client = self.get_client() + history.size = -1 + + for i in range(40): + client.namespace.sum(i, i) + + self.assertEqual(40, len(history.requests)) + self.assertEqual(40, len(history.responses)) + + def test_history_can_be_disabled(self): + client = self.get_client() + history.size = 0 + + for i in range(40): + client.namespace.sum(i, i) + + self.assertEqual(0, len(history.requests)) + self.assertEqual(0, len(history.responses)) def test_multicall_success(self): multicall = self.get_multicall_client() @@ -365,37 +438,40 @@ class InternalTests(unittest.TestCase): result = sub_service_proxy.add(21, 21) self.assertTrue(result == 42) -if jsonrpc.USE_UNIX_SOCKETS: - # We won't do these tests unless Unix Sockets are supported - - @unittest.skip("Skipping Unix socket tests right now.") - class UnixSocketInternalTests(InternalTests): - """ - These tests run the same internal communication tests, - but over a Unix socket instead of a TCP socket. - """ - def setUp(self): - suffix = "%d.sock" % get_port() - - # Open to safer, alternative processes - # for getting a temp file name... - temp = tempfile.NamedTemporaryFile( - suffix=suffix - ) - self.port = temp.name - temp.close() - self.server = server_set_up( - addr=self.port, - address_family=socket.AF_UNIX - ) +@unittest.skipIf( + not jsonrpc.USE_UNIX_SOCKETS or "SKIP_UNIX_SOCKET_TESTS" in os.environ, + "Skipping Unix socket tests -- unsupported in this environment.") +class UnixSocketInternalTests(InternalTests): + """ + These tests run the same internal communication tests, + but over a Unix socket instead of a TCP socket. + """ + def setUp(self): + super().setUp() + suffix = "%d.sock" % get_port() - def get_client(self): - return Server('unix:/%s' % self.port) + # Open to safer, alternative processes + # for getting a temp file name... + temp = tempfile.NamedTemporaryFile( + suffix=suffix + ) + self.port = temp.name + temp.close() - def tearDown(self): - """ Removes the tempory socket file """ - os.unlink(self.port) + 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 """ + self.server.stop() + self.server.join() + os.unlink(self.port) class UnixSocketErrorTests(unittest.TestCase): @@ -473,7 +549,12 @@ def server_set_up(addr, address_family=socket.AF_INET): server.register_function(service.summation, 'sum') server.register_function(service.summation, 'notify_sum') server.register_function(service.summation, 'namespace.sum') + + def stop(): + server.shutdown() + server_proc = Thread(target=server.serve_forever) server_proc.daemon = True + server_proc.stop = stop server_proc.start() return server_proc @@ -1,5 +1,10 @@ [tox] -envlist = py26,py27 +envlist = py35,py36,py37,py38,py39 +isolated_build = True + [testenv] deps= -rdev-requirements.txt -commands=nosetests tests.py --with-coverage --cover-package=jsonrpclib +whitelist_externals=mkdir +commands=mkdir -p reports/{envname} + coverage run --source jsonrpclib -m pytest --junitxml=reports/{envname}/test-results.xml tests.py + coverage report |