Skip to content
Gyubong edited this page Sep 10, 2022 · 6 revisions

RustPython has special attributes to support easy python object building.

pyfunction, pymethod

These attributes are very common for every code chunk. They eventually turn into builtin_function_or_method as Fn(&VirtualMachine, FuncArgs) -> PyResult.

The common form looks like this:

    #[pyfunction]
    pub fn ascii(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<String> {
        let repr = vm.to_repr(&obj)?;
        let ascii = to_ascii(repr.as_str());
        Ok(ascii)
    }

The vm paramter is just suffix. We add it as the last parameter unless we don't use vm at all - very rare case. It takes an object obj as PyObjectRef, which is a general python object. It returns PyResult<String>, which will turn into PyResult<PyObjectRef> the same representation of PyResult.

Every return value must be convertible to PyResult. This is defined as IntoPyResult trait. So any return value of them must implement IntoPyResult. It will be PyResult<PyObjectRef>, PyObjectRef and any PyResult<T> when T implements IntoPyObject. Practically we can list them like:

  • Any T when PyResult<T> is possible
  • PyObjectRef
  • PyResult<()> and () as None
  • PyRef<T: PyValue> like PyIntRef, PyStrRef
  • T: PyValue like PyInt, PyStr
  • Numbers like usize or f64 for PyInt and PyFloat
  • String for PyStr
  • And more types implementing IntoPyObject.

Just like the return type, parameters are also described in similar way.

fn math_comb(n: PyIntRef, k: PyIntRef, vm: &VirtualMachine) -> PyResult<BigInt> {
    ...
}

For this function, n and k are defined as PyIntRef. This conversion is supported by TryFromObject trait. When any parameter type is T: TryFromObject instead of PyObjectRef, it will call the conversion function and return TypeError during the conversion. Practically, they are PyRef<T: PyValue> and a few more types like numbers and strings.

Note that all those conversions are done by runtime, which are identical as manual conversions when we call the conversion function manually from PyObjectRef.

#[pyfunction] is used to create a free function. #[pymethod] is used to create a method which can be bound. So here can be usually another prefix parameter self for PyRef<T: PyValue> and &self for T: PyValue.

#[pyimpl(...)]
impl PyStr {
    ...

    #[pymethod(magic)]
    fn contains(&self, needle: PyStrRef) -> bool {
        self.value.contains(needle.as_str())
    }

    ...
}

These parameters are mostly treated just same as other parameter, especially when Self is PyRef<T>. &self for T is sometimes a bit different. The actual object for self is always PyRef<Self>, but the features are limited when it is represented as just &Self. Then there will be a special pattern like zelf: PyRef<Self>.

...

    #[pymethod(magic)]
    fn mul(zelf: PyRef<Self>, value: isize, vm: &VirtualMachine) -> PyRef<Self> {
        ...
    }

...

This pattern is just same as self for PyRef<T>. So nothing special. Just a different notation.

pyclass, pyimpl

pyclass-pyimpl pair is used to create a python type in Rust side. For now, they essentially consists of 2 steps:

  1. A data type with #[pyclass] attribute.
  2. pyimpl gathering various stuff related with python attributes.

DirEntry type in vm/src/stdlib/os.rs is a nice and compact example of small class.

    #[pyclass(name)]
    #[derive(Debug)]
    struct DirEntry {
        entry: fs::DirEntry,
    }

    #[pyimpl]
    impl DirEntry {
        ...
    }

#[pyclass] and data type

The data type is rust-side payload for the type. For simple usage, check for PyInt and PyStr. There are even empty types like PyNone. For complex types, PyDict will be interesting. pyclass macro helps to implement a few traits with given attrs.

  • module: false for builtins and a string for others. This field will be automatically filled when defined in #[pymodule] macro.
  • name: The class name.
  • base: A rust type name of base class for inheritance. Mostly empty. See PyBool for an example.
  • metaclass: A rust type name of metaclass when required. Mostly empty.

#[pyimpl] and python attributes

This part is the most interesting part. Basically #[pyimpl] collects python attributes. A class can contains #[pymethod], #[pyclassmethod], #[pyproperty] and #[pyslot]. These attributes will be covered in next sections.

One of important feature of #[pyimpl] is filling slots of PyType. Typically - but not necessarily - a group of slots are defiend as a trait in RustPython. with(...) will collect python attributes from the traits. Additionally flags set the type flags. See PyStr and Hashable for the slot traits. See also PyFunction and HAS_DICT for flags.

pymethod, pyclassmethod

This article already covered pymethod with pyfunction.

pyproperty

#[pyproperty] adds property-like attribute called getset in CPython. Getters looks like almost argument-less function and actually work like that. To see setters example, search for #[pyproperty(setter)].

pyslot

slots provide fast path for a few frequently-accessed type attributes. #[pyslot] connects the annotated function to each slot. The function name must be same as slot name or tp_ prefixed slot name.

In RustPython, most of them are conventionally implemented through a trait.

  • Hashable: __hash__
  • Callable: __call__
  • Comparable: __eq__, __ne__, __ge__, __ge__, __le__, __lt__
  • Buffer: tp_as_buffer

...

Note: For now, non-zero-sized payload(#[pyclass]) without tp_new slot will make payload error after creating the instance.

pymodule