diff --git a/.github/workflows/build-20.04.yml b/.github/workflows/build-20.04.yml deleted file mode 100644 index ac9ae76..0000000 --- a/.github/workflows/build-20.04.yml +++ /dev/null @@ -1,47 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: CI Build - -on: - push: - branches: [ "master" ] - tags: '**' - pull_request: - branches: [ "master" ] - -jobs: - build: - timeout-minutes: 10 - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - python-version: ["3.6", "3.7"] - - steps: - - uses: actions/checkout@v4.2.2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.4.0 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test - run: | - ./run_tests.sh - - name: Coveralls - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - run: | - pip install coveralls - coveralls diff --git a/.github/workflows/build-24.04.yml b/.github/workflows/build-24.04.yml index 1d592a6..356af9e 100644 --- a/.github/workflows/build-24.04.yml +++ b/.github/workflows/build-24.04.yml @@ -6,7 +6,8 @@ name: CI Build on: push: branches: [ "master" ] - tags: '**' + tags: + - '**' pull_request: branches: [ "master" ] @@ -17,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4.2.2 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 05d03d0..512cbd7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,9 +8,9 @@ formats: - htmlzip build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" + python: "3.12" sphinx: configuration: docs/conf.py diff --git a/jsonrpclib/SimpleJSONRPCServer.py b/jsonrpclib/SimpleJSONRPCServer.py index 73dcc0b..07fc94e 100644 --- a/jsonrpclib/SimpleJSONRPCServer.py +++ b/jsonrpclib/SimpleJSONRPCServer.py @@ -45,7 +45,6 @@ # variant of this package. SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler - CGIXMLRPCRequestHandler = xmlrpcserver.CGIXMLRPCRequestHandler resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute # type: ignore # noqa: E501 # pylint: disable=invalid-name,line-too-long import socketserver except (ImportError, AttributeError): @@ -55,7 +54,6 @@ SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher # type: ignore # noqa: E501 # pylint: disable=invalid-name,line-too-long SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler # type: ignore # noqa: E501 # pylint: disable=invalid-name,line-too-long - CGIXMLRPCRequestHandler = xmlrpcserver.CGIXMLRPCRequestHandler # type: ignore # noqa: E501 # pylint: disable=invalid-name,line-too-long resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute # type: ignore # noqa: E501 # pylint: disable=invalid-name,line-too-long import SocketServer as socketserver # type: ignore @@ -688,41 +686,43 @@ def server_close(self): # ------------------------------------------------------------------------------ +if sys.version_info < (3, 15): + CGIXMLRPCRequestHandler = xmlrpcserver.CGIXMLRPCRequestHandler -class CGIJSONRPCRequestHandler( - SimpleJSONRPCDispatcher, CGIXMLRPCRequestHandler -): - """ - JSON-RPC CGI handler (and dispatcher) - """ - - def __init__(self, encoding="UTF-8", config=jsonrpclib.config.DEFAULT): + class CGIJSONRPCRequestHandler( + SimpleJSONRPCDispatcher, CGIXMLRPCRequestHandler + ): """ - Sets up the dispatcher - - :param encoding: Dispatcher encoding - :param config: A JSONRPClib Config instance + JSON-RPC CGI handler (and dispatcher) """ - SimpleJSONRPCDispatcher.__init__(self, encoding, config) - CGIXMLRPCRequestHandler.__init__(self, encoding=encoding) - def handle_jsonrpc(self, request_text): - """ - Handle a JSON-RPC request - """ - try: - writer = sys.stdout.buffer - except AttributeError: - writer = sys.stdout - - response = self._marshaled_dispatch(request_text) - response = response.encode(self.encoding) - print("Content-Type:", self.json_config.content_type) - print("Content-Length:", len(response)) - print() - sys.stdout.flush() - writer.write(response) - writer.flush() - - # XML-RPC alias - handle_xmlrpc = handle_jsonrpc + def __init__(self, encoding="UTF-8", config=jsonrpclib.config.DEFAULT): + """ + Sets up the dispatcher + + :param encoding: Dispatcher encoding + :param config: A JSONRPClib Config instance + """ + SimpleJSONRPCDispatcher.__init__(self, encoding, config) + CGIXMLRPCRequestHandler.__init__(self, encoding=encoding) + + def handle_jsonrpc(self, request_text): + """ + Handle a JSON-RPC request + """ + try: + writer = sys.stdout.buffer + except AttributeError: + writer = sys.stdout + + response = self._marshaled_dispatch(request_text) + response = response.encode(self.encoding) + print("Content-Type:", self.json_config.content_type) + print("Content-Length:", len(response)) + print() + sys.stdout.flush() + writer.write(response) + writer.flush() + + # XML-RPC alias + handle_xmlrpc = handle_jsonrpc diff --git a/pyproject.toml b/pyproject.toml index b23b7b2..3c1d82d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] license = "Apache-2.0" license-files = ["LICENSE"] diff --git a/setup.py b/setup.py index 2bd8233..62e9395 100755 --- a/setup.py +++ b/setup.py @@ -75,5 +75,6 @@ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], ) diff --git a/tests/cgi-bin/cgi_server.py b/tests/cgi-bin/cgi_server.py index 45d71a7..8519d0f 100755 --- a/tests/cgi-bin/cgi_server.py +++ b/tests/cgi-bin/cgi_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -Sample CGI server +Sample CGI server. Won't work with Python 3.15 and later """ import os diff --git a/tests/test_cgi.py b/tests/test_cgi.py index e2de085..ab41fef 100644 --- a/tests/test_cgi.py +++ b/tests/test_cgi.py @@ -11,9 +11,15 @@ # Standard library import os import random +import socket import threading +import sys import unittest + +if sys.version_info >= (3, 15): + raise unittest.SkipTest("CGI support has been removed in Python 3.15") + try: from http.server import HTTPServer, CGIHTTPRequestHandler except ImportError: @@ -25,6 +31,8 @@ # ------------------------------------------------------------------------------ +HOST = socket.gethostbyname("localhost") + class CGIHandlerTests(unittest.TestCase): """ @@ -40,7 +48,7 @@ def test_server(self): try: # Setup server os.chdir(os.path.dirname(__file__)) - server = HTTPServer(("localhost", 0), CGIHTTPRequestHandler) + server = HTTPServer((HOST, 0), CGIHTTPRequestHandler) # Serve in a thread thread = threading.Thread(target=server.serve_forever) @@ -52,7 +60,7 @@ def test_server(self): # Make the client client = ServerProxy( - "http://localhost:{0}/cgi-bin/cgi_server.py".format(port) + "http://{0}:{1}/cgi-bin/cgi_server.py".format(HOST, port) ) # Check call diff --git a/tests/test_headers.py b/tests/test_headers.py index ac8f5ce..f662809 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -9,6 +9,7 @@ # Standard library import contextlib import re +import socket import sys import unittest import jsonrpclib @@ -29,6 +30,8 @@ # ------------------------------------------------------------------------------ +HOST = socket.gethostbyname("localhost") + class HeadersTests(unittest.TestCase): """ @@ -85,7 +88,9 @@ def captured_headers(self, check_duplicates=True): # Extract headers raw_headers = request_line.splitlines()[1:-1] - raw_headers = map(lambda h: re.split(r":\s?", h, 1), raw_headers) + raw_headers = map( + lambda h: re.split(r":\s?", h, maxsplit=1), raw_headers + ) for header, value in raw_headers: header = header.lower() if check_duplicates and header in headers: @@ -93,10 +98,10 @@ def captured_headers(self, check_duplicates=True): headers[header] = value def test_should_extract_headers(self): - """ Check client headers capture """ + """Check client headers capture""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), verbose=1 + "http://{0}:{1}".format(HOST, self.port), verbose=1 ) # when @@ -110,10 +115,10 @@ def test_should_extract_headers(self): self.assertEqual(headers["content-type"], "application/json-rpc") def test_should_add_additional_headers(self): - """ Check sending of custom headers """ + """Check sending of custom headers""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"X-My-Header": "Test"}, ) @@ -128,10 +133,10 @@ def test_should_add_additional_headers(self): self.assertEqual(headers["x-my-header"], "Test") def test_should_add_additional_headers_to_notifications(self): - """ Check custom headers on notifications """ + """Check custom headers on notifications""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"X-My-Header": "Test"}, ) @@ -145,10 +150,10 @@ def test_should_add_additional_headers_to_notifications(self): self.assertEqual(headers["x-my-header"], "Test") def test_should_override_headers(self): - """ Custom headers must override default ones """ + """Custom headers must override default ones""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"User-Agent": "jsonrpclib test", "Host": "example.com"}, ) @@ -163,10 +168,10 @@ def test_should_override_headers(self): self.assertEqual(headers["host"], "example.com") def test_should_not_override_content_length(self): - """ Custom headers can't override Content-Length """ + """Custom headers can't override Content-Length""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"Content-Length": "invalid value"}, ) @@ -181,10 +186,10 @@ def test_should_not_override_content_length(self): self.assertNotEqual(headers["content-length"], "invalid value") def test_should_convert_header_values_to_basestring(self): - """ Custom headers values should be converted to str """ + """Custom headers values should be converted to str""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"X-Test": 123}, ) @@ -199,10 +204,10 @@ def test_should_convert_header_values_to_basestring(self): self.assertEqual(headers["x-test"], "123") def test_should_add_custom_headers_to_methods(self): - """ Check method-based custom headers """ + """Check method-based custom headers""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), verbose=1 + "http://{0}:{1}".format(HOST, self.port), verbose=1 ) # when @@ -217,10 +222,10 @@ def test_should_add_custom_headers_to_methods(self): self.assertEqual(headers["x-method"], "Method") def test_should_override_global_headers(self): - """ Method-based custom headers override context ones """ + """Method-based custom headers override context ones""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"X-Test": "Global"}, ) @@ -236,10 +241,10 @@ def test_should_override_global_headers(self): self.assertEqual(headers["x-test"], "Method") def test_should_restore_global_headers(self): - """ Check custom headers context clean up """ + """Check custom headers context clean up""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), + "http://{0}:{1}".format(HOST, self.port), verbose=1, headers={"X-Test": "Global"}, ) @@ -262,10 +267,10 @@ def test_should_restore_global_headers(self): self.assertEqual(headers["x-test"], "Global") def test_should_allow_to_nest_additional_header_blocks(self): - """ Check nested additional headers """ + """Check nested additional headers""" # given client = jsonrpclib.ServerProxy( - "http://localhost:{0}".format(self.port), verbose=1 + "http://{0}:{1}".format(HOST, self.port), verbose=1 ) # when diff --git a/tests/test_server.py b/tests/test_server.py index 3d24c7d..0279dd7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -8,6 +8,7 @@ # Standard library import random +import socket import threading import time import unittest @@ -43,8 +44,10 @@ def test_default_pool(self, pool=None, max_time=3): :param pool: Thread pool to use :param max_time: Max time the sleep test should take """ + host_address = socket.gethostbyname("localhost") + # Setup server - server = PooledJSONRPCServer(("localhost", 0), thread_pool=pool) + server = PooledJSONRPCServer((host_address, 0), thread_pool=pool) server.register_function(add) server.register_function(sleep) @@ -58,7 +61,7 @@ def test_default_pool(self, pool=None, max_time=3): port = server.socket.getsockname()[1] # Make the client - target_url = "http://localhost:{0}".format(port) + target_url = "http://{0}:{1}".format(host_address, port) client = ServerProxy(target_url) # Check calls