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

Protocols: implement __getattribute__ #2187

Merged
merged 2 commits into from
Feb 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for `from_py_with` on struct tuples and enums to override the default from-Python conversion. [#2181](https://github.com/PyO3/pyo3/pull/2181)
- Add `eq`, `ne`, `lt`, `le`, `gt`, `ge` methods to `PyAny` that wrap `rich_compare`. [#2175](https://github.com/PyO3/pyo3/pull/2175)
- Add `Py::is` and `PyAny::is` methods to check for object identity. [#2183](https://github.com/PyO3/pyo3/pull/2183)
- Add support for the `__getattribute__` magic method. [#2187](https://github.com/PyO3/pyo3/pull/2187)

### Changed

Expand Down
10 changes: 10 additions & 0 deletions guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ given signatures should be interpreted as follows:
</details>

- `__getattr__(<self>, object) -> object`
- `__getattribute__(<self>, object) -> object`
<details>
<summary>Differences between `__getattr__` and `__getattribute__`</summary>
As in Python, `__getattr__` is only called if the attribute is not found
by normal attribute lookup. `__getattribute__`, on the other hand, is
called for *every* attribute access. If it wants to access existing
attributes on `self`, it needs to be very careful not to introduce
infinite recursion, and use `baseclass.__getattribute__()`.
</details>

- `__setattr__(<self>, value: object) -> ()`
- `__delattr__(<self>, object) -> ()`

Expand Down
5 changes: 5 additions & 0 deletions pyo3-macros-backend/src/pyimpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ fn add_shared_proto_slots(
}};
}

try_add_shared_slot!(
"__getattribute__",
"__getattr__",
generate_pyclass_getattro_slot
);
try_add_shared_slot!("__setattr__", "__delattr__", generate_pyclass_setattr_slot);
try_add_shared_slot!("__set__", "__delete__", generate_pyclass_setdescr_slot);
try_add_shared_slot!("__setitem__", "__delitem__", generate_pyclass_setitem_slot);
Expand Down
33 changes: 8 additions & 25 deletions pyo3-macros-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ impl PyMethodKind {
fn from_name(name: &str) -> Self {
match name {
// Protocol implemented through slots
"__getattr__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETATTR__)),
"__str__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__STR__)),
"__repr__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__REPR__)),
"__hash__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__HASH__)),
Expand Down Expand Up @@ -85,6 +84,10 @@ impl PyMethodKind {
"__releasebuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RELEASEBUFFER__)),
"__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CLEAR__)),
// Protocols implemented through traits
"__getattribute__" => {
PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTRIBUTE__))
}
"__getattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTR__)),
"__setattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SETATTR__)),
"__delattr__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__DELATTR__)),
"__set__" => PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__SET__)),
Expand Down Expand Up @@ -570,21 +573,6 @@ impl PropertyType<'_> {
}
}

const __GETATTR__: SlotDef = SlotDef::new("Py_tp_getattro", "getattrofunc")
.arguments(&[Ty::Object])
.before_call_method(TokenGenerator(|| {
quote! {
// Behave like python's __getattr__ (as opposed to __getattribute__) and check
// for existing fields and methods first
let existing = _pyo3::ffi::PyObject_GenericGetAttr(_slf, arg0);
if existing.is_null() {
// PyObject_HasAttr also tries to get an object and clears the error if it fails
_pyo3::ffi::PyErr_Clear();
} else {
return existing;
}
}
}));
const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc");
const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc");
const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc")
Expand Down Expand Up @@ -870,7 +858,6 @@ struct SlotDef {
func_ty: StaticIdent,
arguments: &'static [Ty],
ret_ty: Ty,
before_call_method: Option<TokenGenerator>,
extract_error_mode: ExtractErrorMode,
return_mode: Option<ReturnMode>,
require_unsafe: bool,
Expand All @@ -885,7 +872,6 @@ impl SlotDef {
func_ty: StaticIdent(func_ty),
arguments: NO_ARGUMENTS,
ret_ty: Ty::Object,
before_call_method: None,
extract_error_mode: ExtractErrorMode::Raise,
return_mode: None,
require_unsafe: false,
Expand All @@ -902,11 +888,6 @@ impl SlotDef {
self
}

const fn before_call_method(mut self, before_call_method: TokenGenerator) -> Self {
self.before_call_method = Some(before_call_method);
self
}

const fn return_conversion(mut self, return_conversion: TokenGenerator) -> Self {
self.return_mode = Some(ReturnMode::Conversion(return_conversion));
self
Expand Down Expand Up @@ -936,7 +917,6 @@ impl SlotDef {
let SlotDef {
slot,
func_ty,
before_call_method,
arguments,
extract_error_mode,
ret_ty,
Expand All @@ -963,7 +943,6 @@ impl SlotDef {
Ok(quote!({
unsafe extern "C" fn __wrap(_raw_slf: *mut _pyo3::ffi::PyObject, #(#method_arguments),*) -> #ret_ty {
let _slf = _raw_slf;
#before_call_method
let gil = _pyo3::GILPool::new();
let #py = gil.python();
_pyo3::callback::panic_result_into_callback_output(#py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> {
Expand Down Expand Up @@ -1074,6 +1053,10 @@ impl SlotFragmentDef {
}
}

const __GETATTRIBUTE__: SlotFragmentDef =
SlotFragmentDef::new("__getattribute__", &[Ty::Object]).ret_ty(Ty::Object);
const __GETATTR__: SlotFragmentDef =
SlotFragmentDef::new("__getattr__", &[Ty::Object]).ret_ty(Ty::Object);
const __SETATTR__: SlotFragmentDef =
SlotFragmentDef::new("__setattr__", &[Ty::Object, Ty::NonNullObject]);
const __DELATTR__: SlotFragmentDef = SlotFragmentDef::new("__delattr__", &[Ty::Object]);
Expand Down
80 changes: 79 additions & 1 deletion src/impl_/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
pycell::PyCellLayout,
pyclass_init::PyObjectInit,
type_object::{PyLayout, PyTypeObject},
PyCell, PyClass, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python,
Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python,
};
use std::{
marker::PhantomData,
Expand Down Expand Up @@ -198,6 +198,84 @@ macro_rules! slot_fragment_trait {
}
}

slot_fragment_trait! {
PyClass__getattribute__SlotFragment,

/// # Safety: _slf and _attr must be valid non-null Python objects
#[inline]
unsafe fn __getattribute__(
self,
py: Python,
slf: *mut ffi::PyObject,
attr: *mut ffi::PyObject,
) -> PyResult<*mut ffi::PyObject> {
let res = ffi::PyObject_GenericGetAttr(slf, attr);
if res.is_null() {
Err(PyErr::fetch(py))
} else {
Ok(res)
}
}
}

slot_fragment_trait! {
PyClass__getattr__SlotFragment,

/// # Safety: _slf and _attr must be valid non-null Python objects
#[inline]
unsafe fn __getattr__(
self,
py: Python,
_slf: *mut ffi::PyObject,
attr: *mut ffi::PyObject,
) -> PyResult<*mut ffi::PyObject> {
Err(PyErr::new::<PyAttributeError, _>(
(Py::<PyAny>::from_borrowed_ptr(py, attr),)
))
}
}

#[doc(hidden)]
#[macro_export]
macro_rules! generate_pyclass_getattro_slot {
($cls:ty) => {{
unsafe extern "C" fn __wrap(
_slf: *mut $crate::ffi::PyObject,
attr: *mut $crate::ffi::PyObject,
) -> *mut $crate::ffi::PyObject {
use ::std::result::Result::*;
use $crate::impl_::pyclass::*;
let gil = $crate::GILPool::new();
let py = gil.python();
$crate::callback::panic_result_into_callback_output(
py,
::std::panic::catch_unwind(move || -> $crate::PyResult<_> {
let collector = PyClassImplCollector::<$cls>::new();

// Strategy:
// - Try __getattribute__ first. Its default is PyObject_GenericGetAttr.
// - If it returns a result, use it.
// - If it fails with AttributeError, try __getattr__.
// - If it fails otherwise, reraise.
match collector.__getattribute__(py, _slf, attr) {
Ok(obj) => Ok(obj),
Err(e) if e.is_instance_of::<$crate::exceptions::PyAttributeError>(py) => {
collector.__getattr__(py, _slf, attr)
}
Err(e) => Err(e),
}
}),
)
}
$crate::ffi::PyType_Slot {
slot: $crate::ffi::Py_tp_getattro,
pfunc: __wrap as $crate::ffi::getattrofunc as _,
}
}};
}

pub use generate_pyclass_getattro_slot;

/// Macro which expands to three items
/// - Trait for a __setitem__ dunder
/// - Trait for the corresponding __delitem__ dunder
Expand Down
62 changes: 59 additions & 3 deletions tests/test_proto_methods.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#![cfg(feature = "macros")]

use pyo3::exceptions::{PyIndexError, PyValueError};
use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError};
use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType};
use pyo3::{exceptions::PyAttributeError, prelude::*};
use pyo3::{py_run, PyCell};
use pyo3::{prelude::*, py_run, PyCell};
use std::{isize, iter};

mod common;
Expand Down Expand Up @@ -606,6 +605,63 @@ fn getattr_doesnt_override_member() {
py_assert!(py, inst, "inst.a == 8");
}

#[pyclass]
struct ClassWithGetAttribute {
#[pyo3(get, set)]
data: u32,
}

#[pymethods]
impl ClassWithGetAttribute {
fn __getattribute__(&self, _name: &str) -> u32 {
self.data * 2
}
}

#[test]
fn getattribute_overrides_member() {
let gil = Python::acquire_gil();
let py = gil.python();
let inst = PyCell::new(py, ClassWithGetAttribute { data: 4 }).unwrap();
py_assert!(py, inst, "inst.data == 8");
py_assert!(py, inst, "inst.y == 8");
}

#[pyclass]
struct ClassWithGetAttrAndGetAttribute;

#[pymethods]
impl ClassWithGetAttrAndGetAttribute {
fn __getattribute__(&self, name: &str) -> PyResult<u32> {
if name == "exists" {
Ok(42)
} else if name == "error" {
Err(PyValueError::new_err("bad"))
} else {
Err(PyAttributeError::new_err("fallback"))
}
}

fn __getattr__(&self, name: &str) -> PyResult<u32> {
if name == "lucky" {
Ok(57)
} else {
Err(PyAttributeError::new_err("no chance"))
}
}
}

#[test]
fn getattr_and_getattribute() {
let gil = Python::acquire_gil();
let py = gil.python();
let inst = PyCell::new(py, ClassWithGetAttrAndGetAttribute).unwrap();
py_assert!(py, inst, "inst.exists == 42");
py_assert!(py, inst, "inst.lucky == 57");
py_expect_exception!(py, inst, "inst.error", PyValueError);
py_expect_exception!(py, inst, "inst.unlucky", PyAttributeError);
}

/// Wraps a Python future and yield it once.
#[pyclass]
#[derive(Debug)]
Expand Down