Skip to content

Commit

Permalink
Allow more methods to take interned arguments (#2312)
Browse files Browse the repository at this point in the history
* Allow more methods to take interned arguments

* Changelog

* Unify name bounds

* Resolve merge conflict

* reduce use of py_decref

* Add some attr tests

* Update migration
  • Loading branch information
mejrs committed May 2, 2022
1 parent 97db563 commit dce4377
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 187 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Several methods of `Py` and `PyAny` now accept `impl IntoPy<Py<PyString>>` rather than just `&str` to allow use of the `intern!` macro. [#2312](https://github.com/PyO3/pyo3/pull/2312)
- Move `PyTypeObject::type_object` method to `PyTypeInfo` trait, and deprecate `PyTypeObject` trait. [#2287](https://github.com/PyO3/pyo3/pull/2287)
- The deprecated `pyproto` feature is now disabled by default. [#2322](https://github.com/PyO3/pyo3/pull/2322)
- Deprecate `ToBorrowedObject` trait (it is only used as a wrapper for `ToPyObject`). [#2333](https://github.com/PyO3/pyo3/pull/2333)


## [0.16.4] - 2022-04-14

### Added
Expand Down
29 changes: 29 additions & 0 deletions guide/src/migration.md
Expand Up @@ -5,6 +5,35 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).

## from 0.16.* to 0.17


### Added `impl IntoPy<Py<PyString>> for &str`

This may cause inference errors.

Before:
```rust,compile_fail
# use pyo3::prelude::*;
#
# fn main() {
Python::with_gil(|py| {
// Cannot infer either `Py<PyAny>` or `Py<PyString>`
let _test = "test".into_py(py);
});
# }
```

After, some type annotations may be necessary:

```rust
# use pyo3::prelude::*;
#
# fn main() {
Python::with_gil(|py| {
let _test: Py<PyAny> = "test".into_py(py);
});
# }
```

### The `pyproto` feature is now disabled by default

In preparation for removing the deprecated `#[pyproto]` attribute macro in a future PyO3 version, it is now gated behind an opt-in feature flag. This also gives a slight saving to compile times for code which does not use the deprecated macro.
Expand Down
7 changes: 4 additions & 3 deletions src/conversions/path.rs
@@ -1,3 +1,4 @@
use crate::intern;
use crate::types::PyType;
use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
use std::borrow::Cow;
Expand All @@ -18,10 +19,10 @@ impl FromPyObject<'_> for PathBuf {
Ok(s) => s,
Err(err) => {
let py = ob.py();
let pathlib = py.import("pathlib")?;
let pathlib_path: &PyType = pathlib.getattr("Path")?.downcast()?;
let pathlib = py.import(intern!(py, "pathlib"))?;
let pathlib_path: &PyType = pathlib.getattr(intern!(py, "Path"))?.downcast()?;
if ob.is_instance(pathlib_path)? {
let path_str = ob.call_method0("__str__")?;
let path_str = ob.call_method0(intern!(py, "__str__"))?;
OsString::extract(path_str)?
} else {
return Err(err);
Expand Down
7 changes: 5 additions & 2 deletions src/err/mod.rs
Expand Up @@ -135,7 +135,10 @@ impl PyErr {
///
/// # Examples
/// ```rust
/// use pyo3::{exceptions::PyTypeError, types::PyType, IntoPy, PyErr, Python};
/// use pyo3::prelude::*;
/// use pyo3::exceptions::PyTypeError;
/// use pyo3::types::{PyType, PyString};
///
/// Python::with_gil(|py| {
/// // Case #1: Exception object
/// let err = PyErr::from_value(PyTypeError::new_err("some type error").value(py));
Expand All @@ -146,7 +149,7 @@ impl PyErr {
/// assert_eq!(err.to_string(), "TypeError: ");
///
/// // Case #3: Invalid exception value
/// let err = PyErr::from_value("foo".into_py(py).as_ref(py));
/// let err = PyErr::from_value(PyString::new(py, "foo").into());
/// assert_eq!(
/// err.to_string(),
/// "TypeError: exceptions must derive from BaseException"
Expand Down
163 changes: 121 additions & 42 deletions src/instance.rs
Expand Up @@ -3,7 +3,7 @@ use crate::conversion::PyTryFrom;
use crate::err::{self, PyDowncastError, PyErr, PyResult};
use crate::gil;
use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell};
use crate::types::{PyDict, PyTuple};
use crate::types::{PyDict, PyString, PyTuple};
use crate::{
ffi, pyclass::MutablePyClass, AsPyPointer, FromPyObject, IntoPy, IntoPyPointer, PyAny, PyClass,
PyClassInitializer, PyRef, PyRefMut, PyTypeInfo, Python, ToPyObject,
Expand Down Expand Up @@ -561,12 +561,14 @@ impl<T> Py<T> {
/// ```
pub fn getattr<N>(&self, py: Python<'_>, attr_name: N) -> PyResult<PyObject>
where
N: ToPyObject,
N: IntoPy<Py<PyString>>,
{
let attr_name = attr_name.into_py(py);

unsafe {
PyObject::from_owned_ptr_or_err(
py,
ffi::PyObject_GetAttr(self.as_ptr(), attr_name.to_object(py).as_ptr()),
ffi::PyObject_GetAttr(self.as_ptr(), attr_name.as_ptr()),
)
}
}
Expand All @@ -575,8 +577,8 @@ impl<T> Py<T> {
///
/// This is equivalent to the Python expression `self.attr_name = value`.
///
/// If calling this method becomes performance-critical, the [`intern!`] macro can be used
/// to intern `attr_name`, thereby avoiding repeated temporary allocations of Python strings.
/// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used
/// to intern `attr_name`.
///
/// # Example: `intern!`ing the attribute name
///
Expand All @@ -595,17 +597,16 @@ impl<T> Py<T> {
/// ```
pub fn setattr<N, V>(&self, py: Python<'_>, attr_name: N, value: V) -> PyResult<()>
where
N: ToPyObject,
V: ToPyObject,
N: IntoPy<Py<PyString>>,
V: IntoPy<Py<PyAny>>,
{
let attr_name = attr_name.into_py(py);
let value = value.into_py(py);

unsafe {
err::error_on_minusone(
py,
ffi::PyObject_SetAttr(
self.as_ptr(),
attr_name.to_object(py).as_ptr(),
value.to_object(py).as_ptr(),
),
ffi::PyObject_SetAttr(self.as_ptr(), attr_name.as_ptr(), value.as_ptr()),
)
}
}
Expand All @@ -619,16 +620,17 @@ impl<T> Py<T> {
args: impl IntoPy<Py<PyTuple>>,
kwargs: Option<&PyDict>,
) -> PyResult<PyObject> {
let args = args.into_py(py).into_ptr();
let args = args.into_py(py);
let kwargs = kwargs.into_ptr();
let result = unsafe {
PyObject::from_owned_ptr_or_err(py, ffi::PyObject_Call(self.as_ptr(), args, kwargs))
};

unsafe {
ffi::Py_XDECREF(args);
let ret = PyObject::from_owned_ptr_or_err(
py,
ffi::PyObject_Call(self.as_ptr(), args.as_ptr(), kwargs),
);
ffi::Py_XDECREF(kwargs);
ret
}
result
}

/// Calls the object with only positional arguments.
Expand Down Expand Up @@ -657,23 +659,29 @@ impl<T> Py<T> {
/// Calls a method on the object.
///
/// This is equivalent to the Python expression `self.name(*args, **kwargs)`.
pub fn call_method(
///
/// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used
/// to intern `name`.
pub fn call_method<N, A>(
&self,
py: Python<'_>,
name: &str,
args: impl IntoPy<Py<PyTuple>>,
name: N,
args: A,
kwargs: Option<&PyDict>,
) -> PyResult<PyObject> {
) -> PyResult<PyObject>
where
N: IntoPy<Py<PyString>>,
A: IntoPy<Py<PyTuple>>,
{
let callee = self.getattr(py, name)?;
let args: Py<PyTuple> = args.into_py(py);
let kwargs = kwargs.into_ptr();

unsafe {
let args = args.into_py(py).into_ptr();
let kwargs = kwargs.into_ptr();
let ptr = ffi::PyObject_GetAttr(self.as_ptr(), name.to_object(py).as_ptr());
if ptr.is_null() {
return Err(PyErr::fetch(py));
}
let result = PyObject::from_owned_ptr_or_err(py, ffi::PyObject_Call(ptr, args, kwargs));
ffi::Py_DECREF(ptr);
ffi::Py_XDECREF(args);
let result = PyObject::from_owned_ptr_or_err(
py,
ffi::PyObject_Call(callee.as_ptr(), args.as_ptr(), kwargs),
);
ffi::Py_XDECREF(kwargs);
result
}
Expand All @@ -682,24 +690,32 @@ impl<T> Py<T> {
/// Calls a method on the object with only positional arguments.
///
/// This is equivalent to the Python expression `self.name(*args)`.
pub fn call_method1(
&self,
py: Python<'_>,
name: &str,
args: impl IntoPy<Py<PyTuple>>,
) -> PyResult<PyObject> {
///
/// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used
/// to intern `name`.
pub fn call_method1<N, A>(&self, py: Python<'_>, name: N, args: A) -> PyResult<PyObject>
where
N: IntoPy<Py<PyString>>,
A: IntoPy<Py<PyTuple>>,
{
self.call_method(py, name, args, None)
}

/// Calls a method on the object with no arguments.
///
/// This is equivalent to the Python expression `self.name()`.
pub fn call_method0(&self, py: Python<'_>, name: &str) -> PyResult<PyObject> {
///
/// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used
/// to intern `name`.
pub fn call_method0<N>(&self, py: Python<'_>, name: N) -> PyResult<PyObject>
where
N: IntoPy<Py<PyString>>,
{
cfg_if::cfg_if! {
if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] {
// Optimized path on python 3.9+
unsafe {
let name = name.into_py(py);
let name: Py<PyString> = name.into_py(py);
PyObject::from_owned_ptr_or_err(py, ffi::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()))
}
} else {
Expand Down Expand Up @@ -728,7 +744,7 @@ impl<T> Py<T> {

/// Create a `Py<T>` instance by taking ownership of the given FFI pointer.
///
/// If `ptr` is null then the current Python exception is fetched as a `PyErr`.
/// If `ptr` is null then the current Python exception is fetched as a [`PyErr`].
///
/// # Safety
/// If non-null, `ptr` must be a pointer to a Python object of type T.
Expand Down Expand Up @@ -985,8 +1001,8 @@ impl PyObject {
#[cfg(test)]
mod tests {
use super::{Py, PyObject};
use crate::types::PyDict;
use crate::{Python, ToPyObject};
use crate::types::{PyDict, PyString};
use crate::{PyAny, PyResult, Python, ToPyObject};

#[test]
fn test_call0() {
Expand Down Expand Up @@ -1036,4 +1052,67 @@ mod tests {
assert_eq!(p.get_refcnt(py), cnt);
});
}

#[test]
fn attr() -> PyResult<()> {
use crate::types::PyModule;

Python::with_gil(|py| {
const CODE: &str = r#"
class A:
pass
a = A()
"#;
let module = PyModule::from_code(py, CODE, "", "")?;
let instance: Py<PyAny> = module.getattr("a")?.into();

instance.getattr(py, "foo").unwrap_err();

instance.setattr(py, "foo", "bar")?;

assert!(instance
.getattr(py, "foo")?
.as_ref(py)
.eq(PyString::new(py, "bar"))?);

instance.getattr(py, "foo")?;
Ok(())
})
}

#[test]
fn pystring_attr() -> PyResult<()> {
use crate::types::PyModule;

Python::with_gil(|py| {
const CODE: &str = r#"
class A:
pass
a = A()
"#;
let module = PyModule::from_code(py, CODE, "", "")?;
let instance: Py<PyAny> = module.getattr("a")?.into();

let foo = crate::intern!(py, "foo");
let bar = crate::intern!(py, "bar");

instance.getattr(py, foo).unwrap_err();
instance.setattr(py, foo, bar)?;
assert!(instance.getattr(py, foo)?.as_ref(py).eq(bar)?);
Ok(())
})
}

#[test]
fn invalid_attr() -> PyResult<()> {
Python::with_gil(|py| {
let instance: Py<PyAny> = py.eval("object()", None, None)?.into();

instance.getattr(py, "foo").unwrap_err();

// Cannot assign arbitrary attributes to `object`
instance.setattr(py, "foo", "bar").unwrap_err();
Ok(())
})
}
}
10 changes: 7 additions & 3 deletions src/marker.rs
Expand Up @@ -122,10 +122,11 @@
use crate::err::{self, PyDowncastError, PyErr, PyResult};
use crate::gil::{self, GILGuard, GILPool};
use crate::impl_::not_send::NotSend;
use crate::types::{PyAny, PyDict, PyModule, PyType};
use crate::types::{PyAny, PyDict, PyModule, PyString, PyType};
use crate::version::PythonVersionInfo;
use crate::{
ffi, AsPyPointer, FromPyPointer, IntoPyPointer, PyNativeType, PyObject, PyTryFrom, PyTypeInfo,
ffi, AsPyPointer, FromPyPointer, IntoPy, IntoPyPointer, Py, PyNativeType, PyObject, PyTryFrom,
PyTypeInfo,
};
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
Expand Down Expand Up @@ -590,7 +591,10 @@ impl<'py> Python<'py> {
}

/// Imports the Python module with the specified name.
pub fn import(self, name: &str) -> PyResult<&'py PyModule> {
pub fn import<N>(self, name: N) -> PyResult<&'py PyModule>
where
N: IntoPy<Py<PyString>>,
{
PyModule::import(self, name)
}

Expand Down

0 comments on commit dce4377

Please sign in to comment.