diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index a3a4e1f0c7d..b471f5dd3ae 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -19,6 +19,7 @@ | `rename_all = "renaming_rule"` | Applies renaming rules to every getters and setters of a struct, or every variants of an enum. Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". | | `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. | | `set_all` | Generates setters for all fields of the pyclass. | +| `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str=""`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* | | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | | `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | | `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. | diff --git a/guide/src/class/object.md b/guide/src/class/object.md index e9ea549aab4..e2565427838 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -70,6 +70,46 @@ impl Number { } ``` +To automatically generate the `__str__` implementation using a `Display` trait implementation, pass the `str` argument to `pyclass`. + +```rust +# use std::fmt::{Display, Formatter}; +# use pyo3::prelude::*; +# +# #[pyclass(str)] +# struct Coordinate { + x: i32, + y: i32, + z: i32, +} + +impl Display for Coordinate { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} +``` + +For convenience, a shorthand format string can be passed to `str` as `str=""` for **structs only**. It expands and is passed into the `format!` macro in the following ways: + +* `"{x}"` -> `"{}", self.x` +* `"{0}"` -> `"{}", self.0` +* `"{x:?}"` -> `"{:?}", self.x` + +*Note: Depending upon the format string you use, this may require implementation of the `Display` or `Debug` traits for the given Rust types.* +*Note: the pyclass args `name` and `rename_all` are incompatible with the shorthand format string and will raise a compile time error.* + +```rust +# use pyo3::prelude::*; +# +# #[pyclass(str="({x}, {y}, {z})")] +# struct Coordinate { + x: i32, + y: i32, + z: i32, +} +``` + #### Accessing the class name In the `__repr__`, we used a hard-coded class name. This is sometimes not ideal, diff --git a/newsfragments/4233.added.md b/newsfragments/4233.added.md new file mode 100644 index 00000000000..cd45d163951 --- /dev/null +++ b/newsfragments/4233.added.md @@ -0,0 +1 @@ +Added `#[pyclass(str="")]` option to generate `__str__` based on a `Display` implementation or format string. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 6a45ee875e3..780ad7035f0 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,12 +1,13 @@ use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::{quote, ToTokens}; +use syn::parse::Parser; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, token::Comma, - Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, Index, LitStr, Member, Path, Result, Token, }; pub mod kw { @@ -36,6 +37,7 @@ pub mod kw { syn::custom_keyword!(set); syn::custom_keyword!(set_all); syn::custom_keyword!(signature); + syn::custom_keyword!(str); syn::custom_keyword!(subclass); syn::custom_keyword!(submodule); syn::custom_keyword!(text_signature); @@ -44,12 +46,137 @@ pub mod kw { syn::custom_keyword!(weakref); } +fn take_int(read: &mut &str, tracker: &mut usize) -> String { + let mut int = String::new(); + for (i, ch) in read.char_indices() { + match ch { + '0'..='9' => { + *tracker += 1; + int.push(ch) + } + _ => { + *read = &read[i..]; + break; + } + } + } + int +} + +fn take_ident(read: &mut &str, tracker: &mut usize) -> Ident { + let mut ident = String::new(); + if read.starts_with("r#") { + ident.push_str("r#"); + *tracker += 2; + *read = &read[2..]; + } + for (i, ch) in read.char_indices() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { + *tracker += 1; + ident.push(ch) + } + _ => { + *read = &read[i..]; + break; + } + } + } + Ident::parse_any.parse_str(&ident).unwrap() +} + +// shorthand parsing logic inspiration taken from https://github.com/dtolnay/thiserror/blob/master/impl/src/fmt.rs +fn parse_shorthand_format(fmt: LitStr) -> Result<(LitStr, Vec)> { + let span = fmt.span(); + let token = fmt.token(); + let value = fmt.value(); + let mut read = value.as_str(); + let mut out = String::new(); + let mut members = Vec::new(); + let mut tracker = 1; + while let Some(brace) = read.find('{') { + tracker += brace; + out += &read[..brace + 1]; + read = &read[brace + 1..]; + if read.starts_with('{') { + out.push('{'); + read = &read[1..]; + tracker += 2; + continue; + } + let next = match read.chars().next() { + Some(next) => next, + None => break, + }; + tracker += 1; + let member = match next { + '0'..='9' => { + let start = tracker; + let index = take_int(&mut read, &mut tracker).parse::().unwrap(); + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + let idx = Index { + index, + span: subspan, + }; + Member::Unnamed(idx) + } + 'a'..='z' | 'A'..='Z' | '_' => { + let start = tracker; + let mut ident = take_ident(&mut read, &mut tracker); + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + ident.set_span(subspan); + Member::Named(ident) + } + '}' | ':' => { + let start = tracker; + tracker += 1; + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + // we found a closing bracket or formatting ':' without finding a member, we assume the user wants the instance formatted here + bail_spanned!(subspan.span() => "No member found, you must provide a named or positionally specified member.") + } + _ => continue, + }; + members.push(member); + } + out += read; + Ok((LitStr::new(&out, span), members)) +} + +#[derive(Clone, Debug)] +pub struct StringFormatter { + pub fmt: LitStr, + pub args: Vec, +} + +impl Parse for crate::attributes::StringFormatter { + fn parse(input: ParseStream<'_>) -> Result { + let (fmt, args) = parse_shorthand_format(input.parse()?)?; + Ok(Self { fmt, args }) + } +} + +impl ToTokens for crate::attributes::StringFormatter { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.fmt.to_tokens(tokens); + tokens.extend(quote! {self.args}) + } +} + #[derive(Clone, Debug)] pub struct KeywordAttribute { pub kw: K, pub value: V, } +#[derive(Clone, Debug)] +pub struct OptionalKeywordAttribute { + pub kw: K, + pub value: Option, +} + /// A helper type which parses the inner type via a literal string /// e.g. `LitStrValue` -> parses "some::path" in quotes. #[derive(Clone, Debug, PartialEq, Eq)] @@ -178,6 +305,7 @@ pub type FreelistAttribute = KeywordAttribute>; pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; pub type RenameAllAttribute = KeywordAttribute; +pub type StrFormatterAttribute = OptionalKeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; @@ -198,6 +326,27 @@ impl ToTokens for KeywordAttribute { } } +impl Parse for OptionalKeywordAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let kw: K = input.parse()?; + let value = match input.parse::() { + Ok(_) => Some(input.parse()?), + Err(_) => None, + }; + Ok(OptionalKeywordAttribute { kw, value }) + } +} + +impl ToTokens for OptionalKeywordAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.kw.to_tokens(tokens); + if self.value.is_some() { + Token![=](self.kw.span()).to_tokens(tokens); + self.value.to_tokens(tokens); + } + } +} + pub type FromPyWithAttribute = KeywordAttribute>; /// For specifying the path to the pyo3 crate. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 125fdab4927..dd1b023149f 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,16 +1,17 @@ use std::borrow::Cow; +use std::fmt::Debug; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, Result, Token}; +use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result, Token}; use crate::attributes::kw::frozen; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, + ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -18,7 +19,7 @@ use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, + SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__, }; use crate::pyversions; use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc}; @@ -74,6 +75,7 @@ pub struct PyClassPyO3Options { pub rename_all: Option, pub sequence: Option, pub set_all: Option, + pub str: Option, pub subclass: Option, pub unsendable: Option, pub weakref: Option, @@ -96,6 +98,7 @@ pub enum PyClassPyO3Option { RenameAll(RenameAllAttribute), Sequence(kw::sequence), SetAll(kw::set_all), + Str(StrFormatterAttribute), Subclass(kw::subclass), Unsendable(kw::unsendable), Weakref(kw::weakref), @@ -136,6 +139,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Sequence) } else if lookahead.peek(attributes::kw::set_all) { input.parse().map(PyClassPyO3Option::SetAll) + } else if lookahead.peek(attributes::kw::str) { + input.parse().map(PyClassPyO3Option::Str) } else if lookahead.peek(attributes::kw::subclass) { input.parse().map(PyClassPyO3Option::Subclass) } else if lookahead.peek(attributes::kw::unsendable) { @@ -205,6 +210,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all), PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), + PyClassPyO3Option::Str(str) => set_option!(str), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), PyClassPyO3Option::Weakref(weakref) => { @@ -387,6 +393,19 @@ fn impl_class( let Ctx { pyo3_path, .. } = ctx; let pytypeinfo_impl = impl_pytypeinfo(cls, args, ctx); + if let Some(str) = &args.options.str { + if str.value.is_some() { + // check if any renaming is present + let no_naming_conflict = field_options.iter().all(|x| x.1.name.is_none()) + & args.options.name.is_none() + & args.options.rename_all.is_none(); + ensure_spanned!(no_naming_conflict, str.value.span() => "The format string syntax is incompatible with any renaming via `name` or `rename_all`"); + } + } + + let (default_str, default_str_slot) = + implement_pyclass_str(&args.options, &syn::parse_quote!(#cls), ctx); + let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; @@ -396,6 +415,7 @@ fn impl_class( let mut slots = Vec::new(); slots.extend(default_richcmp_slot); slots.extend(default_hash_slot); + slots.extend(default_str_slot); let py_class_impl = PyClassImplsBuilder::new( cls, @@ -425,6 +445,7 @@ fn impl_class( impl #cls { #default_richcmp #default_hash + #default_str } }) } @@ -753,6 +774,60 @@ impl EnumVariantPyO3Options { } } +// todo(remove this dead code allowance once __repr__ is implemented +#[allow(dead_code)] +pub enum PyFmtName { + Str, + Repr, +} + +fn implement_py_formatting( + ty: &syn::Type, + ctx: &Ctx, + option: &StrFormatterAttribute, +) -> (ImplItemFn, MethodAndSlotDef) { + let mut fmt_impl = match &option.value { + Some(opt) => { + let fmt = &opt.fmt; + let args = &opt + .args + .iter() + .map(|member| quote! {self.#member}) + .collect::>(); + let fmt_impl: ImplItemFn = syn::parse_quote! { + fn __pyo3__generated____str__(&self) -> ::std::string::String { + ::std::format!(#fmt, #(#args, )*) + } + }; + fmt_impl + } + None => { + let fmt_impl: syn::ImplItemFn = syn::parse_quote! { + fn __pyo3__generated____str__(&self) -> ::std::string::String { + ::std::format!("{}", &self) + } + }; + fmt_impl + } + }; + let fmt_slot = generate_protocol_slot(ty, &mut fmt_impl, &__STR__, "__str__", ctx).unwrap(); + (fmt_impl, fmt_slot) +} + +fn implement_pyclass_str( + options: &PyClassPyO3Options, + ty: &syn::Type, + ctx: &Ctx, +) -> (Option, Option) { + match &options.str { + Some(option) => { + let (default_str, default_str_slot) = implement_py_formatting(ty, ctx, option); + (Some(default_str), Some(default_str_slot)) + } + _ => (None, None), + } +} + fn impl_enum( enum_: PyClassEnum<'_>, args: &PyClassArgs, @@ -760,6 +835,10 @@ fn impl_enum( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> Result { + if let Some(str_fmt) = &args.options.str { + ensure_spanned!(str_fmt.value.is_none(), str_fmt.value.span() => "The format string syntax cannot be used with enums") + } + match enum_ { PyClassEnum::Simple(simple_enum) => { impl_simple_enum(simple_enum, args, doc, methods_type, ctx) @@ -809,6 +888,8 @@ fn impl_simple_enum( (repr_impl, repr_slot) }; + let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx); + let repr_type = &simple_enum.repr_type; let (default_int, default_int_slot) = { @@ -835,6 +916,7 @@ fn impl_simple_enum( let mut default_slots = vec![default_repr_slot, default_int_slot]; default_slots.extend(default_richcmp_slot); default_slots.extend(default_hash_slot); + default_slots.extend(default_str_slot); let pyclass_impls = PyClassImplsBuilder::new( cls, @@ -862,6 +944,7 @@ fn impl_simple_enum( #default_int #default_richcmp #default_hash + #default_str } }) } @@ -895,9 +978,12 @@ fn impl_complex_enum( let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; + let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx); + let mut default_slots = vec![]; default_slots.extend(default_richcmp_slot); default_slots.extend(default_hash_slot); + default_slots.extend(default_str_slot); let impl_builder = PyClassImplsBuilder::new( cls, @@ -1010,6 +1096,7 @@ fn impl_complex_enum( impl #cls { #default_richcmp #default_hash + #default_str } #(#variant_cls_zsts)* diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 150c29ae64f..77cc9ed5cc6 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -902,7 +902,7 @@ impl PropertyType<'_> { } } -const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); +pub const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); pub const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index dcd347fb5f2..4270da34be3 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -91,3 +91,25 @@ pub enum TupleEnumEqOrd { Variant1(u32, u32), Variant2(u32), } + +#[crate::pyclass(str = "{x}, {y}, {z}")] +#[pyo3(crate = "crate")] +pub struct PointFmt { + x: u32, + y: u32, + z: u32, +} + +#[crate::pyclass(str)] +#[pyo3(crate = "crate")] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +impl ::std::fmt::Display for Point { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::std::write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} diff --git a/tests/test_class_formatting.rs b/tests/test_class_formatting.rs new file mode 100644 index 00000000000..10f760b9c4a --- /dev/null +++ b/tests/test_class_formatting.rs @@ -0,0 +1,179 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; +use pyo3::py_run; +use std::fmt::{Display, Formatter}; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass(eq, str)] +#[derive(Debug, PartialEq)] +pub enum MyEnum2 { + Variant, + OtherVariant, +} + +impl Display for MyEnum2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(eq, str)] +#[derive(Debug, PartialEq)] +pub enum MyEnum3 { + #[pyo3(name = "AwesomeVariant")] + Variant, + OtherVariant, +} + +impl Display for MyEnum3 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let variant = match self { + MyEnum3::Variant => "AwesomeVariant", + MyEnum3::OtherVariant => "OtherVariant", + }; + write!(f, "MyEnum.{}", variant) + } +} + +#[test] +fn test_enum_class_fmt() { + Python::with_gil(|py| { + let var2 = Py::new(py, MyEnum2::Variant).unwrap(); + let var3 = Py::new(py, MyEnum3::Variant).unwrap(); + let var4 = Py::new(py, MyEnum3::OtherVariant).unwrap(); + py_assert!(py, var2, "str(var2) == 'Variant'"); + py_assert!(py, var3, "str(var3) == 'MyEnum.AwesomeVariant'"); + py_assert!(py, var4, "str(var4) == 'MyEnum.OtherVariant'"); + }) +} + +#[pyclass(str = "X: {x}, Y: {y}, Z: {z}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[test] +fn test_custom_struct_custom_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point { x: 1, y: 2, z: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == 'X: 1, Y: 2, Z: 3'"); + }) +} + +#[pyclass(str)] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point2 { + x: i32, + y: i32, + z: i32, +} + +impl Display for Point2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} + +#[test] +fn test_struct_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point2 { x: 1, y: 2, z: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == '(1, 2, 3)'"); + }) +} + +#[pyclass(str)] +#[derive(PartialEq, Debug)] +enum ComplexEnumWithStr { + A(u32), + B { msg: String }, +} + +impl Display for ComplexEnumWithStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[test] +fn test_custom_complex_enum_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, ComplexEnumWithStr::A(45)).unwrap(); + let var2 = Py::new( + py, + ComplexEnumWithStr::B { + msg: "Hello".to_string(), + }, + ) + .unwrap(); + py_assert!(py, var1, "str(var1) == 'A(45)'"); + py_assert!(py, var2, "str(var2) == 'B { msg: \"Hello\" }'"); + }) +} + +#[pyclass(str = "{0}, {1}, {2}")] +#[derive(PartialEq)] +struct Coord(u32, u32, u32); + +#[pyclass(str = "{{{0}, {1}, {2}}}")] +#[derive(PartialEq)] +struct Coord2(u32, u32, u32); + +#[test] +fn test_str_representation_by_position() { + Python::with_gil(|py| { + let var1 = Py::new(py, Coord(1, 2, 3)).unwrap(); + let var2 = Py::new(py, Coord2(1, 2, 3)).unwrap(); + py_assert!(py, var1, "str(var1) == '1, 2, 3'"); + py_assert!(py, var2, "str(var2) == '{1, 2, 3}'"); + }) +} + +#[pyclass(str = "name: {name}: {name}, idn: {idn:03} with message: {msg}")] +#[derive(PartialEq, Debug)] +struct Point4 { + name: String, + msg: String, + idn: u32, +} + +#[test] +fn test_mixed_and_repeated_str_formats() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Point4 { + name: "aaa".to_string(), + msg: "hello".to_string(), + idn: 1, + }, + ) + .unwrap(); + py_run!( + py, + var1, + r#" + assert str(var1) == 'name: aaa: aaa, idn: 001 with message: hello' + "# + ); + }) +} + +#[pyclass(str = "type: {r#type}")] +struct Foo { + r#type: u32, +} + +#[test] +fn test_raw_identifier_struct_custom_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Foo { r#type: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == 'type: 3'"); + }) +} diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index f74fa49d8de..c39deab47bc 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use pyo3::prelude::*; #[pyclass(extend=pyo3::types::PyDict)] @@ -76,4 +77,101 @@ struct InvalidOrderedStruct { inner: i32 } +#[pyclass(str)] +struct StrOptAndManualStr {} + +impl Display for StrOptAndManualStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +#[pymethods] +impl StrOptAndManualStr { + fn __str__( + &self, + ) -> String { + todo!() + } +} + +#[pyclass(str = "{")] +#[derive(PartialEq)] +struct Coord(u32, u32, u32); + +#[pyclass(str = "{$}")] +#[derive(PartialEq)] +struct Coord2(u32, u32, u32); + +#[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point2 { + x: i32, + y: i32, + z: i32, +} + +#[pyclass(str = "{0}, {162543}, {2}")] +#[derive(PartialEq)] +struct Coord3(u32, u32, u32); + +#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter2 { + unsafe_variable: usize, +} + +#[pyclass(str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter3 { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] +struct RenameAllVariantsStruct { + a_a: u32, + b_b: u32, + c_d_e: String, +} + +#[pyclass(str="{:?}")] +#[derive(Debug)] +struct StructWithNoMember { + a: String, + b: String, +} + +#[pyclass(str="{}")] +#[derive(Debug)] +struct StructWithNoMember2 { + a: String, + b: String, +} + +#[pyclass(eq, str="Stuff...")] +#[derive(Debug, PartialEq)] +pub enum MyEnumInvalidStrFmt { + Variant, + OtherVariant, +} + +impl Display for MyEnumInvalidStrFmt { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 23d3c3bbc64..6faeca51502 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,223 +1,356 @@ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` - --> tests/ui/invalid_pyclass_args.rs:3:11 +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` + --> tests/ui/invalid_pyclass_args.rs:4:11 | -3 | #[pyclass(extend=pyo3::types::PyDict)] +4 | #[pyclass(extend=pyo3::types::PyDict)] | ^^^^^^ error: expected identifier - --> tests/ui/invalid_pyclass_args.rs:6:21 + --> tests/ui/invalid_pyclass_args.rs:7:21 | -6 | #[pyclass(extends = "PyDict")] +7 | #[pyclass(extends = "PyDict")] | ^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:9:18 - | -9 | #[pyclass(name = m::MyClass)] - | ^ + --> tests/ui/invalid_pyclass_args.rs:10:18 + | +10 | #[pyclass(name = m::MyClass)] + | ^ error: expected a single identifier in double quotes - --> tests/ui/invalid_pyclass_args.rs:12:18 + --> tests/ui/invalid_pyclass_args.rs:13:18 | -12 | #[pyclass(name = "Custom Name")] +13 | #[pyclass(name = "Custom Name")] | ^^^^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:15:18 + --> tests/ui/invalid_pyclass_args.rs:16:18 | -15 | #[pyclass(name = CustomName)] +16 | #[pyclass(name = CustomName)] | ^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:18:24 + --> tests/ui/invalid_pyclass_args.rs:19:24 | -18 | #[pyclass(rename_all = camelCase)] +19 | #[pyclass(rename_all = camelCase)] | ^^^^^^^^^ error: expected a valid renaming rule, possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE" - --> tests/ui/invalid_pyclass_args.rs:21:24 + --> tests/ui/invalid_pyclass_args.rs:22:24 | -21 | #[pyclass(rename_all = "Camel-Case")] +22 | #[pyclass(rename_all = "Camel-Case")] | ^^^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:24:20 + --> tests/ui/invalid_pyclass_args.rs:25:20 | -24 | #[pyclass(module = my_module)] +25 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` - --> tests/ui/invalid_pyclass_args.rs:27:11 +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` + --> tests/ui/invalid_pyclass_args.rs:28:11 | -27 | #[pyclass(weakrev)] +28 | #[pyclass(weakrev)] | ^^^^^^^ error: a `#[pyclass]` cannot be both a `mapping` and a `sequence` - --> tests/ui/invalid_pyclass_args.rs:31:8 + --> tests/ui/invalid_pyclass_args.rs:32:8 | -31 | struct CannotBeMappingAndSequence {} +32 | struct CannotBeMappingAndSequence {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: `eq_int` can only be used on simple enums. - --> tests/ui/invalid_pyclass_args.rs:52:11 + --> tests/ui/invalid_pyclass_args.rs:53:11 | -52 | #[pyclass(eq_int)] +53 | #[pyclass(eq_int)] | ^^^^^^ error: The `hash` option requires the `frozen` option. - --> tests/ui/invalid_pyclass_args.rs:59:11 + --> tests/ui/invalid_pyclass_args.rs:60:11 | -59 | #[pyclass(hash)] +60 | #[pyclass(hash)] | ^^^^ error: The `hash` option requires the `eq` option. - --> tests/ui/invalid_pyclass_args.rs:59:11 + --> tests/ui/invalid_pyclass_args.rs:60:11 | -59 | #[pyclass(hash)] +60 | #[pyclass(hash)] | ^^^^ error: The `ord` option requires the `eq` option. - --> tests/ui/invalid_pyclass_args.rs:74:11 + --> tests/ui/invalid_pyclass_args.rs:75:11 | -74 | #[pyclass(ord)] +75 | #[pyclass(ord)] | ^^^ +error: invalid format string: expected `'}'` but string was terminated + --> tests/ui/invalid_pyclass_args.rs:98:19 + | +98 | #[pyclass(str = "{")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` + +error: invalid format string: expected `'}'`, found `'$'` + --> tests/ui/invalid_pyclass_args.rs:102:19 + | +102 | #[pyclass(str = "{$}")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:126:29 + | +126 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:132:29 + | +132 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:137:15 + | +137 | #[pyclass(str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:143:52 + | +143 | #[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: No member found, you must provide a named or positionally specified member. + --> tests/ui/invalid_pyclass_args.rs:150:15 + | +150 | #[pyclass(str="{:?}")] + | ^^^^^^ + +error: No member found, you must provide a named or positionally specified member. + --> tests/ui/invalid_pyclass_args.rs:157:15 + | +157 | #[pyclass(str="{}")] + | ^^^^ + +error: The format string syntax cannot be used with enums + --> tests/ui/invalid_pyclass_args.rs:164:19 + | +164 | #[pyclass(eq, str="Stuff...")] + | ^^^^^^^^^^ + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___richcmp____` ... -40 | #[pymethods] +41 | #[pymethods] | ------------ other definition for `__pymethod___richcmp____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod___hash____` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___hash____` ... -67 | #[pymethods] +68 | #[pymethods] | ------------ other definition for `__pymethod___hash____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0592]: duplicate definitions with name `__pymethod___str____` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___str____` +... +89 | #[pymethods] + | ------------ other definition for `__pymethod___str____` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0369]: binary operation `==` cannot be applied to type `&EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:33:11 + --> tests/ui/invalid_pyclass_args.rs:34:11 | -33 | #[pyclass(eq)] +34 | #[pyclass(eq)] | ^^ | note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:34:1 + --> tests/ui/invalid_pyclass_args.rs:35:1 | -34 | struct EqOptRequiresEq {} +35 | struct EqOptRequiresEq {} | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` | -34 + #[derive(PartialEq)] -35 | struct EqOptRequiresEq {} +35 + #[derive(PartialEq)] +36 | struct EqOptRequiresEq {} | error[E0369]: binary operation `!=` cannot be applied to type `&EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:33:11 + --> tests/ui/invalid_pyclass_args.rs:34:11 | -33 | #[pyclass(eq)] +34 | #[pyclass(eq)] | ^^ | note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:34:1 + --> tests/ui/invalid_pyclass_args.rs:35:1 | -34 | struct EqOptRequiresEq {} +35 | struct EqOptRequiresEq {} | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` | -34 + #[derive(PartialEq)] -35 | struct EqOptRequiresEq {} +35 + #[derive(PartialEq)] +36 | struct EqOptRequiresEq {} | error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found | note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found | note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `HashOptRequiresHash: Hash` is not satisfied - --> tests/ui/invalid_pyclass_args.rs:55:23 + --> tests/ui/invalid_pyclass_args.rs:56:23 | -55 | #[pyclass(frozen, eq, hash)] +56 | #[pyclass(frozen, eq, hash)] | ^^^^ the trait `Hash` is not implemented for `HashOptRequiresHash` | help: consider annotating `HashOptRequiresHash` with `#[derive(Hash)]` | -57 + #[derive(Hash)] -58 | struct HashOptRequiresHash; +58 + #[derive(Hash)] +59 | struct HashOptRequiresHash; | error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ multiple `__pymethod___hash____` found | note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 | -67 | #[pymethods] +68 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 | -67 | #[pymethods] +68 | #[pymethods] | ^^^^^^^^^^^^ multiple `__pymethod___hash____` found | note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 + | +68 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ multiple `__pymethod___str____` found + | +note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___str____` found | -67 | #[pymethods] +note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0609]: no field `aaaa` on type `&Point` + --> tests/ui/invalid_pyclass_args.rs:106:17 + | +106 | #[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `x`, `y`, `z` + +error[E0609]: no field `zzz` on type `&Point2` + --> tests/ui/invalid_pyclass_args.rs:114:17 + | +114 | #[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `x`, `y`, `z` + +error[E0609]: no field `162543` on type `&Coord3` + --> tests/ui/invalid_pyclass_args.rs:122:17 + | +122 | #[pyclass(str = "{0}, {162543}, {2}")] + | ^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `0`, `1`, `2`