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

Add PyDict #15

Merged
merged 6 commits into from
Sep 4, 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
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me how this value continues to exist after you decref it at the end of this call? SetItem https://docs.python.org/3/c-api/dict.html#c.PyDict_SetItem says "This function does not steal a reference to val.". Can you explain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally to the SetItem call both the key and value are incref'd - this is what it means by not stealing. If it were to steal, it wouldn't incref and it would assume it now owns the reference.

Because of this both the caller (us) and the dictionary have valid references to the object. So as the caller, we decref so the only reference remaining is that inside the dictionary.

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());
}