Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Trait ergonomics str implementation #4233

Merged
merged 47 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8e7c8c6
feat: Implement custom string formatting for PyClass
Jun 4, 2024
a058296
update: removed debug print statements
Jun 4, 2024
0b4ddbc
update: added members to ToTokens implementation.
Jun 4, 2024
1f3bb49
update: reverted to display
Jun 4, 2024
ae28b72
update: initial tests
Jun 4, 2024
f3ebabe
update: made STR public for pyclass default implementations
Jun 4, 2024
51d52c1
update: generalizing str implementation
Jun 4, 2024
fcd51a0
update: remove redundant test
Jun 4, 2024
804f81e
update: implemented compile test to validate that manually implemente…
Jun 4, 2024
a9b6d5e
update: updated compile time error check
Jun 4, 2024
e7fd6bd
update: rename test file and code cleanup
Jun 4, 2024
d6a3ac6
update: format cleanup
Jun 4, 2024
02f2202
update: added news fragment
Jun 4, 2024
c075611
fix: corrected clippy findings
Jun 5, 2024
5297915
update: fixed mixed formatting case and improved test coverage
Jun 5, 2024
c940310
update: improved test coverage
Jun 5, 2024
4059225
refactor: generalized formatting function to accommodate __repr__ in …
Jun 5, 2024
0ce912f
update: Add support for rename formatting in PyEnum3
Jun 5, 2024
0c33be0
fix: fixed clippy finding
Jun 5, 2024
9b89c40
update: fixed test function names
Jun 6, 2024
d8cd29a
Update pyo3-macros-backend/src/pyclass.rs
Zyell Jun 6, 2024
9d8b170
Update newsfragments/4233.added.md
Zyell Jun 6, 2024
23b0b8f
update: implemented hygienic calls and added hygiene tests.
Jun 6, 2024
c653bef
update: cargo fmt
Jun 6, 2024
f8d1be3
update: retained LitStr usage in the quote in order to preserve a mor…
Jun 6, 2024
6e77f9d
update: retained LitStr usage in the quote in order to preserve a mor…
Jun 6, 2024
9f51d47
update: added compile time error check for invalid fields (looking to…
Jun 7, 2024
12eb032
update: implemented a subspan to improve errors in format string on n…
Jun 7, 2024
4d0e738
update: updated test output
Jun 7, 2024
f4a223f
update: updated with clippy findings
Jun 7, 2024
cc1317c
update: added doc entries.
Jun 7, 2024
8d73c33
Merge branch 'refs/heads/main' into trait_ergonomics_str
Jun 7, 2024
678208e
update: corrected error output for compile errors after updating from…
Jun 7, 2024
1f84dd3
update: added support for raw identifiers used in field names
Jun 10, 2024
d0e7287
Merge branch 'refs/heads/main' into trait_ergonomics_str
Jun 25, 2024
e9e149d
update: aligning branch with main
Jun 25, 2024
0137083
update: added compile time error when mixing rename_all or name pycla…
Jun 25, 2024
a49249e
Merge branch 'refs/heads/main' into trait_ergonomics_str
Jul 15, 2024
5cc07da
Merge branch 'refs/heads/main' into trait_ergonomics_str
Jul 16, 2024
1597175
update: removed self option from str format shorthand, restricted str…
Jul 16, 2024
5709b85
update: removed checks for shorthand and renaming for enums and simpl…
Jul 16, 2024
ba234b2
update: added additional test case to increase coverage in match branch
Jul 17, 2024
a37c24b
fix: updated pyclass heighten check to validate for eq and ord, fixin…
Jul 17, 2024
35f1ed9
Revert "fix: updated pyclass heighten check to validate for eq and or…
Jul 17, 2024
5f1b285
update: improved error comments, naming, and added reference to the P…
Jul 17, 2024
873ac6c
Merge branch 'refs/heads/main' into trait_ergonomics_str
Jul 17, 2024
dc09612
update: fixed merge conflict
Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/4233.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `#[pyclass(str="<formated string>")]` option to generate `__str__` based on a `Display` implementation or formatted string.
131 changes: 128 additions & 3 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use proc_macro2::{Span, TokenStream};
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 {
Expand Down Expand Up @@ -35,19 +36,121 @@ 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!(text_signature);
syn::custom_keyword!(transparent);
syn::custom_keyword!(unsendable);
syn::custom_keyword!(weakref);
}

fn take_int(read: &mut &str) -> String {
let mut int = String::new();
for (i, ch) in read.char_indices() {
match ch {
'0'..='9' => int.push(ch),
_ => {
*read = &read[i..];
break;
}
}
}
int
}

fn take_ident(read: &mut &str) -> Ident {
let mut ident = String::new();
for (i, ch) in read.char_indices() {
match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
_ => {
*read = &read[i..];
break;
}
}
}
Ident::parse_any.parse_str(&ident).unwrap()
}

#[derive(Clone, Debug)]
pub enum FormatIdentity {
Attribute(Member),
Instance(Span),
}

// shorthand parsing logic inspiration taken from https://github.com/dtolnay/thiserror/blob/master/impl/src/fmt.rs
fn parse_shorthand_format(fmt: LitStr) -> (LitStr, Vec<FormatIdentity>) {
let span = fmt.span();
let value = fmt.value();
let mut read = value.as_str();
let mut out = String::new();
let mut members = Vec::new();
while let Some(brace) = read.find('{') {
out += &read[..brace + 1];
read = &read[brace + 1..];
if read.starts_with('{') {
out.push('{');
read = &read[1..];
continue;
}
let next = match read.chars().next() {
Some(next) => next,
None => break,
};
let member = match next {
'0'..='9' => {
let index = take_int(&mut read).parse::<u32>().unwrap();
FormatIdentity::Attribute(Member::Unnamed(Index { index, span }))
}
'a'..='z' | 'A'..='Z' | '_' => {
let mut ident = take_ident(&mut read);
ident.set_span(span);
FormatIdentity::Attribute(Member::Named(ident))
}
'}' | ':' => {
// we found a closing bracket or formatting ':' without finding a member, we assume the user wants the instance formatted here
FormatIdentity::Instance(span)
}
_ => continue,
};
members.push(member);
}
out += read;
(LitStr::new(&out, span), members)
}

#[derive(Clone, Debug)]
pub struct StringFormatter {
pub fmt: LitStr,
pub args: Vec<FormatIdentity>,
}

impl Parse for crate::attributes::StringFormatter {
fn parse(input: ParseStream<'_>) -> Result<Self> {
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<K, V> {
pub kw: K,
pub value: V,
}

#[derive(Clone, Debug)]
pub struct OptionalKeywordAttribute<K, V> {
pub kw: K,
pub value: Option<V>,
}

/// A helper type which parses the inner type via a literal string
/// e.g. `LitStrValue<Path>` -> parses "some::path" in quotes.
#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -176,6 +279,7 @@ pub type FreelistAttribute = KeywordAttribute<kw::freelist, Box<Expr>>;
pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
pub type RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitStr>;
pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatter>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;

impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
Expand All @@ -195,6 +299,27 @@ impl<K: ToTokens, V: ToTokens> ToTokens for KeywordAttribute<K, V> {
}
}

impl<K: Parse + std::fmt::Debug, V: Parse> Parse for OptionalKeywordAttribute<K, V> {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let kw: K = input.parse()?;
let value = match input.parse::<Token![=]>() {
Ok(_) => Some(input.parse()?),
Err(_) => None,
};
Ok(OptionalKeywordAttribute { kw, value })
}
}

impl<K: ToTokens, V: ToTokens> ToTokens for OptionalKeywordAttribute<K, V> {
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<kw::from_py_with, LitStrValue<ExprPath>>;

/// For specifying the path to the pyo3 crate.
Expand Down
Loading
Loading