Skip to content

Commit

Permalink
Merge pull request #2749 from PyO3/2748-fix
Browse files Browse the repository at this point in the history
Fix being able to call arg-less `#[new]` with any args from Python
  • Loading branch information
davidhewitt authored Nov 20, 2022
2 parents 6af4659 + 51eeb6d commit 8ca41be
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 7 deletions.
42 changes: 40 additions & 2 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Consult the table below to determine which type your constructor should return:

## Inheritance

By default, `PyAny` is used as the base class. To override this default,
By default, `object`, i.e. `PyAny` is used as the base class. To override this default,
use the `extends` parameter for `pyclass` with the full path to the base class.

For convenience, `(T, U)` implements `Into<PyClassInitializer<T>>` where `U` is the
Expand Down Expand Up @@ -310,7 +310,8 @@ impl SubSubClass {
```

You can also inherit native types such as `PyDict`, if they implement
[`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html). However, this is not supported when building for the Python limited API (aka the `abi3` feature of PyO3).
[`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html).
This is not supported when building for the Python limited API (aka the `abi3` feature of PyO3).

However, because of some technical problems, we don't currently provide safe upcasting methods for types
that inherit native types. Even in such cases, you can unsafely get a base class by raw pointer conversion.
Expand All @@ -334,6 +335,7 @@ impl DictWithCounter {
fn new() -> Self {
Self::default()
}

fn set(mut self_: PyRefMut<'_, Self>, key: String, value: &PyAny) -> PyResult<()> {
self_.counter.entry(key.clone()).or_insert(0);
let py = self_.py();
Expand Down Expand Up @@ -371,6 +373,42 @@ impl SubClass {
}
```

The `__new__` constructor of a native base class is called implicitly when
creating a new instance from Python. Be sure to accept arguments in the
`#[new]` method that you want the base class to get, even if they are not used
in that `fn`:

```rust
# #[allow(dead_code)]
# #[cfg(not(Py_LIMITED_API))] {
# use pyo3::prelude::*;
use pyo3::types::PyDict;

#[pyclass(extends=PyDict)]
struct MyDict {
private: i32,
}

#[pymethods]
impl MyDict {
#[new]
#[pyo3(signature = (*args, **kwargs))]
fn new(args: &PyAny, kwargs: Option<&PyAny>) -> Self {
Self { private: 0 }
}

// some custom methods that use `private` here...
}
# Python::with_gil(|py| {
# let cls = py.get_type::<MyDict>();
# pyo3::py_run!(py, cls, "cls(a=1, b=2)")
# });
# }
```

Here, the `args` and `kwargs` allow creating instances of the subclass passing
initial items, such as `MyDict(item_sequence)` or `MyDict(a=1, b=2)`.

## Object properties

PyO3 supports two ways to add properties to your `#[pyclass]`:
Expand Down
2 changes: 2 additions & 0 deletions newsfragments/2749.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a bug that allowed `#[new]` pymethods with no arguments to be called from
Python with any argument list.
4 changes: 0 additions & 4 deletions pyo3-macros-backend/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ pub fn impl_arg_params(
py: &syn::Ident,
fastcall: bool,
) -> Result<(TokenStream, Vec<TokenStream>)> {
if spec.signature.arguments.is_empty() {
return Ok((TokenStream::new(), vec![]));
}

let args_array = syn::Ident::new("output", Span::call_site());

if !fastcall && is_forwarded_args(&spec.signature) {
Expand Down
7 changes: 7 additions & 0 deletions tests/test_class_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

#[pyclass]
struct EmptyClassWithNew {}
Expand All @@ -23,6 +24,12 @@ fn empty_class_with_new() {
.unwrap()
.downcast::<PyCell<EmptyClassWithNew>>()
.is_ok());

// Calling with arbitrary args or kwargs is not ok
assert!(typeobj.call(("some", "args"), None).is_err());
assert!(typeobj
.call((), Some([("some", "kwarg")].into_py_dict(py)))
.is_err());
});
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test_inheritance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ mod inheriting_native_type {
#[pymethods]
impl CustomException {
#[new]
fn new() -> Self {
fn new(_exc_arg: &PyAny) -> Self {
CustomException {
context: "Hello :)",
}
Expand Down

0 comments on commit 8ca41be

Please sign in to comment.