Skip to content

Commit c67625d

Browse files
Revamp PyType name functions to match PEP 737 (#4196)
* 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 <[email protected]>
1 parent a2f9399 commit c67625d

File tree

12 files changed

+267
-88
lines changed

12 files changed

+267
-88
lines changed

guide/src/class/numeric.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ use std::hash::{Hash, Hasher};
210210
use pyo3::exceptions::{PyValueError, PyZeroDivisionError};
211211
use pyo3::prelude::*;
212212
use pyo3::class::basic::CompareOp;
213-
use pyo3::types::PyComplex;
213+
use pyo3::types::{PyComplex, PyString};
214214

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

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

guide/src/class/object.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ the subclass name. This is typically done in Python code by accessing
8080

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

286287
use pyo3::prelude::*;
287288
use pyo3::class::basic::CompareOp;
289+
use pyo3::types::PyString;
288290

289291
#[pyclass]
290292
struct Number(i32);
@@ -297,7 +299,7 @@ impl Number {
297299
}
298300

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

guide/src/migration.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,64 @@ enum SimpleEnum {
8181
```
8282
</details>
8383

84+
### `PyType::name` reworked to better match Python `__name__`
85+
<details open>
86+
<summary><small>Click to expand</small></summary>
87+
88+
This function previously would try to read directly from Python type objects' C API field (`tp_name`), in which case it
89+
would return a `Cow::Borrowed`. However the contents of `tp_name` don't have well-defined semantics.
90+
91+
Instead `PyType::name()` now returns the equivalent of Python `__name__` and returns `PyResult<Bound<'py, PyString>>`.
92+
93+
The closest equivalent to PyO3 0.21's version of `PyType::name()` has been introduced as a new function `PyType::fully_qualified_name()`,
94+
which is equivalent to `__module__` and `__qualname__` joined as `module.qualname`.
95+
96+
Before:
97+
98+
```rust,ignore
99+
# #![allow(deprecated, dead_code)]
100+
# use pyo3::prelude::*;
101+
# use pyo3::types::{PyBool};
102+
# fn main() -> PyResult<()> {
103+
Python::with_gil(|py| {
104+
let bool_type = py.get_type_bound::<PyBool>();
105+
let name = bool_type.name()?.into_owned();
106+
println!("Hello, {}", name);
107+
108+
let mut name_upper = bool_type.name()?;
109+
name_upper.to_mut().make_ascii_uppercase();
110+
println!("Hello, {}", name_upper);
111+
112+
Ok(())
113+
})
114+
# }
115+
```
116+
117+
After:
118+
119+
```rust
120+
# #![allow(dead_code)]
121+
# use pyo3::prelude::*;
122+
# use pyo3::types::{PyBool};
123+
# fn main() -> PyResult<()> {
124+
Python::with_gil(|py| {
125+
let bool_type = py.get_type_bound::<PyBool>();
126+
let name = bool_type.name()?;
127+
println!("Hello, {}", name);
128+
129+
// (if the full dotted path was desired, switch from `name()` to `fully_qualified_name()`)
130+
let mut name_upper = bool_type.fully_qualified_name()?.to_string();
131+
name_upper.make_ascii_uppercase();
132+
println!("Hello, {}", name_upper);
133+
134+
Ok(())
135+
})
136+
# }
137+
```
138+
</details>
139+
140+
141+
84142
## from 0.20.* to 0.21
85143
<details>
86144
<summary><small>Click to expand</small></summary>

newsfragments/4196.added.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add `PyType::module`, which always matches Python `__module__`.
2+
Add `PyType::fully_qualified_name` which matches the "fully qualified name"
3+
defined in https://peps.python.org/pep-0737 (not exposed in Python),
4+
which is useful for error messages and `repr()` implementations.

newsfragments/4196.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Change `PyType::name` to always match Python `__name__`.

pyo3-ffi/src/object.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@ extern "C" {
261261
#[cfg_attr(PyPy, link_name = "PyPyType_GetQualName")]
262262
pub fn PyType_GetQualName(arg1: *mut PyTypeObject) -> *mut PyObject;
263263

264+
#[cfg(Py_3_13)]
265+
#[cfg_attr(PyPy, link_name = "PyPyType_GetFullyQualifiedName")]
266+
pub fn PyType_GetFullyQualifiedName(arg1: *mut PyTypeObject) -> *mut PyObject;
267+
268+
#[cfg(Py_3_13)]
269+
#[cfg_attr(PyPy, link_name = "PyPyType_GetModuleName")]
270+
pub fn PyType_GetModuleName(arg1: *mut PyTypeObject) -> *mut PyObject;
271+
264272
#[cfg(Py_3_12)]
265273
#[cfg_attr(PyPy, link_name = "PyPyType_FromMetaclass")]
266274
pub fn PyType_FromMetaclass(

pytests/src/misc.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use pyo3::{prelude::*, types::PyDict};
2-
use std::borrow::Cow;
1+
use pyo3::{
2+
prelude::*,
3+
types::{PyDict, PyString},
4+
};
35

46
#[pyfunction]
57
fn issue_219() {
@@ -8,8 +10,8 @@ fn issue_219() {
810
}
911

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

1517
#[pyfunction]
@@ -33,7 +35,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny>
3335
#[pymodule]
3436
pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> {
3537
m.add_function(wrap_pyfunction!(issue_219, m)?)?;
36-
m.add_function(wrap_pyfunction!(get_type_full_name, m)?)?;
38+
m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?;
3739
m.add_function(wrap_pyfunction!(accepts_bool, m)?)?;
3840
m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?;
3941
Ok(())

pytests/tests/test_misc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ def test_import_in_subinterpreter_forbidden():
5151
subinterpreters.destroy(sub_interpreter)
5252

5353

54-
def test_type_full_name_includes_module():
54+
def test_type_fully_qualified_name_includes_module():
5555
numpy = pytest.importorskip("numpy")
5656

5757
# For numpy 1.x and 2.x
58-
assert pyo3_pytests.misc.get_type_full_name(numpy.bool_(True)) in [
58+
assert pyo3_pytests.misc.get_type_fully_qualified_name(numpy.bool_(True)) in [
5959
"numpy.bool",
6060
"numpy.bool_",
6161
]

src/err/mod.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -971,16 +971,13 @@ struct PyDowncastErrorArguments {
971971

972972
impl PyErrArguments for PyDowncastErrorArguments {
973973
fn arguments(self, py: Python<'_>) -> PyObject {
974-
format!(
975-
"'{}' object cannot be converted to '{}'",
976-
self.from
977-
.bind(py)
978-
.qualname()
979-
.as_deref()
980-
.unwrap_or("<failed to extract type name>"),
981-
self.to
982-
)
983-
.to_object(py)
974+
const FAILED_TO_EXTRACT: Cow<'_, str> = Cow::Borrowed("<failed to extract type name>");
975+
let from = self.from.bind(py).qualname();
976+
let from = match &from {
977+
Ok(qn) => qn.to_cow().unwrap_or(FAILED_TO_EXTRACT),
978+
Err(_) => FAILED_TO_EXTRACT,
979+
};
980+
format!("'{}' object cannot be converted to '{}'", from, self.to).to_object(py)
984981
}
985982
}
986983

src/types/boolobject.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,15 @@ impl FromPyObject<'_> for bool {
111111
Err(err) => err,
112112
};
113113

114-
if obj
115-
.get_type()
116-
.name()
117-
.map_or(false, |name| name == "numpy.bool_" || name == "numpy.bool")
118-
{
114+
let is_numpy_bool = {
115+
let ty = obj.get_type();
116+
ty.module().map_or(false, |module| module == "numpy")
117+
&& ty
118+
.name()
119+
.map_or(false, |name| name == "bool_" || name == "bool")
120+
};
121+
122+
if is_numpy_bool {
119123
let missing_conversion = |obj: &Bound<'_, PyAny>| {
120124
PyTypeError::new_err(format!(
121125
"object of type '{}' does not define a '__bool__' conversion",

0 commit comments

Comments
 (0)