Skip to content

Commit

Permalink
feat: add bun support (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
DjDeveloperr authored Sep 21, 2023
1 parent b887916 commit 3a5246b
Show file tree
Hide file tree
Showing 23 changed files with 358 additions and 152 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand All @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
/*.bat
deno.lock
plug/
bun.lockb
node_modules/
__pycache__/
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -40,6 +40,23 @@ permissions since enabling FFI effectively escapes the permissions sandbox.
deno run -A --unstable <file>
```

### 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
Expand Down
1 change: 1 addition & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
preload = ["./plugin.ts"]
2 changes: 1 addition & 1 deletion examples/hello_python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
7 changes: 7 additions & 0 deletions examples/import.js
Original file line number Diff line number Diff line change
@@ -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]));
2 changes: 2 additions & 0 deletions examples/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def add(a, b):
return a + b
25 changes: 25 additions & 0 deletions ipy.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions mod.bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module "python:*";
declare module "*.py";

import "./src/bun_compat.js";
export * from "./mod.ts";
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
44 changes: 44 additions & 0 deletions plugin.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = {};
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",
};
});
},
});
107 changes: 107 additions & 0 deletions src/bun_compat.js
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 3 additions & 1 deletion src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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;
Expand Down
18 changes: 16 additions & 2 deletions src/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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](),
});
Expand All @@ -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];
}

Expand Down Expand Up @@ -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. */
Expand Down
Loading

0 comments on commit 3a5246b

Please sign in to comment.