pontoneer¶
Pontoneer is a Mojo library that enhances the Python extension capabilities
provided by the standard library. Pontoneer adds support for:
- mapping protocol —
obj[key],len(obj),obj[key] = val - number protocol — arithmetic operators,
abs(),bool(), etc. - sequence protocol — indexed access,
inoperator, concatenation, repetition - rich comparison —
==,!=,<,<=,>,>=
This is an expansion of the work proposed in modular/modular#5562.
Without these extensions, a Mojo struct exported to Python can expose
__getitem__ as a regular method but Python's obj[key] syntax won't work —
because the CPython runtime requires the method to be wired into the type's
tp_slots. pontoneer provides the wiring.
Requirements¶
- pixi package manager
- Mojo 1.0 stable (
pixiwill install it automatically)
Installation¶
pontoneer is published on Mojo Shelf — that page always lists the current version and the matching install commands.
From Mojo Shelf¶
pixi shelf add pontoneer # pixi shelf mode (requires the shelf extension)
shelf add pontoneer # git submodule mode
With plain pixi¶
No shelf extension needed — add the repository as a git source dependency (see the tin page for the revision to pin):
pixi add --git https://github.com/winding-lines/pontoneer.git \
--rev <revision> pontoneer
Your project needs Mojo 1.0 from the stable Modular channel — in pixi.toml:
channels = ["https://conda.modular.com/max/", "conda-forge"]
[dependencies]
mojo = "==1.0.0"
Then include the pontoneer path when building your extension module:
mojo build --emit shared-lib -I external/pontoneer my_module.mojo -o my_module.so
Prebuilt artifacts (pontoneer.mojoc and conda packages for osx-arm64 /
linux-64) are also attached to each
GitHub release.
From source¶
git clone [email protected]:winding-lines/pontoneer.git
cd pontoneer
pixi install
pixi run build # produces pontoneer.mojoc
pixi run test-example # builds and runs the columnar DataFrame example
Quick start¶
from std.python.bindings import PythonModuleBuilder
from pontoneer import (
NotImplementedError,
RichCompareOps,
TypeProtocolBuilder,
MappingProtocolBuilder,
NumberProtocolBuilder,
SequenceProtocolBuilder,
)
struct MyStruct(Defaultable, Movable):
var data: List[Float64]
def __init__(out self):
self.data = []
def py__len__(self) raises -> Int:
return len(self.data)
def py__getitem__(self, key: PythonObject) raises -> PythonObject:
return PythonObject(self.data[Int(py=key)])
def py__setitem__(
mut self, key: PythonObject, value: Variant[PythonObject, Int]
) raises -> None:
if value.isa[PythonObject]():
self.data[Int(py=key)] = Float64(py=value[PythonObject])
else:
_ = self.data.pop(Int(py=key))
def rich_compare(
self, other: PythonObject, op: Int
) raises -> Bool:
var other_ptr = other.downcast_value_ptr[Self]()
if op == RichCompareOps.Py_EQ:
return len(self.data) == len(other_ptr[].data)
raise NotImplementedError()
def py__neg__(self) raises -> PythonObject:
var result = List[Float64](capacity=len(self.data))
for v in self.data:
result.append(-v)
var out = MyStruct()
out.data = result^
return PythonObject(alloc=out^)
def py__add__(self, other: PythonObject) raises -> PythonObject:
try:
var other_ptr = other.downcast_value_ptr[Self]()
var result = MyStruct()
for v in self.data:
result.data.append(v)
for v in other_ptr[].data:
result.data.append(v)
return PythonObject(alloc=result^)
except:
raise NotImplementedError()
@export
def PyInit_mymodule() abi("C") -> PythonObject:
try:
var b = PythonModuleBuilder("mymodule")
var tb = b.add_type[MyStruct]("MyStruct")
.def_init_defaultable[MyStruct]()
# Rich comparison
TypeProtocolBuilder[MyStruct](tb).def_richcompare[MyStruct.rich_compare]()
# Mapping protocol: obj[key], len(obj), obj[key] = val
MappingProtocolBuilder[MyStruct](tb)
.def_len[MyStruct.py__len__]()
.def_getitem[MyStruct.py__getitem__]()
.def_setitem[MyStruct.py__setitem__]()
# Number protocol: arithmetic and unary operators
NumberProtocolBuilder[MyStruct](tb)
.def_neg[MyStruct.py__neg__]()
.def_add[MyStruct.py__add__]()
return b.finalize()
except e:
abort(String("failed to create module: ", e))
Handler signatures¶
Handlers can be written as regular methods on self (value-receiver) or as
@staticmethod functions taking Pointer[T, MutAnyOrigin].
The value-receiver style is shown below.
| Slot | Value-receiver signature |
|---|---|
mp_length |
def py__len__(self) raises -> Int |
mp_getitem |
def py__getitem__(self, key: PythonObject) raises -> PythonObject |
mp_setitem |
def py__setitem__(mut self, key: PythonObject, value: Variant[PythonObject, Int]) raises -> None |
tp_richcompare |
def rich_compare(self, other: PythonObject, op: Int) raises -> Bool |
For mp_setitem, value is Variant[PythonObject, Int](Int(0)) when Python
calls del obj[key], and Variant[PythonObject, Int](val) for obj[key] = val.
For tp_richcompare, compare op against RichCompareOps.Py_LT … Py_GE.
Raise NotImplementedError() to return Python's NotImplemented singleton
(triggering the reflected operation on the other operand).