Skip to content

Commit

Permalink
Revamp PyType name functions to match PEP 737 (#4196)
Browse files Browse the repository at this point in the history
* Revamp PyType name functions to match PEP 737

PyType::name uses `tp_name`, which is not consistent.
[PEP 737](https://peps.python.org/pep-0737/) adds a new path forward,
so update PyType::name and add PyType::{module,fully_qualified_name}
to match the PEP.

* refactor conditional code to handle multiple Python versions better

* return `Bound<'py, str>`

* fixup

---------

Co-authored-by: David Hewitt <mail@davidhewitt.dev>
  • Loading branch information
aneeshusa and davidhewitt authored Jun 22, 2024
1 parent a2f9399 commit c67625d
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 88 deletions.
4 changes: 2 additions & 2 deletions guide/src/class/numeric.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ use std::hash::{Hash, Hasher};
use pyo3::exceptions::{PyValueError, PyZeroDivisionError};
use pyo3::prelude::*;
use pyo3::class::basic::CompareOp;
use pyo3::types::PyComplex;
use pyo3::types::{PyComplex, PyString};

fn wrap(obj: &Bound<'_, PyAny>) -> PyResult<i32> {
let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?;
Expand All @@ -231,7 +231,7 @@ impl Number {

fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
// Get the class name dynamically in case `Number` is subclassed
let class_name: String = slf.get_type().qualname()?;
let class_name: Bound<'_, PyString> = slf.get_type().qualname()?;
Ok(format!("{}({})", class_name, slf.borrow().0))
}

Expand Down
6 changes: 4 additions & 2 deletions guide/src/class/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ the subclass name. This is typically done in Python code by accessing

```rust
# use pyo3::prelude::*;
# use pyo3::types::PyString;
#
# #[pyclass]
# struct Number(i32);
Expand All @@ -88,7 +89,7 @@ the subclass name. This is typically done in Python code by accessing
impl Number {
fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
// This is the equivalent of `self.__class__.__name__` in Python.
let class_name: String = slf.get_type().qualname()?;
let class_name: Bound<'_, PyString> = slf.get_type().qualname()?;
// To access fields of the Rust struct, we need to borrow the `PyCell`.
Ok(format!("{}({})", class_name, slf.borrow().0))
}
Expand Down Expand Up @@ -285,6 +286,7 @@ use std::hash::{Hash, Hasher};

use pyo3::prelude::*;
use pyo3::class::basic::CompareOp;
use pyo3::types::PyString;

#[pyclass]
struct Number(i32);
Expand All @@ -297,7 +299,7 @@ impl Number {
}

fn __repr__(slf: &Bound<'_, Self>) -> PyResult<String> {
let class_name: String = slf.get_type().qualname()?;
let class_name: Bound<'_, PyString> = slf.get_type().qualname()?;
Ok(format!("{}({})", class_name, slf.borrow().0))
}

Expand Down
58 changes: 58 additions & 0 deletions guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,64 @@ enum SimpleEnum {
```
</details>

### `PyType::name` reworked to better match Python `__name__`
<details open>
<summary><small>Click to expand</small></summary>

This function previously would try to read directly from Python type objects' C API field (`tp_name`), in which case it
would return a `Cow::Borrowed`. However the contents of `tp_name` don't have well-defined semantics.

Instead `PyType::name()` now returns the equivalent of Python `__name__` and returns `PyResult<Bound<'py, PyString>>`.

The closest equivalent to PyO3 0.21's version of `PyType::name()` has been introduced as a new function `PyType::fully_qualified_name()`,
which is equivalent to `__module__` and `__qualname__` joined as `module.qualname`.

Before:

```rust,ignore
# #![allow(deprecated, dead_code)]
# use pyo3::prelude::*;
# use pyo3::types::{PyBool};
# fn main() -> PyResult<()> {
Python::with_gil(|py| {
let bool_type = py.get_type_bound::<PyBool>();
let name = bool_type.name()?.into_owned();
println!("Hello, {}", name);
let mut name_upper = bool_type.name()?;
name_upper.to_mut().make_ascii_uppercase();
println!("Hello, {}", name_upper);
Ok(())
})
# }
```

After:

```rust
# #![allow(dead_code)]
# use pyo3::prelude::*;
# use pyo3::types::{PyBool};
# fn main() -> PyResult<()> {
Python::with_gil(|py| {
let bool_type = py.get_type_bound::<PyBool>();
let name = bool_type.name()?;
println!("Hello, {}", name);

// (if the full dotted path was desired, switch from `name()` to `fully_qualified_name()`)
let mut name_upper = bool_type.fully_qualified_name()?.to_string();
name_upper.make_ascii_uppercase();
println!("Hello, {}", name_upper);

Ok(())
})
# }
```
</details>



## from 0.20.* to 0.21
<details>
<summary><small>Click to expand</small></summary>
Expand Down
4 changes: 4 additions & 0 deletions newsfragments/4196.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add `PyType::module`, which always matches Python `__module__`.
Add `PyType::fully_qualified_name` which matches the "fully qualified name"
defined in https://peps.python.org/pep-0737 (not exposed in Python),
which is useful for error messages and `repr()` implementations.
1 change: 1 addition & 0 deletions newsfragments/4196.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Change `PyType::name` to always match Python `__name__`.
8 changes: 8 additions & 0 deletions pyo3-ffi/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ extern "C" {
#[cfg_attr(PyPy, link_name = "PyPyType_GetQualName")]
pub fn PyType_GetQualName(arg1: *mut PyTypeObject) -> *mut PyObject;

#[cfg(Py_3_13)]
#[cfg_attr(PyPy, link_name = "PyPyType_GetFullyQualifiedName")]
pub fn PyType_GetFullyQualifiedName(arg1: *mut PyTypeObject) -> *mut PyObject;

#[cfg(Py_3_13)]
#[cfg_attr(PyPy, link_name = "PyPyType_GetModuleName")]
pub fn PyType_GetModuleName(arg1: *mut PyTypeObject) -> *mut PyObject;

#[cfg(Py_3_12)]
#[cfg_attr(PyPy, link_name = "PyPyType_FromMetaclass")]
pub fn PyType_FromMetaclass(
Expand Down
12 changes: 7 additions & 5 deletions pytests/src/misc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use pyo3::{prelude::*, types::PyDict};
use std::borrow::Cow;
use pyo3::{
prelude::*,
types::{PyDict, PyString},
};

#[pyfunction]
fn issue_219() {
Expand All @@ -8,8 +10,8 @@ fn issue_219() {
}

#[pyfunction]
fn get_type_full_name(obj: &Bound<'_, PyAny>) -> PyResult<String> {
obj.get_type().name().map(Cow::into_owned)
fn get_type_fully_qualified_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyString>> {
obj.get_type().fully_qualified_name()
}

#[pyfunction]
Expand All @@ -33,7 +35,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny>
#[pymodule]
pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(issue_219, m)?)?;
m.add_function(wrap_pyfunction!(get_type_full_name, m)?)?;
m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?;
m.add_function(wrap_pyfunction!(accepts_bool, m)?)?;
m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?;
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions pytests/tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ def test_import_in_subinterpreter_forbidden():
subinterpreters.destroy(sub_interpreter)


def test_type_full_name_includes_module():
def test_type_fully_qualified_name_includes_module():
numpy = pytest.importorskip("numpy")

# For numpy 1.x and 2.x
assert pyo3_pytests.misc.get_type_full_name(numpy.bool_(True)) in [
assert pyo3_pytests.misc.get_type_fully_qualified_name(numpy.bool_(True)) in [
"numpy.bool",
"numpy.bool_",
]
Expand Down
17 changes: 7 additions & 10 deletions src/err/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,16 +971,13 @@ struct PyDowncastErrorArguments {

impl PyErrArguments for PyDowncastErrorArguments {
fn arguments(self, py: Python<'_>) -> PyObject {
format!(
"'{}' object cannot be converted to '{}'",
self.from
.bind(py)
.qualname()
.as_deref()
.unwrap_or("<failed to extract type name>"),
self.to
)
.to_object(py)
const FAILED_TO_EXTRACT: Cow<'_, str> = Cow::Borrowed("<failed to extract type name>");
let from = self.from.bind(py).qualname();
let from = match &from {
Ok(qn) => qn.to_cow().unwrap_or(FAILED_TO_EXTRACT),
Err(_) => FAILED_TO_EXTRACT,
};
format!("'{}' object cannot be converted to '{}'", from, self.to).to_object(py)
}
}

Expand Down
14 changes: 9 additions & 5 deletions src/types/boolobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,15 @@ impl FromPyObject<'_> for bool {
Err(err) => err,
};

if obj
.get_type()
.name()
.map_or(false, |name| name == "numpy.bool_" || name == "numpy.bool")
{
let is_numpy_bool = {
let ty = obj.get_type();
ty.module().map_or(false, |module| module == "numpy")
&& ty
.name()
.map_or(false, |name| name == "bool_" || name == "bool")
};

if is_numpy_bool {
let missing_conversion = |obj: &Bound<'_, PyAny>| {
PyTypeError::new_err(format!(
"object of type '{}' does not define a '__bool__' conversion",
Expand Down
Loading

0 comments on commit c67625d

Please sign in to comment.