Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bun support #47

Merged
merged 15 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
DjDeveloperr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading