Repository: PyO3/pyo3 (Rust)

Issue:
Descriptor __get__ fails on class
### Bug Description

There seems to be an issue with PyO3's handling of the `__get__` method of a descriptor class when it is invoked on the class rather than the instance, in which case it raises a PanicException. The issue is common to both pyproto and (as of PyO3 0.15) pymethods.

A typical scenario in which the issue manifests is in code introspection by a tool such as Sphinx.

### Steps to Reproduce

The following minimal example (requiring PyO3 0.15) serves to demonstrate the issue:

```rust
use pyo3::prelude::*;

#[pyclass]
struct MyClass;

#[pymethods]
impl MyClass {
    #[new]
    fn __new__() -> Self { Self }
    fn __get__(_self: PyObject, instance: PyObject, _owner: PyObject) -> PyResult<PyObject> { Ok(instance) }
}

#[pymodule]
fn mymodule(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<MyClass>()?;
    Ok(())
}
```

First, the expected behaviour:

```python
class MyClass: __get__ = lambda self, instance, owner: instance
class A: attr = MyClass()
print(A().attr) # <__main__.A object at 0x...>
print(A.attr) # None
print(MyClass().__get__(None, object)) # None
```

Then, taking `MyClass` from `mymodule`:

```python
from mymodule import MyClass
class A: attr = MyClass()
print(A().attr) # <__main__.A object at 0x...>
print(A.attr) # pyo3_runtime.PanicException: Python API call failed
print(MyClass().__get__(None, object)) # pyo3_runtime.PanicException: Python API call failed
```

As seen above, getting `attr` from an instance works as intended, but getting it from the class raises an exception, as does calling `__get__` directly with a `None` instance.

### Backtrace

```shell
thread '<unnamed>' panicked at 'Python API call failed', .../pyo3-0.15.0/src/err/mod.rs:582:5
stack backtrace:
   0: std::panicking::begin_panic
             at /rustc/.../library/std/src/panicking.rs:525:12
   1: pyo3::err::panic_after_error
             at .../pyo3-0.15.0/src/err/mod.rs:582:5
   2: pyo3::conversion::FromPyPointer::from_borrowed_ptr_or_panic::{{closure}}
             at .../pyo3-0.15.0/src/conversion.rs:512:67
   3: core::option::Option<T>::unwrap_or_else
             at /rustc/.../library/core/src/option.rs:787:21
   4: pyo3::conversion::FromPyPointer::from_borrowed_ptr_or_panic
             at .../pyo3-0.15.0/src/conversion.rs:512:9
   5: pyo3::conversion::FromPyPointer::from_borrowed_ptr
             at .../pyo3-0.15.0/src/conversion.rs:520:9
   6: pyo3::python::Python::from_borrowed_ptr
             at .../pyo3-0.15.0/src/python.rs:645:9
   7: mymodule::<impl pyo3::class::impl_::PyMethodsProtocolSlots<mymodule::MyClass> for pyo3::class::impl_::PyClassImplCollector<mymodule::MyClass>>
::methods_protocol_slots::__wrap::{{closure}}
             at ./src/lib.rs:6:1
   8: pyo3::callback::handle_panic::{{closure}}
             at .../pyo3-0.15.0/src/callback.rs:247:9
   9: std::panicking::try::do_call
             at /rustc/.../library/std/src/panicking.rs:406:40
  10: __rust_try
  11: std::panicking::try
             at /rustc/.../library/std/src/panicking.rs:370:19
  12: std::panic::catch_unwind
             at /rustc/.../library/std/src/panic.rs:133:14
  13: pyo3::callback::handle_panic
             at .../src/callback.rs:245:24
  14: mymodule::<impl pyo3::class::impl_::PyMethodsProtocolSlots<mymodule::MyClass> for pyo3::class::impl_::PyClassImplCollector<mymodule::MyClass>>
::methods_protocol_slots::__wrap
             at ./src/lib.rs:6:1
```


### Your operating system and version

Debian Linux

### Your Python version (`python --version`)

Python 3.9.2

### Your Rust version (`rustc --version`)

rustc 1.55.0 (c8dfcfe04 2021-09-06)

### Your PyO3 version

0.15.0

### How did you install python? Did you use a virtualenv?

apt

### Additional Info

_No response_


Discussion:
Thanks for the comprehensive report. I've played around with this a bit and can repro. Looks like this null-pointer passing is unique to the `__get__` implementation.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b7bb34b9db..2a416212b92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix use of `catch_unwind` in `allow_threads` which can cause fatal crashes. [#1989](https://github.com/PyO3/pyo3/pull/1989)
 - Fix build failure on PyPy when abi3 features are activated. [#1991](https://github.com/PyO3/pyo3/pull/1991)
 - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993)
+- Fix panic in `__get__` implementation when accessing descriptor on type object. [#1997](https://github.com/PyO3/pyo3/pull/1997)
 
 ## [0.15.0] - 2021-11-03
 
diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs
index eb8611dc961..13a49f92734 100644
--- a/pyo3-macros-backend/src/pymethod.rs
+++ b/pyo3-macros-backend/src/pymethod.rs
@@ -483,8 +483,8 @@ const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc")
 const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc")
     .extract_error_mode(ExtractErrorMode::NotImplemented)
     .arguments(&[Ty::Object, Ty::CompareOp]);
-const __GET__: SlotDef =
-    SlotDef::new("Py_tp_descr_get", "descrgetfunc").arguments(&[Ty::Object, Ty::Object]);
+const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc")
+    .arguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject]);
 const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc");
 const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc").return_conversion(
     TokenGenerator(|| quote! { ::pyo3::class::iter::IterNextOutput::<_, _> }),
@@ -606,6 +606,7 @@ fn pyproto(method_name: &str) -> Option<&'static SlotDef> {
 #[derive(Clone, Copy)]
 enum Ty {
     Object,
+    MaybeNullObject,
     NonNullObject,
     CompareOp,
     Int,
@@ -617,7 +618,7 @@ enum Ty {
 impl Ty {
     fn ffi_type(self) -> TokenStream {
         match self {
-            Ty::Object => quote! { *mut ::pyo3::ffi::PyObject },
+            Ty::Object | Ty::MaybeNullObject => quote! { *mut ::pyo3::ffi::PyObject },
             Ty::NonNullObject => quote! { ::std::ptr::NonNull<::pyo3::ffi::PyObject> },
             Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int },
             Ty::PyHashT => quote! { ::pyo3::ffi::Py_hash_t },
@@ -645,6 +646,22 @@ impl Ty {
                 );
                 extract_object(cls, arg.ty, ident, extract)
             }
+            Ty::MaybeNullObject => {
+                let extract = handle_error(
+                    extract_error_mode,
+                    py,
+                    quote! {
+                        #py.from_borrowed_ptr::<::pyo3::PyAny>(
+                            if #ident.is_null() {
+                                ::pyo3::ffi::Py_None()
+                            } else {
+                                #ident
+                            }
+                        ).extract()
+                    },
+                );
+                extract_object(cls, arg.ty, ident, extract)
+            }
             Ty::NonNullObject => {
                 let extract = handle_error(
                     extract_error_mode,


Test patch:
diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs
index 0b6c30d43af..ad1ce58fd48 100644
--- a/tests/test_proto_methods.rs
+++ b/tests/test_proto_methods.rs
@@ -548,11 +548,23 @@ fn descr_getset() {
         r#"
 class Class:
     counter = Counter()
+
+# access via type
+counter = Class.counter
+assert counter.count == 1
+
+# access with instance directly
+assert Counter.__get__(counter, Class()).count == 2
+
+# access via instance
 c = Class()
-c.counter # count += 1
-assert c.counter.count == 2
-c.counter = Counter()
 assert c.counter.count == 3
+
+# __set__
+c.counter = Counter()
+assert c.counter.count == 4
+
+# __delete__
 del c.counter
 assert c.counter.count == 1
 "#