-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
T
whenPyResult<T>
is possible PyObjectRef
-
PyResult<()>
and()
asNone
-
PyRef<T: PyValue>
likePyIntRef
,PyStrRef
-
T: PyValue
likePyInt
,PyStr
- Numbers like
usize
orf64
forPyInt
andPyFloat
-
String
forPyStr
- 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. -
pyclass
on animpl
block 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:
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.
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.