Repository: ninia/jep (language: python)

Issue:
`Py_DECREF` seems to be missing in `Java_jep_python_PyObject_setAttr`
**Describe the bug**
`Py_DECREF` seems to be missing in `Java_jep_python_PyObject_setAttr`, witch will cause memory leak when using `setAttr` of PyObject.

**To Reproduce**
```java
try (SharedInterpreter inter = new SharedInterpreter()) {
    inter.exec("class Simple:\n" +
               "    def __init__(self, id):\n" +
               "        self.member = None\n"+
               "        self.id = id\n" +
               "    def __del__(self):\n" +
               "        print('obj' + str(self.id) + ' del')"
    );
    PyCallable simple = inter.getValue("Simple", PyCallable.class);
    PyObject obj1 = simple.callAs(PyObject.class, 1);
    PyObject obj2 = simple.callAs(PyObject.class, 2);
    obj1.setAttr("member", obj2);
}
```

Output:

```
obj1 del
```

So only `obj1` was deleted, while `obj2` leaked.

**Expected behavior**

Output:

```
obj1 del
obj2 del
```

Both obj1 and obj2 should be deleted.




Discussion / hints:

Fix patch:
diff --git a/src/main/c/Jep/python/jep_object.c b/src/main/c/Jep/python/jep_object.c
index 793bb8dd..7777c28d 100644
--- a/src/main/c/Jep/python/jep_object.c
+++ b/src/main/c/Jep/python/jep_object.c
@@ -114,6 +114,7 @@ JNIEXPORT void JNICALL Java_jep_python_PyObject_setAttr
     }
 
 EXIT:
+    Py_XDECREF(pyAttr);
     PyEval_ReleaseThread(jepThread->tstate);
     release_utf_char(env, str, attrName);
 }


Test patch:
diff --git a/src/test/java/jep/test/TestGetJPyObject.java b/src/test/java/jep/test/TestGetJPyObject.java
index a0065aed..98a16a64 100644
--- a/src/test/java/jep/test/TestGetJPyObject.java
+++ b/src/test/java/jep/test/TestGetJPyObject.java
@@ -3,6 +3,7 @@
 import java.lang.reflect.UndeclaredThrowableException;
 import java.util.Arrays;
 import java.util.Deque;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import jep.Interpreter;
 import jep.JepException;
@@ -113,6 +114,36 @@ public static void testSetAttr(Interpreter interp) throws JepException {
         }
     }
 
+    public static void testRefCount(Interpreter interp) throws JepException {
+        AtomicInteger delCount = new AtomicInteger();
+        interp.set("delCount", delCount);
+        interp.exec("class Simple:\n" +
+                    "    def __del__(self):\n" +
+                    "        delCount.getAndIncrement()"
+        );
+        PyCallable simple = interp.getValue("Simple", PyCallable.class);
+        /* Check for leaks in setAttr() */
+        PyObject obj1 = simple.callAs(PyObject.class);
+        PyObject obj2 = simple.callAs(PyObject.class);
+        obj1.setAttr("member", obj2);
+        obj2.close();
+        obj1.close();
+        /* When a PyObject is deleted any attributes without other references should also be deleted. */
+        if (delCount.get() != 2) {
+            throw new IllegalStateException("Expecting 2 deletions but got " + delCount.get());
+        }
+        delCount.set(0);
+        /* Check for leaks in equals() */
+        obj1 = simple.callAs(PyObject.class);
+        obj2 = simple.callAs(PyObject.class);
+        obj1.equals(obj2);
+        obj2.close();
+        obj1.close();
+        if (delCount.get() != 2) {
+            throw new IllegalStateException("Expecting 2 deletions but got " + delCount.get());
+        }
+    }
+
     public static void testDelAttr(Interpreter interp) throws JepException {
         interp.eval(buildTestClassPython());
         interp.eval("t = testclass(1, 'A String', None)");
@@ -402,6 +433,7 @@ public static void main(String[] args) throws Exception {
             testIdentity(interp);
             testGetAttr(interp);
             testSetAttr(interp);
+            testRefCount(interp);
             testDelAttr(interp);
             testJPyCallable(interp);
             testToString(interp);