Skip to content

Commit 6db7dd7

Browse files
authored
Merge pull request #2065 from davidhewitt/protos-seq-mapping-fallbacks-2
pymethods: seq methods from mapping methods
2 parents 4305a9a + 53c1700 commit 6db7dd7

File tree

8 files changed

+377
-104
lines changed

8 files changed

+377
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
- `PyErr::new_type` now takes an optional docstring and now returns `PyResult<Py<PyType>>` rather than a `ffi::PyTypeObject` pointer.
4545
- The `create_exception!` macro can now take an optional docstring. This docstring, if supplied, is visible to users (with `.__doc__` and `help()`) and
4646
accompanies your error type in your crate's documentation.
47+
- `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` now implement both a Python mapping and sequence by default. [#2065](https://github.com/PyO3/pyo3/pull/2065)
4748
- Improve performance and error messages for `#[derive(FromPyObject)]` for enums. [#2068](https://github.com/PyO3/pyo3/pull/2068)
4849
- Reduce generated LLVM code size (to improve compile times) for:
4950
- internal `handle_panic` helper [#2074](https://github.com/PyO3/pyo3/pull/2074)

guide/src/class/protocols.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ In PyO3 0.15, if a function name in `#[pymethods]` is a recognised magic method,
1818
The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). Some of the slots do not have a magic method in Python, which leads to a few additional magic methods defined only in PyO3:
1919
- Magic methods for garbage collection
2020
- Magic methods for the buffer protocol
21-
- Magic methods for the sequence protocol
2221

2322
When PyO3 handles a magic method, a couple of changes apply compared to other `#[pymethods]`:
2423
- The `#[pyo3(text_signature = "...")]` attribute is not allowed
@@ -86,11 +85,7 @@ given signatures should be interpreted as follows:
8685
- `__aiter__(<self>) -> object`
8786
- `__anext__(<self>) -> Option<object> or IterANextOutput`
8887

89-
#### Sequence types
90-
91-
TODO; see [#1884](https://github.com/PyO3/pyo3/issues/1884)
92-
93-
#### Mapping types
88+
#### Mapping & Sequence types
9489

9590
- `__len__(<self>) -> usize`
9691
- `__contains__(<self>, object) -> bool`

guide/src/migration.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,40 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).
55

66
## from 0.15.* to 0.16
77

8-
### Drop support for older technogies
8+
### Drop support for older technologies
99

1010
PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables ore use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project.
1111

12+
### Container magic methods now match Python behavior
13+
14+
In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations.
15+
16+
This means that classes implementing these `#[pymethods]` will now also be treated as sequences, same as a Python `class` would be. Small differences in behavior may result:
17+
- PyO3 will allow instances of these classes to be cast to `PySequence` as well as `PyMapping`.
18+
- Python will provide a default implementation of `__iter__` (if the class did not have one) which repeatedly calls `__getitem__` with integers (starting at 0) until an `IndexError` is raised.
19+
20+
To explain this in detail, consider the following Python class:
21+
22+
```python
23+
class ExampleContainer:
24+
25+
def __len__(self):
26+
return 5
27+
28+
def __getitem__(self, idx: int) -> int:
29+
if idx < 0 or idx > 5:
30+
raise IndexError()
31+
return idx
32+
```
33+
34+
This class implements a Python [sequence](https://docs.python.org/3/glossary.html#term-sequence).
35+
36+
The `__len__` and `__getitem__` methods are also used to implement a Python [mapping](https://docs.python.org/3/glossary.html#term-mapping). In the Python C-API, these methods are not shared: the sequence `__len__` and `__getitem__` are defined by the `sq_len` and `sq_item` slots, and the mapping equivalents are `mp_len` and `mp_subscript`. There are similar distinctions for `__setitem__` and `__delitem__`.
37+
38+
Because there is no such distinction from Python, implementing these methods will fill the mapping and sequence slots simultaneously. A Python class with `__len__` implemented, for example, will have both the `sq_len` and `mp_len` slots filled.
39+
40+
The PyO3 behavior in 0.16 has been changed to be closer to this Python behavior by default.
41+
1242
## from 0.14.* to 0.15
1343

1444
### Changes in sequence indexing

pyo3-macros-backend/src/pyimpl.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ fn add_shared_proto_slots(
293293
);
294294
try_add_shared_slot!("__pow__", "__rpow__", generate_pyclass_pow_slot);
295295

296+
// if this assertion trips, a slot fragment has been implemented which has not been added in the
297+
// list above
296298
assert!(implemented_proto_fragments.is_empty());
297299
}
298300

pyo3-macros-backend/src/pymethod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,8 +704,21 @@ impl Ty {
704704
let #ident = #extract;
705705
}
706706
}
707+
Ty::PySsizeT => {
708+
let ty = arg.ty;
709+
let extract = handle_error(
710+
extract_error_mode,
711+
py,
712+
quote! {
713+
::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| _pyo3::exceptions::PyValueError::new_err(e.to_string()))
714+
},
715+
);
716+
quote! {
717+
let #ident = #extract;
718+
}
719+
}
707720
// Just pass other types through unmodified
708-
Ty::PyBuffer | Ty::Int | Ty::PyHashT | Ty::PySsizeT | Ty::Void => quote! {},
721+
Ty::PyBuffer | Ty::Int | Ty::PyHashT | Ty::Void => quote! {},
709722
}
710723
}
711724
}

src/class/impl_.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ use crate::{
99
type_object::{PyLayout, PyTypeObject},
1010
PyClass, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python,
1111
};
12-
use std::{marker::PhantomData, os::raw::c_void, ptr::NonNull, thread};
12+
use std::{
13+
marker::PhantomData,
14+
os::raw::{c_int, c_void},
15+
ptr::NonNull,
16+
thread,
17+
};
1318

1419
/// This type is used as a "dummy" type on which dtolnay specializations are
1520
/// applied to apply implementations from `#[pymethods]` & `#[pyproto]`
@@ -780,3 +785,34 @@ pub(crate) unsafe extern "C" fn fallback_new(
780785
pub(crate) unsafe extern "C" fn tp_dealloc<T: PyClass>(obj: *mut ffi::PyObject) {
781786
crate::callback_body!(py, T::Layout::tp_dealloc(obj, py))
782787
}
788+
789+
pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping(
790+
obj: *mut ffi::PyObject,
791+
index: ffi::Py_ssize_t,
792+
) -> *mut ffi::PyObject {
793+
let index = ffi::PyLong_FromSsize_t(index);
794+
if index.is_null() {
795+
return std::ptr::null_mut();
796+
}
797+
let result = ffi::PyObject_GetItem(obj, index);
798+
ffi::Py_DECREF(index);
799+
result
800+
}
801+
802+
pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping(
803+
obj: *mut ffi::PyObject,
804+
index: ffi::Py_ssize_t,
805+
value: *mut ffi::PyObject,
806+
) -> c_int {
807+
let index = ffi::PyLong_FromSsize_t(index);
808+
if index.is_null() {
809+
return -1;
810+
}
811+
let result = if value.is_null() {
812+
ffi::PyObject_DelItem(obj, index)
813+
} else {
814+
ffi::PyObject_SetItem(obj, index, value)
815+
};
816+
ffi::Py_DECREF(index);
817+
result
818+
}

src/pyclass.rs

Lines changed: 106 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! `PyClass` and related traits.
22
use crate::{
3-
class::impl_::{fallback_new, tp_dealloc, PyClassImpl},
3+
class::impl_::{
4+
assign_sequence_item_from_mapping, fallback_new, get_sequence_item_from_mapping,
5+
tp_dealloc, PyClassImpl,
6+
},
47
ffi,
58
impl_::pyclass::{PyClassDict, PyClassWeakRef},
69
PyCell, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python,
@@ -114,28 +117,35 @@ unsafe fn create_type_object_impl(
114117
}
115118
}
116119

120+
let PyClassInfo {
121+
method_defs,
122+
property_defs,
123+
} = method_defs_to_pyclass_info(for_each_method_def, dict_offset.is_none());
124+
117125
// normal methods
118-
let methods = py_class_method_defs(for_each_method_def);
119-
if !methods.is_empty() {
120-
push_slot(&mut slots, ffi::Py_tp_methods, into_raw(methods));
126+
if !method_defs.is_empty() {
127+
push_slot(&mut slots, ffi::Py_tp_methods, into_raw(method_defs));
121128
}
122129

123130
// properties
124-
let props = py_class_properties(dict_offset.is_none(), for_each_method_def);
125-
if !props.is_empty() {
126-
push_slot(&mut slots, ffi::Py_tp_getset, into_raw(props));
131+
if !property_defs.is_empty() {
132+
push_slot(&mut slots, ffi::Py_tp_getset, into_raw(property_defs));
127133
}
128134

129135
// protocol methods
136+
let mut has_getitem = false;
137+
let mut has_setitem = false;
130138
let mut has_gc_methods = false;
139+
131140
// Before Python 3.9, need to patch in buffer methods manually (they don't work in slots)
132141
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
133142
let mut buffer_procs: ffi::PyBufferProcs = Default::default();
134143

135144
for_each_proto_slot(&mut |proto_slots| {
136145
for slot in proto_slots {
146+
has_getitem |= slot.slot == ffi::Py_mp_subscript;
147+
has_setitem |= slot.slot == ffi::Py_mp_ass_subscript;
137148
has_gc_methods |= slot.slot == ffi::Py_tp_clear || slot.slot == ffi::Py_tp_traverse;
138-
139149
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
140150
if slot.slot == ffi::Py_bf_getbuffer {
141151
// Safety: slot.pfunc is a valid function pointer
@@ -151,7 +161,31 @@ unsafe fn create_type_object_impl(
151161
slots.extend_from_slice(proto_slots);
152162
});
153163

164+
// If mapping methods implemented, define sequence methods get implemented too.
165+
// CPython does the same for Python `class` statements.
166+
167+
// NB we don't implement sq_length to avoid annoying CPython behaviour of automatically adding
168+
// the length to negative indices.
169+
170+
if has_getitem {
171+
push_slot(
172+
&mut slots,
173+
ffi::Py_sq_item,
174+
get_sequence_item_from_mapping as _,
175+
);
176+
}
177+
178+
if has_setitem {
179+
push_slot(
180+
&mut slots,
181+
ffi::Py_sq_ass_item,
182+
assign_sequence_item_from_mapping as _,
183+
);
184+
}
185+
186+
// Add empty sentinel at the end
154187
push_slot(&mut slots, 0, ptr::null_mut());
188+
155189
let mut spec = ffi::PyType_Spec {
156190
name: py_class_qualified_name(module_name, name)?,
157191
basicsize: basicsize as c_int,
@@ -282,26 +316,76 @@ fn py_class_flags(has_gc_methods: bool, is_gc: bool, is_basetype: bool) -> c_uin
282316
flags.try_into().unwrap()
283317
}
284318

285-
fn py_class_method_defs(
319+
struct PyClassInfo {
320+
method_defs: Vec<ffi::PyMethodDef>,
321+
property_defs: Vec<ffi::PyGetSetDef>,
322+
}
323+
324+
fn method_defs_to_pyclass_info(
286325
for_each_method_def: &dyn Fn(&mut dyn FnMut(&[PyMethodDefType])),
287-
) -> Vec<ffi::PyMethodDef> {
288-
let mut defs = Vec::new();
289-
290-
for_each_method_def(&mut |method_defs| {
291-
defs.extend(method_defs.iter().filter_map(|def| match def {
292-
PyMethodDefType::Method(def)
293-
| PyMethodDefType::Class(def)
294-
| PyMethodDefType::Static(def) => Some(def.as_method_def().unwrap()),
295-
_ => None,
296-
}));
326+
has_dict: bool,
327+
) -> PyClassInfo {
328+
let mut method_defs = Vec::new();
329+
let mut property_defs_map = std::collections::HashMap::new();
330+
331+
for_each_method_def(&mut |class_method_defs| {
332+
for def in class_method_defs {
333+
match def {
334+
PyMethodDefType::Getter(getter) => {
335+
getter.copy_to(
336+
property_defs_map
337+
.entry(getter.name)
338+
.or_insert(PY_GET_SET_DEF_INIT),
339+
);
340+
}
341+
PyMethodDefType::Setter(setter) => {
342+
setter.copy_to(
343+
property_defs_map
344+
.entry(setter.name)
345+
.or_insert(PY_GET_SET_DEF_INIT),
346+
);
347+
}
348+
PyMethodDefType::Method(def)
349+
| PyMethodDefType::Class(def)
350+
| PyMethodDefType::Static(def) => method_defs.push(def.as_method_def().unwrap()),
351+
PyMethodDefType::ClassAttribute(_) => {}
352+
}
353+
}
297354
});
298355

299-
if !defs.is_empty() {
356+
// TODO: use into_values when on MSRV Rust >= 1.54
357+
let mut property_defs: Vec<_> = property_defs_map
358+
.into_iter()
359+
.map(|(_, value)| value)
360+
.collect();
361+
362+
if !method_defs.is_empty() {
363+
// Safety: Python expects a zeroed entry to mark the end of the defs
364+
method_defs.push(unsafe { std::mem::zeroed() });
365+
}
366+
367+
// PyPy doesn't automatically add __dict__ getter / setter.
368+
// PyObject_GenericGetDict not in the limited API until Python 3.10.
369+
if !has_dict {
370+
#[cfg(not(any(PyPy, all(Py_LIMITED_API, not(Py_3_10)))))]
371+
property_defs.push(ffi::PyGetSetDef {
372+
name: "__dict__\0".as_ptr() as *mut c_char,
373+
get: Some(ffi::PyObject_GenericGetDict),
374+
set: Some(ffi::PyObject_GenericSetDict),
375+
doc: ptr::null_mut(),
376+
closure: ptr::null_mut(),
377+
});
378+
}
379+
380+
if !property_defs.is_empty() {
300381
// Safety: Python expects a zeroed entry to mark the end of the defs
301-
defs.push(unsafe { std::mem::zeroed() });
382+
property_defs.push(unsafe { std::mem::zeroed() });
302383
}
303384

304-
defs
385+
PyClassInfo {
386+
method_defs,
387+
property_defs,
388+
}
305389
}
306390

307391
/// Generates the __dictoffset__ and __weaklistoffset__ members, to set tp_dictoffset and
@@ -351,52 +435,3 @@ const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef {
351435
doc: ptr::null_mut(),
352436
closure: ptr::null_mut(),
353437
};
354-
355-
fn py_class_properties(
356-
is_dummy: bool,
357-
for_each_method_def: &dyn Fn(&mut dyn FnMut(&[PyMethodDefType])),
358-
) -> Vec<ffi::PyGetSetDef> {
359-
let mut defs = std::collections::HashMap::new();
360-
361-
for_each_method_def(&mut |method_defs| {
362-
for def in method_defs {
363-
match def {
364-
PyMethodDefType::Getter(getter) => {
365-
getter.copy_to(defs.entry(getter.name).or_insert(PY_GET_SET_DEF_INIT));
366-
}
367-
PyMethodDefType::Setter(setter) => {
368-
setter.copy_to(defs.entry(setter.name).or_insert(PY_GET_SET_DEF_INIT));
369-
}
370-
_ => (),
371-
}
372-
}
373-
});
374-
375-
let mut props: Vec<_> = defs.values().cloned().collect();
376-
377-
// PyPy doesn't automatically adds __dict__ getter / setter.
378-
// PyObject_GenericGetDict not in the limited API until Python 3.10.
379-
push_dict_getset(&mut props, is_dummy);
380-
381-
if !props.is_empty() {
382-
// Safety: Python expects a zeroed entry to mark the end of the defs
383-
props.push(unsafe { std::mem::zeroed() });
384-
}
385-
props
386-
}
387-
388-
#[cfg(not(any(PyPy, all(Py_LIMITED_API, not(Py_3_10)))))]
389-
fn push_dict_getset(props: &mut Vec<ffi::PyGetSetDef>, is_dummy: bool) {
390-
if !is_dummy {
391-
props.push(ffi::PyGetSetDef {
392-
name: "__dict__\0".as_ptr() as *mut c_char,
393-
get: Some(ffi::PyObject_GenericGetDict),
394-
set: Some(ffi::PyObject_GenericSetDict),
395-
doc: ptr::null_mut(),
396-
closure: ptr::null_mut(),
397-
});
398-
}
399-
}
400-
401-
#[cfg(any(PyPy, all(Py_LIMITED_API, not(Py_3_10))))]
402-
fn push_dict_getset(_: &mut Vec<ffi::PyGetSetDef>, _is_dummy: bool) {}

0 commit comments

Comments
 (0)