Skip to content

Commit

Permalink
Add PyDict (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
gatesn authored Sep 4, 2023
1 parent a678da3 commit eb5c158
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 15 deletions.
10 changes: 0 additions & 10 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ pub fn build(b: *std.Build) void {
const run_main_tests = b.addRunArtifact(main_tests);
test_step.dependOn(&run_main_tests.step);

const example_lib = b.addSharedLibrary(.{
.name = "examples",
.root_source_file = .{ .path = "example/modules.zig" },
.main_pkg_path = .{ .path = "example/" },
.target = target,
.optimize = optimize,
});
example_lib.addAnonymousModule("pydust", .{ .source_file = .{ .path = "pydust/src/pydust.zig" } });
b.installArtifact(example_lib);

// Option for emitting test binary based on the given root source.
// This is used for debugging as in .vscode/tasks.json
const test_debug_root = b.option([]const u8, "test-debug-root", "The root path of a file emitted as a binary for use with the debugger");
Expand Down
24 changes: 24 additions & 0 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ from our `example/` directory and are tested during CI.

If you do struggle or find any issues with the examples, please do [let us know!](https://github.com/fulcrum-so/ziggy-pydust/issues)

## Conventions

Pydust maintains a consistent set of conventions around structs, function naming, and memory
management to assist with development.

### PyObject

A Pydust `py.PyObject` is an extern struct containing _only_ a pointer to an `ffi.PyObject`. In other words,
wherever a `*ffi.PyObject` appears in CPython docs, it can be replaced with a `py.PyObject` (notice not a
pointer).

``` zig title="PyObject.zig"
const PyObject = extern struct {
py: *ffi.PyObject,
};
```

### Python Type Wrappers

Pydust ships with type wrappers for CPython built-ins, such as PyFloat, PyTuple, etc. These type wrappers
are extern structs containing a single `#!c py.PyObject` field. This again enables them to be used in place
of `#!c *ffi.PyObject`.

## Type Conversions

At comptime, Pydust wraps your function definitions such that native Zig types can be returned
Expand All @@ -29,6 +52,7 @@ For native Zig types however, the following conversions apply:
| `i32`, `i64` | `int` |
| `u32`, `u64` | `int` |
| `f32`, `f64` | `float` |
| `struct` | `dict` |

## Memory Management

Expand Down
6 changes: 6 additions & 0 deletions example/result_types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ pub fn zigf64() f64 {
return 6.4;
}

const StructResult = struct { foo: u64, bar: bool };

pub fn zigstruct() StructResult {
return .{ .foo = 1234, .bar = true };
}

comptime {
py.module(@This());
}
19 changes: 14 additions & 5 deletions pydust/src/trampoline.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ pub fn setErr(err: anyerror) void {

pub fn toPyObject(comptime objType: type) type {
return struct {
pub inline fn unwrap(obj: objType) ?*ffi.PyObject {
pub inline fn unwrap(obj: objType) !*ffi.PyObject {
// Handle the error case explicitly, then we can unwrap the error case entirely.

const typeInfo = @typeInfo(objType);
if (typeInfo == .ErrorUnion) {
_ = obj catch |err| {
return setErrObj(err);
return err;
};
}

Expand All @@ -51,9 +52,9 @@ pub fn toPyObject(comptime objType: type) type {
switch (@typeInfo(resultType)) {
.Bool => return if (result) ffi.Py_True else ffi.Py_False,
.ErrorUnion => @compileError("ErrorUnion already handled"),
.Float => return (py.PyFloat.from(resultType, result) catch |e| return setErrObj(e)).obj.py,
.Int => return (py.PyLong.from(resultType, result) catch |e| return setErrObj(e)).obj.py,
.Struct => {
.Float => return (try py.PyFloat.from(resultType, result)).obj.py,
.Int => return (try py.PyLong.from(resultType, result)).obj.py,
.Struct => |s| {
// Support all extensions of py.PyObject, e.g. py.PyString, py.PyFloat
if (@hasField(resultType, "obj") and @hasField(@TypeOf(result.obj), "py")) {
return result.obj.py;
Expand All @@ -62,6 +63,14 @@ pub fn toPyObject(comptime objType: type) type {
if (resultType == py.PyObject) {
return result.py;
}
// Otherwise, return a Python dictionary
const dict = try py.PyDict.new();
inline for (s.fields) |field| {
// Recursively unwrap the field value
const fieldValue = try toPyObject(field.type).unwrap(@field(result, field.name));
try dict.setItemStr(field.name ++ "\x00", .{ .py = fieldValue });
}
return dict.obj.py;
},
.Void => return ffi.Py_None,
else => {},
Expand Down
1 change: 1 addition & 0 deletions pydust/src/types.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub usingnamespace @import("types/dict.zig");
pub usingnamespace @import("types/error.zig");
pub usingnamespace @import("types/float.zig");
pub usingnamespace @import("types/long.zig");
Expand Down
205 changes: 205 additions & 0 deletions pydust/src/types/dict.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
const std = @import("std");
const py = @import("../pydust.zig");
const ffi = py.ffi;
const PyError = @import("../errors.zig").PyError;
const tramp = @import("../trampoline.zig");

/// See: https://docs.python.org/3/c-api/dict.html
pub const PyDict = extern struct {
obj: py.PyObject,

pub fn of(obj: py.PyObject) PyDict {
return .{ .obj = obj };
}

pub fn incref(self: PyDict) void {
self.obj.incref();
}

pub fn decref(self: PyDict) void {
self.obj.decref();
}

/// Create a PyDict from the given struct.
pub fn from(comptime S: type, value: S) !PyDict {
return switch (@typeInfo(S)) {
.Struct => of(.{ .py = try tramp.toPyObject(S).unwrap(value) }),
else => @compileError("PyDict can only be created from struct types"),
};
}

/// Return a new empty dictionary.
pub fn new() !PyDict {
const dict = ffi.PyDict_New() orelse return PyError.Propagate;
return of(.{ .py = dict });
}

/// Return a new dictionary that contains the same key-value pairs as p.
pub fn copy(self: PyDict) !PyDict {
const dict = ffi.PyDict_Copy(self.obj.py) orelse return PyError.Propagate;
return of(.{ .py = dict });
}

/// Empty an existing dictionary of all key-value pairs.
pub fn clear(self: PyDict) void {
ffi.PyDict_Clear(self.obj.py);
}

/// Return the number of items in the dictionary. This is equivalent to len(p) on a dictionary.
pub fn size(self: PyDict) usize {
return @intCast(ffi.PyDict_Size(self.obj.py));
}

/// Determine if dictionary p contains key.
/// This is equivalent to the Python expression key in p.
pub fn contains(self: PyDict, key: py.PyObject) !bool {
const result = ffi.PyDict_Contains(self.obj.py, key.py);
if (result < 0) return PyError.Propagate;
return result == 1;
}

pub fn containsStr(self: PyDict, key: [:0]const u8) !bool {
const keyObj = try py.PyString.fromSlice(key);
defer keyObj.decref();
return contains(self, keyObj.obj);
}

/// Insert val into the dictionary p with a key of key.
pub fn setItem(self: PyDict, key: py.PyObject, value: py.PyObject) !void {
const result = ffi.PyDict_SetItem(self.obj.py, key.py, value.py);
if (result < 0) return PyError.Propagate;
}

/// Insert val into the dictionary p with a key of key.
/// The dictionary takes ownership of the value.
pub fn setOwnedItem(self: PyDict, key: py.PyObject, value: py.PyObject) !void {
defer value.decref();
try self.setItem(key, value);
}

/// Insert val into the dictionary p with a key of key.
pub fn setItemStr(self: PyDict, key: [:0]const u8, value: py.PyObject) !void {
const result = ffi.PyDict_SetItemString(self.obj.py, key.ptr, value.py);
if (result < 0) return PyError.Propagate;
}

/// Insert val into the dictionary p with a key of key.
pub fn setOwnedItemStr(self: PyDict, key: [:0]const u8, value: py.PyObject) !void {
defer value.decref();
try self.setItemStr(key, value);
}

/// Remove the entry in dictionary p with key key.
pub fn delItem(self: PyDict, key: py.PyObject) !void {
if (ffi.PyDict_DelItem(self.obj.py, key.py) < 0) {
return PyError.Propagate;
}
}

/// Remove the entry in dictionary p with key key.
pub fn delItemStr(self: PyDict, key: [:0]const u8) !void {
if (ffi.PyDict_DelItemString(self.obj.py, key.ptr) < 0) {
return PyError.Propagate;
}
}

/// Return the object from dictionary p which has a key key.
/// Return value is a borrowed reference.
pub fn getItem(self: PyDict, key: py.PyObject) !?py.PyObject {
const result = ffi.PyDict_GetItemWithError(self.obj.py, key.py) orelse return PyError.Propagate;
return .{ .py = result };
}

pub fn getItemStr(self: PyDict, key: [:0]const u8) !?py.PyObject {
const keyObj = try py.PyString.fromSlice(key);
defer keyObj.decref();
return self.getItem(keyObj.obj);
}

pub fn itemsIterator(self: PyDict) ItemIterator {
return .{
.pydict = self,
.position = 0,
.nextKey = null,
.nextValue = null,
};
}

pub const Item = struct {
key: py.PyObject,
value: py.PyObject,
};

pub const ItemIterator = struct {
pydict: PyDict,
position: isize,
nextKey: ?*ffi.PyObject,
nextValue: ?*ffi.PyObject,

pub fn next(self: *@This()) ?Item {
if (ffi.PyDict_Next(
self.pydict.obj.py,
&self.position,
@ptrCast(&self.nextKey),
@ptrCast(&self.nextValue),
) == 0) {
// No more items
return null;
}

return .{ .key = .{ .py = self.nextKey.? }, .value = .{ .py = self.nextValue.? } };
}
};
};

const testing = std.testing;

test "PyDict set and get" {
py.initialize();
defer py.finalize();

const pd = try PyDict.new();
defer pd.decref();

const bar = try py.PyString.fromSlice("bar");
defer bar.decref();
try pd.setItemStr("foo", bar.obj);
try testing.expect(try pd.containsStr("foo"));
try testing.expectEqual(@as(usize, 1), pd.size());

try testing.expectEqual(bar.obj, (try pd.getItemStr("foo")).?);

try pd.delItemStr("foo");
try testing.expect(!try pd.containsStr("foo"));
try testing.expectEqual(@as(usize, 0), pd.size());

try pd.setItemStr("foo", bar.obj);
try testing.expectEqual(@as(usize, 1), pd.size());
pd.clear();
try testing.expectEqual(@as(usize, 0), pd.size());
}

test "PyDict iterator" {
py.initialize();
defer py.finalize();

const pd = try PyDict.new();
defer pd.decref();

const foo = try py.PyString.fromSlice("foo");
defer foo.decref();

try pd.setItemStr("bar", foo.obj);
try pd.setItemStr("baz", foo.obj);

var iter = pd.itemsIterator();
const first = iter.next().?;
try testing.expectEqualStrings("bar", try py.PyString.of(first.key).asSlice());
try testing.expectEqual(foo.obj, first.value);

const second = iter.next().?;
try testing.expectEqualStrings("baz", try py.PyString.of(second.key).asSlice());
try testing.expectEqual(foo.obj, second.value);

try testing.expectEqual(@as(?PyDict.Item, null), iter.next());
}

0 comments on commit eb5c158

Please sign in to comment.