From c47bae9e56e52dfa183a08387601bdfb75f0a5a5 Mon Sep 17 00:00:00 2001 From: "ThienWLe@gmail.com" Date: Sun, 8 Mar 2026 11:12:37 -0600 Subject: [PATCH 1/4] Add obj() getter method to PyUntypedBuffer --- src/buffer.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/buffer.rs b/src/buffer.rs index 51f6563b690..cad8c8a55cc 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -570,6 +570,18 @@ impl PyUntypedBuffer { self.raw().buf } + ///Returns the Python object that owns the buffer data. + /// + ///This is the object passed to [`PyUntypedBuffer::get()`] + ///Calling this before [`release()`][Self::release] allows you to clone an owned reference and + ///keeps the object alive after the buffer is released. + pub fn obj<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> { + // Safety: `PyObject_GetBuffer` increments the reference count of `obj` automatically + // and `PyBuffer_Release` decrements it on drop. The `obj` is guaranteed to be valid + // and non-null for the entire lifetime of `self`. + unsafe { Bound::from_borrowed_ptr(py, self.raw().obj) } + } + /// Gets a pointer to the specified item. /// /// If `indices.len() < self.dimensions()`, returns the start address of the sub-array at the specified dimension. @@ -1044,4 +1056,16 @@ mod tests { assert_eq!(typed.shape(), [5]); }); } + + #[test] + fn test_obj_getter() { + Python::attach(|py| { + let bytes = py.eval(ffi::c_str!("b'hello'"), None, None).unwrap(); + let buf = PyUntypedBuffer::get(&bytes).unwrap(); + let owner = buf.obj(py); + assert!(owner.is_instance_of::()); + //owner and bytes should point to the same object + assert!(owner.is(&bytes)); + }) + } } From e93c9efb4fa5d3edd91e105d55ccdd98a9bfa97f Mon Sep 17 00:00:00 2001 From: "ThienWLe@gmail.com" Date: Sun, 8 Mar 2026 11:25:25 -0600 Subject: [PATCH 2/4] Add changelog entry --- newsfragments/5870.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/5870.added.md diff --git a/newsfragments/5870.added.md b/newsfragments/5870.added.md new file mode 100644 index 00000000000..42907aaa792 --- /dev/null +++ b/newsfragments/5870.added.md @@ -0,0 +1 @@ +Add `obj()` getter to `PyUntypedBuffer` to retrieve the Python object owning the buffer. From 717bf1418b0baab22692333797ba3e69be8d0557 Mon Sep 17 00:00:00 2001 From: "ThienWLe@gmail.com" Date: Sun, 8 Mar 2026 11:35:26 -0600 Subject: [PATCH 3/4] Address Copilot review feedback: null check, doc fixes, test improvement --- src/buffer.rs | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index cad8c8a55cc..35ee3e5f8a0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -572,14 +572,20 @@ impl PyUntypedBuffer { ///Returns the Python object that owns the buffer data. /// + ///This is the object that was passed to [`PyBuffer::get()`] when the buffer was created. + ///Returns `None` if the buffer was created without owning object. + /// ///This is the object passed to [`PyUntypedBuffer::get()`] ///Calling this before [`release()`][Self::release] allows you to clone an owned reference and ///keeps the object alive after the buffer is released. - pub fn obj<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> { - // Safety: `PyObject_GetBuffer` increments the reference count of `obj` automatically - // and `PyBuffer_Release` decrements it on drop. The `obj` is guaranteed to be valid - // and non-null for the entire lifetime of `self`. - unsafe { Bound::from_borrowed_ptr(py, self.raw().obj) } + pub fn obj<'py>(&self, py: Python<'py>) -> Option> { + let ptr = self.raw().obj; + // SAFETY: Py_buffer.obj is a borrowed reference to a Python object, so it is always valid to create a Bound from it + if ptr.is_null() { + None + } else { + Some(unsafe { Bound::from_borrowed_ptr(py, ptr) }) + } } /// Gets a pointer to the specified item. @@ -1060,12 +1066,23 @@ mod tests { #[test] fn test_obj_getter() { Python::attach(|py| { - let bytes = py.eval(ffi::c_str!("b'hello'"), None, None).unwrap(); - let buf = PyUntypedBuffer::get(&bytes).unwrap(); - let owner = buf.obj(py); - assert!(owner.is_instance_of::()); - //owner and bytes should point to the same object + let bytes = PyBytes::new(py, b"hello"); + let buf = PyUntypedBuffer::get(bytes.as_any()).unwrap(); + + // obj() returns the same object that owns the buffer + let owner = buf.obj(py).unwrap(); + assert!(owner.is_instance_of::()); assert!(owner.is(&bytes)); - }) + + // can keep the owner alive after releasing the buffer + let owner_ref: crate::Py = owner.unbind(); + buf.release(py); + drop(bytes); + // owner_ref still valid after buffer and original are dropped + Python::attach(|py| { + let rebound = owner_ref.bind(py); + assert!(rebound.is_instance_of::()); + }); + }); } } From 43d470e1b6ff052eb78af90afd9a4d43c1d4a6c3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 17 Mar 2026 15:56:44 +0000 Subject: [PATCH 4/4] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/buffer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 35ee3e5f8a0..4a46e003c30 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -573,11 +573,11 @@ impl PyUntypedBuffer { ///Returns the Python object that owns the buffer data. /// ///This is the object that was passed to [`PyBuffer::get()`] when the buffer was created. - ///Returns `None` if the buffer was created without owning object. + /// Returns the Python object that owns the buffer data. /// - ///This is the object passed to [`PyUntypedBuffer::get()`] - ///Calling this before [`release()`][Self::release] allows you to clone an owned reference and - ///keeps the object alive after the buffer is released. + /// This is the object passed to [`PyUntypedBuffer::get()`]. + /// Calling this before [`release()`][Self::release] allows you to clone an owned reference + /// and keeps the object alive after the buffer is released. pub fn obj<'py>(&self, py: Python<'py>) -> Option> { let ptr = self.raw().obj; // SAFETY: Py_buffer.obj is a borrowed reference to a Python object, so it is always valid to create a Bound from it