diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1e9c7e3b3..01b0e0165 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.2_dev0 +current_version = 1.0.3_dev0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\_(?P[a-z]+)(?P\d+))? diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst index 020e9ac8b..d7b9ad9ba 100644 --- a/doc/CHANGELOG.rst +++ b/doc/CHANGELOG.rst @@ -4,7 +4,29 @@ Changelog This changelog *only* contains changes from the *first* pypi release (0.5.4.3) onwards. Latest Changes: -- **1.0.2_dev0 - 2020-07-16** + +- **1.0.3_dev0 - unreleased** + + - Fixed crash when manually creating wrappers for anonymous classes. + + - Fixed reference count problem in stackframes used for exceptions. + + - Errors report `*static*` when the matching with a static method + so that it is clear when a member method was called statically. + + - java.lang.String slices function like Python string slice. + + - Java packages now operate as normal Python modules. Removed restrictions + regarding setattr. All package instances for the same package name are + shared so that functionality added to one instance is shared wiht all + instances. + +- **1.0.2 - 2020-07-27** + + - The wrapper for Throwable was getting the wrapper for Object rather than + the expected wrapper resulting in odd conversions from Python classes. + + - Typos within the import system resulting in "jname" not found corrected. - ^C propogates to a KeyboardInterrupt properly. @@ -21,6 +43,24 @@ Latest Changes: - Corrected symbol problem with Python 3.5.3. PySlice_Unpack was introduced in a later patch release and should not have been used. + - **shutdown** The behavior log entry for changes on shutdown were lost in + the 1.0 release. JPype now calls the JVM shutdown routine which tries to + gracefully exit when shutdown is called. This results in several changes + in behavior. Non daemon threads can now hold open the JVM until they have + completed. Proxy calls will hold the shutdown until the call is completed + but will receive an interrupt message. Files now close properly and will + flush if the threads properly handle the exception. Resource clean up + hooks and finalizers are executed. AtExit hooks in the JVM are called as + spawned threads. Automatic attachment of threads by use of the JVM from + Python are done as daemon but can be reattached as user threads on demand. + Buggy code that fails to properly handle thread clean up will likely hang + on shutdown. Additional documentation is located in the user guide. + + - A bug was reported with numpy.linalg.inv resulting in crashes. This was + traced to an interaction with threading between the JVM and some compilations + of numpy. The workaround appears to be calling numpy.linalg.inv prior to + starting the JVM. + - **1.0.1 - 2020-07-16** - Workarounds for Python 3.8.4 release. Python altered logic regarding the diff --git a/doc/release.rst b/doc/release.rst index 68ec08f94..7d63ad300 100644 --- a/doc/release.rst +++ b/doc/release.rst @@ -24,9 +24,9 @@ Full process: ``git checkout -b releases/{version}`` - [ ] Merge the current master with the release ``git pull origin master`` -- [ ] Edit doc/CHANGELOG.rst - [ ] Start a release ``bumpversion release`` +- [ ] Edit doc/CHANGELOG.rst - [ ] Send the release to be evaluated ``git push`` - [ ] Verify CI on azure diff --git a/jpype/__init__.py b/jpype/__init__.py index 306b10b5b..2e50aa6c0 100644 --- a/jpype/__init__.py +++ b/jpype/__init__.py @@ -51,7 +51,7 @@ __all__.extend(_jcustomizer.__all__) __all__.extend(_gui.__all__) -__version__ = "1.0.2_dev0" +__version__ = "1.0.3_dev0" __version_info__ = __version__.split('.') diff --git a/jpype/_jstring.py b/jpype/_jstring.py index ec47ebec9..0f8d8cfa2 100644 --- a/jpype/_jstring.py +++ b/jpype/_jstring.py @@ -45,6 +45,9 @@ def __len__(self): return self.length() def __getitem__(self, i): + if isinstance(i, slice): + return str(self)[i] + if i < 0: i += len(self) if i < 0: diff --git a/jpype/protocol.py b/jpype/protocol.py index c0e32b9ce..f0bbb44de 100644 --- a/jpype/protocol.py +++ b/jpype/protocol.py @@ -66,9 +66,9 @@ def _JPathConvert(jcls, obj): def _JFileConvert(jcls, obj): return jcls(obj.__fspath__()) - -@_jcustomizer.JConversion("java.util.Collection", instanceof=Sequence, - excludes=str) +# To be added in 1.1.x +#@_jcustomizer.JConversion("java.util.Iterable", instanceof=Sequence, excludes=str) +@_jcustomizer.JConversion("java.util.Collection", instanceof=Sequence, excludes=str) def _JSequenceConvert(jcls, obj): return _jclass.JClass('java.util.Arrays').asList(obj) diff --git a/native/common/include/jp_context.h b/native/common/include/jp_context.h index 2066c870e..0cbc1bbf6 100644 --- a/native/common/include/jp_context.h +++ b/native/common/include/jp_context.h @@ -16,6 +16,7 @@ #ifndef JP_CONTEXT_H #define JP_CONTEXT_H #include +#include /** JPClass is a bit heavy when we just need to hold a * class reference. It causes issues during bootstrap. Thus we @@ -260,6 +261,9 @@ class JPContext bool m_Embedded; public: JPGarbageCollection *m_GC; + + // This will gather C++ resources to clean up after shutdown. + std::list m_Resources; } ; extern void JPRef_failed(); diff --git a/native/common/jp_boxedtype.cpp b/native/common/jp_boxedtype.cpp index 077ca9647..241e1287b 100644 --- a/native/common/jp_boxedtype.cpp +++ b/native/common/jp_boxedtype.cpp @@ -84,6 +84,7 @@ JPMatch::Type JPBoxedType::findJavaConversion(JPMatch &match) void JPBoxedType::getConversionInfo(JPConversionInfo &info) { + JP_TRACE_IN("JPBoxedType::getConversionInfo"); JPJavaFrame frame = JPJavaFrame::outer(m_Context); m_PrimitiveType->getConversionInfo(info); JPPyObject::call(PyObject_CallMethod(info.expl, "extend", "O", info.implicit)); @@ -91,6 +92,7 @@ void JPBoxedType::getConversionInfo(JPConversionInfo &info) JPPyObject::call(PyObject_CallMethod(info.implicit, "extend", "O", info.exact)); JPPyObject::call(PyObject_CallMethod(info.exact, "clear", "")); JPClass::getConversionInfo(info); + JP_TRACE_OUT; } jobject JPBoxedType::box(JPJavaFrame &frame, jvalue v) diff --git a/native/common/jp_class.cpp b/native/common/jp_class.cpp index 99f368af1..5e220635b 100644 --- a/native/common/jp_class.cpp +++ b/native/common/jp_class.cpp @@ -416,10 +416,12 @@ JPMatch::Type JPClass::findJavaConversion(JPMatch &match) void JPClass::getConversionInfo(JPConversionInfo &info) { + JP_TRACE_IN("JPClass::getConversionInfo"); JPJavaFrame frame = JPJavaFrame::outer(m_Context); objectConversion->getInfo(this, info); hintsConversion->getInfo(this, info); PyList_Append(info.ret, PyJPClass_create(frame, this).get()); + JP_TRACE_OUT; } diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp index bf4456ba9..955bac4e2 100644 --- a/native/common/jp_context.cpp +++ b/native/common/jp_context.cpp @@ -386,6 +386,14 @@ void JPContext::shutdownJVM() m_JavaVM->DestroyJavaVM(); } + JP_TRACE("Delete resources"); + for (std::list::iterator iter = m_Resources.begin(); + iter != m_Resources.end(); ++iter) + { + delete *iter; + } + m_Resources.clear(); + // unload the jvm library JP_TRACE("Unload JVM"); m_JavaVM = NULL; diff --git a/native/common/jp_exception.cpp b/native/common/jp_exception.cpp index 17b08e922..21fb8d0a4 100644 --- a/native/common/jp_exception.cpp +++ b/native/common/jp_exception.cpp @@ -490,7 +490,10 @@ PyTracebackObject *tb_create( PyFrameObject *frame = (PyFrameObject*) PyFrame_Type.tp_alloc(&PyFrame_Type, 0); frame->f_back = NULL; if (last_traceback != NULL) + { frame->f_back = last_traceback->tb_frame; + Py_INCREF(frame->f_back); + } frame->f_builtins = dict; Py_INCREF(frame->f_builtins); frame->f_code = (PyCodeObject*) code; diff --git a/native/common/jp_methoddispatch.cpp b/native/common/jp_methoddispatch.cpp index 3ddc006b7..3778dff4a 100644 --- a/native/common/jp_methoddispatch.cpp +++ b/native/common/jp_methoddispatch.cpp @@ -159,7 +159,12 @@ bool JPMethodDispatch::findOverload(JPJavaFrame& frame, JPMethodMatch &bestMatch if (JPModifier::isConstructor(m_Modifiers)) ss << "No matching overloads found for constructor " << m_Class->getCanonicalName() << "("; else - ss << "No matching overloads found for " << m_Class->getCanonicalName() << "." << getName() << "("; + { + ss << "No matching overloads found for "; + if (!callInstance) + ss << "*static* "; + ss << m_Class->getCanonicalName() << "." << getName() << "("; + } size_t start = callInstance ? 1 : 0; for (size_t i = start; i < arg.size(); ++i) { diff --git a/native/common/jp_numbertype.cpp b/native/common/jp_numbertype.cpp index 701e0b1e7..c851d6e2a 100644 --- a/native/common/jp_numbertype.cpp +++ b/native/common/jp_numbertype.cpp @@ -48,10 +48,12 @@ JPMatch::Type JPNumberType::findJavaConversion(JPMatch& match) void JPNumberType::getConversionInfo(JPConversionInfo &info) { + JP_TRACE_IN("JPNumberType::getConversionInfo"); JPJavaFrame frame = JPJavaFrame::outer(m_Context); javaNumberAnyConversion->getInfo(this, info); boxLongConversion->getInfo(this, info); boxDoubleConversion->getInfo(this, info); hintsConversion->getInfo(this, info); PyList_Append(info.ret, PyJPClass_create(frame, this).get()); + JP_TRACE_OUT; } \ No newline at end of file diff --git a/native/common/jp_objecttype.cpp b/native/common/jp_objecttype.cpp index 003a0f31b..1d5657bec 100644 --- a/native/common/jp_objecttype.cpp +++ b/native/common/jp_objecttype.cpp @@ -52,6 +52,7 @@ JPMatch::Type JPObjectType::findJavaConversion(JPMatch& match) void JPObjectType::getConversionInfo(JPConversionInfo &info) { + JP_TRACE_IN("JPObjectType::getConversionInfo"); JPJavaFrame frame = JPJavaFrame::outer(m_Context); nullConversion->getInfo(this, info); objectConversion->getInfo(this, info); @@ -63,4 +64,5 @@ void JPObjectType::getConversionInfo(JPConversionInfo &info) proxyConversion->getInfo(this, info); hintsConversion->getInfo(this, info); PyList_Append(info.ret, PyJPClass_create(frame, this).get()); + JP_TRACE_OUT; } \ No newline at end of file diff --git a/native/common/jp_typefactory.cpp b/native/common/jp_typefactory.cpp index ebeb859c8..df74f4133 100644 --- a/native/common/jp_typefactory.cpp +++ b/native/common/jp_typefactory.cpp @@ -113,7 +113,7 @@ JNIEXPORT void JNICALL Java_org_jpype_manager_TypeFactoryNative_destroy( jlong* values = accessor.get(); for (int i = 0; i < sz; ++i) { - delete (JPResource*) values[i]; + context->m_Resources.push_back((JPResource*) values[i]); } return; JP_JAVA_CATCH(); // GCOVR_EXCL_LINE @@ -205,7 +205,7 @@ JNIEXPORT jlong JNICALL Java_org_jpype_manager_TypeFactoryNative_defineObjectCla (JPClass*) superClass, interfaces, modifiers)); if (className == "java.lang.Throwable") return (jlong) (context->_java_lang_Throwable - = new JPObjectType(frame, cls, className, + = new JPClassType(frame, cls, className, (JPClass*) superClass, interfaces, modifiers)); if (className == "java.lang.Number") diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java index 1252ffbf2..ab2281d98 100644 --- a/native/java/org/jpype/JPypeContext.java +++ b/native/java/org/jpype/JPypeContext.java @@ -69,7 +69,7 @@ public class JPypeContext { - public final String VERSION = "1.0.2_dev0"; + public final String VERSION = "1.0.3_dev0"; private static JPypeContext INSTANCE = new JPypeContext(); // This is the C++ portion of the context. @@ -78,7 +78,6 @@ public class JPypeContext private TypeManager typeManager; private DynamicClassLoader classLoader; private final AtomicInteger shutdownFlag = new AtomicInteger(); - private final AtomicInteger proxyCount = new AtomicInteger(); private final List shutdownHooks = new ArrayList<>(); private final List postHooks = new ArrayList<>(); @@ -208,15 +207,15 @@ private void shutdown() // Wait for any unregistered proxies to finish so that we don't yank // the rug out from under them result in a segfault. - while (this.proxyCount.get() > 0) - { - try - { - Thread.sleep(10); - } catch (InterruptedException ex) - { - } - } +// while (this.proxyCount.get() > 0) +// { +// try +// { +// Thread.sleep(10); +// } catch (InterruptedException ex) +// { +// } +// } // // Check to see if who is alive // threads = Thread.getAllStackTraces(); @@ -452,15 +451,15 @@ public boolean isShutdown() return shutdownFlag.get() > 0; } - public void incrementProxy() - { - proxyCount.incrementAndGet(); - } - - public void decrementProxy() - { - proxyCount.decrementAndGet(); - } +// public void incrementProxy() +// { +// proxyCount.incrementAndGet(); +// } +// +// public void decrementProxy() +// { +// proxyCount.decrementAndGet(); +// } public long getExcClass(Throwable th) { diff --git a/native/java/org/jpype/manager/TypeManager.java b/native/java/org/jpype/manager/TypeManager.java index 70158f0a9..2ba8b2c4d 100644 --- a/native/java/org/jpype/manager/TypeManager.java +++ b/native/java/org/jpype/manager/TypeManager.java @@ -445,6 +445,8 @@ private ClassDescriptor createOrdinaryClass(Class cls, boolean special, boole // FIXME watch out for anonyous and lambda here. String name = cls.getCanonicalName(); + if (name == null) + name = cls.getName(); // Create the JPClass long classPtr = typeFactory.defineObjectClass(context, cls, name, diff --git a/native/java/org/jpype/proxy/JPypeProxy.java b/native/java/org/jpype/proxy/JPypeProxy.java index 8091aea69..f2c8dd445 100644 --- a/native/java/org/jpype/proxy/JPypeProxy.java +++ b/native/java/org/jpype/proxy/JPypeProxy.java @@ -71,7 +71,7 @@ public Object invoke(Object proxy, Method method, Object[] args) { try { - context.incrementProxy(); +// context.incrementProxy(); if (context.isShutdown()) throw new RuntimeException("Proxy called during shutdown"); @@ -94,7 +94,7 @@ public Object invoke(Object proxy, Method method, Object[] args) return hostInvoke(context.getContext(), method.getName(), instance, returnType, parameterTypes, args); } finally { - context.decrementProxy(); +// context.decrementProxy(); } } diff --git a/native/python/pyjp_module.cpp b/native/python/pyjp_module.cpp index 8c0e7ae39..b3503323b 100644 --- a/native/python/pyjp_module.cpp +++ b/native/python/pyjp_module.cpp @@ -711,7 +711,7 @@ PyMODINIT_FUNC PyInit__jpype() // PyJPModule = module; Py_INCREF(module); PyJPModule = module; - PyModule_AddStringConstant(module, "__version__", "1.0.2_dev0"); + PyModule_AddStringConstant(module, "__version__", "1.0.3_dev0"); // Initialize each of the python extension types PyJPClass_initType(module); diff --git a/native/python/pyjp_package.cpp b/native/python/pyjp_package.cpp index ae654a74f..84317deef 100644 --- a/native/python/pyjp_package.cpp +++ b/native/python/pyjp_package.cpp @@ -18,95 +18,98 @@ #include "jp_stringtype.h" #include -class JPPackage -{ -public: - string m_Name; - JPObjectRef m_Object; - - JPPackage(const char *v) - : m_Name(v) - { - } -} ; - #ifdef __cplusplus extern "C" { #endif - -struct PyJPPackage -{ - PyObject_HEAD - PyObject *m_Dict; - JPPackage *m_Package; - PyObject *m_Handler; -} ; - PyTypeObject *PyJPPackage_Type = NULL; +static PyObject *PyJPPackage_Dict = NULL; -static PyJPPackage *PyJPPackage_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +static PyObject *PyJPPackage_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { JP_PY_TRY("PyJPPackage_new"); - PyJPPackage *self = (PyJPPackage*) type->tp_alloc(type, 0); - JP_PY_CHECK(); + PyObject *name = NULL; + if (!PyArg_Parse(args, "(U)", &name)) + return 0; - char *v; - if (!PyArg_ParseTuple(args, "s", &v)) - return NULL; // GCOVR_EXCL_LINE + // Check the cache + PyObject *obj = PyDict_GetItem(PyJPPackage_Dict, name); + if (obj != NULL) + { + Py_INCREF(obj); + return obj; + } - self->m_Dict = PyDict_New(); - self->m_Package = new JPPackage(v); - self->m_Handler = NULL; + // Otherwise create a new object + PyObject *self = PyModule_Type.tp_new(PyJPPackage_Type, args, NULL); + int rc = PyModule_Type.tp_init(self, args, NULL); + if (rc != 0) + { + // If we fail clean up the mess. + Py_DECREF(self); + return 0; + } + + // Place in cache + PyDict_SetItem(PyJPPackage_Dict, name, self); return self; JP_PY_CATCH(NULL); // GCOVR_EXCL_LINE } -static int PyJPPackage_traverse(PyJPPackage *self, visitproc visit, void *arg) +static void dtor(PyObject *self) { - Py_VISIT(self->m_Dict); - Py_VISIT(self->m_Handler); - return 0; -} - -static int PyJPPackage_clear(PyJPPackage *self) -{ - Py_CLEAR(self->m_Dict); - Py_CLEAR(self->m_Handler); - return 0; -} - -static void PyJPPackage_dealloc(PyJPPackage *self) -{ - JP_PY_TRY("PyJPPackage_dealloc"); - delete self->m_Package; - PyObject_GC_UnTrack(self); - PyJPPackage_clear(self); - Py_TYPE(self)->tp_free(self); - JP_PY_CATCH_NONE(); + JPContext *context = JPContext_global; + if (context == NULL || !context->isRunning()) + return; + jobject jo = (jobject) PyCapsule_GetPointer(self, NULL); + if (jo == NULL) + return; + JPJavaFrame frame = JPJavaFrame::outer(context); + frame.DeleteGlobalRef(jo); } -static jobject getPackage(JPJavaFrame &frame, PyJPPackage *self) +static jobject getPackage(JPJavaFrame &frame, PyObject *self) { - // If we already have a loaded object, use it. - if (self->m_Package->m_Object.get() != NULL) - return self->m_Package->m_Object.get(); + PyObject *dict = PyModule_GetDict(self); // borrowed + PyObject *capsule = PyDict_GetItemString(dict, "_jpackage"); // borrowed + jobject jo; + if (capsule != NULL) + { + jo = (jobject) PyCapsule_GetPointer(capsule, NULL); + return jo; + } + const char *name = PyModule_GetName(self); // Attempt to load the object. - self->m_Package->m_Object = JPObjectRef(frame.getContext(), - frame.getPackage(self->m_Package->m_Name)); + jo = frame.getPackage(name); // Found it, use it. - if (self->m_Package->m_Object.get() != NULL) - return self->m_Package->m_Object.get(); + if (jo != NULL) + { + jo = frame.NewGlobalRef(jo); + capsule = PyCapsule_New(jo, NULL, dtor); + PyDict_SetItemString(dict, "_jpackage", capsule); // no steal + // Py_DECREF(capsule); + return jo; + } // Otherwise, this is a bad package. - PyErr_Format(PyExc_AttributeError, "Java package '%s' is not valid", - self->m_Package->m_Name.c_str()); + PyErr_Format(PyExc_AttributeError, "Java package '%s' is not valid", name); return NULL; } -static PyObject *PyJPPackage_getattro(PyJPPackage *self, PyObject *attr) +/** + * Get an attribute from the package. + * + * This will auto load packages and classes when encounter, + * but first checks the cache. This acts like an standard Python + * module otherwise. + * + * @param self + * @param attr + * @return + */ +static PyObject *PyJPPackage_getattro(PyObject *self, PyObject *attr) { JP_PY_TRY("PyJPPackage_getattro"); if (!PyUnicode_Check(attr)) @@ -115,9 +118,11 @@ static PyObject *PyJPPackage_getattro(PyJPPackage *self, PyObject *attr) return NULL; } + PyObject *dict = PyModule_GetDict(self); + if (dict != NULL) { // Check the cache - PyObject *out = PyDict_GetItem(self->m_Dict, attr); + PyObject *out = PyDict_GetItem(PyModule_GetDict(self), attr); if (out != NULL) { Py_INCREF(out); @@ -131,134 +136,116 @@ static PyObject *PyJPPackage_getattro(PyJPPackage *self, PyObject *attr) return PyObject_GenericGetAttr((PyObject*) self, attr); JPContext* context = JPContext_global; - if (context->isRunning()) + if (!context->isRunning()) { - JPJavaFrame frame = JPJavaFrame::outer(context); - jobject pkg = getPackage(frame, self); - if (pkg == NULL) - return NULL; - - JPPyObject out; - jobject obj; - try - { - obj = frame.getPackageObject(pkg, attrName); - } catch (JPypeException& ex) - { - // If something fails, we need to go to a handler - if (self->m_Handler != NULL) - { - ex.toPython(); - JPPyErrFrame err; - err.normalize(); - err.clear(); - JPPyObject tuple0 = JPPyObject::call(PyTuple_Pack(3, self, attr, err.m_ExceptionValue.get())); - PyObject *rc = PyObject_Call(self->m_Handler, tuple0.get(), NULL); - if (rc == 0) - return 0; - Py_DECREF(rc); // GCOVR_EXCL_LINE - } - throw; // GCOVR_EXCL_LINE - } - if (obj == NULL) - { - PyErr_Format(PyExc_AttributeError, "Java package '%s' has no attribute '%U'", - self->m_Package->m_Name.c_str(), attr); - return NULL; - } else if (frame.IsInstanceOf(obj, context->_java_lang_Class->getJavaClass())) - out = PyJPClass_create(frame, frame.findClass((jclass) obj)); - else if (frame.IsInstanceOf(obj, context->_java_lang_String->getJavaClass())) - { - JPPyObject u = JPPyObject::call(PyUnicode_FromFormat("%s.%U", - self->m_Package->m_Name.c_str(), attr)); - JPPyObject args = JPPyObject::call(PyTuple_Pack(1, u.get())); - out = JPPyObject::call(PyObject_Call((PyObject*) PyJPPackage_Type, args.get(), NULL)); - } else + PyErr_Format(PyExc_RuntimeError, + "Unable to import '%s.%U' without JVM", + PyModule_GetName(self), attr); + return 0; + } + JPJavaFrame frame = JPJavaFrame::outer(context); + jobject pkg = getPackage(frame, self); + if (pkg == NULL) + return NULL; + + JPPyObject out; + jobject obj; + try + { + obj = frame.getPackageObject(pkg, attrName); + } catch (JPypeException& ex) + { + JPPyObject h = JPPyObject::accept(PyObject_GetAttrString(self, "_handler")); + // If something fails, we need to go to a handler + if (!h.isNull()) { - // We should be able to handle Python classes, datafiles, etc, - // but that will take time to implement. In principle, things - // that are not packages or classes should appear as Buffers or - // some other resource type. - PyErr_Format(PyExc_AttributeError, "'%U' is unknown object type in Java package", attr); - return NULL; + ex.toPython(); + JPPyErrFrame err; + err.normalize(); + err.clear(); + JPPyObject tuple0 = JPPyObject::call(PyTuple_Pack(3, self, attr, err.m_ExceptionValue.get())); + PyObject *rc = PyObject_Call(h.get(), tuple0.get(), NULL); + if (rc == 0) + return 0; + Py_DECREF(rc); // GCOVR_EXCL_LINE } - // Cache the item for now - PyDict_SetItem(self->m_Dict, attr, out.get()); // This does not steal - return out.keep(); - } else + throw; // GCOVR_EXCL_LINE + } + if (obj == NULL) + { + PyErr_Format(PyExc_AttributeError, "Java package '%s' has no attribute '%U'", + PyModule_GetName(self), attr); + return NULL; + } else if (frame.IsInstanceOf(obj, context->_java_lang_Class->getJavaClass())) + out = PyJPClass_create(frame, frame.findClass((jclass) obj)); + else if (frame.IsInstanceOf(obj, context->_java_lang_String->getJavaClass())) { - // Prior to starting the JVM we always return a package to be - // consistent with old behavior. This is somewhat unsafe as - // we cannot check if it is a valid package. JPPyObject u = JPPyObject::call(PyUnicode_FromFormat("%s.%U", - self->m_Package->m_Name.c_str(), attr)); + PyModule_GetName(self), attr)); JPPyObject args = JPPyObject::call(PyTuple_Pack(1, u.get())); - - // Note that we will not cache packages prior to starting so that - // we don't end up with a package which is actually a class here. - return PyObject_Call((PyObject*) PyJPPackage_Type, args.get(), NULL); + out = JPPyObject::call(PyObject_Call((PyObject*) PyJPPackage_Type, args.get(), NULL)); + } else + { + // We should be able to handle Python classes, datafiles, etc, + // but that will take time to implement. In principle, things + // that are not packages or classes should appear as Buffers or + // some other resource type. + PyErr_Format(PyExc_AttributeError, "'%U' is unknown object type in Java package", attr); + return NULL; } - - return NULL; + // Cache the item for now + PyDict_SetItem(dict, attr, out.get()); // no steal + return out.keep(); JP_PY_CATCH(NULL); // GCOVR_EXCL_LINE } -static int PyJPPackage_setattro(PyJPPackage *self, PyObject *attr, PyObject *value) +/** + * This next method is required, I have no clue why. Seems + * likely that the default PyObject traverse does not agree + * with modules. + */ +static int PyJPPackage_traverse(PyObject *m, visitproc visit, void *arg) { - JP_PY_TRY("PyJPPackage_setattro"); - string attrName = JPPyString::asStringUTF8(attr).c_str(); - if (attrName.compare(0, 2, "__") == 0) - { - PyDict_SetItem(self->m_Dict, attr, value); - return 0; - } - if (Py_TYPE(value) == PyJPPackage_Type || Py_IsInstanceSingle(value, PyJPClass_Type)) - return 0; - if (attrName.compare(0, 1, "_") == 0) - return PyObject_GenericSetAttr((PyObject*) self, attr, value); + return PyModule_Type.tp_traverse(m, visit, arg); +} - PyErr_Format(PyExc_AttributeError, "Cannot set '%U' on Java packages", attr); - return -1; - JP_PY_CATCH(-1); // GCOVR_EXCL_LINE +static int PyJPPackage_clear(PyObject *m) +{ + return PyModule_Type.tp_clear(m); } -static PyObject *PyJPPackage_str(PyJPPackage *self, PyObject *args, PyObject *kwargs) +static PyObject *PyJPPackage_str(PyObject *self, PyObject *args, PyObject *kwargs) { JP_PY_TRY("PyJPPackage_str"); - return PyUnicode_FromFormat("%s", self->m_Package->m_Name.c_str()); + return PyModule_GetNameObject(self); JP_PY_CATCH(NULL); } -static PyObject *PyJPPackage_repr(PyJPPackage *self, PyObject *args, PyObject *kwargs) +static PyObject *PyJPPackage_repr(PyObject *self, PyObject *args, PyObject *kwargs) { JP_PY_TRY("PyJPPackage_repr"); - return PyUnicode_FromFormat("", self->m_Package->m_Name.c_str()); + return PyUnicode_FromFormat("", PyModule_GetName(self)); JP_PY_CATCH(NULL); } -static PyObject *PyJPPackage_call(PyJPPackage *self, PyObject *args, PyObject *kwargs) +static PyObject *PyJPPackage_call(PyObject *self, PyObject *args, PyObject *kwargs) { JP_PY_TRY("PyJPPackage_call"); - PyErr_Format(PyExc_TypeError, "Package `%s` is not callable.", self->m_Package->m_Name.c_str()); + PyErr_Format(PyExc_TypeError, "Package `%s` is not callable.", PyModule_GetName(self)); JP_PY_CATCH(NULL); } -static PyObject *PyJPPackage_name(PyJPPackage *self) -{ - return PyUnicode_FromFormat("%s", self->m_Package->m_Name.c_str()); -} - -static PyObject *PyJPPackage_package(PyJPPackage *self) +static PyObject *PyJPPackage_package(PyObject *self) { return PyUnicode_FromFormat("java"); } -static PyObject *PyJPPackage_path(PyJPPackage *self) +static PyObject *PyJPPackage_path(PyObject *self) { return PyList_New(0); } -static PyObject *PyJPPackage_dir(PyJPPackage *self) +static PyObject *PyJPPackage_dir(PyObject *self) { JP_PY_TRY("PyJPPackage_dir"); JPContext* context = PyJPModule_getContext(); @@ -280,27 +267,34 @@ static PyObject *PyJPPackage_dir(PyJPPackage *self) JP_PY_CATCH(NULL); } -static PyObject *PyJPPackage_handler(PyJPPackage *self) +/** + * Add redirect for matmul in package modules. + * + * This will be used to support "java@obj" which will be used + * to force cast a Python object into Java. + * + * @param self + * @param other + * @return + */ +static PyObject *PyJPPackage_cast(PyObject *self, PyObject *other) { - if (self->m_Handler == NULL) - Py_RETURN_NONE; - Py_INCREF(self->m_Handler); - return self->m_Handler; + JP_PY_TRY("PyJPPackage_cast"); + PyObject *dict = PyModule_GetDict(self); + PyObject* matmul = PyDict_GetItemString(dict, "__matmul__"); + if (matmul == NULL) + Py_RETURN_NOTIMPLEMENTED; + JPPyObject args = JPPyObject::call(PyTuple_Pack(2, self, other)); + return PyObject_Call(matmul, args.get(), NULL); + JP_PY_CATCH(NULL); } -static int PyJPPackage_setHandler(PyJPPackage *self , PyObject *handler, void *) +static PyObject *PyJPPackage_castEq(PyObject *self, PyObject *other) { - Py_INCREF(handler); - Py_CLEAR(self->m_Handler); - self->m_Handler = handler; - return 0; + PyErr_Format(PyExc_TypeError, "Matmul equals not support for Java packages"); + return NULL; } -static PyMemberDef packageMembers[] = { - {"__dictoffset__", T_PYSSIZET, offsetof(PyJPPackage, m_Dict), READONLY}, - {NULL}, -}; - static PyMethodDef packageMethods[] = { {"__dir__", (PyCFunction) PyJPPackage_dir, METH_NOARGS}, {NULL}, @@ -308,10 +302,9 @@ static PyMethodDef packageMethods[] = { static PyGetSetDef packageGetSets[] = { {"__all__", (getter) PyJPPackage_dir, NULL, ""}, - {"__name__", (getter) PyJPPackage_name, NULL, ""}, + {"__name__", (getter) PyJPPackage_str, NULL, ""}, {"__package__", (getter) PyJPPackage_package, NULL, ""}, {"__path__", (getter) PyJPPackage_path, NULL, ""}, - {"_handler", (getter) PyJPPackage_handler, (setter) PyJPPackage_setHandler, ""}, {0} }; @@ -319,13 +312,12 @@ static PyType_Slot packageSlots[] = { {Py_tp_new, (void*) PyJPPackage_new}, {Py_tp_traverse, (void*) PyJPPackage_traverse}, {Py_tp_clear, (void*) PyJPPackage_clear}, - {Py_tp_dealloc, (void*) PyJPPackage_dealloc}, {Py_tp_getattro, (void*) PyJPPackage_getattro}, - {Py_tp_setattro, (void*) PyJPPackage_setattro}, {Py_tp_str, (void*) PyJPPackage_str}, {Py_tp_repr, (void*) PyJPPackage_repr}, {Py_tp_call, (void*) PyJPPackage_call}, - {Py_tp_members, (void*) packageMembers}, + {Py_nb_matrix_multiply, (void*) PyJPPackage_cast}, + {Py_nb_inplace_matrix_multiply, (void*) PyJPPackage_castEq}, {Py_tp_methods, (void*) packageMethods}, {Py_tp_getset, (void*) packageGetSets}, {0} @@ -333,7 +325,7 @@ static PyType_Slot packageSlots[] = { static PyType_Spec packageSpec = { "_jpype._JPackage", - sizeof (PyJPPackage), + -1, 0, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, packageSlots @@ -345,8 +337,15 @@ static PyType_Spec packageSpec = { void PyJPPackage_initType(PyObject* module) { - PyJPPackage_Type = (PyTypeObject*) PyType_FromSpecWithBases(&packageSpec, NULL); + // Inherit from module. + JPPyObject bases = JPPyObject::call(PyTuple_Pack(1, &PyModule_Type)); + packageSpec.basicsize = PyModule_Type.tp_basicsize; + PyJPPackage_Type = (PyTypeObject*) PyType_FromSpecWithBases(&packageSpec, bases.get()); JP_PY_CHECK(); PyModule_AddObject(module, "_JPackage", (PyObject*) PyJPPackage_Type); JP_PY_CHECK(); + + // Set up a dictionary so we can reuse packages + PyJPPackage_Dict = PyDict_New(); + PyModule_AddObject(module, "_packages", PyJPPackage_Dict); } diff --git a/setup.py b/setup.py index 5e44013e8..53b36ddd4 100644 --- a/setup.py +++ b/setup.py @@ -18,15 +18,21 @@ # # ***************************************************************************** import sys -import setupext from pathlib import Path from setuptools import setup from setuptools import Extension import glob +if sys.version_info[0] < 3 and sys.version_info[1] < 5: + raise RuntimeError("JPype requires Python 3.5 or later") + +import setupext + + if '--android' in sys.argv: sys.platform = 'android' + jpypeLib = Extension(name='_jpype', **setupext.platform.Platform( include_dirs=[Path('native', 'common', 'include'), Path('native', 'python', 'include'), @@ -41,9 +47,10 @@ libraries=["lib/asm-8.0.1.jar"] ) + setup( name='JPype1', - version='1.0.2_dev0', + version='1.0.3_dev0', description='A Python to Java bridge.', long_description=open('README.rst').read(), license='License :: OSI Approved :: Apache Software License', diff --git a/setupext/__init__.py b/setupext/__init__.py index cdee1ac73..39e85b481 100644 --- a/setupext/__init__.py +++ b/setupext/__init__.py @@ -15,6 +15,7 @@ # See NOTICE file for details. # # ***************************************************************************** + from . import utils from . import dist from . import platform diff --git a/test/jpypetest/test_jmethod.py b/test/jpypetest/test_jmethod.py index ad6c8e250..8fe663568 100644 --- a/test/jpypetest/test_jmethod.py +++ b/test/jpypetest/test_jmethod.py @@ -73,7 +73,6 @@ def testMethodName(self): self.assertEqual(self.cls.substring.__name__, "substring") self.assertEqual(self.obj.substring.__name__, "substring") - @common.unittest.skipIf(sys.version_info[0] < 3, "skip on Python2") def testMethodQualName(self): self.assertEqual(self.cls.substring.__qualname__, "java.lang.String.substring") @@ -111,7 +110,6 @@ def testMethodAnnotations(self): "return"], self.cls) self.assertEqual(self.cls.getBytes.__annotations__, {}) - @common.unittest.skipIf(sys.version_info[0] < 3, "skip on Python2") def testMethodInspectSignature(self): self.assertIsInstance(inspect.signature( self.cls.substring), inspect.Signature) diff --git a/test/jpypetest/test_jstring.py b/test/jpypetest/test_jstring.py index f4191c3c6..4c6b46190 100644 --- a/test/jpypetest/test_jstring.py +++ b/test/jpypetest/test_jstring.py @@ -168,3 +168,12 @@ def testNullAdd(self): def testNullHash(self): jsn = JObject(None, JString) self.assertEqual(hash(jsn), hash(None)) + + def testSlice(self): + s = 'abcdefghijklmnop' + s2 = JString(s) + self.assertEqual(s[:], s2[:]) + self.assertEqual(s[1:5], s2[1:5]) + self.assertEqual(s[:5], s2[:5]) + self.assertEqual(s[3:], s2[3:]) + self.assertEqual(s[::-1], s2[::-1])