From 93ef056711ce1769c6fb95d1f6cc063b34015cb1 Mon Sep 17 00:00:00 2001 From: "A. Cody Schuffelen" Date: Mon, 3 Jun 2024 12:45:36 -0700 Subject: [PATCH] Use `Ident::parse_any` for `name` attributes (#4226) This makes it possible to use rust keywords as the name of python class methods and standalone functions. For example: ``` struct MyClass { } impl MyClass { #[new] fn new() -> Self { MyClass {} } #[pyo3(name = "struct")] fn struct_method(&self) -> usize { 42 } } fn struct_function() -> usize { 42 } ``` From the [`syn::Ident` documentation](https://docs.rs/syn/2.0.66/syn/struct.Ident.html): > An identifier constructed with `Ident::new` is permitted to be a Rust keyword, though parsing one through its [`Parse`](https://docs.rs/syn/2.0.66/syn/parse/trait.Parse.html) implementation rejects Rust keywords. Use `input.call(Ident::parse_any)` when parsing to match the behaviour of `Ident::new`. Fixes issue #4225 --- newsfragments/4226.fixed.md | 1 + pyo3-macros-backend/src/attributes.rs | 3 ++- tests/test_class_basics.rs | 30 +++++++++++++++++++++++++++ tests/test_pyfunction.rs | 12 +++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4226.fixed.md diff --git a/newsfragments/4226.fixed.md b/newsfragments/4226.fixed.md new file mode 100644 index 00000000..b2b7d7d1 --- /dev/null +++ b/newsfragments/4226.fixed.md @@ -0,0 +1 @@ +Fixes a compile error when declaring a standalone function or class method with a Python name that is a Rust keyword. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index b7ef2ae6..52479552 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,6 +1,7 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ + ext::IdentExt, parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, @@ -72,7 +73,7 @@ pub struct NameLitStr(pub Ident); impl Parse for NameLitStr { fn parse(input: ParseStream<'_>) -> Result { let string_literal: LitStr = input.parse()?; - if let Ok(ident) = string_literal.parse() { + if let Ok(ident) = string_literal.parse_with(Ident::parse_any) { Ok(NameLitStr(ident)) } else { bail_spanned!(string_literal.span() => "expected a single identifier in double quotes") diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 5b14e8a6..325b3d52 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -123,6 +123,36 @@ fn custom_names() { }); } +#[pyclass(name = "loop")] +struct ClassRustKeywords { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pymethods] +impl ClassRustKeywords { + #[pyo3(name = "struct")] + fn struct_method(&self) {} + + #[staticmethod] + #[pyo3(name = "type")] + fn type_method() {} +} + +#[test] +fn keyword_names() { + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + py_assert!(py, typeobj, "typeobj.__name__ == 'loop'"); + py_assert!(py, typeobj, "typeobj.struct.__name__ == 'struct'"); + py_assert!(py, typeobj, "typeobj.type.__name__ == 'type'"); + py_assert!(py, typeobj, "typeobj.unsafe.__name__ == 'unsafe'"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'unsafe_variable')"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'struct_method')"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'type_method')"); + }); +} + #[pyclass] struct RawIdents { #[pyo3(get, set)] diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index ce4dff7b..9028f71a 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -14,6 +14,18 @@ use pyo3::types::{self, PyCFunction}; #[path = "../src/tests/common.rs"] mod common; +#[pyfunction(name = "struct")] +fn struct_function() {} + +#[test] +fn test_rust_keyword_name() { + Python::with_gil(|py| { + let f = wrap_pyfunction_bound!(struct_function)(py).unwrap(); + + py_assert!(py, f, "f.__name__ == 'struct'"); + }); +} + #[pyfunction(signature = (arg = true))] fn optional_bool(arg: Option) -> String { format!("{:?}", arg)