From 3a5246b3bcb82dce5bf30342d5fbf946fe3daf72 Mon Sep 17 00:00:00 2001 From: Dj <43033058+DjDeveloperr@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:05:46 +0530 Subject: [PATCH] feat: add bun support (#47) --- .github/workflows/checks.yml | 8 +++ .gitignore | 3 + README.md | 37 +++++++--- bunfig.toml | 1 + examples/hello_python.ts | 2 +- examples/import.js | 7 ++ examples/test.py | 2 + ipy.ts | 25 +++++++ mod.bun.ts | 5 ++ package.json | 26 +++++++ plugin.ts | 44 ++++++++++++ src/bun_compat.js | 107 +++++++++++++++++++++++++++++ src/ffi.ts | 4 +- src/python.ts | 18 ++++- src/symbols.ts | 129 ----------------------------------- src/util.ts | 7 +- test/asserts.ts | 29 ++++++++ test/bench.ts | 15 ++++ test/bun.test.js | 3 + test/bun_compat.js | 18 +++++ test/deps.ts | 1 - test/package.json | 15 ++++ test/test.ts | 4 +- 23 files changed, 358 insertions(+), 152 deletions(-) create mode 100644 bunfig.toml create mode 100644 examples/import.js create mode 100644 examples/test.py create mode 100644 ipy.ts create mode 100644 mod.bun.ts create mode 100644 package.json create mode 100644 plugin.ts create mode 100644 src/bun_compat.js create mode 100644 test/asserts.ts create mode 100644 test/bench.ts create mode 100644 test/bun.test.js create mode 100644 test/bun_compat.js delete mode 100644 test/deps.ts create mode 100644 test/package.json diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d939b01..dd8a084 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -54,6 +54,10 @@ jobs: with: deno-version: v1.x + - name: Setup Bun + if: ${{ matrix.os != 'windows-latest' }} + uses: oven-sh/setup-bun@v1 + - name: Setup Python (Windows) uses: actions/setup-python@v2 if: ${{ matrix.os == 'windows-latest' }} @@ -65,3 +69,7 @@ jobs: - name: Run deno test run: deno task test + + - name: Run bun test + if: ${{ matrix.os != 'windows-latest' }} + run: bun test diff --git a/.gitignore b/.gitignore index a05adb0..63efb2e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /*.bat deno.lock plug/ +bun.lockb +node_modules/ +__pycache__/ diff --git a/README.md b/README.md index 2d21b3e..b7df25d 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# deno_python +# Python Bridge [![Tags](https://img.shields.io/github/release/denosaurs/deno_python)](https://github.com/denosaurs/deno_python/releases) [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/python/mod.ts) [![checks](https://github.com/denosaurs/deno_python/actions/workflows/checks.yml/badge.svg)](https://github.com/denosaurs/deno_python/actions/workflows/checks.yml) [![License](https://img.shields.io/github/license/denosaurs/deno_python)](https://github.com/denosaurs/deno_python/blob/master/LICENSE) -This module provides a seamless integration between deno and python by -integrating with the [Python/C API](https://docs.python.org/3/c-api/index.html). -It acts as a bridge between the two languages, enabling you to pass data and -execute python code from within your deno applications. This enables access to -the large and wonderful [python ecosystem](https://pypi.org/) while remaining -native (unlike a runtime like the wonderful -[pyodide](https://github.com/pyodide/pyodide) which is compiled to wasm, -sandboxed and may not work with all python packages) and simply using the -existing python installation. +This module provides a seamless integration between JavaScript (Deno/Bun) and +Python by integrating with the +[Python/C API](https://docs.python.org/3/c-api/index.html). It acts as a bridge +between the two languages, enabling you to pass data and execute python code +from within your JS applications. This enables access to the large and wonderful +[python ecosystem](https://pypi.org/) while remaining native (unlike a runtime +like the wonderful [pyodide](https://github.com/pyodide/pyodide) which is +compiled to wasm, sandboxed and may not work with all python packages) and +simply using the existing python installation. ## Example @@ -40,6 +40,23 @@ permissions since enabling FFI effectively escapes the permissions sandbox. deno run -A --unstable ``` +### Usage in Bun + +You can import from the `bunpy` NPM package to use this module in Bun. + +```ts +import { python } from "bunpy"; + +const np = python.import("numpy"); +const plt = python.import("matplotlib.pyplot"); + +const xpoints = np.array([1, 8]); +const ypoints = np.array([3, 10]); + +plt.plot(xpoints, ypoints); +plt.show(); +``` + ### Dependencies Normally deno_python follows the default python way of resolving imports, going diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..62b79f8 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["./plugin.ts"] diff --git a/examples/hello_python.ts b/examples/hello_python.ts index 868a771..ab20bfd 100644 --- a/examples/hello_python.ts +++ b/examples/hello_python.ts @@ -4,4 +4,4 @@ const { print, str } = python.builtins; const { version } = python.import("sys"); print(str("Hello, World!").lower()); -print(`Python version: ${version}`); +print("Python version:", version); diff --git a/examples/import.js b/examples/import.js new file mode 100644 index 0000000..c29561f --- /dev/null +++ b/examples/import.js @@ -0,0 +1,7 @@ +import { add } from "./test.py"; +import { print } from "python:builtins"; +import * as np from "python:numpy"; + +console.log(add(1, 2)); +print("Hello, world!"); +console.log(np.array([1, 2, 3])); diff --git a/examples/test.py b/examples/test.py new file mode 100644 index 0000000..2a99cdf --- /dev/null +++ b/examples/test.py @@ -0,0 +1,2 @@ +def add(a, b): + return a + b diff --git a/ipy.ts b/ipy.ts new file mode 100644 index 0000000..089a16b --- /dev/null +++ b/ipy.ts @@ -0,0 +1,25 @@ +import py, { Python } from "./mod.ts"; +import { Pip, pip } from "./ext/pip.ts"; + +declare global { + const py: Python; + const pip: Pip; +} + +Object.defineProperty(globalThis, "py", { + value: py, + writable: false, + enumerable: false, + configurable: false, +}); + +Object.defineProperty(globalThis, "pip", { + value: pip, + writable: false, + enumerable: false, + configurable: false, +}); + +export * from "./mod.ts"; +export * from "./ext/pip.ts"; +export default py; diff --git a/mod.bun.ts b/mod.bun.ts new file mode 100644 index 0000000..ad5cf7b --- /dev/null +++ b/mod.bun.ts @@ -0,0 +1,5 @@ +declare module "python:*"; +declare module "*.py"; + +import "./src/bun_compat.js"; +export * from "./mod.ts"; diff --git a/package.json b/package.json new file mode 100644 index 0000000..291f790 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "bunpy", + "version": "0.3.3", + "description": "JavaScript -> Python Bridge for Deno and Bun", + "main": "mod.bun.ts", + "directories": { + "example": "examples", + "test": "test" + }, + "scripts": { + "test": "deno task test && bun test" + }, + "files": [ + "mod.ts", + "ext/pip.ts", + "src/bun_compat.js", + "src/ffi.ts", + "src/python.ts", + "src/symbols.ts", + "src/util.ts", + "plugin.ts" + ], + "keywords": [], + "author": "DjDeveloperr", + "license": "MIT" +} diff --git a/plugin.ts b/plugin.ts new file mode 100644 index 0000000..c65f38d --- /dev/null +++ b/plugin.ts @@ -0,0 +1,44 @@ +// TODO: Maybe add support for pip: namespace that automatically installs the module if it's not found. + +// deno-lint-ignore-file no-explicit-any +import { plugin } from "bun"; +import { python } from "./mod.ts"; + +const { dir } = python.builtins; +const { SourceFileLoader } = python.import("importlib.machinery"); + +export function exportModule(mod: any) { + const props = dir(mod).valueOf(); + const exports: Record = {}; + for (let prop of props) { + prop = prop.toString(); + exports[prop] = mod[prop]; + } + return exports; +} + +plugin({ + name: "Python Loader", + setup: (build) => { + build.onLoad({ filter: /\.py$/ }, (args) => { + const name = args.path.split("/").pop()!.split(".py")[0]; + const exports = SourceFileLoader(name, args.path).load_module(); + return { + exports: exportModule(exports), + loader: "object", + }; + }); + + build.onResolve({ filter: /.+/, namespace: "python" }, (args) => { + return { path: args.path, namespace: "python" }; + }); + + build.onLoad({ filter: /.+/, namespace: "python" }, (args) => { + const exports = python.import(args.path); + return { + exports: exportModule(exports), + loader: "object", + }; + }); + }, +}); diff --git a/src/bun_compat.js b/src/bun_compat.js new file mode 100644 index 0000000..36788bc --- /dev/null +++ b/src/bun_compat.js @@ -0,0 +1,107 @@ +import { type } from "node:os"; + +if (!("Deno" in globalThis) && "Bun" in globalThis) { + const { dlopen, FFIType, CString, JSCallback, ptr } = await import("bun:ffi"); + class Deno { + static env = { + get(name) { + return Bun.env[name]; + }, + }; + + static build = { + os: type().toLowerCase(), + }; + + static transformFFIType(type) { + switch (type) { + case "void": + return FFIType.void; + case "i32": + return FFIType.i64_fast; + case "i64": + return FFIType.i64; + case "f32": + return FFIType.f32; + case "f64": + return FFIType.f64; + case "pointer": + case "buffer": + return FFIType.ptr; + case "u32": + return FFIType.u64_fast; + default: + throw new Error("Type not supported: " + type); + } + } + + static dlopen(path, symbols) { + const bunSymbols = {}; + for (const name in symbols) { + const symbol = symbols[name]; + if ("type" in symbol) { + throw new Error("Symbol type not supported"); + } else { + bunSymbols[name] = { + args: symbol.parameters.map((type) => this.transformFFIType(type)), + returns: this.transformFFIType(symbol.result), + }; + } + } + const lib = dlopen(path, bunSymbols); + return lib; + } + + static UnsafeCallback = class UnsafeCallback { + constructor(def, fn) { + this.inner = new JSCallback(fn, { + args: def.parameters.map((type) => Deno.transformFFIType(type)), + returns: Deno.transformFFIType(def.result), + }); + this.pointer = this.inner.ptr; + } + + close() { + this.inner.close(); + } + }; + + static UnsafePointerView = class UnsafePointerView { + static getCString(ptr) { + return new CString(ptr); + } + + constructor(ptr) { + this.ptr = ptr; + } + + getCString() { + return new CString(this.ptr); + } + }; + + static UnsafePointer = class UnsafePointer { + static equals(a, b) { + return a === b; + } + + static create(a) { + return Number(a); + } + + static of(buf) { + return ptr(buf); + } + + static value(ptr) { + return ptr; + } + }; + + static test(name, fn) { + globalThis.DenoTestCompat(name, fn); + } + } + + globalThis.Deno = Deno; +} diff --git a/src/ffi.ts b/src/ffi.ts index 64c72b1..f2259e9 100644 --- a/src/ffi.ts +++ b/src/ffi.ts @@ -20,6 +20,7 @@ if (DENO_PYTHON_PATH) { } else if (Deno.build.os === "darwin") { for ( const framework of [ + "/Library/Frameworks/Python.framework/Versions", "/opt/homebrew/Frameworks/Python.framework/Versions", "/usr/local/Frameworks/Python.framework/Versions", ] @@ -41,9 +42,10 @@ for (const path of searchPath) { postSetup(path); break; } catch (err) { - if (err instanceof TypeError) { + if (err instanceof TypeError && !("Bun" in globalThis)) { throw new Error( "Cannot load dynamic library because --unstable flag was not set", + { cause: err }, ); } continue; diff --git a/src/python.ts b/src/python.ts index 53881eb..f8dc2ba 100644 --- a/src/python.ts +++ b/src/python.ts @@ -195,7 +195,9 @@ export class PyObject { * Check if the object is NULL (pointer) or None type in Python. */ get isNone() { - return this.handle === null || + // deno-lint-ignore ban-ts-comment + // @ts-expect-error + return this.handle === null || this.handle === 0 || this.handle === python.None[ProxiedPyObject].handle; } @@ -260,6 +262,14 @@ export class PyObject { 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](), }); @@ -282,7 +292,7 @@ export class PyObject { return new Proxy(object, { get: (_, name) => { // For the symbols. - if (typeof name === "symbol" && name in object) { + if ((typeof name === "symbol") && name in object) { return (object as any)[name]; } @@ -783,6 +793,10 @@ export class PyObject { [Symbol.for("Deno.customInspect")]() { return this.toString(); } + + [Symbol.for("nodejs.util.inspect.custom")]() { + return this.toString(); + } } /** Python-related error. */ diff --git a/src/symbols.ts b/src/symbols.ts index 176bb0e..4e84320 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -1,14 +1,4 @@ export const SYMBOLS = { - Py_DecodeLocale: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - Py_SetProgramName: { - parameters: ["pointer"], - result: "void", - }, - Py_Initialize: { parameters: [], result: "void", @@ -29,11 +19,6 @@ export const SYMBOLS = { result: "pointer", }, - PyEval_GetBuiltins: { - parameters: [], - result: "pointer", - }, - PyRun_SimpleString: { parameters: ["buffer"], result: "i32", @@ -85,11 +70,6 @@ export const SYMBOLS = { result: "pointer", }, - PyObject_CallObject: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - PyObject_GetAttrString: { parameters: ["pointer", "buffer"], result: "pointer", @@ -130,11 +110,6 @@ export const SYMBOLS = { result: "i32", }, - PyDict_Next: { - parameters: ["pointer", "pointer", "pointer", "pointer"], - result: "i32", - }, - PyDict_SetItem: { parameters: ["pointer", "pointer", "pointer"], result: "i32", @@ -205,106 +180,6 @@ export const SYMBOLS = { result: "pointer", }, - PyBytes_FromStringAndSize: { - parameters: ["pointer", "i32"], - result: "pointer", - }, - - PyBytes_AsStringAndSize: { - parameters: ["pointer", "pointer", "pointer"], - result: "i32", - }, - - PyBool_Type: { - parameters: [], - result: "pointer", - }, - - PySlice_Type: { - parameters: [], - result: "pointer", - }, - - PyNumber_Add: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Subtract: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Multiply: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_TrueDivide: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceAdd: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceSubtract: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceMultiply: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceTrueDivide: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Negative: { - parameters: ["pointer"], - result: "pointer", - }, - - PyNumber_And: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Or: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Xor: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceAnd: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceOr: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_InPlaceXor: { - parameters: ["pointer", "pointer"], - result: "pointer", - }, - - PyNumber_Invert: { - parameters: ["pointer"], - result: "pointer", - }, - PyList_Size: { parameters: ["pointer"], result: "i32", @@ -370,10 +245,6 @@ export const SYMBOLS = { result: "pointer", }, - PyTuple_Pack: { - type: "pointer", - }, - PyCFunction_NewEx: { parameters: ["buffer", "pointer", "pointer"], result: "pointer", diff --git a/src/util.ts b/src/util.ts index a4a6101..e4566b6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -32,12 +32,7 @@ export function postSetup(lib: string) { libDlDef, ); } else if (Deno.build.os === "darwin") { - libdl = Deno.dlopen(`libc.dylib`, { - dlopen: { - parameters: ["buffer", "i32"], - result: "pointer", - }, - }); + libdl = Deno.dlopen(`libc.dylib`, libDlDef); } else { return; } diff --git a/test/asserts.ts b/test/asserts.ts new file mode 100644 index 0000000..28d0bf0 --- /dev/null +++ b/test/asserts.ts @@ -0,0 +1,29 @@ +// deno-lint-ignore no-explicit-any +export function assert(condition: any) { + if (!condition) { + throw new Error("Assertion failed"); + } +} + +export function assertEquals(actual: T, expected: T) { + if ( + (actual instanceof Map && expected instanceof Map) || + (actual instanceof Set && expected instanceof Set) + ) { + return assertEquals([...actual], [...expected]); + } + const actualS = JSON.stringify(actual); + const expectedS = JSON.stringify(expected); + if (actualS !== expectedS) { + throw new Error(`Expected ${expectedS}, got ${actualS}`); + } +} + +export function assertThrows(fn: () => void) { + try { + fn(); + } catch (_e) { + return; + } + throw new Error("Expected exception"); +} diff --git a/test/bench.ts b/test/bench.ts new file mode 100644 index 0000000..419d894 --- /dev/null +++ b/test/bench.ts @@ -0,0 +1,15 @@ +import { python } from "../mod.ts"; +import { bench, run } from "mitata"; + +const { add } = python.runModule(` +def add(a, b): + return a + b +`); + +bench("noop", () => {}); + +bench("python.add", () => { + add(1, 2); +}); + +await run(); diff --git a/test/bun.test.js b/test/bun.test.js new file mode 100644 index 0000000..cb81242 --- /dev/null +++ b/test/bun.test.js @@ -0,0 +1,3 @@ +import "../src/bun_compat.js"; +import "./bun_compat.js"; +import "./test.ts"; diff --git a/test/bun_compat.js b/test/bun_compat.js new file mode 100644 index 0000000..1822dff --- /dev/null +++ b/test/bun_compat.js @@ -0,0 +1,18 @@ +import { describe, test } from "bun:test"; + +globalThis.DenoTestCompat = function (name, fn) { + const isGroup = (fn + "").includes("(t)"); + if (isGroup) { + describe(name, async () => { + await fn({ + step: async (name, fn) => { + await test(name, fn); + }, + }); + }); + } else { + test(name, async () => { + await fn(); + }); + } +}; diff --git a/test/deps.ts b/test/deps.ts deleted file mode 100644 index 6fc4eab..0000000 --- a/test/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/std@0.198.0/assert/mod.ts"; diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..e920656 --- /dev/null +++ b/test/package.json @@ -0,0 +1,15 @@ +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "mitata": "^0.1.6" + } +} diff --git a/test/test.ts b/test/test.ts index af063a5..6338510 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,4 +1,4 @@ -import { assert, assertEquals, assertThrows } from "./deps.ts"; +import { assert, assertEquals, assertThrows } from "./asserts.ts"; import { kw, NamedArgument, @@ -13,7 +13,7 @@ console.log("Python version:", version); console.log("Executable:", executable); Deno.test("python version", () => { - assert(String(version).match(/^\d+\.\d+\.\d+/)); + assert(version.toString().match(/^\d+\.\d+\.\d+/)); }); Deno.test("types", async (t) => {