pyclass: mapping flag
This commit is contained in:
parent
601e3d6517
commit
c16cc35b30
|
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `as_bytes` on `Py<PyBytes>`. [#2235](https://github.com/PyO3/pyo3/pull/2235)
|
- Added `as_bytes` on `Py<PyBytes>`. [#2235](https://github.com/PyO3/pyo3/pull/2235)
|
||||||
|
- Add `#[pyclass(mapping)]` option to leave sequence slots empty in container implementations. [#2265](https://github.com/PyO3/pyo3/pull/2265)
|
||||||
|
|
||||||
### Packaging
|
### Packaging
|
||||||
|
|
||||||
|
|
|
@ -203,6 +203,7 @@ Python::with_gil(|py|{
|
||||||
[params-4]: https://doc.rust-lang.org/stable/std/rc/struct.Rc.html
|
[params-4]: https://doc.rust-lang.org/stable/std/rc/struct.Rc.html
|
||||||
[params-5]: https://doc.rust-lang.org/stable/std/sync/struct.Rc.html
|
[params-5]: https://doc.rust-lang.org/stable/std/sync/struct.Rc.html
|
||||||
[params-6]: https://docs.python.org/3/library/weakref.html
|
[params-6]: https://docs.python.org/3/library/weakref.html
|
||||||
|
[params-mapping]: ./class/protocols.md#mapping--sequence-types
|
||||||
|
|
||||||
These parameters are covered in various sections of this guide.
|
These parameters are covered in various sections of this guide.
|
||||||
|
|
||||||
|
|
|
@ -192,6 +192,16 @@ and `Return` a final value - see its docs for further details and an example.
|
||||||
|
|
||||||
### Mapping & Sequence types
|
### Mapping & Sequence types
|
||||||
|
|
||||||
|
The magic methods in this section can be used to implement Python container types. They are two main categories of container in Python: "mappings" such as `dict`, with arbitrary keys, and "sequences" such as `list` and `tuple`, with integer keys.
|
||||||
|
|
||||||
|
The Python C-API which PyO3 is built upon has separate "slots" for sequences and mappings. When writing a `class` in pure Python, there is no such distinction in the implementation - a `__getitem__` implementation will fill the slots for both the mapping and sequence forms, for example.
|
||||||
|
|
||||||
|
By default PyO3 reproduces the Python behaviour of filling both mapping and sequence slots. This makes sense for the "simple" case which matches Python, and also for sequences, where the mapping slot is used anyway to implement slice indexing.
|
||||||
|
|
||||||
|
For mapping types, it may be desirable to not have the sequence slots filled. Use the `#[pyclass(mapping)]` annotation to instruct PyO3 to only fill the mapping slots, leaving the sequence ones empty.
|
||||||
|
|
||||||
|
This behaviour affects the implementation of `__getitem__`, `__setitem__`, and `__delitem__`.
|
||||||
|
|
||||||
- `__len__(<self>) -> usize`
|
- `__len__(<self>) -> usize`
|
||||||
|
|
||||||
Implements the built-in function `len()` for the sequence.
|
Implements the built-in function `len()` for the sequence.
|
||||||
|
@ -265,7 +275,6 @@ and `Return` a final value - see its docs for further details and an example.
|
||||||
Used by the `*=` operator, after trying the numeric multiplication via
|
Used by the `*=` operator, after trying the numeric multiplication via
|
||||||
the `__imul__` method.
|
the `__imul__` method.
|
||||||
|
|
||||||
|
|
||||||
### Descriptors
|
### Descriptors
|
||||||
|
|
||||||
- `__get__(<self>, object, object) -> object`
|
- `__get__(<self>, object, object) -> object`
|
||||||
|
|
|
@ -337,6 +337,12 @@ extern "C" {
|
||||||
// Flag bits for printing:
|
// Flag bits for printing:
|
||||||
pub const Py_PRINT_RAW: c_int = 1; // No string quotes etc.
|
pub const Py_PRINT_RAW: c_int = 1; // No string quotes etc.
|
||||||
|
|
||||||
|
#[cfg(all(Py_3_10, not(Py_LIMITED_API)))]
|
||||||
|
pub const Py_TPFLAGS_SEQUENCE: c_ulong = 1 << 5;
|
||||||
|
|
||||||
|
#[cfg(all(Py_3_10, not(Py_LIMITED_API)))]
|
||||||
|
pub const Py_TPFLAGS_MAPPING: c_ulong = 1 << 6;
|
||||||
|
|
||||||
#[cfg(Py_3_10)]
|
#[cfg(Py_3_10)]
|
||||||
pub const Py_TPFLAGS_DISALLOW_INSTANTIATION: c_ulong = 1 << 7;
|
pub const Py_TPFLAGS_DISALLOW_INSTANTIATION: c_ulong = 1 << 7;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub mod kw {
|
||||||
syn::custom_keyword!(gc);
|
syn::custom_keyword!(gc);
|
||||||
syn::custom_keyword!(get);
|
syn::custom_keyword!(get);
|
||||||
syn::custom_keyword!(item);
|
syn::custom_keyword!(item);
|
||||||
|
syn::custom_keyword!(mapping);
|
||||||
syn::custom_keyword!(module);
|
syn::custom_keyword!(module);
|
||||||
syn::custom_keyword!(name);
|
syn::custom_keyword!(name);
|
||||||
syn::custom_keyword!(pass_module);
|
syn::custom_keyword!(pass_module);
|
||||||
|
|
|
@ -54,6 +54,7 @@ pub struct PyClassPyO3Options {
|
||||||
pub dict: Option<kw::dict>,
|
pub dict: Option<kw::dict>,
|
||||||
pub extends: Option<ExtendsAttribute>,
|
pub extends: Option<ExtendsAttribute>,
|
||||||
pub freelist: Option<FreelistAttribute>,
|
pub freelist: Option<FreelistAttribute>,
|
||||||
|
pub mapping: Option<kw::mapping>,
|
||||||
pub module: Option<ModuleAttribute>,
|
pub module: Option<ModuleAttribute>,
|
||||||
pub name: Option<NameAttribute>,
|
pub name: Option<NameAttribute>,
|
||||||
pub subclass: Option<kw::subclass>,
|
pub subclass: Option<kw::subclass>,
|
||||||
|
@ -69,6 +70,7 @@ enum PyClassPyO3Option {
|
||||||
Dict(kw::dict),
|
Dict(kw::dict),
|
||||||
Extends(ExtendsAttribute),
|
Extends(ExtendsAttribute),
|
||||||
Freelist(FreelistAttribute),
|
Freelist(FreelistAttribute),
|
||||||
|
Mapping(kw::mapping),
|
||||||
Module(ModuleAttribute),
|
Module(ModuleAttribute),
|
||||||
Name(NameAttribute),
|
Name(NameAttribute),
|
||||||
Subclass(kw::subclass),
|
Subclass(kw::subclass),
|
||||||
|
@ -90,6 +92,8 @@ impl Parse for PyClassPyO3Option {
|
||||||
input.parse().map(PyClassPyO3Option::Extends)
|
input.parse().map(PyClassPyO3Option::Extends)
|
||||||
} else if lookahead.peek(attributes::kw::freelist) {
|
} else if lookahead.peek(attributes::kw::freelist) {
|
||||||
input.parse().map(PyClassPyO3Option::Freelist)
|
input.parse().map(PyClassPyO3Option::Freelist)
|
||||||
|
} else if lookahead.peek(attributes::kw::mapping) {
|
||||||
|
input.parse().map(PyClassPyO3Option::Mapping)
|
||||||
} else if lookahead.peek(attributes::kw::module) {
|
} else if lookahead.peek(attributes::kw::module) {
|
||||||
input.parse().map(PyClassPyO3Option::Module)
|
input.parse().map(PyClassPyO3Option::Module)
|
||||||
} else if lookahead.peek(kw::name) {
|
} else if lookahead.peek(kw::name) {
|
||||||
|
@ -145,6 +149,7 @@ impl PyClassPyO3Options {
|
||||||
PyClassPyO3Option::Dict(dict) => set_option!(dict),
|
PyClassPyO3Option::Dict(dict) => set_option!(dict),
|
||||||
PyClassPyO3Option::Extends(extends) => set_option!(extends),
|
PyClassPyO3Option::Extends(extends) => set_option!(extends),
|
||||||
PyClassPyO3Option::Freelist(freelist) => set_option!(freelist),
|
PyClassPyO3Option::Freelist(freelist) => set_option!(freelist),
|
||||||
|
PyClassPyO3Option::Mapping(mapping) => set_option!(mapping),
|
||||||
PyClassPyO3Option::Module(module) => set_option!(module),
|
PyClassPyO3Option::Module(module) => set_option!(module),
|
||||||
PyClassPyO3Option::Name(name) => set_option!(name),
|
PyClassPyO3Option::Name(name) => set_option!(name),
|
||||||
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
|
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
|
||||||
|
@ -749,6 +754,7 @@ impl<'a> PyClassImplsBuilder<'a> {
|
||||||
.map(|extends_attr| extends_attr.value.clone())
|
.map(|extends_attr| extends_attr.value.clone())
|
||||||
.unwrap_or_else(|| parse_quote! { _pyo3::PyAny });
|
.unwrap_or_else(|| parse_quote! { _pyo3::PyAny });
|
||||||
let is_subclass = self.attr.options.extends.is_some();
|
let is_subclass = self.attr.options.extends.is_some();
|
||||||
|
let is_mapping: bool = self.attr.options.mapping.is_some();
|
||||||
|
|
||||||
let dict_offset = if self.attr.options.dict.is_some() {
|
let dict_offset = if self.attr.options.dict.is_some() {
|
||||||
quote! {
|
quote! {
|
||||||
|
@ -830,6 +836,7 @@ impl<'a> PyClassImplsBuilder<'a> {
|
||||||
const DOC: &'static str = #doc;
|
const DOC: &'static str = #doc;
|
||||||
const IS_BASETYPE: bool = #is_basetype;
|
const IS_BASETYPE: bool = #is_basetype;
|
||||||
const IS_SUBCLASS: bool = #is_subclass;
|
const IS_SUBCLASS: bool = #is_subclass;
|
||||||
|
const IS_MAPPING: bool = #is_mapping;
|
||||||
|
|
||||||
type Layout = _pyo3::PyCell<Self>;
|
type Layout = _pyo3::PyCell<Self>;
|
||||||
type BaseType = #base;
|
type BaseType = #base;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
| `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. |
|
| `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. |
|
||||||
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
|
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
|
||||||
| <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
|
| <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
|
||||||
|
| `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. |
|
||||||
| <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
|
| <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
|
||||||
| <span style="white-space: pre">`name = "python_name"`</span> | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
|
| <span style="white-space: pre">`name = "python_name"`</span> | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
|
||||||
| <span style="white-space: pre">`text_signature = "(arg1, arg2, ...)"`</span> | Sets the text signature for the Python class' `__new__` method. |
|
| <span style="white-space: pre">`text_signature = "(arg1, arg2, ...)"`</span> | Sets the text signature for the Python class' `__new__` method. |
|
||||||
|
|
|
@ -93,6 +93,7 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
/// [params-4]: std::rc::Rc
|
/// [params-4]: std::rc::Rc
|
||||||
/// [params-5]: std::sync::Arc
|
/// [params-5]: std::sync::Arc
|
||||||
/// [params-6]: https://docs.python.org/3/library/weakref.html
|
/// [params-6]: https://docs.python.org/3/library/weakref.html
|
||||||
|
/// [params-mapping]: https://pyo3.rs/latest/class/protocols.html#mapping--sequence-types
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
|
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
use syn::Item;
|
use syn::Item;
|
||||||
|
|
|
@ -155,6 +155,9 @@ pub trait PyClassImpl: Sized {
|
||||||
/// #[pyclass(extends=...)]
|
/// #[pyclass(extends=...)]
|
||||||
const IS_SUBCLASS: bool = false;
|
const IS_SUBCLASS: bool = false;
|
||||||
|
|
||||||
|
/// #[pyclass(mapping)]
|
||||||
|
const IS_MAPPING: bool = false;
|
||||||
|
|
||||||
/// Layout
|
/// Layout
|
||||||
type Layout: PyLayout<Self>;
|
type Layout: PyLayout<Self>;
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ where
|
||||||
T::weaklist_offset(),
|
T::weaklist_offset(),
|
||||||
&T::for_all_items,
|
&T::for_all_items,
|
||||||
T::IS_BASETYPE,
|
T::IS_BASETYPE,
|
||||||
|
T::IS_MAPPING,
|
||||||
)
|
)
|
||||||
} {
|
} {
|
||||||
Ok(type_object) => type_object,
|
Ok(type_object) => type_object,
|
||||||
|
@ -75,6 +76,7 @@ unsafe fn create_type_object_impl(
|
||||||
weaklist_offset: Option<ffi::Py_ssize_t>,
|
weaklist_offset: Option<ffi::Py_ssize_t>,
|
||||||
for_all_items: &dyn Fn(&mut dyn FnMut(&PyClassItems)),
|
for_all_items: &dyn Fn(&mut dyn FnMut(&PyClassItems)),
|
||||||
is_basetype: bool,
|
is_basetype: bool,
|
||||||
|
is_mapping: bool,
|
||||||
) -> PyResult<*mut ffi::PyTypeObject> {
|
) -> PyResult<*mut ffi::PyTypeObject> {
|
||||||
let mut slots = Vec::new();
|
let mut slots = Vec::new();
|
||||||
|
|
||||||
|
@ -147,26 +149,30 @@ unsafe fn create_type_object_impl(
|
||||||
slots.extend_from_slice(items.slots);
|
slots.extend_from_slice(items.slots);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If mapping methods implemented, define sequence methods get implemented too.
|
if !is_mapping {
|
||||||
// CPython does the same for Python `class` statements.
|
// If mapping methods implemented, define sequence methods get implemented too.
|
||||||
|
// CPython does the same for Python `class` statements.
|
||||||
|
|
||||||
// NB we don't implement sq_length to avoid annoying CPython behaviour of automatically adding
|
// NB we don't implement sq_length to avoid annoying CPython behaviour of automatically adding
|
||||||
// the length to negative indices.
|
// the length to negative indices.
|
||||||
|
|
||||||
if has_getitem {
|
// Don't add these methods for "pure" mappings.
|
||||||
push_slot(
|
|
||||||
&mut slots,
|
|
||||||
ffi::Py_sq_item,
|
|
||||||
get_sequence_item_from_mapping as _,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_setitem {
|
if has_getitem {
|
||||||
push_slot(
|
push_slot(
|
||||||
&mut slots,
|
&mut slots,
|
||||||
ffi::Py_sq_ass_item,
|
ffi::Py_sq_item,
|
||||||
assign_sequence_item_from_mapping as _,
|
get_sequence_item_from_mapping as _,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_setitem {
|
||||||
|
push_slot(
|
||||||
|
&mut slots,
|
||||||
|
ffi::Py_sq_ass_item,
|
||||||
|
assign_sequence_item_from_mapping as _,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_new {
|
if !has_new {
|
||||||
|
|
|
@ -7,10 +7,12 @@ use pyo3::prelude::*;
|
||||||
use pyo3::py_run;
|
use pyo3::py_run;
|
||||||
use pyo3::types::IntoPyDict;
|
use pyo3::types::IntoPyDict;
|
||||||
use pyo3::types::PyList;
|
use pyo3::types::PyList;
|
||||||
|
use pyo3::types::PyMapping;
|
||||||
|
use pyo3::types::PySequence;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass(mapping)]
|
||||||
struct Mapping {
|
struct Mapping {
|
||||||
index: HashMap<String, usize>,
|
index: HashMap<String, usize>,
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,13 @@ impl Mapping {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get(&self, py: Python<'_>, key: &str, default: Option<PyObject>) -> Option<PyObject> {
|
||||||
|
self.index
|
||||||
|
.get(key)
|
||||||
|
.map(|value| value.into_py(py))
|
||||||
|
.or(default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a dict with `m = Mapping(['1', '2', '3'])`.
|
/// Return a dict with `m = Mapping(['1', '2', '3'])`.
|
||||||
|
@ -103,3 +112,15 @@ fn test_delitem() {
|
||||||
py_expect_exception!(py, *d, "del m[-1]", PyTypeError);
|
py_expect_exception!(py, *d, "del m[-1]", PyTypeError);
|
||||||
py_expect_exception!(py, *d, "del m['4']", PyKeyError);
|
py_expect_exception!(py, *d, "del m['4']", PyKeyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapping_is_not_sequence() {
|
||||||
|
Python::with_gil(|py| {
|
||||||
|
let mut index = HashMap::new();
|
||||||
|
index.insert("Foo".into(), 1);
|
||||||
|
index.insert("Bar".into(), 2);
|
||||||
|
let m = Py::new(py, Mapping { index }).unwrap();
|
||||||
|
assert!(m.as_ref(py).downcast::<PyMapping>().is_ok());
|
||||||
|
assert!(m.as_ref(py).downcast::<PySequence>().is_err());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
|
error: expected one of: `crate`, `dict`, `extends`, `freelist`, `mapping`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
|
||||||
--> tests/ui/invalid_pyclass_args.rs:3:11
|
--> tests/ui/invalid_pyclass_args.rs:3:11
|
||||||
|
|
|
|
||||||
3 | #[pyclass(extend=pyo3::types::PyDict)]
|
3 | #[pyclass(extend=pyo3::types::PyDict)]
|
||||||
|
@ -34,7 +34,7 @@ error: expected string literal
|
||||||
18 | #[pyclass(module = my_module)]
|
18 | #[pyclass(module = my_module)]
|
||||||
| ^^^^^^^^^
|
| ^^^^^^^^^
|
||||||
|
|
||||||
error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
|
error: expected one of: `crate`, `dict`, `extends`, `freelist`, `mapping`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
|
||||||
--> tests/ui/invalid_pyclass_args.rs:21:11
|
--> tests/ui/invalid_pyclass_args.rs:21:11
|
||||||
|
|
|
|
||||||
21 | #[pyclass(weakrev)]
|
21 | #[pyclass(weakrev)]
|
||||||
|
|
Loading…
Reference in New Issue