Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
ricohageman authored May 6, 2023
2 parents 8ff97aa + 1cdac4f commit 0bd9e7b
Show file tree
Hide file tree
Showing 30 changed files with 581 additions and 274 deletions.
27 changes: 17 additions & 10 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,22 +722,20 @@ py_args=(), py_kwargs=None, name=World, num=-1, num_before=44

## Making class method signatures available to Python

The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods:
The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for `#[pymethods]`:

```rust
# #![allow(dead_code)]
use pyo3::prelude::*;
use pyo3::types::PyType;

// it works even if the item is not documented:
#[pyclass(text_signature = "(c, d, /)")]
#[pyclass]
struct MyClass {}

#[pymethods]
impl MyClass {
// the signature for the constructor is attached
// to the struct definition instead.
#[new]
#[pyo3(text_signature = "(c, d)")]
fn new(c: i32, d: &str) -> Self {
Self {}
}
Expand All @@ -746,8 +744,9 @@ impl MyClass {
fn my_method(&self, e: i32, f: i32) -> i32 {
e + f
}
// similarly for classmethod arguments, use $cls
#[classmethod]
#[pyo3(text_signature = "(cls, e, f)")]
#[pyo3(text_signature = "($cls, e, f)")]
fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
e + f
}
Expand All @@ -773,7 +772,7 @@ impl MyClass {
# .call1((class,))?
# .call_method0("__str__")?
# .extract()?;
# assert_eq!(sig, "(c, d, /)");
# assert_eq!(sig, "(c, d)");
# } else {
# let doc: String = class.getattr("__doc__")?.extract()?;
# assert_eq!(doc, "");
Expand Down Expand Up @@ -802,7 +801,7 @@ impl MyClass {
# .call1((method,))?
# .call_method0("__str__")?
# .extract()?;
# assert_eq!(sig, "(cls, e, f)");
# assert_eq!(sig, "(e, f)"); // inspect.signature skips the $cls arg
# }
#
# {
Expand All @@ -822,7 +821,7 @@ impl MyClass {
# }
```

Note that `text_signature` on classes is not compatible with compilation in
Note that `text_signature` on `#[new]` is not compatible with compilation in
`abi3` mode until Python 3.10 or greater.

## #[pyclass] enums
Expand Down Expand Up @@ -1038,7 +1037,6 @@ impl pyo3::IntoPy<PyObject> for MyClass {
}

impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
const DOC: &'static str = "Class for demonstration\u{0}";
const IS_BASETYPE: bool = false;
const IS_SUBCLASS: bool = false;
type Layout = PyCell<MyClass>;
Expand All @@ -1061,6 +1059,15 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
static TYPE_OBJECT: LazyTypeObject<MyClass> = LazyTypeObject::new();
&TYPE_OBJECT
}

fn doc(py: Python<'_>) -> pyo3::PyResult<&'static ::std::ffi::CStr> {
use pyo3::impl_::pyclass::*;
static DOC: pyo3::once_cell::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::once_cell::GILOnceCell::new();
DOC.get_or_try_init(py, || {
let collector = PyClassImplCollector::<Self>::new();
build_pyclass_doc(<MyClass as pyo3::PyTypeInfo>::NAME, "", None.or_else(|| collector.new_text_signature()))
}).map(::std::ops::Deref::deref)
}
}

# Python::with_gil(|py| {
Expand Down
6 changes: 2 additions & 4 deletions guide/src/class/numeric.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
Ok(val as i32)
}
```
We also add documentation, via `///` comments and the `#[pyo3(text_signature = "...")]` attribute, both of which are visible to Python users.
We also add documentation, via `///` comments, which are visible to Python users.

```rust
# #![allow(dead_code)]
Expand All @@ -57,7 +57,6 @@ fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.
/// It's not a story C would tell you. It's a Rust legend.
#[pyclass(module = "my_module")]
#[pyo3(text_signature = "(int)")]
struct Number(i32);

#[pymethods]
Expand Down Expand Up @@ -223,7 +222,6 @@ fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
/// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not.
/// It's not a story C would tell you. It's a Rust legend.
#[pyclass(module = "my_module")]
#[pyo3(text_signature = "(int)")]
struct Number(i32);

#[pymethods]
Expand Down Expand Up @@ -377,7 +375,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
# assert Number(12345234523452) == Number(1498514748)
# try:
# import inspect
# assert inspect.signature(Number).__str__() == '(int)'
# assert inspect.signature(Number).__str__() == '(value)'
# except ValueError:
# # Not supported with `abi3` before Python 3.10
# pass
Expand Down
2 changes: 1 addition & 1 deletion guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,6 @@ impl ClassWithGCSupport {

> Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3899).
[`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/class/iter/enum.IterNextOutput.html
[`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.IterNextOutput.html
[`PySequence`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PySequence.html
[`CompareOp::matches`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.CompareOp.html#method.matches
29 changes: 29 additions & 0 deletions guide/src/conversions/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ from a mapping with the key `"key"`. The arguments for `attribute` are restricte
non-empty string literals while `item` can take any valid literal that implements
`ToBorrowedObject`.

You can use `#[pyo3(from_item_all)]` on a struct to extract every field with `get_item` method.
In this case, you can't use `#[pyo3(attribute)]` or barely use `#[pyo3(item)]` on any field.
However, using `#[pyo3(item("key"))]` to specify the key for a field is still allowed.

```rust
use pyo3::prelude::*;

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct RustyStruct {
foo: String,
bar: String,
#[pyo3(item("foobar"))]
baz: String,
}
#
# fn main() -> PyResult<()> {
# Python::with_gil(|py| -> PyResult<()> {
# let py_dict = py.eval("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?;
# let rustystruct: RustyStruct = py_dict.extract()?;
# assert_eq!(rustystruct.foo, "foo");
# assert_eq!(rustystruct.bar, "bar");
# assert_eq!(rustystruct.baz, "foobar");
#
# Ok(())
# })
# }
```

#### Deriving [`FromPyObject`] for tuple structs

Tuple structs are also supported but do not allow customizing the extraction. The input is
Expand Down
4 changes: 1 addition & 3 deletions guide/src/function/signature.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,7 @@ impl MyClass {

The function signature is exposed to Python via the `__text_signature__` attribute. PyO3 automatically generates this for every `#[pyfunction]` and all `#[pymethods]` directly from the Rust function, taking into account any override done with the `#[pyo3(signature = (...))]` option.

This automatic generation has some limitations, which may be improved in the future:
- It will not include the value of default arguments, replacing them all with `...`. (`.pyi` type stub files commonly also use `...` for all default arguments in the same way.)
- Nothing is generated for the `#[new]` method of a `#[pyclass]`.
This automatic generation can only display the value of default arguments for strings, integers, boolean types, and `None`. Any other default arguments will be displayed as `...`. (`.pyi` type stub files commonly also use `...` for default arguments in the same way.)

In cases where the automatically-generated signature needs adjusting, it can [be overridden](#overriding-the-generated-signature) using the `#[pyo3(text_signature)]` option.)

Expand Down
1 change: 1 addition & 0 deletions newsfragments/2980.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support `text_signature` option (and automatically generate signature) for `#[new]` in `#[pymethods]`.
1 change: 1 addition & 0 deletions newsfragments/2980.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate `text_signature` option on `#[pyclass]`.
1 change: 1 addition & 0 deletions newsfragments/3120.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow using `#[pyo3(from_item_all)]` when deriving `FromPyObject` to specify `get_item` as getter for all fields.
1 change: 1 addition & 0 deletions newsfragments/3131.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extend the lifetime of the GIL token returned by `PyRef::py` and `PyRefMut::py` to match the underlying borrow.
1 change: 1 addition & 0 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod kw {
syn::custom_keyword!(get);
syn::custom_keyword!(get_all);
syn::custom_keyword!(item);
syn::custom_keyword!(from_item_all);
syn::custom_keyword!(mapping);
syn::custom_keyword!(module);
syn::custom_keyword!(name);
Expand Down
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote_spanned, ToTokens};

pub enum Deprecation {
PyClassTextSignature,
PyFunctionArguments,
PyMethodArgsAttribute,
RequiredArgumentAfterOption,
Expand All @@ -10,6 +11,7 @@ pub enum Deprecation {
impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::PyClassTextSignature => "PYCLASS_TEXT_SIGNATURE",
Deprecation::PyFunctionArguments => "PYFUNCTION_ARGUMENTS",
Deprecation::PyMethodArgsAttribute => "PYMETHODS_ARGS_ATTRIBUTE",
Deprecation::RequiredArgumentAfterOption => "REQUIRED_ARGUMENT_AFTER_OPTION",
Expand Down
33 changes: 31 additions & 2 deletions pyo3-macros-backend/src/frompyobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,22 @@ impl<'a> Container<'a> {
.ident
.as_ref()
.expect("Named fields should have identifiers");
let attrs = FieldPyO3Attributes::from_attrs(&field.attrs)?;
let mut attrs = FieldPyO3Attributes::from_attrs(&field.attrs)?;

if let Some(ref from_item_all) = options.from_item_all {
if let Some(replaced) = attrs.getter.replace(FieldGetter::GetItem(None))
{
match replaced {
FieldGetter::GetItem(Some(item_name)) => {
attrs.getter = Some(FieldGetter::GetItem(Some(item_name)));
}
FieldGetter::GetItem(None) => bail_spanned!(from_item_all.span() => "Useless `item` - the struct is already annotated with `from_item_all`"),
FieldGetter::GetAttr(_) => bail_spanned!(
from_item_all.span() => "The struct is already annotated with `from_item_all`, `attribute` is not allowed"
),
}
}
}

Ok(NamedStructField {
ident,
Expand Down Expand Up @@ -343,6 +358,8 @@ impl<'a> Container<'a> {
struct ContainerOptions {
/// Treat the Container as a Wrapper, directly extract its fields from the input object.
transparent: bool,
/// Force every field to be extracted from item of source Python object.
from_item_all: Option<attributes::kw::from_item_all>,
/// Change the name of an enum variant in the generated error message.
annotation: Option<syn::LitStr>,
/// Change the path for the pyo3 crate
Expand All @@ -353,6 +370,8 @@ struct ContainerOptions {
enum ContainerPyO3Attribute {
/// Treat the Container as a Wrapper, directly extract its fields from the input object.
Transparent(attributes::kw::transparent),
/// Force every field to be extracted from item of source Python object.
ItemAll(attributes::kw::from_item_all),
/// Change the name of an enum variant in the generated error message.
ErrorAnnotation(LitStr),
/// Change the path for the pyo3 crate
Expand All @@ -365,6 +384,9 @@ impl Parse for ContainerPyO3Attribute {
if lookahead.peek(attributes::kw::transparent) {
let kw: attributes::kw::transparent = input.parse()?;
Ok(ContainerPyO3Attribute::Transparent(kw))
} else if lookahead.peek(attributes::kw::from_item_all) {
let kw: attributes::kw::from_item_all = input.parse()?;
Ok(ContainerPyO3Attribute::ItemAll(kw))
} else if lookahead.peek(attributes::kw::annotation) {
let _: attributes::kw::annotation = input.parse()?;
let _: Token![=] = input.parse()?;
Expand Down Expand Up @@ -392,6 +414,13 @@ impl ContainerOptions {
);
options.transparent = true;
}
ContainerPyO3Attribute::ItemAll(kw) => {
ensure_spanned!(
matches!(options.from_item_all, None),
kw.span() => "`from_item_all` may only be provided once"
);
options.from_item_all = Some(kw);
}
ContainerPyO3Attribute::ErrorAnnotation(lit_str) => {
ensure_spanned!(
options.annotation.is_none(),
Expand Down Expand Up @@ -494,7 +523,7 @@ impl FieldPyO3Attributes {
getter.is_none(),
attr.span() => "only one of `attribute` or `item` can be provided"
);
getter = Some(field_getter)
getter = Some(field_getter);
}
FieldPyO3Attribute::FromPyWith(from_py_with_attr) => {
ensure_spanned!(
Expand Down
34 changes: 28 additions & 6 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2017-present PyO3 Project and Contributors

use crate::attributes::TextSignatureAttribute;
use crate::attributes::{TextSignatureAttribute, TextSignatureAttributeValue};
use crate::deprecations::{Deprecation, Deprecations};
use crate::params::impl_arg_params;
use crate::pyfunction::{DeprecatedArgs, FunctionSignature, PyFunctionArgPyO3Attributes};
Expand Down Expand Up @@ -593,6 +593,33 @@ impl<'a> FnSpec<'a> {
CallingConvention::TpNew => unreachable!("tp_new cannot get a methoddef"),
}
}

/// Forwards to [utils::get_doc] with the text signature of this spec.
pub fn get_doc(&self, attrs: &[syn::Attribute]) -> PythonDoc {
let text_signature = self
.text_signature_call_signature()
.map(|sig| format!("{}{}", self.python_name, sig));
utils::get_doc(attrs, text_signature)
}

/// Creates the parenthesised arguments list for `__text_signature__` snippet based on this spec's signature
/// and/or attributes. Prepend the callable name to make a complete `__text_signature__`.
pub fn text_signature_call_signature(&self) -> Option<String> {
let self_argument = match &self.tp {
// Getters / Setters / ClassAttribute are not callables on the Python side
FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None,
FnType::Fn(_) => Some("self"),
FnType::FnModule => Some("module"),
FnType::FnClass => Some("cls"),
FnType::FnStatic | FnType::FnNew => None,
};

match self.text_signature.as_ref().map(|attr| &attr.value) {
Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()),
None => Some(self.signature.text_signature(self_argument)),
Some(TextSignatureAttributeValue::Disabled(_)) => None,
}
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -755,11 +782,6 @@ fn ensure_signatures_on_valid_method(
}
if let Some(text_signature) = text_signature {
match fn_type {
FnType::FnNew => bail_spanned!(
text_signature.kw.span() =>
"`text_signature` not allowed on `__new__`; if you want to add a signature on \
`__new__`, put it on the struct definition instead"
),
FnType::Getter(_) => {
bail_spanned!(text_signature.kw.span() => "`text_signature` not allowed with `getter`")
}
Expand Down
Loading

0 comments on commit 0bd9e7b

Please sign in to comment.