com.xiaopy.python.PyObject Maven / Gradle / Ivy
Show all versions of chaquopy_java Show documentation
package com.xiaopy.python;
import java.lang.ref.*;
import java.lang.reflect.*;
import java.util.*;
import org.jetbrains.annotations.*;
import static java.util.Objects.requireNonNull;
/** Interface to a Python object.
*
*
* - Python {@code None} is represented by Java {@code null}. Other PyObjects can be
* converted to their Java equivalents using {@link #toJava toJava()}.
*
* - If a Python object is retrieved for which a PyObject already exists, the same
* PyObject will be returned.
*
*
* Unless otherwise specified, all methods in this class throw {@link PyException} on
* failure.
*/
public class PyObject extends AbstractMap implements AutoCloseable {
private static final Map> cache = new HashMap<>();
/** @deprecated Internal use in conversion.pxi */
public long addr;
/** @deprecated Internal use in conversion.pxi */
public static PyObject getInstance(long addr) {
if (addr == 0) return null;
synchronized (cache) {
WeakReference wr = cache.get(addr);
if (wr != null) {
// wr.get() will return null if the PyObject is unreachable but it has not yet
// been removed from the cache. In that case, the constructor call below will
// overwrite it.
PyObject po = wr.get();
if (po != null) return po;
}
PyObject po = new PyObject(addr);
cache.put(addr, new WeakReference<>(po));
return po;
}
}
private PyObject(long addr) {
this.addr = addr;
}
/** Releases the reference to the Python object. Unless the object represents an
* expensive resource, there's no need to call this method: it will be called
* automatically when the PyObject is garbage-collected.
*
* After calling {@code close}, the PyObject can no longer be used. If there are no
* other references to the underlying object, it may be destroyed by Python. If it
* continues to exist and is retrieved by Java code again, a new PyObject will be
* returned.
*
* Caution: any references to the same Python object elsewhere in your program will be
* represented by the same PyObject, so they will all be invalidated by this call.
*/
public void close() {
if (addr == 0) return;
synchronized (cache) {
WeakReference wr = cache.remove(addr);
if (wr != null) {
PyObject po = wr.get();
if (po != null && po != this) {
// We're running in the finalizer and we've already been replaced by a new PyObject.
cache.put(addr, wr);
}
}
}
// Don't take the GIL within the cache lock, because most other methods do it in the
// opposite order.
closeNative();
addr = 0;
}
private native void closeNative();
/** Converts the given Java object to a Python object. There's usually no need to call
* this method: it will be called automatically by the methods of this class which take
* {@code Object} parameters.
*
* For details of how Java objects are represented in Python, see the
* Python API.
*/
public static PyObject fromJava(Object o) {
return getInstance(fromJavaNative(o));
}
private static native long fromJavaNative(Object o);
/** Converts the Python object to the given Java type.
*
* If {@code klass} is a primitive type such as {@code int}, or an immutable value type
* such as {@code Integer} or {@code String}, and the Python object is compatible with it,
* an equivalent Java object will be returned. However, it's more efficient to use the
* specific methods like {@link #toInt}, {@link #toString}, etc.
*
* If {@code klass} is an array type, and the Python object is a sequence, then a copy
* of the sequence will be returned as a new array. In general, each element will be
* converted as if {@code toJava} was called on it recursively. However, when converting a
* Python {@code bytes} or {@code bytearray} object to a Java {@code byte[]} array, there
* is an unsigned-to-signed conversion: Python values 128 to 255 will be mapped to Java
* values -128 to -1.
*
* If the Python object is a jclass or
* jarray object which is compatible with
* {@code klass}, the underlying Java object will be returned.
*
* Otherwise, a {@code ClassCastException} will be thrown.
*/
public native @NotNull T toJava(@NotNull Class klass);
// === Primitive conversions =============================================
/** Converts a Python {@code bool} to a Java {@code boolean}.
* @throws ClassCastException if the Python object is not compatible */
public native boolean toBoolean();
/** Converts a Python {@code int} to a Java {@code byte}.
* @throws ClassCastException if the Python object is not compatible */
public native byte toByte();
/** Converts a 1-character Python string to a Java {@code char}.
* @throws ClassCastException if the Python object is not compatible */
public native char toChar();
/** Converts a Python {@code int} to a Java {@code short}.
* @throws ClassCastException if the Python object is not compatible */
public native short toShort();
/** Converts a Python {@code int} to a Java {@code int}.
* @throws ClassCastException if the Python object is not compatible */
public native int toInt();
/** Converts a Python {@code int} to a Java {@code long}.
* @throws ClassCastException if the Python object is not compatible */
public native long toLong();
/** Converts a Python {@code float} or {@code int} to a Java {@code float}.
* @throws ClassCastException if the Python object is not compatible */
public native float toFloat();
/** Converts a Python {@code float} or {@code int} to a Java {@code double}.
* @throws ClassCastException if the Python object is not compatible */
public native double toDouble();
// === Container views ===================================================
/** Returns a view of the Python object as a list. The view is backed by the object, so
* changes to the object are reflected in the view, and vice-versa.
*
* To add Java objects to the Python container through the view, first convert them
* using {@link #fromJava fromJava()}.
*
* @throws UnsupportedOperationException if the Python object does not implement the
* methods {@code __getitem__} and {@code __len__}. */
public @NotNull List asList() { return new PyList(this); }
/** Returns a view of the Python object as a map. The view is backed by the object, so
* changes to the object are reflected in the view, and vice-versa.
*
* PyObject already implements the {@code Map} interface, but that is for attribute
* access (Python "{@code .}" syntax), whereas the {@code Map} returned by this method is
* for container access (Python "{@code []}" syntax).
*
* To add Java objects to the Python container through the view, first convert them
* using {@link #fromJava fromJava()}.
*
* @throws UnsupportedOperationException if the Python object does not implement the
* methods {@code __contains__}, {@code __getitem__}, {@code __iter__} and {@code __len__}. */
public @NotNull Map asMap() { return new PyMap(this); }
/** Returns a view of the Python object as a set. The view is backed by the object, so
* changes to the object are reflected in the view, and vice-versa.
*
* To add Java objects to the Python container through the view, first convert them
* using {@link #fromJava fromJava()}.
*
* @throws UnsupportedOperationException if the Python object does not implement the
* methods {@code __contains__}, {@code __iter__} and {@code __len__}. */
public @NotNull Set asSet() { return new PySet(this); }
// === Miscellaneous =====================================================
/** Equivalent to Python {@code id()}. */
public native long id();
/** Equivalent to Python {@code type()}. */
public @NotNull PyObject type() {
return requireNonNull(getInstance(typeNative()));
}
private native long typeNative();
/** Equivalent to Python {@code ()} syntax. Arguments will be converted as described at
* {@link #fromJava fromJava()}. Keyword arguments can be passed using instances of {@link
* Kwarg} at the end of the argument list. */
public PyObject call(Object... args) {
try {
return callThrows(args);
} catch (PyException e) {
throw e;
} catch (Throwable e) {
throw new PyException(e);
}
}
/** Same as {@link #call call()}, except it directly passes any Java exception thrown
* by the Python code. */
public PyObject callThrows(Object... args) throws Throwable {
return getInstance(callThrowsNative(args));
}
private native long callThrowsNative(Object... args) throws Throwable;
/** Same as {@link #get get}{@code (key).}{@link #call call}{@code (args)}, except it
* throws a {@link PyException} if the attribute does not exist. */
public PyObject callAttr(@NotNull String key, Object... args) {
try {
return callAttrThrows(key, args);
} catch (PyException e) {
throw e;
} catch (Throwable e) {
throw new PyException(e);
}
}
/** Same as {@link #callAttr callAttr()}, except it directly passes any Java exception
* thrown by the Python code. */
public PyObject callAttrThrows(@NotNull String key, Object... args) throws Throwable {
return getInstance(callAttrThrowsNative(key, args));
}
private native long callAttrThrowsNative(String key, Object... args) throws Throwable;
/** @deprecated internal use in files generated by static_proxy.py */
public static PyObject _chaquopyCall(StaticProxy sp, String name, Object... args) {
try {
return PyObject.fromJava(sp).callAttrThrows(name, args);
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new UndeclaredThrowableException(e);
}
}
// ==== Attributes =======================================================
/** Attempts to remove all attributes returned by {@link #keySet()}. See notes on
* {@link #isEmpty}.
*/
@Override public void clear() { super.clear(); }
/** Equivalent to {@link #keySet()}{@code .isEmpty()}. Because {@code keySet} usually
* includes the object's class attributes, {@code isEmpty} will never return true even
* after calling {@link #clear}, unless the object has a custom {@code __dir__} method. */
@Override public boolean isEmpty() { return super.isEmpty(); }
/** Equivalent to Python {@code hasattr()}. */
@Override public boolean containsKey(@NotNull Object key) {
return containsKeyNative((String)key);
}
private native boolean containsKeyNative(String key);
/** Returns whether any attribute has the given value. The value will be converted as
* described at {@link #fromJava fromJava()}. */
@Override public boolean containsValue(Object o) {
return super.containsValue(fromJava(o));
}
/** Equivalent to Python {@code getattr()}. In accordance with the {@code Map} interface,
* when the attribute does not exist, this method returns {@code null} rather than
* throwing an exception. To distinguish this from an attribute with a value of {@code
* None}, use {@link #containsKey containsKey()}. */
@Override public PyObject get(@NotNull Object key) {
return getInstance(getNative((String)key));
}
private native long getNative(String key);
/** Equivalent to Python {@code setattr()}. */
@Override public PyObject put(@NotNull String key, PyObject value) {
return put(key, (Object)value);
}
/** Equivalent to Python {@code setattr()}. The value will be converted as described at
* {@link #fromJava fromJava()}.*/
public PyObject put(@NotNull String key, Object value) {
return getInstance(putNative(key, value));
}
private native long putNative(String key, Object value);
/** Equivalent to Python {@code delattr()}. This means it can only remove attributes of
* the object itself, even though {@link #keySet()} usually includes the object's class
* attributes as well.
*
* In accordance with the {@code Map} interface, when the attribute does not exist, this
* method returns {@code null} rather than throwing an exception.
*/
@Override public PyObject remove(@NotNull Object key) {
return getInstance(removeNative((String)key));
}
private native long removeNative(String key);
/** Equivalent to Python {@code dir()}. Unless the object has a custom {@code __dir__}
* method, this means the result will include attributes from the object's class as well
* as the object itself.
*
* The returned set is backed by the Python object, so changes to the object are
* reflected in the set, and vice-versa. If the object is modified while an iteration over
* the set is in progress (except through the iterator's own {@code remove} operation), the
* results of the iteration are undefined. The set supports element removal, but see the
* notes on {@link #remove remove()}. It does not support the {@code add} or {@code addAll}
* operations.
*/
@Override public @NotNull Set keySet() { return super.keySet(); }
/** See notes on {@link #keySet()}. */
@Override public @NotNull Set> entrySet() {
return new AbstractSet>() {
@Override public int size() { return dir().size(); }
@Override public @NotNull Iterator> iterator() {
return new Iterator>() {
List keys = dir();
int i = 0;
@Override public boolean hasNext() {
return i < keys.size();
}
@Override public Entry next() {
if (! hasNext()) throw new NoSuchElementException();
Entry entry = new Entry() {
String key = keys.get(i);
@Override public String getKey() { return key; }
@Override public PyObject getValue() { return get(key); }
@Override public PyObject setValue(PyObject newValue) {
return put(key, newValue);
}
};
i += 1;
return entry;
}
@Override public void remove() {
PyObject.this.remove(keys.get(i-1));
}
};
}
};
}
private native List dir();
// === Object methods ====================================================
/** Equivalent to Python {@code ==} operator. The given object will be converted as
* described at {@link #fromJava fromJava()} */
@Override public native boolean equals(Object that);
/** Equivalent to Python {@code str()}. */
@Override public native @NotNull String toString();
/** Equivalent to Python {@code repr()}. */
public native @NotNull String repr();
/** Equivalent to Python {@code hash()}. */
@Override public native int hashCode();
/** Calls {@link #close}. */
@Override protected void finalize() throws Throwable {
close();
super.finalize();
}
}