diff --git a/example/code.pyi b/example/code.pyi new file mode 100644 index 00000000..67f7f12b --- /dev/null +++ b/example/code.pyi @@ -0,0 +1,6 @@ +from __future__ import annotations + +def file_name(): ... +def first_line_number(): ... +def function_name(): ... +def line_number(): ... diff --git a/example/code.zig b/example/code.zig new file mode 100644 index 00000000..c9224d3a --- /dev/null +++ b/example/code.zig @@ -0,0 +1,34 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const std = @import("std"); +const py = @import("pydust"); + +pub fn line_number() u32 { + return py.PyFrame.get().?.lineNumber(); +} + +pub fn function_name() !py.PyString { + return py.PyFrame.get().?.code().name(); +} + +pub fn file_name() !py.PyString { + return py.PyFrame.get().?.code().fileName(); +} + +pub fn first_line_number() !u32 { + return py.PyFrame.get().?.code().firstLineNumber(); +} + +comptime { + py.rootmodule(@This()); +} diff --git a/pydust/src/types.zig b/pydust/src/types.zig index c37760b2..8dd5b535 100644 --- a/pydust/src/types.zig +++ b/pydust/src/types.zig @@ -13,9 +13,11 @@ pub usingnamespace @import("types/bool.zig"); pub usingnamespace @import("types/buffer.zig"); pub usingnamespace @import("types/bytes.zig"); +pub usingnamespace @import("types/code.zig"); pub usingnamespace @import("types/dict.zig"); pub usingnamespace @import("types/error.zig"); pub usingnamespace @import("types/float.zig"); +pub usingnamespace @import("types/frame.zig"); pub usingnamespace @import("types/gil.zig"); pub usingnamespace @import("types/iter.zig"); pub usingnamespace @import("types/list.zig"); diff --git a/pydust/src/types/code.zig b/pydust/src/types/code.zig new file mode 100644 index 00000000..4ea83796 --- /dev/null +++ b/pydust/src/types/code.zig @@ -0,0 +1,45 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const std = @import("std"); +const py = @import("../pydust.zig"); +const PyObjectMixin = @import("./obj.zig").PyObjectMixin; + +const ffi = py.ffi; + +/// Wrapper for Python PyCode. +/// See: https://docs.python.org/3/c-api/code.html +pub const PyCode = extern struct { + obj: py.PyObject, + + pub inline fn firstLineNumber(self: *const PyCode) !u32 { + const lineNo = try self.obj.getAs(py.PyLong, "co_firstlineno"); + defer lineNo.decref(); + return lineNo.as(u32); + } + + pub inline fn fileName(self: *const PyCode) !py.PyString { + return self.obj.getAs(py.PyString, "co_filename"); + } + + pub inline fn name(self: *const PyCode) !py.PyString { + return self.obj.getAs(py.PyString, "co_name"); + } +}; + +test "PyCode" { + py.initialize(); + defer py.finalize(); + + const pf = py.PyFrame.get(); + try std.testing.expectEqual(@as(?py.PyFrame, null), pf); +} diff --git a/pydust/src/types/frame.zig b/pydust/src/types/frame.zig new file mode 100644 index 00000000..452d6950 --- /dev/null +++ b/pydust/src/types/frame.zig @@ -0,0 +1,53 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const std = @import("std"); +const py = @import("../pydust.zig"); +const PyObjectMixin = @import("./obj.zig").PyObjectMixin; + +const ffi = py.ffi; + +/// Wrapper for Python PyFrame. +/// See: https://docs.python.org/3/c-api/frame.html +pub const PyFrame = extern struct { + obj: py.PyObject, + + pub fn get() ?PyFrame { + const frame = ffi.PyEval_GetFrame(); + return if (frame) |f| .{ .obj = .{ .py = objPtr(f) } } else null; + } + + pub fn code(self: PyFrame) py.PyCode { + const codeObj = ffi.PyFrame_GetCode(framePtr(self.obj.py)); + return .{ .obj = .{ .py = @alignCast(@ptrCast(codeObj)) } }; + } + + pub inline fn lineNumber(self: PyFrame) u32 { + return @intCast(ffi.PyFrame_GetLineNumber(framePtr(self.obj.py))); + } + + inline fn framePtr(obj: *ffi.PyObject) *ffi.PyFrameObject { + return @alignCast(@ptrCast(obj)); + } + + inline fn objPtr(obj: *ffi.PyFrameObject) *ffi.PyObject { + return @alignCast(@ptrCast(obj)); + } +}; + +test "PyFrame" { + py.initialize(); + defer py.finalize(); + + const pf = PyFrame.get(); + try std.testing.expectEqual(@as(?PyFrame, null), pf); +} diff --git a/pyproject.toml b/pyproject.toml index 1cb7b62d..34632a6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,3 +103,7 @@ root = "example/iterators.zig" [[tool.pydust.ext_module]] name = "example.operators" root = "example/operators.zig" + +[[tool.pydust.ext_module]] +name = "example.code" +root = "example/code.zig" diff --git a/test/test_code.py b/test/test_code.py new file mode 100644 index 00000000..dbf2382f --- /dev/null +++ b/test/test_code.py @@ -0,0 +1,30 @@ +""" +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from pathlib import Path + +from example import code + + +def test_line_no(): + assert code.line_number() == 21 + assert code.first_line_number() == 20 + + +def test_function_name(): + assert code.function_name() == "test_function_name" + + +def test_file_name(): + assert Path(code.file_name()).name == "test_code.py"