-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Python Attributes
RustPython has special attributes to support easy python object building.
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
TwhenPyResult<T>is possible PyObjectRef-
PyResult<()>and()asNone -
PyRef<T: PyValue>likePyIntRef,PyStrRef -
T: PyValuelikePyInt,PyStr - Numbers like
usizeorf64forPyIntandPyFloat -
StringforPyStr - 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.
#[pyclass(...)]
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 when applied to a struct and its' associated impl is used to create a python type in Rust side. For now, they essentially consists of 2 steps:
- A data type with
#[pyclass]attribute. -
pyclasson animplblock gathering various stuff related to 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,
}
#[pyclass]
impl DirEntry {
...
}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:
falsefor 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
PyBoolfor an example. - metaclass: A rust type name of metaclass when required. Mostly empty.
This part is the most interesting part. Basically #[pyclass] collects python attributes. A class can contains #[pymethod], #[pyclassmethod], #[pygetset] and #[pyslot]. These attributes will be covered in next sections.
One of important feature of #[pyclass] 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.
This article already covered pymethod with pyfunction.
Sometimes it is required to expose attributes of a class #[pygetset] allows this to happen
#[pygetset]
fn co_posonlyargcount(&self) -> usize {
self.code.posonlyarg_count as usize
}If it is desirable to make the variable editable, consider returning and AtomicCell, RwLock, or Mutex.
If this is not feasible, or if it is desired to run some checks when writing to the attribute, using #[pygetset] coupled with #[pygetset(setter)] allows for separate get and set functions.
#[pygetset]
fn name(&self) -> PyStrRef {
self.inner.name()
}
#[pygetset(setter)]
fn set_name(&self, name: PyStrRef) {
self.inner.set_name(name)
}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.