diff --git a/py4j-python/.pydevproject b/py4j-python/.pydevproject index 0ca60ff5..f64f44ed 100644 --- a/py4j-python/.pydevproject +++ b/py4j-python/.pydevproject @@ -2,6 +2,6 @@ /py4j-python/src -python 2.6 +python 3.9 Default \ No newline at end of file diff --git a/py4j-python/nose.cfg b/py4j-python/nose.cfg deleted file mode 100644 index f8747ca0..00000000 --- a/py4j-python/nose.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[nosetests] -verbosity=3 diff --git a/py4j-python/setup.cfg b/py4j-python/setup.cfg deleted file mode 100644 index 79bc6784..00000000 --- a/py4j-python/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. -universal=1 diff --git a/py4j-python/setup.py b/py4j-python/setup.py index 221b575f..c61ad460 100644 --- a/py4j-python/setup.py +++ b/py4j-python/setup.py @@ -1,13 +1,9 @@ -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from setuptools import setup import os DOC_DIR = "doc" DIST_DIR = "dist" VERSION_PATH = os.path.join("src", "py4j", "version.py") -# For Python 3 compatibility, we can't use execfile; this is 2to3's conversion: exec(compile(open(VERSION_PATH).read(), VERSION_PATH, "exec")) VERSION = __version__ # noqa @@ -37,18 +33,20 @@ author="Barthelemy Dagenais", author_email="barthelemy@infobart.com", license="BSD License", + python_requires=">=3.9", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Java", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Object Brokering", diff --git a/py4j-python/src/py4j/__init__.py b/py4j-python/src/py4j/__init__.py index e6c041ac..be09cd99 100644 --- a/py4j-python/src/py4j/__init__.py +++ b/py4j-python/src/py4j/__init__.py @@ -1,5 +1,4 @@ # Py4J Package -from __future__ import absolute_import from . import version __version__ = version.__version__ diff --git a/py4j-python/src/py4j/clientserver.py b/py4j-python/src/py4j/clientserver.py index 7301a9ed..d0d7e247 100644 --- a/py4j-python/src/py4j/clientserver.py +++ b/py4j-python/src/py4j/clientserver.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- """Module that implements a different threading model between a Java Virtual Machine a Python interpreter. @@ -6,9 +5,6 @@ thread. For example, if a request is started in a Java UI thread and the Python code calls some Java code, the Java code will be executed in the UI thread. """ - -from __future__ import unicode_literals, absolute_import - from collections import deque import logging import socket diff --git a/py4j-python/src/py4j/compat.py b/py4j-python/src/py4j/compat.py index 6e9b2ce9..8958080b 100644 --- a/py4j-python/src/py4j/compat.py +++ b/py4j-python/src/py4j/compat.py @@ -1,112 +1,95 @@ -# coding: utf-8 """ -Compatibility functions for unified behavior between Python 2.x and 3.x. +Compatibility shims left over from py4j's Python 2 / 3 dual-support era. + +Python 2 support has been removed (py4j requires Python 3.9+). The +names below are kept solely so that downstream code that still imports +from this module (``from py4j.compat import unicode``, etc.) continues +to work. + +New code inside py4j should use the Python 3 built-ins directly: + + - ``unicode`` -> ``str`` + - ``basestring`` -> ``str`` + - ``long`` -> ``int`` + - ``range`` -> built-in ``range`` + - ``unichr`` -> ``chr`` + - ``bytestr`` -> ``bytes`` + - ``bytearray2`` -> ``bytes`` (or ``bytearray``; alias kept as ``bytes``) + - ``iteritems(d)`` -> ``d.items()`` + - ``items(d)`` -> ``list(d.items())`` + - ``next(x)`` -> built-in ``next(x)`` + - ``Queue`` -> ``from queue import Queue`` + - ``Empty`` -> ``from queue import Empty`` + - ``CompatThread`` -> ``threading.Thread`` (already supports ``daemon``) + +Only ``hasattr2`` has no Python-3-builtin replacement: it uses +``inspect.getattr_static`` to test for an attribute without triggering +descriptor / ``__getattr__`` side effects, which is genuinely +different behavior from the built-in ``hasattr``. + +This module may be removed in a future major release; downstream code +should migrate off the imports above. :author: Alex Grönholm """ -from __future__ import unicode_literals, absolute_import - import inspect import sys from threading import Thread +from queue import Queue as _StdQueue, Empty as _StdEmpty -version_info = sys.version_info - -if version_info.major < 3: - def items(d): - return d.items() - - def iteritems(d): - return d.iteritems() - - def next(x): - return x.next() - - range = xrange # noqa - - long = long # noqa - - basestring = basestring # noqa - - unicode = unicode # noqa - - bytearray2 = bytearray - unichr = unichr # noqa - - bytestr = str - - tobytestr = str - - def isbytestr(s): - return isinstance(s, str) - - def ispython3bytestr(s): - return False - - def isbytearray(s): - return isinstance(s, bytearray) +version_info = sys.version_info - def bytetoint(b): - return ord(b) - def bytetostr(b): - return b +def items(d): + return list(d.items()) - def strtobyte(b): - return b - import Queue - Empty = Queue.Empty - Queue = Queue.Queue +def iteritems(d): + return d.items() -else: - def items(d): - return list(d.items()) - def iteritems(d): - return d.items() +# Built-in passthroughs — kept as module attributes so existing +# ``from py4j.compat import range, unicode, ...`` imports still work. +range = range +long = int +basestring = str +unicode = str +bytearray2 = bytes +unichr = chr +bytestr = bytes - next = next - range = range +def tobytestr(s): + return bytes(s, "ascii") - long = int - basestring = str +def isbytestr(s): + return isinstance(s, bytes) - unicode = str - bytearray2 = bytes +def ispython3bytestr(s): + return isinstance(s, bytes) - unichr = chr - bytestr = bytes +def isbytearray(s): + return isinstance(s, bytearray) - def tobytestr(s): - return bytes(s, "ascii") - def isbytestr(s): - return isinstance(s, bytes) +def bytetoint(b): + return b - def ispython3bytestr(s): - return isinstance(s, bytes) - def isbytearray(s): - return isinstance(s, bytearray) +def bytetostr(b): + return str(b, encoding="ascii") - def bytetoint(b): - return b - def bytetostr(b): - return str(b, encoding="ascii") +def strtobyte(s): + return bytes(s, encoding="ascii") - def strtobyte(s): - return bytes(s, encoding="ascii") - import queue - Queue = queue.Queue - Empty = queue.Empty +Queue = _StdQueue +Empty = _StdEmpty if hasattr(inspect, "getattr_static"): @@ -116,19 +99,7 @@ def hasattr2(obj, attr): hasattr2 = hasattr -class CompatThread(Thread): - """Compatibility Thread class. - - Allows Python 2 Thread class to accept daemon kwarg in init. - """ - - def __init__(self, *args, **kwargs): - daemon = None - try: - daemon = kwargs.pop("daemon") - except KeyError: - pass - super(CompatThread, self).__init__(*args, **kwargs) - - if daemon: - self.daemon = daemon +# Python 3's ``threading.Thread`` accepts the ``daemon`` kwarg natively, +# so this is just a passthrough. Kept as a name for external callers +# that imported ``CompatThread`` from this module. +CompatThread = Thread diff --git a/py4j-python/src/py4j/finalizer.py b/py4j-python/src/py4j/finalizer.py index 381ba5e5..c131b44b 100644 --- a/py4j-python/src/py4j/finalizer.py +++ b/py4j-python/src/py4j/finalizer.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- """ Module that defines a Finalizer class responsible for registering and cleaning finalizer @@ -7,12 +6,8 @@ :author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - from threading import RLock -from py4j.compat import items - class ThreadSafeFinalizer(object): """A `ThreadSafeFinalizer` is a global class used to register weak @@ -64,7 +59,7 @@ def clear_finalizers(cls, clear_all=False): if clear_all: cls.finalizers.clear() else: - for id, ref in items(cls.finalizers): + for id, ref in list(cls.finalizers.items()): if ref() is None: cls.finalizers.pop(id, None) @@ -116,7 +111,7 @@ def clear_finalizers(cls, clear_all=False): if clear_all: cls.finalizers.clear() else: - for id, ref in items(cls.finalizers): + for id, ref in list(cls.finalizers.items()): if ref() is None: cls.finalizers.pop(id, None) diff --git a/py4j-python/src/py4j/java_collections.py b/py4j-python/src/py4j/java_collections.py index 3ae3636e..b478309d 100644 --- a/py4j-python/src/py4j/java_collections.py +++ b/py4j-python/src/py4j/java_collections.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- """ Module responsible for converting Java collection classes to Python collection classes. This module is optional but loaded by default. @@ -8,26 +7,11 @@ :author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - -# As of Python 3.3, the abstract base classes in the collections module have -# been moved to collections.abc. -# (see https://docs.python.org/3.3/library/collections.abc.html) -try: - # Python >=3.3 - from collections.abc import ( - MutableMapping, Sequence, MutableSequence, - MutableSet, Set) -except ImportError: - # Python <=3.2 - from collections import ( - MutableMapping, Sequence, MutableSequence, - MutableSet, Set) -import sys - -from py4j.compat import ( - iteritems, next, hasattr2, isbytearray, - ispython3bytestr, basestring) +from collections.abc import ( + MutableMapping, Sequence, MutableSequence, + MutableSet, Set) + +from py4j.compat import hasattr2 from py4j.java_gateway import JavaObject, JavaMember, get_method, JavaClass from py4j import protocol as proto from py4j.protocol import ( @@ -105,7 +89,7 @@ def __str__(self): def __repr__(self): items = ( "{0}: {1}".format(repr(k), repr(v)) - for k, v in iteritems(self)) + for k, v in self.items()) return "{{{0}}}".format(", ".join(items)) @@ -114,8 +98,8 @@ class JavaSet(JavaObject, MutableSet): All operations possible on a Python set are implemented.""" - __EMPTY_SET = "set([])" if sys.version_info.major < 3 else "set()" - __SET_TEMPLATE = "set([{0}])" if sys.version_info.major < 3 else "{{{0}}}" + __EMPTY_SET = "set()" + __SET_TEMPLATE = "{{{0}}}" def __init__(self, target_id, gateway_client): JavaObject.__init__(self, target_id, gateway_client) @@ -507,8 +491,9 @@ class ListConverter(object): def can_convert(self, object): # Check for iterator protocol and should not be an instance of byte # array (taken care of by protocol) - return hasattr2(object, "__iter__") and not isbytearray(object) and\ - not ispython3bytestr(object) and not isinstance(object, basestring) + return hasattr2(object, "__iter__") and \ + not isinstance(object, bytearray) and \ + not isinstance(object, bytes) and not isinstance(object, str) def convert(self, object, gateway_client): ArrayList = JavaClass("java.util.ArrayList", gateway_client) diff --git a/py4j-python/src/py4j/java_gateway.py b/py4j-python/src/py4j/java_gateway.py index 1607c00f..f65e8369 100644 --- a/py4j-python/src/py4j/java_gateway.py +++ b/py4j-python/src/py4j/java_gateway.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- """Module to interact with objects in a Java Virtual Machine from a Python Virtual Machine. @@ -10,12 +9,11 @@ :author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - from collections import deque import logging import os from pydoc import pager +from queue import Queue import select import socket import struct @@ -26,8 +24,7 @@ from threading import Thread, RLock import weakref -from py4j.compat import ( - range, hasattr2, basestring, CompatThread, Queue) +from py4j.compat import hasattr2 from py4j.finalizer import ThreadSafeFinalizer from py4j import protocol as proto from py4j.protocol import ( @@ -451,7 +448,7 @@ def is_instance_of(gateway, java_object, java_class): :param java_class: can be a string (fully qualified name), a JavaClass instance, or a JavaObject instance) """ - if isinstance(java_class, basestring): + if isinstance(java_class, str): param = java_class elif isinstance(java_class, JavaClass): param = java_class._fqn @@ -694,7 +691,7 @@ def _garbage_collect_proxy(pool, proxy_id): return success -class OutputConsumer(CompatThread): +class OutputConsumer(Thread): """Thread that consumes output """ @@ -725,7 +722,7 @@ def run(self): self.redirect_func(smart_decode(line)) -class ProcessConsumer(CompatThread): +class ProcessConsumer(Thread): """Thread that ensures process stdout and stderr are properly closed. """ diff --git a/py4j-python/src/py4j/protocol.py b/py4j-python/src/py4j/protocol.py index da13fdd2..2f01514e 100644 --- a/py4j-python/src/py4j/protocol.py +++ b/py4j-python/src/py4j/protocol.py @@ -16,17 +16,10 @@ :author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - from base64 import standard_b64encode, standard_b64decode from decimal import Decimal -from py4j.compat import ( - long, basestring, unicode, bytearray2, - bytestr, isbytestr, isbytearray, ispython3bytestr, - bytetoint, bytetostr, strtobyte) - JAVA_MAX_INT = 2147483647 JAVA_MIN_INT = -2147483648 @@ -159,7 +152,7 @@ OUTPUT_CONVERTER = { NULL_TYPE: (lambda x, y: None), BOOLEAN_TYPE: (lambda value, y: value.lower() == "true"), - LONG_TYPE: (lambda value, y: long(value)), + LONG_TYPE: (lambda value, y: int(value)), DECIMAL_TYPE: (lambda value, y: Decimal(value)), INTEGER_TYPE: (lambda value, y: int(value)), BYTES_TYPE: (lambda value, y: decode_bytearray(value)), @@ -213,13 +206,12 @@ def unescape_new_line(escaped): def smart_decode(s): - if isinstance(s, unicode): + if isinstance(s, str): return s - elif isinstance(s, bytestr): - # Should never reach this case in Python 3 - return unicode(s, "utf-8") + elif isinstance(s, bytes): + return str(s, "utf-8") else: - return unicode(s) + return str(s) def encode_float(float_value): @@ -234,16 +226,16 @@ def encode_float(float_value): def encode_bytearray(barray): - if isbytestr(barray): - return bytetostr(standard_b64encode(barray)) + if isinstance(barray, bytes): + return str(standard_b64encode(barray), encoding="ascii") else: - newbytestr = bytestr(barray) - return bytetostr(standard_b64encode(newbytestr)) + newbytestr = bytes(barray) + return str(standard_b64encode(newbytestr), encoding="ascii") def decode_bytearray(encoded): - new_bytes = strtobyte(encoded) - return bytearray2([bytetoint(b) for b in standard_b64decode(new_bytes)]) + new_bytes = bytes(encoded, encoding="ascii") + return bytes([b for b in standard_b64decode(new_bytes)]) def is_python_proxy(parameter): @@ -265,7 +257,7 @@ def get_command_part(parameter, python_proxy_pool=None): """Converts a Python object into a string representation respecting the Py4J protocol. - For example, the integer `1` is converted to `u"i1"` + For example, the integer `1` is converted to `"i1"` :param parameter: the object to convert :rtype: the string representing the command part @@ -281,15 +273,15 @@ def get_command_part(parameter, python_proxy_pool=None): elif isinstance(parameter, int) and parameter <= JAVA_MAX_INT\ and parameter >= JAVA_MIN_INT: command_part = INTEGER_TYPE + smart_decode(parameter) - elif isinstance(parameter, long) or isinstance(parameter, int): + elif isinstance(parameter, int): command_part = LONG_TYPE + smart_decode(parameter) elif isinstance(parameter, float): command_part = DOUBLE_TYPE + encode_float(parameter) - elif isbytearray(parameter): + elif isinstance(parameter, bytearray): command_part = BYTES_TYPE + encode_bytearray(parameter) - elif ispython3bytestr(parameter): + elif isinstance(parameter, bytes): command_part = BYTES_TYPE + encode_bytearray(parameter) - elif isinstance(parameter, basestring): + elif isinstance(parameter, str): command_part = STRING_TYPE + escape_new_line(parameter) elif is_python_proxy(parameter): command_part = PYTHON_PROXY_TYPE + python_proxy_pool.put(parameter) @@ -410,7 +402,7 @@ def register_input_converter(converter, prepend=False): When initialized with `auto_convert=True`, a :class:`JavaGateway ` will use the input converters on any parameter that is not a :class:`JavaObject ` - or `basestring` instance. + or `str` instance. :param converter: A converter that declares the methods `can_convert(object)` and `convert(object,gateway_client)`. @@ -456,8 +448,6 @@ class Py4JJavaError(Py4JError): `str(py4j_java_error)` returns the error message and the stack trace available on the Java side (similar to printStackTrace()). - Note that `str(py4j_java_error)` in Python 2 might not automatically handle - a non-ascii unicode string but throw an error if the exception contains it. """ def __init__(self, msg, java_exception): @@ -471,7 +461,4 @@ def __str__(self): gateway_client = self.java_exception._gateway_client answer = gateway_client.send_command(self.exception_cmd) return_value = get_return_value(answer, gateway_client, None, None) - # Note: technically this should return a bytestring 'str' rather than - # unicodes in Python 2; however, it can return unicodes for now. - # See https://github.com/bartdag/py4j/issues/306 for more details. return "{0}: {1}".format(self.errmsg, return_value) diff --git a/py4j-python/src/py4j/signals.py b/py4j-python/src/py4j/signals.py index cc3d37fc..1627a8d1 100644 --- a/py4j-python/src/py4j/signals.py +++ b/py4j-python/src/py4j/signals.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- """Module that provides a simple signals library. The signals pattern is very similar to the listener/observer pattern. @@ -7,8 +6,6 @@ from inspect import ismethod from threading import Lock -from py4j.compat import range - def make_id(func): if ismethod(func): diff --git a/py4j-python/src/py4j/tests/byte_string_test.py b/py4j-python/src/py4j/tests/byte_string_test.py index 2f9ea060..9f7a3919 100644 --- a/py4j-python/src/py4j/tests/byte_string_test.py +++ b/py4j-python/src/py4j/tests/byte_string_test.py @@ -1,6 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import absolute_import - import unittest import time diff --git a/py4j-python/src/py4j/tests/client_server_test.py b/py4j-python/src/py4j/tests/client_server_test.py index 3e83e226..ef3fd04a 100644 --- a/py4j-python/src/py4j/tests/client_server_test.py +++ b/py4j-python/src/py4j/tests/client_server_test.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- from contextlib import contextmanager import gc from multiprocessing import Process diff --git a/py4j-python/src/py4j/tests/finalizer_test.py b/py4j-python/src/py4j/tests/finalizer_test.py index edebf741..0f2baed3 100644 --- a/py4j-python/src/py4j/tests/finalizer_test.py +++ b/py4j-python/src/py4j/tests/finalizer_test.py @@ -3,8 +3,6 @@ @author: barthelemy """ -from __future__ import unicode_literals, absolute_import - import gc import unittest from weakref import ref diff --git a/py4j-python/src/py4j/tests/java_array_test.py b/py4j-python/src/py4j/tests/java_array_test.py index 694f0b92..0d06964f 100644 --- a/py4j-python/src/py4j/tests/java_array_test.py +++ b/py4j-python/src/py4j/tests/java_array_test.py @@ -3,8 +3,6 @@ @author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - import time import unittest diff --git a/py4j-python/src/py4j/tests/java_callback_test.py b/py4j-python/src/py4j/tests/java_callback_test.py index 8e1d634f..a2f7b179 100644 --- a/py4j-python/src/py4j/tests/java_callback_test.py +++ b/py4j-python/src/py4j/tests/java_callback_test.py @@ -3,8 +3,6 @@ @author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - from contextlib import contextmanager from multiprocessing import Process import subprocess @@ -12,7 +10,6 @@ from traceback import print_exc import unittest -from py4j.compat import range from py4j.java_gateway import ( JavaGateway, PythonProxyPool, CallbackServerParameters, set_default_callback_accept_timeout, is_instance_of) diff --git a/py4j-python/src/py4j/tests/java_dir_test.py b/py4j-python/src/py4j/tests/java_dir_test.py index f8a1e0bd..7cc2de47 100644 --- a/py4j-python/src/py4j/tests/java_dir_test.py +++ b/py4j-python/src/py4j/tests/java_dir_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals, absolute_import - from py4j.java_gateway import ( java_import, UserHelpAutoCompletion) from py4j.protocol import Py4JError diff --git a/py4j-python/src/py4j/tests/java_gateway_test.py b/py4j-python/src/py4j/tests/java_gateway_test.py index 7392179e..66e81679 100644 --- a/py4j-python/src/py4j/tests/java_gateway_test.py +++ b/py4j-python/src/py4j/tests/java_gateway_test.py @@ -1,11 +1,8 @@ -# -*- coding: UTF-8 -*- """ Created on Dec 10, 2009 @author: barthelemy """ -from __future__ import unicode_literals, absolute_import, print_function - from collections import deque from contextlib import contextmanager from decimal import Decimal @@ -14,6 +11,7 @@ from multiprocessing import Process import os import sys +from queue import Queue from socket import AF_INET, SOCK_STREAM, socket import subprocess import tempfile @@ -22,9 +20,6 @@ from traceback import print_exc import unittest -from py4j.compat import ( - range, isbytearray, ispython3bytestr, bytearray2, long, - Queue) from py4j.finalizer import ThreadSafeFinalizer from py4j.java_gateway import ( JavaGateway, JavaMember, get_field, get_method, @@ -366,7 +361,7 @@ def testProtocolReceive(self): self.assertAlmostEqual(1.25, ex.method3()) self.assertTrue(ex.method2() is None) self.assertTrue(ex.method4()) - self.assertEqual(long(123), ex.method8()) + self.assertEqual(123, ex.method8()) self.assertEqual(float("inf"), ex.method8()) self.gateway.shutdown() @@ -496,7 +491,7 @@ def testAutoField(self): gateway_parameters=GatewayParameters(auto_field=True)) ex = self.gateway.getNewExample() self.assertEqual(ex.field10, 10) - self.assertEqual(ex.field11, long(11)) + self.assertEqual(ex.field11, 11) sb = ex.field20 sb.append("Hello") self.assertEqual("Hello", sb.toString()) @@ -733,11 +728,11 @@ def testLongInt(self): self.assertEqual(1, ex.method7(1234)) self.assertEqual(4, ex.method7(2147483648)) self.assertEqual(4, ex.method7(-2147483649)) - self.assertEqual(4, ex.method7(long(2147483648))) - self.assertEqual(long(4), ex.method8(3)) + self.assertEqual(4, ex.method7(2147483648)) + self.assertEqual(4, ex.method8(3)) self.assertEqual(4, ex.method8(3)) - self.assertEqual(long(4), ex.method8(long(3))) - self.assertEqual(long(4), ex.method9(long(3))) + self.assertEqual(4, ex.method8(3)) + self.assertEqual(4, ex.method9(3)) try: ex.method8(3000000000000000000000000000000000000) self.fail("Should not be able to convert overflowing long") @@ -874,7 +869,7 @@ def testBytesType(self): int_list = [0, 1, 10, 127, 128, 255] ba1 = bytearray(int_list) # Same for Python2, bytes for Python 3 - ba2 = bytearray2(int_list) + ba2 = bytes(int_list) a1 = ex.getBytesValue(ba1) a2 = ex.getBytesValue(ba2) for i1, i2 in zip(a1, int_list): @@ -891,7 +886,7 @@ def testBytesType2(self): # strings) # Python 3: bytes (because bytes is closer to the byte[] representation # in Java) - self.assertTrue(isbytearray(a1) or ispython3bytestr(a1)) + self.assertTrue(isinstance(a1, (bytearray, bytes))) for i1, i2 in zip(a1, int_list): self.assertEqual(i1, i2) @@ -1164,11 +1159,7 @@ def testShutdownSubprocess(self): # giving two orders of magnitude of headroom over a healthy # JVM's tens-of-ms shutdown - reaching 5 s indicates a real # hang, not scheduler jitter. - if sys.version_info < (3,): - sleep() - self.assertFalse(self.gateway.java_process.poll() is None) - else: - self.gateway.java_process.wait(5) + self.gateway.java_process.wait(5) # Popen.wait() will raise a TimeoutExpired exception if the subprocess # has not yet terminated. @@ -1183,13 +1174,9 @@ def testShutdownSubprocessThatDiesOnExit(self): self.assertTrue(self.gateway.java_process.poll() is None) self.gateway.java_process.stdin.write("\n".encode("utf-8")) self.gateway.java_process.stdin.flush() - if sys.version_info < (3,): - sleep() - self.assertFalse(self.gateway.java_process.poll() is None) - else: - # Same rationale as testShutdownSubprocess: 1 s timed out on - # Py3.11/Java 21/macOS. 5 s gives headroom for noisy CI. - self.gateway.java_process.wait(5) + # Same rationale as testShutdownSubprocess: 1 s timed out on + # Py3.11/Java 21/macOS. 5 s gives headroom for noisy CI. + self.gateway.java_process.wait(5) def testJavaopts(self): self.gateway = JavaGateway.launch_gateway(javaopts=["-Xmx64m"]) diff --git a/py4j-python/src/py4j/tests/java_help_test.py b/py4j-python/src/py4j/tests/java_help_test.py index 3a6cf26d..3f2f1abb 100644 --- a/py4j-python/src/py4j/tests/java_help_test.py +++ b/py4j-python/src/py4j/tests/java_help_test.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals, absolute_import - - from py4j.tests.java_gateway_test import gateway, example_app_process diff --git a/py4j-python/src/py4j/tests/java_iter_test.py b/py4j-python/src/py4j/tests/java_iter_test.py index 9873c633..317462b5 100644 --- a/py4j-python/src/py4j/tests/java_iter_test.py +++ b/py4j-python/src/py4j/tests/java_iter_test.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals, absolute_import - import unittest from py4j.java_gateway import JavaGateway, is_instance_of diff --git a/py4j-python/src/py4j/tests/java_list_test.py b/py4j-python/src/py4j/tests/java_list_test.py index bccef6e0..b6f6c0d6 100644 --- a/py4j-python/src/py4j/tests/java_list_test.py +++ b/py4j-python/src/py4j/tests/java_list_test.py @@ -3,11 +3,8 @@ @author: barthelemy """ -from __future__ import unicode_literals, absolute_import - import unittest -from py4j.compat import unicode from py4j.java_gateway import JavaGateway, GatewayParameters from py4j.protocol import Py4JJavaError, Py4JError from py4j.tests.java_gateway_test import ( @@ -15,7 +12,7 @@ def get_list(count): - return [unicode(i) for i in range(count)] + return [str(i) for i in range(count)] class AutoConvertTest(unittest.TestCase): diff --git a/py4j-python/src/py4j/tests/java_map_test.py b/py4j-python/src/py4j/tests/java_map_test.py index b8abf22f..f3d00783 100644 --- a/py4j-python/src/py4j/tests/java_map_test.py +++ b/py4j-python/src/py4j/tests/java_map_test.py @@ -3,8 +3,6 @@ @author: barthelemy """ -from __future__ import unicode_literals, absolute_import - import unittest from py4j.java_gateway import JavaGateway, GatewayParameters diff --git a/py4j-python/src/py4j/tests/java_set_test.py b/py4j-python/src/py4j/tests/java_set_test.py index 8afdf45d..d5901701 100644 --- a/py4j-python/src/py4j/tests/java_set_test.py +++ b/py4j-python/src/py4j/tests/java_set_test.py @@ -3,8 +3,6 @@ @author: Barthelemy Dagenais """ -from __future__ import unicode_literals, absolute_import - import unittest from py4j.java_gateway import JavaGateway, GatewayParameters diff --git a/py4j-python/src/py4j/tests/java_tls_test.py b/py4j-python/src/py4j/tests/java_tls_test.py index fefcd6c3..7a2947d9 100644 --- a/py4j-python/src/py4j/tests/java_tls_test.py +++ b/py4j-python/src/py4j/tests/java_tls_test.py @@ -3,8 +3,6 @@ @author: Nick White """ -from __future__ import unicode_literals, absolute_import - from multiprocessing import Process import subprocess import unittest diff --git a/py4j-python/src/py4j/tests/memory_leak_test.py b/py4j-python/src/py4j/tests/memory_leak_test.py index 950d3b66..e3ba9c88 100644 --- a/py4j-python/src/py4j/tests/memory_leak_test.py +++ b/py4j-python/src/py4j/tests/memory_leak_test.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- import platform from contextlib import contextmanager import gc diff --git a/py4j-python/src/py4j/tests/multithreadtest.py b/py4j-python/src/py4j/tests/multithreadtest.py index eeb53f8d..f0492574 100644 --- a/py4j-python/src/py4j/tests/multithreadtest.py +++ b/py4j-python/src/py4j/tests/multithreadtest.py @@ -3,12 +3,9 @@ @author: barthelemy """ -from __future__ import unicode_literals, absolute_import - from threading import Thread import unittest -from py4j.compat import range from py4j.java_gateway import JavaGateway from py4j.tests.java_gateway_test import ( start_example_app_process, sleep) diff --git a/py4j-python/src/py4j/tests/protocol_test.py b/py4j-python/src/py4j/tests/protocol_test.py new file mode 100644 index 00000000..c24e548b --- /dev/null +++ b/py4j-python/src/py4j/tests/protocol_test.py @@ -0,0 +1,229 @@ +""" +Pure-protocol unit tests covering Python 2 / 3 boundary semantics. + +Pinned here so the Python 2 removal can't silently regress the +externally-visible behaviour of ``py4j.compat`` (which downstream +projects still import from) or the protocol-encoding paths that +deal with ``bytes`` vs ``str`` distinctions. + +No JVM dependency — fast pure-Python tests. +""" +import unittest + +from py4j.protocol import ( + decode_bytearray, encode_bytearray, encode_float, + escape_new_line, unescape_new_line, get_command_part, + smart_decode, + NULL_TYPE, BOOLEAN_TYPE, INTEGER_TYPE, LONG_TYPE, DOUBLE_TYPE, + BYTES_TYPE, STRING_TYPE, JAVA_INFINITY, JAVA_NEGATIVE_INFINITY, + JAVA_NAN, JAVA_MAX_INT, JAVA_MIN_INT, +) + + +class EncodeBytearrayRoundTripTest(unittest.TestCase): + """``encode_bytearray`` / ``decode_bytearray`` round-trip protected + after the Python 2 removal collapsed the ``bytearray2`` / + ``bytestr`` aliases into ``bytes``. + """ + + def test_round_trip_bytes(self): + original = b"\x00\x01\x02\xff\xfe\xfd" + encoded = encode_bytearray(original) + self.assertIsInstance(encoded, str) + self.assertEqual(decode_bytearray(encoded), original) + + def test_round_trip_bytearray(self): + original = bytearray(b"abc\x00xyz") + encoded = encode_bytearray(original) + self.assertIsInstance(encoded, str) + # decode returns bytes, but content must compare equal. + self.assertEqual(decode_bytearray(encoded), bytes(original)) + + def test_round_trip_empty(self): + self.assertEqual(decode_bytearray(encode_bytearray(b"")), b"") + + def test_round_trip_high_bytes(self): + # All single-byte values, exercises the full 0-255 range. + original = bytes(range(256)) + self.assertEqual(decode_bytearray(encode_bytearray(original)), original) + + +class EscapeNewLineTest(unittest.TestCase): + """``escape_new_line`` / ``unescape_new_line`` round-trip protected. + The function preserves a long-standing wrapping behaviour for + ``bytes`` inputs (via ``smart_decode``) so removing ``smart_decode`` + here would be a behaviour change, not a Python 2 removal. + """ + + def test_str_passthrough_simple(self): + self.assertEqual(escape_new_line("hello"), "hello") + + def test_escapes_backslash_then_newlines(self): + # Order matters: backslash must be doubled first so that the + # ``\n`` / ``\r`` insertions don't get re-escaped. + self.assertEqual(escape_new_line("a\\b\rc\nd"), "a\\\\b\\rc\\nd") + + def test_round_trip_with_unescape(self): + original = "first line\nsecond\tline\rthird\\fourth" + self.assertEqual(unescape_new_line(escape_new_line(original)), original) + + def test_falsy_passthrough(self): + # Documented behaviour: empty / None returns input unchanged. + self.assertEqual(escape_new_line(""), "") + self.assertIsNone(escape_new_line(None)) + + +class GetCommandPartTest(unittest.TestCase): + """Spot-check ``get_command_part`` after the type-check aliases + (``isinstance(x, long)`` etc.) were collapsed into native py3 + types. + """ + + def _strip(self, s): + self.assertTrue(s.endswith("\n")) + return s[:-1] + + def test_none_uses_null_prefix(self): + self.assertEqual(self._strip(get_command_part(None)), NULL_TYPE.strip()) + + def test_bool_dispatched_before_int(self): + # ``isinstance(True, int)`` is True in Python — the bool branch + # must run first so booleans don't get INTEGER-encoded. + self.assertTrue( + self._strip(get_command_part(True)).startswith(BOOLEAN_TYPE)) + self.assertTrue( + self._strip(get_command_part(False)).startswith(BOOLEAN_TYPE)) + + def test_small_int_uses_integer_prefix(self): + self.assertEqual( + self._strip(get_command_part(42)), + INTEGER_TYPE + "42") + + def test_int_at_java_max_uses_integer_prefix(self): + # Inclusive boundary. + self.assertEqual( + self._strip(get_command_part(JAVA_MAX_INT)), + INTEGER_TYPE + str(JAVA_MAX_INT)) + + def test_int_above_java_max_uses_long_prefix(self): + big = JAVA_MAX_INT + 1 + self.assertEqual( + self._strip(get_command_part(big)), + LONG_TYPE + str(big)) + + def test_int_below_java_min_uses_long_prefix(self): + small = JAVA_MIN_INT - 1 + self.assertEqual( + self._strip(get_command_part(small)), + LONG_TYPE + str(small)) + + def test_float_uses_double_prefix(self): + self.assertEqual( + self._strip(get_command_part(0.5)), + DOUBLE_TYPE + "0.5") + + def test_str_uses_string_prefix_with_escaping(self): + out = self._strip(get_command_part("a\nb")) + self.assertEqual(out, STRING_TYPE + "a\\nb") + + def test_bytes_uses_bytes_prefix(self): + out = self._strip(get_command_part(b"abc")) + self.assertTrue(out.startswith(BYTES_TYPE)) + + def test_bytearray_uses_bytes_prefix(self): + out = self._strip(get_command_part(bytearray(b"abc"))) + self.assertTrue(out.startswith(BYTES_TYPE)) + + +class EncodeFloatTest(unittest.TestCase): + """``encode_float`` after the ``smart_decode(repr(...))`` / + ``unicode(...)`` aliases were inlined. Java-side spellings for + inf / -inf / NaN preserved. + """ + + def test_positive_infinity(self): + self.assertEqual(encode_float(float("inf")), JAVA_INFINITY) + + def test_negative_infinity(self): + self.assertEqual(encode_float(float("-inf")), JAVA_NEGATIVE_INFINITY) + + def test_nan(self): + self.assertEqual(encode_float(float("nan")), JAVA_NAN) + + def test_finite_float(self): + self.assertEqual(encode_float(1.0), "1.0") + + +class SmartDecodeTest(unittest.TestCase): + """``smart_decode`` body was rewritten with native py3 types + (``str`` instead of ``unicode``; ``bytes`` instead of ``bytestr``). + Function-level behaviour must be unchanged. + """ + + def test_str_input_returns_input_unchanged(self): + self.assertEqual(smart_decode("hello"), "hello") + + def test_bytes_input_decodes_as_utf8(self): + self.assertEqual(smart_decode(b"hello"), "hello") + self.assertEqual(smart_decode("café".encode("utf-8")), "café") + + def test_non_string_falls_back_to_str(self): + self.assertEqual(smart_decode(123), "123") + self.assertEqual(smart_decode(0.5), "0.5") + self.assertEqual(smart_decode(None), "None") + + +class CompatModuleBackcompatTest(unittest.TestCase): + """``py4j.compat`` is kept around as a soft-deprecated shim for any + external callers that imported its py2/3 names + (``from py4j.compat import unicode``, etc.). This pins every + exported name to its py3 equivalent so a future cleanup of the + module can't silently break downstream imports. + """ + + def test_compat_exports_resolve(self): + from py4j.compat import ( # noqa: F401 + items, iteritems, range, long, basestring, unicode, + bytearray2, unichr, bytestr, tobytestr, isbytestr, + ispython3bytestr, isbytearray, bytetoint, bytetostr, + strtobyte, Queue, Empty, hasattr2, CompatThread, + version_info, + ) + + def test_compat_aliases_resolve_to_py3_builtins(self): + from py4j import compat + self.assertIs(compat.long, int) + self.assertIs(compat.basestring, str) + self.assertIs(compat.unicode, str) + self.assertIs(compat.bytestr, bytes) + self.assertIs(compat.range, range) + self.assertIs(compat.unichr, chr) + + def test_compat_helpers_use_py3_semantics(self): + from py4j import compat + self.assertTrue(compat.isbytestr(b"")) + self.assertFalse(compat.isbytestr("")) + self.assertTrue(compat.ispython3bytestr(b"")) + self.assertFalse(compat.ispython3bytestr("")) + self.assertTrue(compat.isbytearray(bytearray())) + self.assertFalse(compat.isbytearray(b"")) + self.assertEqual(compat.bytetoint(b"a"[0]), b"a"[0]) + self.assertEqual(compat.bytetostr(b"abc"), "abc") + self.assertEqual(compat.strtobyte("abc"), b"abc") + self.assertEqual(compat.items({"a": 1}), [("a", 1)]) + self.assertEqual(list(compat.iteritems({"a": 1})), [("a", 1)]) + + def test_compat_thread_is_threading_thread(self): + from threading import Thread + from py4j.compat import CompatThread + self.assertIs(CompatThread, Thread) + + def test_compat_queue_is_stdlib_queue(self): + from queue import Queue as StdQueue, Empty as StdEmpty + from py4j.compat import Queue, Empty + self.assertIs(Queue, StdQueue) + self.assertIs(Empty, StdEmpty) + + +if __name__ == "__main__": + unittest.main() diff --git a/py4j-python/src/py4j/tests/py4j_signals_test.py b/py4j-python/src/py4j/tests/py4j_signals_test.py index 05ffab9e..118fade9 100644 --- a/py4j-python/src/py4j/tests/py4j_signals_test.py +++ b/py4j-python/src/py4j/tests/py4j_signals_test.py @@ -1,6 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import unicode_literals, absolute_import - from collections import defaultdict import unittest diff --git a/py4j-python/src/py4j/tests/signals_test.py b/py4j-python/src/py4j/tests/signals_test.py index 6406418d..85f2d102 100644 --- a/py4j-python/src/py4j/tests/signals_test.py +++ b/py4j-python/src/py4j/tests/signals_test.py @@ -1,6 +1,3 @@ -# -*- coding: UTF-8 -*- -from __future__ import unicode_literals, absolute_import - from unittest import TestCase from py4j.signals import Signal diff --git a/py4j-python/tox.ini b/py4j-python/tox.ini index a28ffe50..00793a7f 100644 --- a/py4j-python/tox.ini +++ b/py4j-python/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist=py27,py34,py35 +envlist = py{39,310,311,312,313} [testenv] -deps=nose -commands=nosetests -w src -c nose.cfg +deps = -r requirements-test.txt +commands = pytest -k "not java_tls_test." src/py4j/tests {posargs} diff --git a/py4j-web/.pydevproject b/py4j-web/.pydevproject index 266ba195..257f7e4c 100644 --- a/py4j-web/.pydevproject +++ b/py4j-web/.pydevproject @@ -1,4 +1,4 @@ -python 2.6 +python 3.9 Default \ No newline at end of file diff --git a/py4j-web/advanced_topics.rst b/py4j-web/advanced_topics.rst index e7f9f0fa..5a50b479 100644 --- a/py4j-web/advanced_topics.rst +++ b/py4j-web/advanced_topics.rst @@ -146,7 +146,7 @@ access to a byte required a call between the Python and the Java interpreter In summary: * If from Java, you return a byte[], Py4J will convert the byte[] to a - bytearray (Python 2.x) or bytes (Python 3.x) variable in Python. + bytes variable in Python. * If from Python, you pass a bytearray or bytes variable to the Java side, Py4J will convert it to a byte[].