Skip to content

Commit

Permalink
[stdlib] feat: Add trait for conversion of Python to Mojo values
Browse files Browse the repository at this point in the history
* Add ConvertibleFromPython trait, that types can implement to define
logic for constructing an owned Mojo value from a borrowed PythonObject.

* Add Python.py_long_as_ssize_t() helper
* Add PythonObject._get_type_name() helper, for use in error messages

MODULAR_ORIG_COMMIT_REV_ID: 6ed12f789b41912a2fd02788190a5082a7c24824
  • Loading branch information
ConnorGray authored and modularbot committed Nov 2, 2024
1 parent bb4a60c commit fa074d0
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 9 deletions.
28 changes: 28 additions & 0 deletions stdlib/src/builtin/_pybind.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ from python._cpython import (
)
from python._bindings import (
Pythonable,
ConvertibleFromPython,
PyMojoObject,
py_c_function_wrapper,
check_argument_type,
Expand Down Expand Up @@ -122,3 +123,30 @@ fn check_and_get_arg[
index: Int,
) raises -> UnsafePointer[T]:
return check_argument_type[T](func_name, type_name_id, py_args[index])


fn try_convert_arg[
T: ConvertibleFromPython
](
func_name: StringLiteral,
type_name_id: StringLiteral,
py_args: TypedPythonObject["Tuple"],
argidx: Int,
) raises -> T as result:
try:
result = T.try_from_python(py_args[argidx])
except convert_err:
raise Error(
String.format(
(
"TypeError: {}() expected argument at position {} to be"
" instance of (or convertible to) Mojo '{}'; got '{}'."
" (Note: attempted conversion failed due to: {})"
),
func_name,
argidx,
type_name_id,
py_args[argidx]._get_type_name(),
convert_err,
)
)
15 changes: 15 additions & 0 deletions stdlib/src/builtin/int.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ from collections.string import (
_calc_initial_buffer_size_int32,
_calc_initial_buffer_size_int64,
)
from python import Python, PythonObject
from python._cpython import Py_ssize_t
from memory import UnsafePointer

from utils import Writable, Writer
from utils._visualizers import lldb_formatter_wrapping_type
from utils._select import _select_register_value as select
from sys import triple_is_nvidia_cuda, bitwidthof
from sys.ffi import OpaquePointer

# ===----------------------------------------------------------------------=== #
# Indexer
Expand Down Expand Up @@ -1123,6 +1127,17 @@ struct Int(
"""
hasher._update_with_simd(Int64(self))

@doc_private
@staticmethod
fn try_from_python(obj: PythonObject) raises -> Self as result:
"""Construct an `Int` from a Python integer value.
Raises:
An error if conversion failed.
"""

result = Python.py_long_as_ssize_t(obj)

# ===-------------------------------------------------------------------===#
# Methods
# ===-------------------------------------------------------------------===#
Expand Down
29 changes: 22 additions & 7 deletions stdlib/src/python/_bindings.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from memory import UnsafePointer, Box

from sys.ffi import c_int
from sys.ffi import c_int, OpaquePointer
from sys.info import sizeof

from os import abort
Expand All @@ -34,6 +34,26 @@ from python._cpython import (
destructor,
)


trait ConvertibleFromPython(CollectionElement):
"""Denotes a type that can attempt construction from a borrowed Python
object.
"""

@staticmethod
fn try_from_python(obj: PythonObject) raises -> Self:
"""Attempt to construct an instance of this object from a borrowed
Python value.
Args:
obj: The Python object to convert from.
Raises:
If conversion was not successful.
"""
...


# ===-----------------------------------------------------------------------===#
# Mojo Object
# ===-----------------------------------------------------------------------===#
Expand Down Expand Up @@ -339,17 +359,12 @@ fn check_argument_type[
](type_name_id)

if not opt:
var cpython = _get_global_python_itf().cpython()

var actual_type = cpython.Py_TYPE(obj.unsafe_as_py_object_ptr())
var actual_type_name = PythonObject(cpython.PyType_GetName(actual_type))

raise Error(
String.format(
"TypeError: {}() expected Mojo '{}' type argument, got '{}'",
func_name,
type_name_id,
str(actual_type_name),
obj._get_type_name(),
)
)

Expand Down
39 changes: 38 additions & 1 deletion stdlib/src/python/python.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ from memory import UnsafePointer
from utils import StringRef

from .python_object import PythonObject, TypedPythonObject
from ._cpython import CPython, Py_eval_input, Py_file_input, PyMethodDef
from ._cpython import (
CPython,
Py_eval_input,
Py_file_input,
PyMethodDef,
Py_ssize_t,
)


fn _init_global(ignored: OpaquePointer) -> OpaquePointer:
Expand Down Expand Up @@ -436,3 +442,34 @@ struct Python:
`PythonObject` representing `None`.
"""
return PythonObject(None)

# ===-------------------------------------------------------------------===#
# Checked Conversions
# ===-------------------------------------------------------------------===#

@staticmethod
fn py_long_as_ssize_t(obj: PythonObject) raises -> Py_ssize_t:
"""Get the value of a Python `long` object.
Args:
obj: The Python `long` object.
Raises:
If `obj` is not a Python `long` object, or if the `long` object
value overflows `Py_ssize_t`.
Returns:
The value of the `long` object as a `Py_ssize_t`.
"""
var cpython = Python().impl.cpython()

var long: Py_ssize_t = cpython.PyLong_AsSsize_t(
obj.unsafe_as_py_object_ptr()
)

# Disambiguate if this is an error return setinel, or a legitimate
# value.
if long == -1:
Python.throw_python_exception_if_error_state(cpython)

return long
8 changes: 8 additions & 0 deletions stdlib/src/python/python_object.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,14 @@ struct PythonObject(
fn _get_ptr_as_int(self) -> Int:
return self.py_object._get_ptr_as_int()

fn _get_type_name(self) -> String:
var cpython = Python().impl.cpython()

var actual_type = cpython.Py_TYPE(self.unsafe_as_py_object_ptr())
var actual_type_name = PythonObject(cpython.PyType_GetName(actual_type))

return str(actual_type_name)


# ===-----------------------------------------------------------------------===#
# Helper functions
Expand Down
18 changes: 17 additions & 1 deletion stdlib/test/builtin/test_int.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

from sys.info import bitwidthof

from testing import assert_equal, assert_true, assert_false
from testing import assert_equal, assert_true, assert_false, assert_raises

from python import PythonObject
from memory import UnsafePointer


def test_properties():
Expand Down Expand Up @@ -231,6 +234,18 @@ def test_float_conversion():
assert_equal(float(Int(45)), Float64(45))


def test_conversion_from_python():
# Test conversion from Python '5'
assert_equal(Int.try_from_python(PythonObject(5)), 5)

# Test error trying conversion from Python '"str"'
with assert_raises(contains="an integer is required"):
_ = Int.try_from_python(PythonObject("str"))

# Test conversion from Python '-1'
assert_equal(Int.try_from_python(PythonObject(-1)), -1)


def main():
test_properties()
test_add()
Expand All @@ -253,3 +268,4 @@ def main():
test_comparison()
test_int_uint()
test_float_conversion()
test_conversion_from_python()

0 comments on commit fa074d0

Please sign in to comment.