From 5ac5cef965ed50ba8e0a17e8cfed31dc5bd22072 Mon Sep 17 00:00:00 2001 From: Michael Gilbert Date: Wed, 17 Jul 2024 15:39:04 -0700 Subject: [PATCH] Trait ergonomics str implementation (#4233) * feat: Implement custom string formatting for PyClass This update brings custom string formatting for PyClass with a #[pyclass(str = "format string")] attribute. It allows users to specify how their PyClass objects are converted to string in Python. The implementation includes additional tests and parsing logic. * update: removed debug print statements * update: added members to ToTokens implementation. * update: reverted to display * update: initial tests * update: made STR public for pyclass default implementations * update: generalizing str implementation * update: remove redundant test * update: implemented compile test to validate that manually implemented str is not allowed when automated str is requested * update: updated compile time error check * update: rename test file and code cleanup * update: format cleanup * update: added news fragment * fix: corrected clippy findings * update: fixed mixed formatting case and improved test coverage * update: improved test coverage * refactor: generalized formatting function to accommodate __repr__ in a future implementation since it will use the same shorthand formatting logic * update: Add support for rename formatting in PyEnum3 Implemented the capacity to handle renamed variants in enum string representation. Now, custom Python names for enum variants will be correctly reflected when calling the __str__() method on an enum instance. Additionally, the related test has been updated to reflect this change. * fix: fixed clippy finding * update: fixed test function names * Update pyo3-macros-backend/src/pyclass.rs Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * Update newsfragments/4233.added.md Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * update: implemented hygienic calls and added hygiene tests. * update: cargo fmt * update: retained LitStr usage in the quote in order to preserve a more targeted span for the format string. * update: retained LitStr usage in the quote in order to preserve a more targeted span for the format string. * update: added compile time error check for invalid fields (looking to reduce span of invalid member) * update: implemented a subspan to improve errors in format string on nightly, verified additional test cases on both nightly and stable * update: updated test output * update: updated with clippy findings * update: added doc entries. * update: corrected error output for compile errors after updating from main. * update: added support for raw identifiers used in field names * update: aligning branch with main * update: added compile time error when mixing rename_all or name pyclass, field, or variant args when mixed with a str shorthand formatter. * update: removed self option from str format shorthand, restricted str shorthand format to structs only, updated docs with changes, refactored renaming incompatibility check with str shorthand. * update: removed checks for shorthand and renaming for enums and simplified back to inline check for structs * update: added additional test case to increase coverage in match branch * fix: updated pyclass heighten check to validate for eq and ord, fixing Ok issue in eq implementation. * Revert "fix: updated pyclass heighten check to validate for eq and ord, fixing Ok issue in eq implementation." This reverts commit a37c24bce6c263bd553d08ef94ecf52c3026ebfc. * update: improved error comments, naming, and added reference to the PR for additional details regarding the implementation of `str` * update: fixed merge conflict --------- Co-authored-by: Michael Gilbert Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: MG --- guide/pyclass-parameters.md | 1 + guide/src/class/object.md | 40 ++++ newsfragments/4233.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 153 +++++++++++++- pyo3-macros-backend/src/pyclass.rs | 93 ++++++++- pyo3-macros-backend/src/pymethod.rs | 2 +- src/tests/hygiene/pyclass.rs | 22 ++ tests/test_class_formatting.rs | 179 ++++++++++++++++ tests/ui/invalid_pyclass_args.rs | 98 +++++++++ tests/ui/invalid_pyclass_args.stderr | 289 +++++++++++++++++++------- 10 files changed, 794 insertions(+), 84 deletions(-) create mode 100644 newsfragments/4233.added.md create mode 100644 tests/test_class_formatting.rs 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`