Skip to content

pontoneer

Pontoneer is a Mojo library that enhances the Python extension capabilities provided by the standard library. Pontoneer adds support for:

  • mapping protocolobj[key], len(obj), obj[key] = val
  • number protocol — arithmetic operators, abs(), bool(), etc.
  • sequence protocol — indexed access, in operator, 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 (pixi will 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_LTPy_GE. Raise NotImplementedError() to return Python's NotImplemented singleton (triggering the reflected operation on the other operand).