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

[break] Refactor call API #179

Merged
merged 3 commits into from
Oct 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
48 changes: 48 additions & 0 deletions pydust/src/builtins.zig
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,54 @@ pub fn callable(object: anytype) bool {
return ffi.PyCallable_Check(obj.py) == 1;
}

/// Call a callable object with no arguments.
///
/// If the result is a new reference, then as always the caller is responsible for calling decref on it.
/// That means for new references the caller should ask for a return type that they are unable to decref,
/// for example []const u8.
pub fn call0(comptime T: type, object: anytype) !T {
const result = ffi.PyObject_CallNoArgs(py.object(object).py) orelse return PyError.PyRaised;
return try py.as(T, result);
}

/// Call a callable object with the given arguments.
///
/// If the result is a new reference, then as always the caller is responsible for calling decref on it.
/// That means for new references the caller should ask for a return type that they are unable to decref,
/// for example []const u8.
pub fn call(comptime ReturnType: type, object: anytype, args: anytype, kwargs: anytype) !ReturnType {
const pyobj = py.object(object);

var argsPy: py.PyTuple = undefined;
if (@typeInfo(@TypeOf(args)) == .Optional and args == null) {
argsPy = try py.PyTuple.new(0);
} else {
argsPy = try py.PyTuple.checked(try py.create(args));
}
defer argsPy.decref();

// TODO(ngates): avoid creating empty dict for kwargs
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 valid to pass NULL for kwargs in PyObject_Call

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I know, we just don't atm

var kwargsPy: py.PyDict = undefined;
if (@typeInfo(@TypeOf(kwargs)) == .Optional and kwargs == null) {
kwargsPy = try py.PyDict.new();
} else {
// Annoyingly our trampoline turns an empty kwargs struct into a PyTuple.
// This will be fixed by #94
const kwobj = try py.create(kwargs);
if (try py.len(kwobj) == 0) {
kwobj.decref();
kwargsPy = try py.PyDict.new();
} else {
kwargsPy = try py.PyDict.checked(kwobj);
}
}
defer kwargsPy.decref();

// Note, the caller is responsible for returning a result type that they are able to decref.
const result = ffi.PyObject_Call(pyobj.py, argsPy.obj.py, kwargsPy.obj.py) orelse return PyError.PyRaised;
return try py.as(ReturnType, result);
}

/// Convert an object into a dictionary. Equivalent of Python dict(o).
pub fn dict(object: anytype) !py.PyDict {
const Dict: py.PyObject = .{ .py = @alignCast(@ptrCast(&ffi.PyDict_Type)) };
Expand Down
6 changes: 3 additions & 3 deletions pydust/src/modules.zig
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ fn Slots(comptime definition: type) type {
const pySubmodDef: *ffi.PyModuleDef = @ptrCast((try submodDef.init()).py);

// Create a dumb ModuleSpec with a name attribute using types.SimpleNamespace
const SimpleNamespace = try py.importFrom("types", "SimpleNamespace");

const types = try py.import("types");
defer types.decref();
const pyname = try py.PyString.create(name);
defer pyname.decref();
const spec = try SimpleNamespace.call(.{}, .{ .name = pyname });
const spec = try types.call(py.PyObject, "SimpleNamespace", .{}, .{ .name = pyname });
defer spec.decref();

const submod: py.PyObject = .{ .py = ffi.PyModule_FromDefAndSpec(pySubmodDef, spec.py) orelse return PyError.PyRaised };
Expand Down
17 changes: 3 additions & 14 deletions pydust/src/types/error.zig
Original file line number Diff line number Diff line change
Expand Up @@ -166,25 +166,14 @@ const PyExc = struct {
);
defer py.allocator.free(code);

// Compilation should succeed, but execution will fail.
const filename = try py.allocator.dupeZ(u8, line_info.file_name);
defer py.allocator.free(filename);
const compiled = ffi.Py_CompileString(code.ptr, filename.ptr, ffi.Py_file_input) orelse continue;
defer py.decref(compiled);

// Import the compiled code as a module and invoke the failing function
const module_name = try py.allocator.dupeZ(u8, symbol_info.compile_unit_name);
defer py.allocator.free(module_name);
const fake_module: py.PyObject = .{
.py = ffi.PyImport_ExecCodeModule(module_name.ptr, compiled) orelse continue,
};
const fake_module = try py.PyModule.fromCode(code, line_info.file_name, symbol_info.compile_unit_name);
defer fake_module.decref();

const func_name = try py.allocator.dupeZ(u8, symbol_info.symbol_name);
defer py.allocator.free(func_name);
const fake_function = try fake_module.get(func_name);
_ = fake_function.call(.{}, .{}) catch null;
defer fake_function.decref();

_ = fake_module.obj.call(void, func_name, .{}, .{}) catch null;

// Grab our forced exception info.
// We can ignore qtype and qvalue, we just want to get the traceback object.
Expand Down
50 changes: 10 additions & 40 deletions pydust/src/types/obj.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,45 +33,20 @@ pub const PyObject = extern struct {
return name.asSlice();
}

/// Call this object without any arguments.
pub fn call0(self: PyObject) !PyObject {
return .{ .py = ffi.PyObject_CallNoArgs(self.py) orelse return PyError.PyRaised };
}

/// Call this object with the given args and kwargs.
pub fn call(self: PyObject, args: anytype, kwargs: anytype) !PyObject {
var argsPy: py.PyTuple = undefined;
if (@typeInfo(@TypeOf(args)) == .Optional and args == null) {
argsPy = try py.PyTuple.new(0);
} else {
argsPy = try py.PyTuple.checked(try py.create(args));
}
defer argsPy.decref();

// FIXME(ngates): avoid creating empty dict for kwargs
var kwargsPy: py.PyDict = undefined;
if (@typeInfo(@TypeOf(kwargs)) == .Optional and kwargs == null) {
kwargsPy = try py.PyDict.new();
} else {
const kwobj = try py.create(kwargs);
if (try py.len(kwobj) == 0) {
kwobj.decref();
kwargsPy = try py.PyDict.new();
} else {
kwargsPy = try py.PyDict.checked(kwobj);
}
}
defer kwargsPy.decref();

// We _must_ return a PyObject to the user to let them handle the lifetime of the object.
const result = ffi.PyObject_Call(self.py, argsPy.obj.py, kwargsPy.obj.py) orelse return PyError.PyRaised;
return PyObject{ .py = result };
pub fn call(self: PyObject, comptime T: type, method: [:0]const u8, args: anytype, kwargs: anytype) !T {
const methodObj = try self.get(method);
return py.call(T, methodObj, args, kwargs);
robert3005 marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn get(self: PyObject, attr: [:0]const u8) !PyObject {
pub fn get(self: PyObject, attr: [:0]const u8) !py.PyObject {
return .{ .py = ffi.PyObject_GetAttrString(self.py, attr) orelse return PyError.PyRaised };
}

pub fn getAs(self: PyObject, comptime T: type, attr: [:0]const u8) !T {
return try py.as(T, try self.get(attr));
}

// See: https://docs.python.org/3/c-api/buffer.html#buffer-request-types
pub fn getBuffer(self: py.PyObject, flags: c_int) !py.PyBuffer {
if (ffi.PyObject_CheckBuffer(self.py) != 1) {
Expand Down Expand Up @@ -151,11 +126,6 @@ test "call" {
defer py.finalize();

const pow = try py.importFrom("math", "pow");
const result = try pow.call(.{ 2, 3 }, .{});

if (py.PyFloat.checkedCast(result)) |f| {
try std.testing.expectEqual(f.as(f32), 8.0);
}

try std.testing.expectEqual(@as(f32, 8.0), try py.as(f32, result));
const result = try py.call(f32, pow, .{ 2, 3 }, .{});
try std.testing.expectEqual(@as(f32, 8.0), result);
}
6 changes: 3 additions & 3 deletions pydust/src/types/slice.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ pub const PySlice = extern struct {
}

pub fn getStart(self: PySlice, comptime T: type) !T {
return try py.as(T, try self.obj.get("start"));
return try self.obj.getAs(T, "start");
}

pub fn getStop(self: PySlice, comptime T: type) !T {
return try py.as(T, try self.obj.get("stop"));
return try try self.obj.getAs(T, "stop");
}

pub fn getStep(self: PySlice, comptime T: type) !T {
return try py.as(T, try self.obj.get("step"));
return try self.obj.getAs(T, "step");
}
};

Expand Down
8 changes: 5 additions & 3 deletions pydust/src/types/type.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ test "PyType" {
py.initialize();
defer py.finalize();

const StringIO = try PyType.checked(try py.importFrom("io", "StringIO"));
defer StringIO.decref();
const io = try py.import("io");
defer io.decref();

const StringIO = try io.getAs(py.PyType, "StringIO");
try std.testing.expectEqualSlices(u8, "StringIO", try (try StringIO.name()).asSlice());

const sio = try StringIO.obj.call0();
const sio = try py.call0(py.PyObject, StringIO);
defer sio.decref();
const sioType = try py.type_(sio);
try std.testing.expectEqualSlices(u8, "StringIO", try (try sioType.name()).asSlice());
Expand Down