import { py } from "./ffi.ts";import { cstr, SliceItemRegExp } from "./util.ts";
const refregistry = new FinalizationRegistry(py.Py_DecRef);
export const ProxiedPyObject = Symbol("ProxiedPyObject");
export interface PythonProxy { [ProxiedPyObject]: PyObject;}
export type PythonConvertible = | number | bigint | void | null | undefined | boolean | PyObject | string | Symbol | PythonProxy | PythonConvertible[] | { [key: string]: PythonConvertible } | Map<PythonConvertible, PythonConvertible> | Set<PythonConvertible> | Callback;
export type PythonJSCallback = ( kwargs: any, ...args: any[]) => PythonConvertible;
export class NamedArgument { name: string; value: PyObject;
constructor(name: string, value: PythonConvertible) { this.name = name; this.value = PyObject.from(value); }}
export function kw( strings: TemplateStringsArray, value: PythonConvertible,): NamedArgument { return new NamedArgument(strings[0].split("=")[0].trim(), value);}
export class Callback { unsafe;
constructor(public callback: PythonJSCallback) { this.unsafe = new Deno.UnsafeCallback( { parameters: ["pointer", "pointer", "pointer"], result: "pointer", }, ( _self: Deno.PointerValue, args: Deno.PointerValue, kwargs: Deno.PointerValue, ) => { return PyObject.from(callback( kwargs === null ? {} : Object.fromEntries( new PyObject(kwargs).asDict() .entries(), ), ...(args === null ? [] : new PyObject(args).valueOf()), )).handle; }, ); }
destroy() { this.unsafe.close(); }}
export class PyObject { #pyMethodDef?: Uint8Array; constructor(public handle: Deno.PointerValue) {}
get isNone() { return this.handle === null || this.handle === 0 || this.handle === python.None[ProxiedPyObject].handle; }
get owned(): PyObject { py.Py_IncRef(this.handle); refregistry.register(this, this.handle); return this; }
get proxy(): any { const scope = this; function object(...args: any[]) { return scope.call(args)?.proxy; }
Object.defineProperty(object, Symbol.for("Deno.customInspect"), { value: () => this.toString(), });
Object.defineProperty(object, Symbol.for("nodejs.util.inspect.custom"), { value: () => this.toString(), });
Object.defineProperty(object, Symbol.toStringTag, { value: () => this.toString(), });
Object.defineProperty(object, Symbol.iterator, { value: () => this[Symbol.iterator](), });
Object.defineProperty(object, ProxiedPyObject, { value: this, enumerable: false, });
Object.defineProperty(object, "toString", { value: () => this.toString(), });
Object.defineProperty(object, "valueOf", { value: () => this.valueOf(), });
return new Proxy(object, { get: (_, name) => { if ((typeof name === "symbol") && name in object) { return (object as any)[name]; }
if (typeof name === "string" && /^\d+$/.test(name)) { if (this.isInstance(python.list) || this.isInstance(python.tuple)) { const item = py.PyList_GetItem( this.handle, parseInt(name), ); if (item !== null) { return new PyObject(item).proxy; } } }
if (typeof name === "string" && isSlice(name)) { const slice = toSlice(name); const item = py.PyObject_GetItem( this.handle, slice.handle, ); if (item !== null) { return new PyObject(item).proxy; } }
const attr = this.maybeGetAttr(String(name))?.proxy;
if (attr === undefined) { if (name in object) { return (object as any)[name]; } else if (typeof name === "string" && this.isInstance(python.dict)) { const value = py.PyDict_GetItemString( this.handle, cstr(name), ); if (value !== null) { return new PyObject(value).proxy; } } } else { return attr; } },
set: (_, name, value) => { name = String(name); if (this.hasAttr(name)) { this.setAttr(String(name), value); return true; } else if (this.isInstance(python.dict)) { py.PyDict_SetItemString( this.handle, cstr(name), PyObject.from(value).handle, ); return true; } else if ((this.isInstance(python.list)) && /^\d+$/.test(name)) { py.PyList_SetItem( this.handle, Number(name), PyObject.from(value).handle, ); return true; } else if (isSlice(name)) { const slice = toSlice(name); py.PyObject_SetItem( this.handle, slice.handle, PyObject.from(value).handle, ); return true; } else { return false; } },
has: (_, name) => { if (typeof name === "symbol" && name in object) { return true; }
name = String(name);
return this.hasAttr(name) || (this.isInstance(python.dict) && this.proxy.__contains__(name).valueOf()) || name in object; }, }) as any; }
isInstance(cls: PythonConvertible): boolean { return py.PyObject_IsInstance(this.handle, PyObject.from(cls).handle) !== 0; }
equals(rhs: PythonConvertible) { const rhsObject = PyObject.from(rhs); return py.PyObject_RichCompareBool(this.handle, rhsObject.handle, 3); }
static from<T extends PythonConvertible>(v: T): PyObject { switch (typeof v) { case "boolean": { return new PyObject( py.PyBool_FromLong(v ? 1 : 0), ); }
case "number": { if (Number.isInteger(v)) { return new PyObject(py.PyLong_FromLong(v)); } else { return new PyObject(py.PyFloat_FromDouble(v)); } }
case "bigint": { return new PyObject( py.PyLong_FromLong(Number(v)), ); }
case "object": { if (v === null ) { return python.builtins.None[ProxiedPyObject]; } else if (ProxiedPyObject in v) { const proxy = v as PythonProxy; return proxy[ProxiedPyObject]; } else if (Array.isArray(v)) { const list = py.PyList_New(v.length); for (let i = 0; i < v.length; i++) { py.PyList_SetItem(list, i, PyObject.from(v[i]).owned.handle); } return new PyObject(list); } else if (v instanceof Callback) { const pyMethodDef = new Uint8Array(8 + 8 + 4 + 8); const view = new DataView(pyMethodDef.buffer); const LE = new Uint8Array(new Uint32Array([0x12345678]).buffer)[0] !== 0x7; const nameBuf = new TextEncoder().encode( "JSCallback:" + (v.callback.name || "anonymous") + "\0", ); view.setBigUint64( 0, BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(nameBuf)!)), LE, ); view.setBigUint64( 8, BigInt(Deno.UnsafePointer.value(v.unsafe.pointer)), LE, ); view.setInt32(16, 0x1 | 0x2, LE); view.setBigUint64( 20, BigInt(Deno.UnsafePointer.value(Deno.UnsafePointer.of(nameBuf)!)), LE, ); const fn = py.PyCFunction_NewEx( pyMethodDef, PyObject.from(null).handle, null, );
const pyObject = new PyObject(fn); pyObject.#pyMethodDef = pyMethodDef; return pyObject; } else if (v instanceof PyObject) { return v; } else if (v instanceof Set) { const set = py.PySet_New(null); for (const i of v) { const item = PyObject.from(i); py.PySet_Add(set, item.owned.handle); py.Py_DecRef(item.handle); } return new PyObject(set); } else { const dict = py.PyDict_New(); for ( const [key, value] of (v instanceof Map ? v.entries() : Object.entries(v)) ) { const keyObj = PyObject.from(key); const valueObj = PyObject.from(value); py.PyDict_SetItem( dict, keyObj.owned.handle, valueObj.owned.handle, ); py.Py_DecRef(keyObj.handle); py.Py_DecRef(valueObj.handle); } return new PyObject(dict); } }
case "symbol": case "string": { const str = String(v); const encoder = new TextEncoder(); const u8 = encoder.encode(str); return new PyObject( py.PyUnicode_DecodeUTF8( u8, u8.byteLength, null, ), ); }
case "undefined": { return PyObject.from(null); }
case "function": { if (ProxiedPyObject in v) { return (v as any)[ProxiedPyObject]; } }
default: throw new TypeError(`Unsupported type: ${typeof v}`); } }
maybeGetAttr(name: string): PyObject | undefined { const obj = new PyObject( py.PyObject_GetAttrString(this.handle, cstr(name)), ); if (obj.handle === null) { py.PyErr_Clear(); return undefined; } else { return obj; } }
getAttr(name: string): PyObject { const obj = this.maybeGetAttr(name); if (obj === undefined) { throw new Error(`Attribute '${name}' not found`); } else { return obj; } }
setAttr(name: string, v: PythonConvertible) { if ( py.PyObject_SetAttrString( this.handle, cstr(name), PyObject.from(v).handle, ) !== 0 ) { maybeThrowError(); } }
hasAttr(attr: string) { return py.PyObject_HasAttrString(this.handle, cstr(attr)) !== 0; }
asBoolean() { return py.PyLong_AsLong(this.handle) === 1; }
asLong() { return py.PyLong_AsLong(this.handle) as number; }
asDouble() { return py.PyFloat_AsDouble(this.handle) as number; }
asString() { const str = py.PyUnicode_AsUTF8(this.handle); return str !== null ? Deno.UnsafePointerView.getCString(str) : null; }
asArray() { const array: PythonConvertible[] = []; for (const i of this) { array.push(i.valueOf()); } return array; }
asDict() { const dict = new Map<PythonConvertible, PythonConvertible>(); const keys = py.PyDict_Keys(this.handle); const length = py.PyList_Size(keys) as number; for (let i = 0; i < length; i++) { const key = new PyObject( py.PyList_GetItem(keys, i), ); const value = new PyObject( py.PyDict_GetItem(this.handle, key.handle), ); dict.set(key.valueOf(), value.valueOf()); } return dict; }
*[Symbol.iterator]() { const iter = py.PyObject_GetIter(this.handle); let item = py.PyIter_Next(iter); while (item !== null) { yield new PyObject(item); item = py.PyIter_Next(iter); } py.Py_DecRef(iter); }
asSet() { const set = new Set(); for (const i of this) { set.add(i.valueOf()); } return set; }
asTuple() { const tuple = new Array<PythonConvertible>(); const length = py.PyTuple_Size(this.handle) as number; for (let i = 0; i < length; i++) { tuple.push( new PyObject(py.PyTuple_GetItem(this.handle, i)) .valueOf(), ); } return tuple; }
valueOf() { const type = py.PyObject_Type(this.handle);
if (Deno.UnsafePointer.equals(type, python.None[ProxiedPyObject].handle)) { return null; } else if ( Deno.UnsafePointer.equals(type, python.bool[ProxiedPyObject].handle) ) { return this.asBoolean(); } else if ( Deno.UnsafePointer.equals(type, python.int[ProxiedPyObject].handle) ) { return this.asLong(); } else if ( Deno.UnsafePointer.equals(type, python.float[ProxiedPyObject].handle) ) { return this.asDouble(); } else if ( Deno.UnsafePointer.equals(type, python.str[ProxiedPyObject].handle) ) { return this.asString(); } else if ( Deno.UnsafePointer.equals(type, python.list[ProxiedPyObject].handle) ) { return this.asArray(); } else if ( Deno.UnsafePointer.equals(type, python.dict[ProxiedPyObject].handle) ) { return this.asDict(); } else if ( Deno.UnsafePointer.equals(type, python.set[ProxiedPyObject].handle) ) { return this.asSet(); } else if ( Deno.UnsafePointer.equals(type, python.tuple[ProxiedPyObject].handle) ) { return this.asTuple(); } else { return this.proxy; } }
call( positional: (PythonConvertible | NamedArgument)[] = [], named: Record<string, PythonConvertible> = {}, ) { const namedCount = positional.filter( (arg) => arg instanceof NamedArgument, ).length;
const positionalCount = positional.length - namedCount; if (positionalCount < 0) { throw new TypeError( `${this.toString()} requires at least ${namedCount} arguments, but only ${positional.length} were passed`, ); }
const args = py.PyTuple_New(positionalCount);
let startIndex = 0; for (let i = 0; i < positional.length; i++) { const arg = positional[i]; if (arg instanceof NamedArgument) { named[arg.name] = arg.value; } else { py.PyTuple_SetItem(args, startIndex++, PyObject.from(arg).owned.handle); } } const kwargs = py.PyDict_New(); for (const [key, value] of Object.entries(named)) { py.PyDict_SetItemString( kwargs, cstr(key), PyObject.from(value).owned.handle, ); } const result = py.PyObject_Call( this.handle, args, kwargs, );
py.Py_DecRef(args); py.Py_DecRef(kwargs);
maybeThrowError();
return new PyObject(result); }
toString() { return new PyObject(py.PyObject_Str(this.handle)) .asString(); }
[Symbol.for("Deno.customInspect")]() { return this.toString(); }
[Symbol.for("nodejs.util.inspect.custom")]() { return this.toString(); }}
export class PythonError extends Error { name = "PythonError";
constructor( public type: PyObject, public value: PyObject, public traceback: PyObject, ) { let message = (value ?? type).toString() ?? "Unknown error"; let stack: string | undefined; if (!traceback.isNone) { const tb = python.import("traceback"); stack = (tb.format_tb(traceback).valueOf() as string[]).join(""); message += stack; }
super(message); this.stack = stack; }}
export function maybeThrowError() { const error = py.PyErr_Occurred(); if (error === null) { return; }
const pointers = new BigUint64Array(3); py.PyErr_Fetch( pointers.subarray(0, 1), pointers.subarray(1, 2), pointers.subarray(2, 3), );
const type = new PyObject(Deno.UnsafePointer.create(pointers[0])), value = new PyObject(Deno.UnsafePointer.create(pointers[1])), traceback = new PyObject(Deno.UnsafePointer.create(pointers[2]));
throw new PythonError(type, value, traceback);}
export class Python { builtins: any; bool: any; int: any; float: any; str: any; list: any; dict: any; set: any; tuple: any; None: any; Ellipsis: any;
kw = kw;
constructor() { py.Py_Initialize(); this.builtins = this.import("builtins");
this.int = this.builtins.int; this.float = this.builtins.float; this.str = this.builtins.str; this.list = this.builtins.list; this.dict = this.builtins.dict; this.None = this.builtins.None; this.bool = this.builtins.bool; this.set = this.builtins.set; this.tuple = this.builtins.tuple; this.Ellipsis = this.builtins.Ellipsis;
const sys = this.import("sys"); const os = this.import("os");
sys.argv = [""];
if (Deno.build.os === "darwin") { sys.executable = os.path.join(sys.exec_prefix, "bin", "python3"); } }
run(code: string) { if (py.PyRun_SimpleString(cstr(code)) !== 0) { throw new EvalError("Failed to run python code"); } }
runModule(code: string, name?: string) { const module = py.PyImport_ExecCodeModule( cstr(name ?? "__main__"), PyObject.from( this.builtins.compile(code, name ?? "__main__", "exec"), ) .handle, ); if (module === null) { throw new EvalError("Failed to run python module"); } return new PyObject(module)?.proxy; }
importObject(name: string) { const mod = py.PyImport_ImportModule(cstr(name)); if (mod === null) { maybeThrowError(); throw new TypeError(`Failed to import module ${name}`); } return new PyObject(mod); }
import(name: string) { return this.importObject(name).proxy; }
callback(cb: PythonJSCallback): Callback { return new Callback(cb); }}
export const python = new Python();
function isSlice(value: unknown): boolean { if (typeof value !== "string") return false; if (!value.includes(":") && !value.includes("...")) return false; return value .split(",") .map((item) => ( SliceItemRegExp.test(item) || /^\s*-?\d+\s*$/.test(item) || /^\s*\.\.\.\s*$/.test(item) )) .reduce((a, b) => a && b, true);}
function toSlice(sliceList: string): PyObject { if (sliceList.includes(",")) { const pySlicesHandle = sliceList.split(",").map(toSlice); return python.tuple(pySlicesHandle)[ProxiedPyObject]; } else if (/^\s*-?\d+\s*$/.test(sliceList)) { return PyObject.from(parseInt(sliceList)); } else if (/^\s*\.\.\.\s*$/.test(sliceList)) { return PyObject.from(python.Ellipsis); } else { const [start, stop, step] = sliceList .split(":") .map(( bound, ) => (/^\s*-?\d+\s*$/.test(bound) ? parseInt(bound) : undefined));
const pySliceHandle = py.PySlice_New( PyObject.from(start).handle, PyObject.from(stop).handle, PyObject.from(step).handle, ); return new PyObject(pySliceHandle); }}