Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New #[pyclass] system that is aware of its concrete layout #683

Merged
merged 27 commits into from
Jan 11, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4b5fa7e
Introduce PyClass trait and PyClassShell
kngwyu Dec 7, 2019
bdb66af
Make PyClassShell have dict&weakref
kngwyu Dec 8, 2019
4d7dfaf
Allow slf: &PyClassShell<Self>
kngwyu Dec 8, 2019
a663907
Introduce PyInternalCaster
kngwyu Dec 14, 2019
b86de93
Introduce PyClassInitializer
kngwyu Dec 15, 2019
8175d6f
Merge branch 'master' into pyclass-new-layout
kngwyu Dec 19, 2019
6b84401
Make it enable to safely inherit native types
kngwyu Dec 21, 2019
efa16a6
Fix documents accompanied by PyClassShell
kngwyu Dec 22, 2019
e2dc843
Fix a corner case for PyClassInitializer
kngwyu Dec 22, 2019
acb1120
Fix examples with the new #[new] API
kngwyu Dec 22, 2019
d5cff05
Fix documents and a clippy warning
kngwyu Dec 22, 2019
ea51756
Resolve some clippy complains
kngwyu Dec 23, 2019
2e3ece8
Try to enhance class section in the guide
kngwyu Dec 23, 2019
5859039
Fix accidently changed file permission
kngwyu Dec 24, 2019
766a520
Documentation enhancement
kngwyu Dec 28, 2019
8f8785d
Merge branch 'master' into pyclass-new-layout
kngwyu Dec 29, 2019
18e565f
New PyClassInitializer
kngwyu Jan 5, 2020
60edeb8
Simplify IntoInitializer
davidhewitt Jan 6, 2020
b04d0af
Merge pull request #1 from davidhewitt/pyclass-new-layout
kngwyu Jan 7, 2020
b602b4b
Enhance documentation and tests around #[new]
kngwyu Jan 7, 2020
f26e07c
Replace IntoInitializer<T> with Into<PyClassInitializer<T>>
kngwyu Jan 7, 2020
67a98d6
Remove unnecessary Box
kngwyu Jan 7, 2020
ab0a731
Fix use order in prelude
kngwyu Jan 7, 2020
451de18
Merge branch 'master' into pyclass-new-layout
kngwyu Jan 8, 2020
c57177a
Refine tests and documents around pyclass.rs
kngwyu Jan 8, 2020
302b3bb
Merge branch 'master' into pyclass-new-layout
kngwyu Jan 11, 2020
439efbb
Update CHANGELOG
kngwyu Jan 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/rustapi_module/src/buf_and_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ struct BytesExtractor {}
#[pymethods]
impl BytesExtractor {
#[new]
pub fn __new__(obj: &PyRawObject) {
obj.init({ BytesExtractor {} });
pub fn __new__() -> Self {
BytesExtractor {}
}

pub fn from_bytes(&mut self, bytes: &PyBytes) -> PyResult<usize> {
Expand Down
4 changes: 2 additions & 2 deletions examples/rustapi_module/src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ pub struct TzClass {}
#[pymethods]
impl TzClass {
#[new]
fn new(obj: &PyRawObject) {
obj.init(TzClass {})
fn new() -> Self {
TzClass {}
}

fn utcoffset<'p>(&self, py: Python<'p>, _dt: &PyDateTime) -> PyResult<&'p PyDelta> {
Expand Down
4 changes: 2 additions & 2 deletions examples/rustapi_module/src/dict_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ pub struct DictSize {
#[pymethods]
impl DictSize {
#[new]
fn new(obj: &PyRawObject, expected: u32) {
obj.init(DictSize { expected })
fn new(expected: u32) -> Self {
DictSize { expected }
}

fn iter_dict(&mut self, _py: Python<'_>, dict: &PyDict) -> PyResult<u32> {
Expand Down
6 changes: 3 additions & 3 deletions examples/rustapi_module/src/othermod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ pub struct ModClass {
#[pymethods]
impl ModClass {
#[new]
fn new(obj: &PyRawObject) {
obj.init(ModClass {
fn new() -> Self {
ModClass {
_somefield: String::from("contents"),
})
}
}

fn noop(&self, x: usize) -> usize {
Expand Down
4 changes: 2 additions & 2 deletions examples/rustapi_module/src/subclassing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub struct Subclassable {}
#[pymethods]
impl Subclassable {
#[new]
fn new(obj: &PyRawObject) {
obj.init(Subclassable {});
fn new() -> Self {
Subclassable {}
}
}

Expand Down
6 changes: 3 additions & 3 deletions examples/word-count/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ struct WordCounter {
#[pymethods]
impl WordCounter {
#[new]
fn new(obj: &PyRawObject, path: String) {
obj.init(WordCounter {
fn new(path: String) -> Self {
WordCounter {
path: PathBuf::from(path),
});
}
}

/// Searches for the word, parallelized by rayon
Expand Down
183 changes: 120 additions & 63 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,110 @@ struct MyClass {
}
```

The above example generates implementations for `PyTypeInfo` and `PyTypeObject` for `MyClass`.
The above example generates implementations for `PyTypeInfo`, `PyTypeObject`
and `PyClass` for `MyClass`.

## Get Python objects from `pyclass`
Specifically, the following implementation is generated.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm cautious about including the generated implementation in the doc; it seems like something which could easily fall out of sync with actual code if we made any internal changes in the future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what do you think is a good way to tell users what it does?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally like this, but it would be nice if it were possible to add a test that checks that the two don't fall out of sync. I don't know of a great way to do this, but a quick-and-dirty hack might be, be something like:

<!-- Generated Type: pyclass -->

``​`rust
use pyo3::prelude::*;
use pyo3::{PyClassShell, PyTypeInfo}
...
`​``

<!-- /Generated Type: pyclass -->

Then pull the text between the comments and pull the code out from the rust block inside the comments and compare it (possibly in a more abstract way) to the code generated from an equivalent pyclass. Maybe that's too much work for what it buys you, though...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note, I would end this sentence with : instead of . if you keep it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do also like this. It is helpful for users to see it. We will just need to be careful to keep it up to date in future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... 🤔
I think it's a rough skecth and just 'compile pass' is suffcient.


You can use `pyclass`es like normal rust structs.
```rust
use pyo3::prelude::*;
use pyo3::{PyClassShell, PyTypeInfo};

However, if instantiated normally, you can't treat `pyclass`es as Python objects.
struct MyClass {
num: i32,
debug: bool,
}

impl pyo3::pyclass::PyClassAlloc for MyClass {}

To get a Python object which includes `pyclass`, we have to use some special methods.
impl PyTypeInfo for MyClass {
type Type = MyClass;
type BaseType = pyo3::types::PyAny;
type ConcreteLayout = PyClassShell<Self>;

### `PyRef`
const NAME: &'static str = "MyClass";
const MODULE: Option<&'static str> = None;
const DESCRIPTION: &'static str = "This is a demo class";
kngwyu marked this conversation as resolved.
Show resolved Hide resolved
const FLAGS: usize = 0;

`PyRef` is a special reference, which ensures that the referred struct is a part of
a Python object, and you are also holding the GIL.
#[inline]
unsafe fn type_object() -> &'static mut pyo3::ffi::PyTypeObject {
static mut TYPE_OBJECT: pyo3::ffi::PyTypeObject = pyo3::ffi::PyTypeObject_INIT;
&mut TYPE_OBJECT
}
kngwyu marked this conversation as resolved.
Show resolved Hide resolved
}

impl pyo3::pyclass::PyClass for MyClass {
type Dict = pyo3::pyclass_slots::PyClassDummySlot;
type WeakRef = pyo3::pyclass_slots::PyClassDummySlot;
}

impl pyo3::IntoPy<PyObject> for MyClass {
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
pyo3::IntoPy::into_py(pyo3::Py::new(py, self).unwrap(), py)
}
}

pub struct MyClassGeneratedPyo3Inventory {
methods: &'static [pyo3::class::PyMethodDefType],
}

impl pyo3::class::methods::PyMethodsInventory for MyClassGeneratedPyo3Inventory {
fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self {
Self { methods }
}

fn get_methods(&self) -> &'static [pyo3::class::PyMethodDefType] {
self.methods
}
}

impl pyo3::class::methods::PyMethodsInventoryDispatch for MyClass {
type InventoryType = MyClassGeneratedPyo3Inventory;
}

pyo3::inventory::collect!(MyClassGeneratedPyo3Inventory);

# let gil = Python::acquire_gil();
# let py = gil.python();
# let cls = py.get_type::<MyClass>();
# pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
```

You can get an instance of `PyRef` by `PyRef::new`, which does 3 things:
1. Allocates a Python object in the Python heap
2. Copies the Rust struct into the Python object
3. Returns a reference to it

You can use `PyRef` just like `&T`, because it implements `Deref<Target=T>`.
## Get Python objects from `pyclass`
You sometimes need to convert your `pyclass` into a Python object in Rust code (e.g., for testing it).

For getting *GIL-bounded*(i.e., with `'py` lifetime) references of `pyclass`,
kngwyu marked this conversation as resolved.
Show resolved Hide resolved
you can use `PyClassShell<T>`.
Or you can use `Py<T>` directly, for *not-GIL-bounded* references.

### `PyClassShell`
`PyClassShell` represents the actual layout of `pyclass` on the Python heap.

If you want to instantiate `pyclass` in Python and get the the reference,
kngwyu marked this conversation as resolved.
Show resolved Hide resolved
you can use `PyClassShell::new_ref` or `PyClassShell::new_mut`.

```rust
# use pyo3::prelude::*;
# use pyo3::types::PyDict;
# use pyo3::PyClassShell;
#[pyclass]
struct MyClass {
num: i32,
debug: bool,
}
let gil = Python::acquire_gil();
let py = gil.python();
let obj = PyRef::new(py, MyClass { num: 3, debug: true }).unwrap();
let obj = PyClassShell::new_ref(py, MyClass { num: 3, debug: true }).unwrap();
// You can use deref
assert_eq!(obj.num, 3);
let dict = PyDict::new(py);
// You can treat a `PyRef` as a Python object
// You can treat a `&PyClassShell` as a normal Python object
dict.set_item("obj", obj).unwrap();
```

### `PyRefMut`

`PyRefMut` is a mutable version of `PyRef`.
```rust
# use pyo3::prelude::*;
#[pyclass]
struct MyClass {
num: i32,
debug: bool,
}
let gil = Python::acquire_gil();
let py = gil.python();
let mut obj = PyRefMut::new(py, MyClass { num: 3, debug: true }).unwrap();
// return &mut PyClassShell<MyClass>
let obj = PyClassShell::new_mut(py, MyClass { num: 3, debug: true }).unwrap();
obj.num = 5;
```

Expand Down Expand Up @@ -115,22 +168,16 @@ attribute. Only Python's `__new__` method can be specified, `__init__` is not av

```rust
# use pyo3::prelude::*;
# use pyo3::PyRawObject;
#[pyclass]
struct MyClass {
num: i32,
}

#[pymethods]
impl MyClass {

#[new]
fn new(obj: &PyRawObject, num: i32) {
obj.init({
MyClass {
num,
}
});
fn new(num: i32) -> Self {
MyClass { num }
}
}
```
Expand All @@ -139,24 +186,29 @@ Rules for the `new` method:

* If no method marked with `#[new]` is declared, object instances can only be created
from Rust, but not from Python.
* The first parameter is the raw object and the custom `new` method must initialize the object
with an instance of the struct using the `init` method. The type of the object may be the type object of
a derived class declared in Python.
* The first parameter must have type `&PyRawObject`.
* All parameters are from Python.
* It can return one of these types:
- `T`
- `PyResult<T>`
- `PyClassInitializer<T>`
- `PyResult<PyClassInitializer<T>>`
kngwyu marked this conversation as resolved.
Show resolved Hide resolved
* If you pyclass declared with `#[pyclass(extends=BaseType)]` and `BaseType`
is also `#[pyclass]`, you have to return `PyClassInitializer<T>` or
`PyResult<PyClassInitializer<T>>` with the baseclass initialized. See the
below Inheritance section for detail.
* For details on the parameter list, see the `Method arguments` section below.
* The return value must be `T` or `PyResult<T>` where `T` is ignored, so it can
be just `()` as in the example above.


## Inheritance

By default, `PyObject` is used as the base class. To override this default,
use the `extends` parameter for `pyclass` with the full path to the base class.
The `new` method of subclasses must call their parent's `new` method.
The `new` method of subclasses must call `initializer.get_super().init`,
where `initializer` is `PyClassInitializer<Self>`.

```rust,ignore
```rust
# use pyo3::prelude::*;
# use pyo3::PyRawObject;
use pyo3::PyClassShell;

#[pyclass]
struct BaseClass {
val1: usize,
Expand All @@ -165,12 +217,12 @@ struct BaseClass {
#[pymethods]
impl BaseClass {
#[new]
fn new(obj: &PyRawObject) {
obj.init(BaseClass { val1: 10 });
fn new() -> Self {
BaseClass { val1: 10 }
}

pub fn method(&self) -> PyResult<()> {
Ok(())
pub fn method(&self) -> PyResult<usize> {
Ok(5)
}
}

Expand All @@ -182,15 +234,22 @@ struct SubClass {
#[pymethods]
impl SubClass {
#[new]
fn new(obj: &PyRawObject) {
obj.init(SubClass { val2: 10 });
BaseClass::new(obj);
fn new() -> PyClassInitializer<Self> {
let mut init = PyClassInitializer::from_value(SubClass { val2: 10 });
init.get_super().init(BaseClass::new());
init
}

fn method2(&self) -> PyResult<()> {
self.get_base().method()
fn method2(self_: &PyClassShell<Self>) -> PyResult<usize> {
self_.get_super().method().map(|x| x * 2)
}
}


# let gil = Python::acquire_gil();
# let py = gil.python();
# let sub = pyo3::PyClassShell::new_ref(py, SubClass::new()).unwrap();
# pyo3::py_run!(py, sub, "assert sub.method2() == 10")
```

The `ObjectProtocol` trait provides a `get_base()` method, which returns a reference
Expand Down Expand Up @@ -589,18 +648,16 @@ struct GCTracked {} // Fails because it does not implement PyGCProtocol
Iterators can be defined using the
[`PyIterProtocol`](https://docs.rs/pyo3/latest/pyo3/class/iter/trait.PyIterProtocol.html) trait.
It includes two methods `__iter__` and `__next__`:
* `fn __iter__(slf: PyRefMut<Self>) -> PyResult<impl IntoPy<PyObject>>`
* `fn __next__(slf: PyRefMut<Self>) -> PyResult<Option<impl IntoPy<PyObject>>>`
* `fn __iter__(slf: &mut PyClassShell<Self>) -> PyResult<impl IntoPy<PyObject>>`
* `fn __next__(slf: &mut PyClassShell<Self>) -> PyResult<Option<impl IntoPy<PyObject>>>`

Returning `Ok(None)` from `__next__` indicates that that there are no further items.

Example:

```rust
extern crate pyo3;

use pyo3::prelude::*;
use pyo3::PyIterProtocol;
use pyo3::{PyIterProtocol, PyClassShell};

#[pyclass]
struct MyIterator {
Expand All @@ -609,10 +666,10 @@ struct MyIterator {

#[pyproto]
impl PyIterProtocol for MyIterator {
fn __iter__(slf: PyRefMut<Self>) -> PyResult<Py<MyIterator>> {
fn __iter__(slf: &mut PyClassShell<Self>) -> PyResult<Py<MyIterator>> {
Ok(slf.into())
}
fn __next__(mut slf: PyRefMut<Self>) -> PyResult<Option<PyObject>> {
fn __next__(slf: &mut PyClassShell<Self>) -> PyResult<Option<PyObject>> {
Ok(slf.iter.next())
}
}
Expand Down
4 changes: 2 additions & 2 deletions guide/src/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ impl MyClass {
// the signature for the constructor is attached
// to the struct definition instead.
#[new]
fn new(obj: &PyRawObject, c: i32, d: &str) {
obj.init(Self {});
fn new(c: i32, d: &str) -> Self {
Self {}
}
// the self argument should be written $self
#[text_signature = "($self, e, f)"]
Expand Down
5 changes: 3 additions & 2 deletions guide/src/python_from_rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Since `py_run!` can cause panic, we recommend you to use this macro only for tes
your Python extensions quickly.

```rust
use pyo3::{PyObjectProtocol, prelude::*, py_run};
use pyo3::prelude::*;
use pyo3::{PyClassShell, PyObjectProtocol, py_run};
# fn main() {
#[pyclass]
struct UserData {
Expand All @@ -60,7 +61,7 @@ let userdata = UserData {
id: 34,
name: "Yu".to_string(),
};
let userdata = PyRef::new(py, userdata).unwrap();
let userdata = PyClassShell::new_ref(py, userdata).unwrap();
let userdata_as_tuple = (34, "Yu");
py_run!(py, userdata userdata_as_tuple, r#"
assert repr(userdata) == "User Yu(id: 34)"
Expand Down
Loading