diff --git a/.azure/build.yml b/.azure/build.yml index 4b16e8e04..a4a01b625 100644 --- a/.azure/build.yml +++ b/.azure/build.yml @@ -65,6 +65,9 @@ jobs: linux-3.11: imageName: "ubuntu-latest" python.version: '3.11' + linux-3.12: + imageName: "ubuntu-latest" + python.version: '3.12' windows-3.7: imageName: "windows-2019" python.version: '3.7' @@ -81,6 +84,9 @@ jobs: windows-3.11: imageName: "windows-2019" python.version: '3.11' + windows-3.12: + imageName: "windows-2019" + python.version: '3.12' mac-3.9: imageName: "macos-11" python.version: '3.9' diff --git a/.azure/doc-requirements.txt b/.azure/doc-requirements.txt index 5b2b51d90..c6e277493 100644 --- a/.azure/doc-requirements.txt +++ b/.azure/doc-requirements.txt @@ -1,6 +1,6 @@ # TODO: consider unpinning these? -Pygments==2.7.4 +Pygments==2.15.0 docutils==0.14 commonmark==0.8.1 recommonmark==0.5.0 diff --git a/.azure/scripts/test.yml b/.azure/scripts/test.yml index 3177c59ab..c67cdcc2a 100644 --- a/.azure/scripts/test.yml +++ b/.azure/scripts/test.yml @@ -9,6 +9,7 @@ steps: version: 11 - script: | + python -m pip install --upgrade pytest setuptools python setup.py build_ext --inplace displayName: 'Build module' diff --git a/.readthedocs.yml b/.readthedocs.yml index 4b12f640c..253f5707c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,6 @@ sphinx: # Optionally set the version of Python and requirements required to build your docs python: - version: "3.8" install: - method: pip path: . @@ -21,7 +20,9 @@ python: - docs build: - image: latest + tools: + python: "3.8" + os: ubuntu-22.04 apt_packages: - openjdk-8-jdk diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst index 4c5684a58..c4e08c0ab 100644 --- a/doc/CHANGELOG.rst +++ b/doc/CHANGELOG.rst @@ -5,11 +5,20 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o Latest Changes: - **1.5.0_dev0 - 2023-04-03** -- **1.4.2_dev0 - 2022-10-26** + + - Switched ``__eq__`` and ``__ne__`` operator to use ``equals`` rather than + ``compareTo`` for comparable objects to avoid exception when comparing + object of different types. + + - Fixed segmentation fault when comparing Java Comparable to primitives. + + - Java exceptions that occur in inequality comparisons now map to Python + TypeError. - Fixed crash when calling subscript on JArray. - - Fixed direct byte buffers not reporting nbytes correctly when cast to memoryview. + - Fixed direct byte buffers not reporting nbytes correctly when cast to + memoryview. - Expand the defintion for Functional interface to include classes without FunctionInterface annotation. diff --git a/doc/install.rst b/doc/install.rst index 9fab4ae32..e5453dce8 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -34,7 +34,7 @@ Java JPype source distribution includes a copy of the Java JNI header and precompiled Java code, thus the Java Development Kit (JDK) is not required. - JPype has been tested with Java versions from Java 1.7 to Java 13. + JPype has been tested with Java versions from Java 1.8 to Java 13. C++ A C++ compiler which matches the ABI used to build CPython. diff --git a/doc/userguide.rst b/doc/userguide.rst index 3fd9bb467..bd7874158 100644 --- a/doc/userguide.rst +++ b/doc/userguide.rst @@ -36,11 +36,11 @@ hard at work on your latest project but you just need to pip in the database driver for your customers database and you can call it a night. Unfortunately, it appears that your customers database will not connect to the Python database API. The whole thing is custom and the customer isn't going to supply you with -a Python version. They did sent you a Java driver for the database but fat +a Python version. They did send you a Java driver for the database but fat lot of good that will do for you. Stumbling through the internet you find a module that says it can natively -load Java packages as Python modules. Well, it worth a shot... +load Java packages as Python modules. Well, it's worth a shot... So first thing the guide says is that you need to install Java and set up a ``JAVA_HOME`` environment variable pointing to the JRE. Then start the @@ -305,9 +305,9 @@ design goals. - Favor clarity over performance. This doesn't mean not trying to optimize paths, but just as premature optimization is the bane of programmers, requiring writing to maximize speed is a poor long term choice, especially - in a language such as Python were weak typing can promote bit rot. + in a language such as Python where weak typing can promote bit rot. -- If a new method has to be introduced, make look familiar. +- If a new method has to be introduced, make it look familiar. Java programmers look to a method named "of" to convert to a type on factories such as a Stream, thus ``JArray.of`` converts a Python NumPy array to Java. Python programmers expect that memory backed objects can be converted @@ -546,7 +546,7 @@ JPype Concepts *************** At its heart, JPype is about providing a bridge to use Java within Python. -Depending on your prospective that can either be a means of accessing Java +Depending on your perspective that can either be a means of accessing Java libraries from within Python or a way to use Java using Python syntax for interactivity and visualization. This mean not only exposing a limited API but instead trying to provide the entirety of the Java language with Python. @@ -2179,7 +2179,7 @@ NumPy arrays, and conversion of NumPy integer types to Java boxed types. Transfers to Java ================= -Memory from a NumPy array can be transferred Java in bulk. The transfer of +Memory from a NumPy array can be transferred to Java in bulk. The transfer of a one dimensional NumPy array to Java can either be done at initialization or by use of the Python slice operator. @@ -2271,7 +2271,7 @@ all buffers become invalid and any access to NumPy arrays backed by Java risk crashing. To avoid this fate, either create the memory for the buffer from within Python and pass it to Java. Or use the Java ``java.lang.Runtime.exit`` which will terminate both the Java and Python process without leaving any -opertunity to access a dangling buffer. +opportunity to access a dangling buffer. Buffer backed memory is not limited to use with NumPy. Buffer transfers are supported to provide shared memory between processes or memory mapped files. @@ -2282,7 +2282,7 @@ NumPy Primitives ================ When converting a Python type to a boxed Java type, there is the difficulty -that Java has no way to known the size of a Python numerical value. But when +that Java has no way to know the size of a Python numerical value. But when converting NumPy numerical types, this is not an issue. The following conversions apply to NumPy primitive types. diff --git a/examples/jms/testJpypePublisher.py b/examples/jms/testJpypePublisher.py index 8e0bf92eb..b658fd4cd 100644 --- a/examples/jms/testJpypePublisher.py +++ b/examples/jms/testJpypePublisher.py @@ -27,7 +27,7 @@ def pyPublisher(javaNamingFactory="weblogic.jndi.WLInitialContextFactory", t0 = time.time() for i in range(NUMMSGS): publisher.publish("Hello World! %s" % i) -print "MessageRate =", float(NUMMSGS) / (time.time() - t0) +print("MessageRate =", float(NUMMSGS) / (time.time() - t0)) # The "Stop" message signals the subscriber to stop timing message receipts publisher.publish("Stop") diff --git a/examples/jms/testJpypeSubscriber.py b/examples/jms/testJpypeSubscriber.py index df682b757..2737df680 100644 --- a/examples/jms/testJpypeSubscriber.py +++ b/examples/jms/testJpypeSubscriber.py @@ -24,12 +24,12 @@ class pyCallback: count = 0 def onMessage(self, text): - print text + print(text) if text == 'Start': pyCallback.startTime = time.time() pyCallback.count = 0 elif text == 'Stop': - print "Message Rate =", float(pyCallback.count) / (time.time() - pyCallback.startTime) + print("Message Rate =", float(pyCallback.count) / (time.time() - pyCallback.startTime)) else: pyCallback.count += 1 @@ -39,7 +39,7 @@ def onMessage(self, text): # Get a subscriber sub = pySubscriber(proxy) -print "Listening..." +print("Listening...") # Prevent this thread from exiting time.sleep(1000) diff --git a/examples/linux/findjvm.py b/examples/linux/findjvm.py index 95fe73e0d..1865a3515 100644 --- a/examples/linux/findjvm.py +++ b/examples/linux/findjvm.py @@ -18,4 +18,4 @@ import os.path jvmlib = jpype.getDefaultJVMPath() -print os.path.dirname(os.path.dirname(jvmlib)) +print(os.path.dirname(os.path.dirname(jvmlib))) diff --git a/examples/rmi.py b/examples/rmi.py index b9dd38634..4955df8e7 100644 --- a/examples/rmi.py +++ b/examples/rmi.py @@ -25,7 +25,7 @@ p = java.rmi.Naming.lookup("rmi://localhost:2004/server") -print p, p.__class__ +print(p, p.__class__) p.callRemote() diff --git a/jpype/_classpath.py b/jpype/_classpath.py index 78383ed2d..2d8c919eb 100644 --- a/jpype/_classpath.py +++ b/jpype/_classpath.py @@ -16,6 +16,8 @@ # # ***************************************************************************** import os as _os +import typing + import _jpype __all__ = ['addClassPath', 'getClassPath'] @@ -24,7 +26,7 @@ _SEP = _os.path.pathsep -def addClassPath(path1): +def addClassPath(path1: typing.Union[str, _os.PathLike]) -> None: """ Add a path to the Java class path Classpath items can be a java, a directory, or a @@ -66,7 +68,7 @@ def addClassPath(path1): _CLASSPATHS.append(path1) -def getClassPath(env=True): +def getClassPath(env: bool = True) -> str: """ Get the full Java class path. Includes user added paths and the environment CLASSPATH. @@ -79,7 +81,7 @@ def getClassPath(env=True): global _CLASSPATHS global _SEP - # Merge the evironment path + # Merge the environment path classPath = list(_CLASSPATHS) envPath = _os.environ.get("CLASSPATH") if env and envPath: diff --git a/jpype/_core.py b/jpype/_core.py index e038c8f77..3e2db229e 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -15,8 +15,13 @@ # See NOTICE file for details. # # ***************************************************************************** -import sys +from __future__ import annotations + import atexit +import os +import sys +import typing + import _jpype from . import types as _jtypes from . import _classpath @@ -56,6 +61,10 @@ class JVMNotRunning(RuntimeError): _w.warn(f"provided Jedi seems out of date. Version is {_jedi_version}.") +if typing.TYPE_CHECKING: + _PathOrStr = typing.Union[str, os.PathLike] + + # See http://scottlobdell.me/2015/04/decorators-arguments-python/ def deprecated(*args): """ Marks a function a deprecated when used as decorator. @@ -99,23 +108,48 @@ def isJVMStarted(): return _jpype.isStarted() -def _hasClassPath(args): +def _hasClassPath(args: typing.Tuple[_PathOrStr, ...]) -> bool: for i in args: - if i.startswith('-Djava.class.path'): + if isinstance(i, str) and i.startswith('-Djava.class.path'): return True return False -def _handleClassPath(clsList): +def _handleClassPath( + classpath: typing.Union[ + _PathOrStr, + typing.Tuple[_PathOrStr, ...] + ], +) -> str: + """ + Return a classpath which represents the given tuple of classpath specifications + """ out = [] - for s in clsList: - if not isinstance(s, str): - raise TypeError("Classpath elements must be strings") - if s.endswith('*'): + + if isinstance(classpath, (str, os.PathLike)): + classpath = (classpath,) + try: + # Convert anything iterable into a tuple. + classpath = tuple(classpath) + except TypeError: + raise TypeError("Unknown class path element") + + for element in classpath: + try: + pth = os.fspath(element) + except TypeError as err: + raise TypeError("Classpath elements must be strings or Path-like") from err + + if isinstance(pth, bytes): + # In the future we may allow this to support Paths which are undecodable. + # https://docs.python.org/3/howto/unicode.html#unicode-filenames. + raise TypeError("Classpath elements must be strings or Path-like") + + if pth.endswith('*'): import glob - out.extend(glob.glob(s + '.jar')) + out.extend(glob.glob(pth + '.jar')) else: - out.append(s) + out.append(pth) return _classpath._SEP.join(out) @@ -126,7 +160,14 @@ def interactive(): return bool(getattr(sys, 'ps1', sys.flags.interactive)) -def startJVM(*args, **kwargs): +def startJVM( + *jvmargs: str, + jvmpath: typing.Optional[_PathOrStr] = None, + classpath: typing.Optional[typing.Sequence[_PathOrStr], _PathOrStr] = None, + ignoreUnrecognized: bool = False, + convertStrings: bool = False, + interrupt: bool = not interactive(), +) -> None: """ Starts a Java Virtual Machine. Without options it will start the JVM with the default classpath and jvmpath. @@ -135,14 +176,14 @@ def startJVM(*args, **kwargs): The default jvmpath is determined by ``jpype.getDefaultJVMPath()``. Parameters: - *args (Optional, str[]): Arguments to give to the JVM. - The first argument may be the path the JVM. + *jvmargs (Optional, str[]): Arguments to give to the JVM. + The first argument may be the path to the JVM. Keyword Arguments: - jvmpath (str): Path to the jvm library file, + jvmpath (str, PathLike): Path to the jvm library file, Typically one of (``libjvm.so``, ``jvm.dll``, ...) Using None will apply the default jvmpath. - classpath (str,[str]): Set the classpath for the JVM. + classpath (str, PathLike, [str, PathLike]): Set the classpath for the JVM. This will override any classpath supplied in the arguments list. A value of None will give no classpath to JVM. ignoreUnrecognized (bool): Option to ignore @@ -161,8 +202,7 @@ def startJVM(*args, **kwargs): Raises: OSError: if the JVM cannot be started or is already running. - TypeError: if an invalid keyword argument is supplied - or a keyword argument conflicts with the arguments. + TypeError: if a keyword argument conflicts with the positional arguments. """ if _jpype.isStarted(): @@ -171,51 +211,36 @@ def startJVM(*args, **kwargs): if _JVM_started: raise OSError('JVM cannot be restarted') - args = list(args) - # JVM path - jvmpath = None - if args: + if jvmargs: # jvm is the first argument the first argument is a path or None - if not args[0] or not args[0].startswith('-'): - jvmpath = args.pop(0) - if 'jvmpath' in kwargs: - if jvmpath: - raise TypeError('jvmpath specified twice') - jvmpath = kwargs.pop('jvmpath') + if jvmargs[0] is None or (isinstance(jvmargs[0], str) and not jvmargs[0].startswith('-')): + if jvmpath: + raise TypeError('jvmpath specified twice') + jvmpath = jvmargs[0] + jvmargs = jvmargs[1:] + if not jvmpath: jvmpath = getDefaultJVMPath() + else: + # Allow the path to be a PathLike. + jvmpath = os.fspath(jvmpath) + + extra_jvm_args: typing.Tuple[str, ...] = tuple() # Classpath handling - if _hasClassPath(args): + if _hasClassPath(jvmargs): # Old style, specified in the arguments - if 'classpath' in kwargs: + if classpath is not None: # Cannot apply both styles, conflict raise TypeError('classpath specified twice') - classpath = None - elif 'classpath' in kwargs: - # New style, as a keywork - classpath = kwargs.pop('classpath') - else: - # Not speficied at all, use the default classpath + elif classpath is None: + # Not specified at all, use the default classpath. classpath = _classpath.getClassPath() # Handle strings and list of strings. if classpath: - if isinstance(classpath, str): - args.append('-Djava.class.path=%s' % _handleClassPath([classpath])) - elif hasattr(classpath, '__iter__'): - args.append('-Djava.class.path=%s' % _handleClassPath(classpath)) - else: - raise TypeError("Unknown class path element") - - ignoreUnrecognized = kwargs.pop('ignoreUnrecognized', False) - convertStrings = kwargs.pop('convertStrings', False) - interrupt = kwargs.pop('interrupt', not interactive()) - - if kwargs: - raise TypeError("startJVM() got an unexpected keyword argument '%s'" - % (','.join([str(i) for i in kwargs]))) + extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', ) try: import locale @@ -224,7 +249,7 @@ def startJVM(*args, **kwargs): # Keep the current locale settings, else Java will replace them. prior = [locale.getlocale(i) for i in categories] # Start the JVM - _jpype.startup(jvmpath, tuple(args), + _jpype.startup(jvmpath, jvmargs + extra_jvm_args, ignoreUnrecognized, convertStrings, interrupt) # Collect required resources for operation initializeResources() @@ -241,8 +266,7 @@ def startJVM(*args, **kwargs): match = re.search(r"([0-9]+)\.[0-9]+", source) if match: version = int(match.group(1)) - 44 - raise RuntimeError("%s is older than required Java version %d" % ( - jvmpath, version)) from ex + raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex raise diff --git a/jpype/_core.pyi b/jpype/_core.pyi deleted file mode 100644 index f4eb32c79..000000000 --- a/jpype/_core.pyi +++ /dev/null @@ -1,2 +0,0 @@ -class _JRuntime: - pass diff --git a/native/common/jp_gc.cpp b/native/common/jp_gc.cpp index 505373580..029d673dd 100644 --- a/native/common/jp_gc.cpp +++ b/native/common/jp_gc.cpp @@ -85,8 +85,8 @@ size_t getWorkingSize() return sz * page_size; #elif defined(USE_MALLINFO) - struct mallinfo mi; - mi = mallinfo(); + struct mallinfo2 mi; + mi = mallinfo2(); current = (size_t) mi.uordblks; #endif diff --git a/native/common/jp_javaframe.cpp b/native/common/jp_javaframe.cpp index ddf8e4c20..62bfd2626 100644 --- a/native/common/jp_javaframe.cpp +++ b/native/common/jp_javaframe.cpp @@ -1194,7 +1194,13 @@ jint JPJavaFrame::compareTo(jobject obj, jobject obj2) { jvalue v; v.l = obj2; - return CallIntMethodA(obj, m_Context->m_CompareToID, &v); + jint ret = m_Env->CallIntMethodA(obj, m_Context->m_CompareToID, &v); + if (m_Env->ExceptionOccurred()) + { + m_Env->ExceptionClear(); + JP_RAISE(PyExc_TypeError, "Unable to compare") + } + return ret; } jthrowable JPJavaFrame::getCause(jthrowable th) diff --git a/native/common/jp_primitivetype.cpp b/native/common/jp_primitivetype.cpp index 711533b68..743ee33b7 100644 --- a/native/common/jp_primitivetype.cpp +++ b/native/common/jp_primitivetype.cpp @@ -28,6 +28,7 @@ bool JPPrimitiveType::isPrimitive() const return true; } +extern "C" Py_ssize_t PyJPValue_getJavaSlotOffset(PyObject* self); // equivalent of long_subtype_new as it isn't exposed @@ -35,6 +36,9 @@ PyObject *JPPrimitiveType::convertLong(PyTypeObject* wrapper, PyLongObject* tmp) { if (wrapper == nullptr) JP_RAISE(PyExc_SystemError, "bad wrapper"); + +#if PY_VERSION_HEX<0x030c0000 + // Determine number of digits to copy Py_ssize_t n = Py_SIZE(tmp); if (n < 0) n = -n; @@ -43,11 +47,26 @@ PyObject *JPPrimitiveType::convertLong(PyTypeObject* wrapper, PyLongObject* tmp) if (newobj == nullptr) return nullptr; + // Size is in units of digits ((PyVarObject*) newobj)->ob_size = Py_SIZE(tmp); for (Py_ssize_t i = 0; i < n; i++) { newobj->ob_digit[i] = tmp->ob_digit[i]; } + +#else + // 3.12 completely does away with ob_size field and repurposes it + + // Determine the number of digits to copy + int n = (tmp->long_value.lv_tag >> 3); + + PyLongObject *newobj = (PyLongObject *) wrapper->tp_alloc(wrapper, n); + if (newobj == NULL) + return NULL; + + newobj->long_value.lv_tag = tmp->long_value.lv_tag; + memcpy(&newobj->long_value.ob_digit, &tmp->long_value.ob_digit, n*sizeof(digit)); +#endif return (PyObject*) newobj; } diff --git a/native/python/pyjp_array.cpp b/native/python/pyjp_array.cpp index 1d9f56421..3a3b0d043 100644 --- a/native/python/pyjp_array.cpp +++ b/native/python/pyjp_array.cpp @@ -428,13 +428,19 @@ static PyType_Slot arraySlots[] = { { Py_sq_length, (void*) &PyJPArray_len}, { Py_tp_getset, (void*) &arrayGetSets}, { Py_mp_ass_subscript, (void*) &PyJPArray_assignSubscript}, +#if PY_VERSION_HEX >= 0x03090000 + { Py_bf_getbuffer, (void*) &PyJPArray_getBuffer}, + { Py_bf_releasebuffer, (void*) &PyJPArray_releaseBuffer}, +#endif {0} }; +#if PY_VERSION_HEX < 0x03090000 static PyBufferProcs arrayBuffer = { (getbufferproc) & PyJPArray_getBuffer, (releasebufferproc) & PyJPArray_releaseBuffer }; +#endif PyTypeObject *PyJPArray_Type = nullptr; static PyType_Spec arraySpec = { @@ -445,12 +451,18 @@ static PyType_Spec arraySpec = { arraySlots }; +#if PY_VERSION_HEX < 0x03090000 static PyBufferProcs arrayPrimBuffer = { (getbufferproc) & PyJPArrayPrimitive_getBuffer, (releasebufferproc) & PyJPArray_releaseBuffer }; +#endif static PyType_Slot arrayPrimSlots[] = { +#if PY_VERSION_HEX >= 0x03090000 + { Py_bf_getbuffer, (void*) &PyJPArrayPrimitive_getBuffer}, + { Py_bf_releasebuffer, (void*) &PyJPArray_releaseBuffer}, +#endif {0} }; @@ -472,14 +484,18 @@ void PyJPArray_initType(PyObject * module) JPPyObject tuple = JPPyObject::call(PyTuple_Pack(1, PyJPObject_Type)); PyJPArray_Type = (PyTypeObject*) PyJPClass_FromSpecWithBases(&arraySpec, tuple.get()); JP_PY_CHECK(); +#if PY_VERSION_HEX < 0x03090000 PyJPArray_Type->tp_as_buffer = &arrayBuffer; +#endif PyModule_AddObject(module, "_JArray", (PyObject*) PyJPArray_Type); JP_PY_CHECK(); tuple = JPPyObject::call(PyTuple_Pack(1, PyJPArray_Type)); PyJPArrayPrimitive_Type = (PyTypeObject*) PyJPClass_FromSpecWithBases(&arrayPrimSpec, tuple.get()); +#if PY_VERSION_HEX < 0x03090000 PyJPArrayPrimitive_Type->tp_as_buffer = &arrayPrimBuffer; +#endif JP_PY_CHECK(); PyModule_AddObject(module, "_JArrayPrimitive", (PyObject*) PyJPArrayPrimitive_Type); diff --git a/native/python/pyjp_buffer.cpp b/native/python/pyjp_buffer.cpp index c944f20e8..14e7fb49e 100644 --- a/native/python/pyjp_buffer.cpp +++ b/native/python/pyjp_buffer.cpp @@ -113,13 +113,19 @@ int PyJPBuffer_getBuffer(PyJPBuffer *self, Py_buffer *view, int flags) static PyType_Slot bufferSlots[] = { { Py_tp_dealloc, (void*) PyJPBuffer_dealloc}, { Py_tp_repr, (void*) PyJPBuffer_repr}, +#if PY_VERSION_HEX >= 0x03090000 + { Py_bf_getbuffer, (void*) PyJPBuffer_getBuffer}, + { Py_bf_releasebuffer, (void*) PyJPBuffer_releaseBuffer}, +#endif {0} }; +#if PY_VERSION_HEX < 0x03090000 static PyBufferProcs directBuffer = { (getbufferproc) & PyJPBuffer_getBuffer, (releasebufferproc) & PyJPBuffer_releaseBuffer }; +#endif PyTypeObject *PyJPBuffer_Type = nullptr; static PyType_Spec bufferSpec = { @@ -138,7 +144,9 @@ void PyJPBuffer_initType(PyObject * module) { JPPyObject tuple = JPPyObject::call(PyTuple_Pack(1, PyJPObject_Type)); PyJPBuffer_Type = (PyTypeObject*) PyJPClass_FromSpecWithBases(&bufferSpec, tuple.get()); +#if PY_VERSION_HEX < 0x03090000 PyJPBuffer_Type->tp_as_buffer = &directBuffer; +#endif JP_PY_CHECK(); PyModule_AddObject(module, "_JBuffer", (PyObject*) PyJPBuffer_Type); JP_PY_CHECK(); diff --git a/native/python/pyjp_char.cpp b/native/python/pyjp_char.cpp index 431f93a13..a867d69ab 100644 --- a/native/python/pyjp_char.cpp +++ b/native/python/pyjp_char.cpp @@ -79,46 +79,63 @@ static int assertNotNull(JPValue *javaSlot) PyObject *PyJPChar_Create(PyTypeObject *type, Py_UCS2 p) { - auto *self = (PyJPChar*) PyJPValue_alloc(type, 0); + // Allocate a new string type (derived from UNICODE) + PyJPChar *self = (PyJPChar*) PyJPValue_alloc(type, 0); if (self == nullptr) return nullptr; + + // Set up a wide char with value of zero self->m_Data[0] = 0; self->m_Data[1] = 0; self->m_Data[2] = 0; self->m_Data[3] = 0; + // Values taken from internal/cpython/unicode.h + + // Mark the type in unicode _PyUnicode_LENGTH(self) = 1; _PyUnicode_HASH(self) = -1; - _PyUnicode_STATE(self).kind = PyUnicode_1BYTE_KIND; - _PyUnicode_STATE(self).ascii = 0; - _PyUnicode_STATE(self).ready = 1; - _PyUnicode_STATE(self).interned = 0; _PyUnicode_STATE(self).compact = 1; + _PyUnicode_STATE(self).interned = 0; +#if PY_VERSION_HEX < 0x030c0000 + _PyUnicode_STATE(self).ready = 1; +#endif + + // Copy the value based on the length if (p < 128) { _PyUnicode_STATE(self).ascii = 1; + _PyUnicode_STATE(self).kind = PyUnicode_1BYTE_KIND; + char *data = (char*) (((PyASCIIObject*) self) + 1); data[0] = p; data[1] = 0; - } else - if (p < 256) + } else if (p < 256) { + _PyUnicode_STATE(self).ascii = 0; + _PyUnicode_STATE(self).kind = PyUnicode_1BYTE_KIND; + char *data = (char*) ( ((PyCompactUnicodeObject*) self) + 1); data[0] = p; data[1] = 0; + +#if PY_VERSION_HEX < 0x030c0000 _PyUnicode_WSTR_LENGTH(self) = 0; _PyUnicode_WSTR(self) = nullptr; - self->m_Obj.utf8 = nullptr; +#endif + self->m_Obj.utf8 = NULL; self->m_Obj.utf8_length = 0; } else { + _PyUnicode_STATE(self).ascii = 0; + _PyUnicode_STATE(self).kind = PyUnicode_2BYTE_KIND; auto *data = (Py_UCS2*) ( ((PyCompactUnicodeObject*) self) + 1); data[0] = p; data[1] = 0; - _PyUnicode_STATE(self).kind = PyUnicode_2BYTE_KIND; +#if PY_VERSION_HEX < 0x030c0000 if (sizeof (wchar_t) == 2) { _PyUnicode_WSTR_LENGTH(self) = 1; @@ -128,9 +145,11 @@ PyObject *PyJPChar_Create(PyTypeObject *type, Py_UCS2 p) _PyUnicode_WSTR_LENGTH(self) = 0; _PyUnicode_WSTR(self) = nullptr; } +#endif self->m_Obj.utf8 = nullptr; self->m_Obj.utf8_length = 0; } + return (PyObject*) self; } diff --git a/native/python/pyjp_class.cpp b/native/python/pyjp_class.cpp index 63cc9af06..fc633e11c 100644 --- a/native/python/pyjp_class.cpp +++ b/native/python/pyjp_class.cpp @@ -276,6 +276,14 @@ PyObject* PyJPClass_FromSpecWithBases(PyType_Spec *spec, PyObject *bases) case Py_tp_getset: type->tp_getset = (PyGetSetDef*) slot->pfunc; break; +#if PY_VERSION_HEX >= 0x03090000 + case Py_bf_getbuffer: + type->tp_as_buffer->bf_getbuffer = (getbufferproc) slot->pfunc; + break; + case Py_bf_releasebuffer: + type->tp_as_buffer->bf_releasebuffer = (releasebufferproc) slot->pfunc; + break; +#endif // GCOVR_EXCL_START default: PyErr_Format(PyExc_TypeError, "slot %d not implemented", slot->slot); diff --git a/native/python/pyjp_module.cpp b/native/python/pyjp_module.cpp index 62c491878..628694b69 100644 --- a/native/python/pyjp_module.cpp +++ b/native/python/pyjp_module.cpp @@ -169,11 +169,21 @@ PyObject* PyJP_GetAttrDescriptor(PyTypeObject *type, PyObject *attr_name) if (type->tp_mro == nullptr) return nullptr; // GCOVR_EXCL_LINE + // Grab the mro PyObject *mro = type->tp_mro; + + // mro should be a tuple Py_ssize_t n = PyTuple_Size(mro); + + // Search the tuple for the attribute for (Py_ssize_t i = 0; i < n; ++i) { auto *type2 = (PyTypeObject*) PyTuple_GetItem(mro, i); + + // Skip objects without a functioning dictionary + if (type2->tp_dict == NULL) + continue; + PyObject *res = PyDict_GetItem(type2->tp_dict, attr_name); if (res) { diff --git a/native/python/pyjp_object.cpp b/native/python/pyjp_object.cpp index 0924170cf..0de9f9f2c 100644 --- a/native/python/pyjp_object.cpp +++ b/native/python/pyjp_object.cpp @@ -153,8 +153,16 @@ static PyObject *PyJPComparable_compare(PyObject *self, PyObject *other, int op) Py_INCREF(out); return out; } + if (match.type < JPMatch::Type::_implicit) + { + if (op == Py_EQ || op == Py_NE) + return PyBool_FromLong(op == Py_NE); + PyObject *out = Py_NotImplemented; + Py_INCREF(out); + return out; + } obj1 = match.convert().l; - } else if (!null1 && javaSlot1 != nullptr) + } else if (!null1 && javaSlot1 != nullptr && !javaSlot1->getClass()->isPrimitive()) obj1 = javaSlot1->getValue().l; switch (op) @@ -164,14 +172,13 @@ static PyObject *PyJPComparable_compare(PyObject *self, PyObject *other, int op) Py_RETURN_TRUE; if (null0 || null1) Py_RETURN_FALSE; - return PyBool_FromLong(frame.compareTo(obj0, obj1) == 0); - + return PyBool_FromLong(frame.equals(obj0, obj1)); case Py_NE: if (null0 && null1) Py_RETURN_FALSE; if (null0 || null1) Py_RETURN_TRUE; - return PyBool_FromLong(frame.compareTo(obj0, obj1) != 0); + return PyBool_FromLong(!frame.equals(obj0, obj1)); case Py_LT: if (null0 || null1) break; diff --git a/native/python/pyjp_value.cpp b/native/python/pyjp_value.cpp index 510c2e12c..1deecaa92 100644 --- a/native/python/pyjp_value.cpp +++ b/native/python/pyjp_value.cpp @@ -68,7 +68,10 @@ PyObject* PyJPValue_alloc(PyTypeObject* type, Py_ssize_t nitems) return PyErr_NoMemory(); // GCOVR_EXCL_LINE memset(obj, 0, size); + Py_ssize_t refcnt = ((PyObject*) type)->ob_refcnt; + obj->ob_type = type; + if (type->tp_itemsize == 0) PyObject_Init(obj, type); else @@ -106,8 +109,17 @@ Py_ssize_t PyJPValue_getJavaSlotOffset(PyObject* self) || type->tp_finalize != (destructor) PyJPValue_finalize) return 0; Py_ssize_t offset; - Py_ssize_t sz = Py_SIZE(self); - // I have no clue what negative sizes mean + Py_ssize_t sz = 0; + +#if PY_VERSION_HEX>=0x030c0000 + // starting in 3.12 there is no longer ob_size in PyLong + if (PyType_HasFeature(self->ob_type, Py_TPFLAGS_LONG_SUBCLASS)) + sz = (((PyLongObject*)self)->long_value.lv_tag) >> 3; // Private NON_SIZE_BITS + else +#endif + if (type->tp_itemsize != 0) + sz = Py_SIZE(self); + // PyLong abuses ob_size with negative values prior to 3.12 if (sz < 0) sz = -sz; if (type->tp_itemsize == 0) diff --git a/test/jpypetest/test_bytebuffer.py b/test/jpypetest/test_bytebuffer.py index b4c7e4757..d81ed3dd8 100644 --- a/test/jpypetest/test_bytebuffer.py +++ b/test/jpypetest/test_bytebuffer.py @@ -48,4 +48,4 @@ def testRepr(self): self.assertEqual(repr(bb), "") def testMemoryView(self): - self.assertEquals(memoryview(jpype.java.nio.ByteBuffer.allocateDirect(100)).nbytes, 100) + self.assertEqual(memoryview(jpype.java.nio.ByteBuffer.allocateDirect(100)).nbytes, 100) diff --git a/test/jpypetest/test_classhints.py b/test/jpypetest/test_classhints.py index bcf895622..d200d798a 100644 --- a/test/jpypetest/test_classhints.py +++ b/test/jpypetest/test_classhints.py @@ -17,6 +17,7 @@ # ***************************************************************************** import jpype import common +import sys class MyImpl(object): @@ -59,7 +60,10 @@ def testCharSequence(self): def testInstant(self): import datetime - now = datetime.datetime.utcnow() + if sys.version_info.major == 3 and sys.version_info.minor < 12: + now = datetime.datetime.utcnow() + else: + now = datetime.datetime.now(datetime.UTC) Instant = jpype.JClass("java.time.Instant") self.assertIsInstance(jpype.JObject(now, Instant), Instant) diff --git a/test/jpypetest/test_comparable.py b/test/jpypetest/test_comparable.py index 273bdc2de..ce54b223f 100644 --- a/test/jpypetest/test_comparable.py +++ b/test/jpypetest/test_comparable.py @@ -79,3 +79,43 @@ def testComparableNull(self): print(i3 > i3) with self.assertRaises(ValueError): print(i3 >= i3) + + def testComparableObj(self): + C1 = jpype.JClass("java.time.temporal.ChronoUnit") + C2 = jpype.JClass("java.util.concurrent.TimeUnit") + O1 = C1.SECONDS + O2 = C2.SECONDS + N1 = jpype.JObject(None, C1) + N2 = jpype.JObject(None, C2) + V = jpype.JInt(1) + # Test dissimilar objects + self.assertTrue(O1 != O2) + self.assertFalse(O1 == O2) + # test Nulls + self.assertTrue(N1 == N2) + self.assertFalse(N1 != N2) + self.assertTrue(N1 == None) + self.assertFalse(N1 != None) + # test primitives + self.assertFalse(O1 == V) + self.assertFalse(V == O1) + # test null to primitives + self.assertFalse(N1 == V) + self.assertFalse(V == N1) + + self.assertFalse(1 == O1) + self.assertFalse("M" == O1) + self.assertFalse(O1 == 1) + self.assertFalse(O1 == "M") + + self.assertTrue(1 != O1) + self.assertTrue("M" != O1) + self.assertTrue(O1 != 1) + self.assertTrue(O1 != "M") + + with self.assertRaises(TypeError): + self.assertTrue(O1 > 1) + with self.assertRaises(TypeError): + self.assertTrue(1 > O1) + with self.assertRaises(TypeError): + self.assertTrue(O1 > O2) diff --git a/test/jpypetest/test_javadoc.py b/test/jpypetest/test_javadoc.py index 0f5f93171..a17f11d03 100644 --- a/test/jpypetest/test_javadoc.py +++ b/test/jpypetest/test_javadoc.py @@ -38,7 +38,9 @@ def testClass(self): JC = jpype.JClass("jpype.doc.Test") jd = JC.__doc__ self.assertIsInstance(jd, str) - self.assertRegex(jd, "random stuff") + # Disabled this test for now. Java needs a better API for accessing Java doc. + # It is hard to deal with random changes every version. + #self.assertRegex(jd, "random stuff") def testMethod(self): JC = jpype.JClass("jpype.doc.Test") diff --git a/test/jpypetest/test_objectwrapper.py b/test/jpypetest/test_objectwrapper.py index 80f18c7eb..f6a64cf85 100644 --- a/test/jpypetest/test_objectwrapper.py +++ b/test/jpypetest/test_objectwrapper.py @@ -113,6 +113,37 @@ class Fred(object): with self.assertRaises(TypeError): jpype.JObject(Fred()) + def testObjectEq(self): + C1 = jpype.JClass("java.lang.StringBuffer") + C2 = jpype.JClass("java.lang.Exception") + O1 = C1() + O2 = C2() + N1 = jpype.JObject(None, C1) + N2 = jpype.JObject(None, C2) + V = jpype.JInt(1) + # Test dissimilar objects + self.assertTrue(O1 != O2) + self.assertFalse(O1 == O2) + self.assertTrue(O2 != O1) + self.assertFalse(O2 == O1) + # test Nulls + self.assertTrue(N1 == N2) + self.assertFalse(N1 != N2) + self.assertTrue(N1 == None) + self.assertFalse(N1 != None) + self.assertTrue(None == N1) + self.assertFalse(None != N1) + # test primitives + self.assertFalse(O1 == V) + self.assertFalse(V == O1) + self.assertFalse(O2 == V) + self.assertFalse(V == O2) + # test null to primitives + self.assertFalse(N1 == V) + self.assertFalse(V == N1) + self.assertFalse(N2 == V) + self.assertFalse(V == N2) + # def testMakeSureWeCanLoadAllClasses(self): # def get_system_jars(): diff --git a/test/jpypetest/test_startup.py b/test/jpypetest/test_startup.py index c64d99cbf..350971f09 100644 --- a/test/jpypetest/test_startup.py +++ b/test/jpypetest/test_startup.py @@ -16,27 +16,12 @@ # # ***************************************************************************** import jpype -import common import subrun +import functools import os -import sys +from pathlib import Path import unittest - -def runStartJVM(*args, **kwargs): - jpype.startJVM(*args, **kwargs) - - -def runStartJVMTest(*args, **kwargs): - jpype.startJVM(*args, **kwargs) - try: - jclass = jpype.JClass('jpype.array.TestArray') - return - except: - pass - raise RuntimeError("Test class not found") - - root = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) cp = os.path.join(root, 'classes').replace('\\', '/') @@ -57,46 +42,102 @@ def testRestart(self): jpype.shutdownJVM() jpype.startJVM(convertStrings=False) - def testJVMPathKeyword(self): - runStartJVM(jvmpath=self.jvmpath) - def testInvalidArgsFalse(self): with self.assertRaises(RuntimeError): - runStartJVM("-for_sure_InVaLiD", - ignoreUnrecognized=False, convertStrings=False) + jpype.startJVM( + "-for_sure_InVaLiD", + ignoreUnrecognized=False, convertStrings=False, + ) def testInvalidArgsTrue(self): - runStartJVM("-for_sure_InVaLiD", - ignoreUnrecognized=True, convertStrings=False) + jpype.startJVM( + "-for_sure_InVaLiD", + ignoreUnrecognized=True, + convertStrings=False, + ) def testClasspathArgKeyword(self): - runStartJVMTest(classpath=cp, convertStrings=False) + jpype.startJVM(classpath=cp, convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None def testClasspathArgList(self): - runStartJVMTest(classpath=[cp], convertStrings=False) + jpype.startJVM( + classpath=[cp], + convertStrings=False, + ) + assert jpype.JClass('jpype.array.TestArray') is not None def testClasspathArgListEmpty(self): - runStartJVMTest(classpath=[cp, ''], convertStrings=False) + jpype.startJVM( + classpath=[cp, ''], + convertStrings=False, + ) + assert jpype.JClass('jpype.array.TestArray') is not None def testClasspathArgDef(self): - runStartJVMTest('-Djava.class.path=%s' % cp, convertStrings=False) + jpype.startJVM('-Djava.class.path=%s' % cp, convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testClasspathArgPath(self): + jpype.startJVM(classpath=Path(cp), convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testClasspathArgPathList(self): + jpype.startJVM(classpath=[Path(cp)], convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testClasspathArgGlob(self): + jpype.startJVM(classpath=os.path.join(cp, '..', 'jar', 'mrjar*')) + assert jpype.JClass('org.jpype.mrjar.A') is not None def testClasspathTwice(self): with self.assertRaises(TypeError): - runStartJVMTest('-Djava.class.path=%s' % + jpype.startJVM('-Djava.class.path=%s' % cp, classpath=cp, convertStrings=False) def testClasspathBadType(self): with self.assertRaises(TypeError): - runStartJVMTest(classpath=1, convertStrings=False) - - def testPathArg(self): - runStartJVMTest(self.jvmpath, classpath=cp, convertStrings=False) - - def testPathKeyword(self): - path = jpype.getDefaultJVMPath() - runStartJVMTest(classpath=cp, jvmpath=self.jvmpath, - convertStrings=False) + jpype.startJVM(classpath=1, convertStrings=False) + + def testJVMPathArg_Str(self): + jpype.startJVM(self.jvmpath, classpath=cp, convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testJVMPathArg_None(self): + # It is allowed to pass None as a JVM path + jpype.startJVM( + None, # type: ignore + classpath=cp, + ) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testJVMPathArg_NoArgs(self): + jpype.startJVM( + classpath=cp, + ) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testJVMPathArg_Path(self): + with self.assertRaises(TypeError): + jpype.startJVM( + # Pass a path as the first argument. This isn't supported (this is + # reflected in the type definition), but the fact that it "works" + # gives rise to this test. + Path(self.jvmpath), # type: ignore + convertStrings=False, + ) + + def testJVMPathKeyword_str(self): + jpype.startJVM( + classpath=cp, + jvmpath=self.jvmpath, + convertStrings=False, + ) + assert jpype.JClass('jpype.array.TestArray') is not None + + def testJVMPathKeyword_Path(self): + jpype.startJVM(jvmpath=Path(self.jvmpath), classpath=cp, convertStrings=False) + assert jpype.JClass('jpype.array.TestArray') is not None def testPathTwice(self): with self.assertRaises(TypeError): @@ -104,4 +145,4 @@ def testPathTwice(self): def testBadKeyword(self): with self.assertRaises(TypeError): - jpype.startJVM(invalid=True) + jpype.startJVM(invalid=True) # type: ignore