diff --git a/newsfragments/5618.changed.md b/newsfragments/5618.changed.md new file mode 100644 index 00000000000..3c430238bd0 --- /dev/null +++ b/newsfragments/5618.changed.md @@ -0,0 +1 @@ +Introspection: properly represent decorators as module + name (allows to get imports working) \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 3cc50817fca..706ac870f8e 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,6 +1,6 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, - VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, PythonIdentifier, TypeHint, + TypeHintExpr, VariableLengthArgument, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use goblin::elf::section_header::SHN_XINDEX; @@ -134,7 +134,7 @@ fn convert_members<'a>( parent: _, decorators, returns, - } => functions.push(convert_function(name, arguments, decorators, returns)), + } => functions.push(convert_function(name, arguments, decorators, returns)?), Chunk::Attribute { name, id: _, @@ -149,14 +149,24 @@ fn convert_members<'a>( classes.sort_by(|l, r| l.name.cmp(&r.name)); functions.sort_by(|l, r| match l.name.cmp(&r.name) { Ordering::Equal => { - // We put the getter before the setter - if l.decorators.iter().any(|d| d == "property") { + // We put the getter before the setter. For that, we put @property before the other ones + if l.decorators + .iter() + .any(|d| d.name == "property" && d.module.as_deref() == Some("builtins")) + { Ordering::Less - } else if r.decorators.iter().any(|d| d == "property") { + } else if r + .decorators + .iter() + .any(|d| d.name == "property" && d.module.as_deref() == Some("builtins")) + { Ordering::Greater } else { // We pick an ordering based on decorators - l.decorators.cmp(&r.decorators) + l.decorators + .iter() + .map(|d| &d.name) + .cmp(r.decorators.iter().map(|d| &d.name)) } } o => o, @@ -194,12 +204,27 @@ fn convert_class( fn convert_function( name: &str, arguments: &ChunkArguments, - decorators: &[String], + decorators: &[ChunkTypeHint], returns: &Option, -) -> Function { - Function { +) -> Result { + Ok(Function { name: name.into(), - decorators: decorators.to_vec(), + decorators: decorators + .iter() + .map(|d| match convert_type_hint(d) { + TypeHint::Plain(id) => Ok(PythonIdentifier { + module: None, + name: id.clone(), + }), + TypeHint::Ast(expr) => { + if let TypeHintExpr::Identifier(i) = expr { + Ok(i) + } else { + bail!("A decorator must be the identifier of a Python function") + } + } + }) + .collect::>()?, arguments: Arguments { positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(), arguments: arguments.args.iter().map(convert_argument).collect(), @@ -214,7 +239,7 @@ fn convert_function( .map(convert_variable_length_argument), }, returns: returns.as_ref().map(convert_type_hint), - } + }) } fn convert_argument(arg: &ChunkArgument) -> Argument { @@ -253,15 +278,24 @@ fn convert_type_hint(arg: &ChunkTypeHint) -> TypeHint { fn convert_type_hint_expr(expr: &ChunkTypeHintExpr) -> TypeHintExpr { match expr { - ChunkTypeHintExpr::Local { id } => TypeHintExpr::Local { id: id.clone() }, - ChunkTypeHintExpr::Builtin { id } => TypeHintExpr::Builtin { id: id.clone() }, - ChunkTypeHintExpr::Attribute { module, attr } => TypeHintExpr::Attribute { - module: module.clone(), - attr: attr.clone(), - }, - ChunkTypeHintExpr::Union { elts } => TypeHintExpr::Union { - elts: elts.iter().map(convert_type_hint_expr).collect(), - }, + ChunkTypeHintExpr::Local { id } => PythonIdentifier { + module: None, + name: id.clone(), + } + .into(), + ChunkTypeHintExpr::Builtin { id } => PythonIdentifier { + module: Some("builtins".into()), + name: id.clone(), + } + .into(), + ChunkTypeHintExpr::Attribute { module, attr } => PythonIdentifier { + module: Some(module.clone()), + name: attr.clone(), + } + .into(), + ChunkTypeHintExpr::Union { elts } => { + TypeHintExpr::Union(elts.iter().map(convert_type_hint_expr).collect()) + } ChunkTypeHintExpr::Subscript { value, slice } => TypeHintExpr::Subscript { value: Box::new(convert_type_hint_expr(value)), slice: slice.iter().map(convert_type_hint_expr).collect(), @@ -419,8 +453,8 @@ enum Chunk { #[serde(default)] parent: Option, #[serde(default)] - decorators: Vec, - #[serde(default, deserialize_with = "deserialize_type_hint")] + decorators: Vec, + #[serde(default)] returns: Option, }, Attribute { @@ -431,7 +465,7 @@ enum Chunk { name: String, #[serde(default)] value: Option, - #[serde(default, deserialize_with = "deserialize_type_hint")] + #[serde(default)] annotation: Option, }, } @@ -455,7 +489,7 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, - #[serde(default, deserialize_with = "deserialize_type_hint")] + #[serde(default)] annotation: Option, } @@ -467,6 +501,45 @@ enum ChunkTypeHint { Plain(String), } +impl<'de> Deserialize<'de> for ChunkTypeHint { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AnnotationVisitor; + + impl<'de> Visitor<'de> for AnnotationVisitor { + type Value = ChunkTypeHint; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("annotation") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + self.visit_string(v.into()) + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + Ok(ChunkTypeHint::Plain(v)) + } + + fn visit_map>(self, map: M) -> Result { + Ok(ChunkTypeHint::Ast(Deserialize::deserialize( + MapAccessDeserializer::new(map), + )?)) + } + } + + deserializer.deserialize_any(AnnotationVisitor) + } +} + #[derive(Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] enum ChunkTypeHintExpr { @@ -488,39 +561,3 @@ enum ChunkTypeHintExpr { slice: Vec, }, } - -fn deserialize_type_hint<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - struct AnnotationVisitor; - - impl<'de> Visitor<'de> for AnnotationVisitor { - type Value = ChunkTypeHint; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("annotation") - } - - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - self.visit_string(v.into()) - } - - fn visit_string(self, v: String) -> Result - where - E: Error, - { - Ok(ChunkTypeHint::Plain(v)) - } - - fn visit_map>(self, map: M) -> Result { - Ok(ChunkTypeHint::Ast(Deserialize::deserialize( - MapAccessDeserializer::new(map), - )?)) - } - } - - Ok(Some(deserializer.deserialize_any(AnnotationVisitor)?)) -} diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 3b6bbfe7981..d6fdd8945ee 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -19,7 +19,7 @@ pub struct Class { pub struct Function { pub name: String, /// decorator like 'property' or 'staticmethod' - pub decorators: Vec, + pub decorators: Vec, pub arguments: Arguments, /// return type pub returns: Option, @@ -77,17 +77,27 @@ pub enum TypeHint { /// A type hint annotation as an AST fragment #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum TypeHintExpr { - /// A local identifier which module is unknown - Local { id: String }, - /// A Python builtin like `int` - Builtin { id: String }, - /// The attribute of a python object like `{value}.{attr}` - Attribute { module: String, attr: String }, + /// An identifier + Identifier(PythonIdentifier), /// A union `{left} | {right}` - Union { elts: Vec }, + Union(Vec), /// A subscript `{value}[*slice]` Subscript { value: Box, slice: Vec, }, } + +impl From for TypeHintExpr { + #[inline] + fn from(value: PythonIdentifier) -> Self { + Self::Identifier(value) + } +} + +/// An Python identifier, either local (with `module = None`) or global (with `module = Some(_)`) +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct PythonIdentifier { + pub module: Option, + pub name: String, +} diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index d00cbed7b75..7181a02ab18 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,6 +1,6 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, - VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, PythonIdentifier, TypeHint, + TypeHintExpr, VariableLengthArgument, }; use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::PathBuf; @@ -69,16 +69,25 @@ fn module_stubs(module: &Module, parents: &[&str]) -> String { arguments: vec![Argument { name: "name".to_string(), default_value: None, - annotation: Some(TypeHint::Ast(TypeHintExpr::Builtin { id: "str".into() })), + annotation: Some(TypeHint::Ast( + PythonIdentifier { + module: Some("builtins".into()), + name: "str".into(), + } + .into(), + )), }], vararg: None, keyword_only_arguments: Vec::new(), kwarg: None, }, - returns: Some(TypeHint::Ast(TypeHintExpr::Attribute { - module: "_typeshed".into(), - attr: "Incomplete".into(), - })), + returns: Some(TypeHint::Ast( + PythonIdentifier { + module: Some("_typeshed".into()), + name: "Incomplete".into(), + } + .into(), + )), }, &imports, )); @@ -160,7 +169,7 @@ fn function_stubs(function: &Function, imports: &Imports) -> String { let mut buffer = String::new(); for decorator in &function.decorators { buffer.push('@'); - buffer.push_str(decorator); + imports.serialize_identifier(decorator, &mut buffer); buffer.push('\n'); } buffer.push_str("def "); @@ -350,22 +359,10 @@ impl Imports { fn serialize_type_hint(&self, expr: &TypeHintExpr, buffer: &mut String) { match expr { - TypeHintExpr::Local { id } => buffer.push_str(id), - TypeHintExpr::Builtin { id } => { - let alias = self - .renaming - .get(&("builtins".to_string(), id.clone())) - .expect("All type hint attributes should have been visited"); - buffer.push_str(alias) - } - TypeHintExpr::Attribute { module, attr } => { - let alias = self - .renaming - .get(&(module.clone(), attr.clone())) - .expect("All type hint attributes should have been visited"); - buffer.push_str(alias) + TypeHintExpr::Identifier(id) => { + self.serialize_identifier(id, buffer); } - TypeHintExpr::Union { elts } => { + TypeHintExpr::Union(elts) => { for (i, elt) in elts.iter().enumerate() { if i > 0 { buffer.push_str(" | "); @@ -386,6 +383,16 @@ impl Imports { } } } + + fn serialize_identifier(&self, id: &PythonIdentifier, buffer: &mut String) { + buffer.push_str(if let Some(module) = &id.module { + self.renaming + .get(&(module.clone(), id.name.clone())) + .expect("All type hint attributes should have been visited") + } else { + &id.name + }); + } } /// Lists all the elements used in annotations @@ -442,7 +449,7 @@ impl ElementsUsedInAnnotations { fn walk_function(&mut self, function: &Function) { for decorator in &function.decorators { - self.locals.insert(decorator.clone()); // TODO: better decorator support + self.walk_identifier(decorator); } for arg in function .arguments @@ -479,22 +486,10 @@ impl ElementsUsedInAnnotations { fn walk_type_hint_expr(&mut self, expr: &TypeHintExpr) { match expr { - TypeHintExpr::Local { id } => { - self.locals.insert(id.clone()); - } - TypeHintExpr::Builtin { id } => { - self.module_members - .entry("builtins".into()) - .or_default() - .insert(id.clone()); + TypeHintExpr::Identifier(id) => { + self.walk_identifier(id); } - TypeHintExpr::Attribute { module, attr } => { - self.module_members - .entry(module.clone()) - .or_default() - .insert(attr.clone()); - } - TypeHintExpr::Union { elts } => { + TypeHintExpr::Union(elts) => { for elt in elts { self.walk_type_hint_expr(elt) } @@ -507,6 +502,17 @@ impl ElementsUsedInAnnotations { } } } + + fn walk_identifier(&mut self, id: &PythonIdentifier) { + if let Some(module) = &id.module { + self.module_members + .entry(module.clone()) + .or_default() + .insert(id.name.clone()); + } else { + self.locals.insert(id.name.clone()); + } + } } #[cfg(test)] @@ -587,39 +593,61 @@ mod tests { #[test] fn test_import() { let big_type = TypeHintExpr::Subscript { - value: Box::new(TypeHintExpr::Builtin { id: "dict".into() }), + value: Box::new( + PythonIdentifier { + module: Some("builtins".into()), + name: "dict".into(), + } + .into(), + ), slice: vec![ - TypeHintExpr::Attribute { - module: "foo.bar".into(), - attr: "A".into(), - }, - TypeHintExpr::Union { - elts: vec![ - TypeHintExpr::Attribute { - module: "bar".into(), - attr: "A".into(), - }, - TypeHintExpr::Attribute { - module: "foo".into(), - attr: "A.C".into(), - }, - TypeHintExpr::Attribute { - module: "foo".into(), - attr: "A.D".into(), - }, - TypeHintExpr::Attribute { - module: "foo".into(), - attr: "B".into(), - }, - TypeHintExpr::Attribute { - module: "bat".into(), - attr: "A".into(), - }, - TypeHintExpr::Local { id: "int".into() }, - TypeHintExpr::Builtin { id: "int".into() }, - TypeHintExpr::Builtin { id: "float".into() }, - ], - }, + PythonIdentifier { + module: Some("foo.bar".into()), + name: "A".into(), + } + .into(), + TypeHintExpr::Union(vec![ + PythonIdentifier { + module: Some("bar".into()), + name: "A".into(), + } + .into(), + PythonIdentifier { + module: Some("foo".into()), + name: "A.C".into(), + } + .into(), + PythonIdentifier { + module: Some("foo".into()), + name: "A.D".into(), + } + .into(), + PythonIdentifier { + module: Some("foo".into()), + name: "B".into(), + } + .into(), + PythonIdentifier { + module: Some("bat".into()), + name: "A".into(), + } + .into(), + PythonIdentifier { + module: None, + name: "int".into(), + } + .into(), + PythonIdentifier { + module: Some("builtins".into()), + name: "int".into(), + } + .into(), + PythonIdentifier { + module: Some("builtins".into()), + name: "float".into(), + } + .into(), + ]), ], }; let imports = Imports::create( diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index eb1371ced8d..bab266b960b 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -24,6 +24,35 @@ use syn::{Attribute, Ident, Lifetime, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); +/// A Python type hint +pub struct PythonIdentifier { + module: Option<&'static str>, + name: Cow<'static, str>, +} + +impl PythonIdentifier { + pub fn builtins(name: impl Into>) -> Self { + Self { + module: Some("builtins"), + name: name.into(), + } + } + + pub fn local(name: impl Into>) -> Self { + Self { + module: None, + name: name.into(), + } + } + + pub fn module_attr(module: &'static str, name: impl Into>) -> Self { + Self { + module: Some(module), + name: name.into(), + } + } +} + pub fn module_introspection_code<'a>( pyo3_crate_path: &PyO3CratePath, name: &str, @@ -83,7 +112,7 @@ pub fn function_introspection_code( signature: &FunctionSignature<'_>, first_argument: Option<&'static str>, returns: ReturnType, - decorators: impl IntoIterator, + decorators: impl IntoIterator, parent: Option<&Type>, ) -> TokenStream { let mut desc = HashMap::from([ @@ -103,17 +132,11 @@ pub fn function_introspection_code( IntrospectionNode::String(returns.to_python().into()) } else { match returns { - ReturnType::Default => IntrospectionNode::ConstantType { - name: "None", - module: "builtins", - }, + ReturnType::Default => PythonIdentifier::builtins("None").into(), ReturnType::Type(_, ty) => match *ty { Type::Tuple(t) if t.elems.is_empty() => { // () is converted to None in return types - IntrospectionNode::ConstantType { - name: "None", - module: "builtins", - } + PythonIdentifier::builtins("None").into() } mut ty => { if let Some(class_type) = parent { @@ -136,10 +159,7 @@ pub fn function_introspection_code( IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), ); } - let decorators = decorators - .into_iter() - .map(|d| IntrospectionNode::String(d.into()).into()) - .collect::>(); + let decorators = decorators.into_iter().map(|d| d.into()).collect::>(); if !decorators.is_empty() { desc.insert("decorators", IntrospectionNode::List(decorators)); } @@ -188,10 +208,7 @@ pub fn attribute_introspection_code( // Type checkers can infer the type from the value because it's typing.Literal[value] // So, following stubs best practices, we only write typing.Final and not // typing.Final[typing.literal[value]] - IntrospectionNode::ConstantType { - name: "Final", - module: "typing", - } + PythonIdentifier::module_attr("typing", "Final").into() } else { IntrospectionNode::OutputType { rust_type, @@ -351,18 +368,9 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), Bool(bool), IntrospectionId(Option>), - InputType { - rust_type: Type, - nullable: bool, - }, - OutputType { - rust_type: Type, - is_final: bool, - }, - ConstantType { - name: &'static str, - module: &'static str, - }, + InputType { rust_type: Type, nullable: bool }, + OutputType { rust_type: Type, is_final: bool }, + ConstantType(PythonIdentifier), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -424,9 +432,13 @@ impl IntrospectionNode<'_> { } content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } - Self::ConstantType { name, module } => { - let annotation = - quote! { #pyo3_crate_path::inspect::TypeHint::module_attr(#module, #name) }; + Self::ConstantType(hint) => { + let name = &hint.name; + let annotation = if let Some(module) = &hint.module { + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr(#module, #name) } + } else { + quote! { #pyo3_crate_path::inspect::TypeHint::local(#name) } + }; content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::Map(map) => { @@ -468,6 +480,12 @@ impl IntrospectionNode<'_> { } } +impl From for IntrospectionNode<'static> { + fn from(element: PythonIdentifier) -> Self { + Self::ConstantType(element) + } +} + fn serialize_type_hint(hint: TokenStream, pyo3_crate_path: &PyO3CratePath) -> TokenStream { quote! {{ const TYPE_HINT: #pyo3_crate_path::inspect::TypeHint = #hint; @@ -495,6 +513,12 @@ impl<'a> From> for AttributedIntrospectionNode<'a> { } } +impl<'a> From for AttributedIntrospectionNode<'a> { + fn from(node: PythonIdentifier) -> Self { + IntrospectionNode::from(node).into() + } +} + #[derive(Default)] pub struct ConcatenationBuilder { elements: Vec, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 46817999684..cb35a654763 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -16,7 +16,7 @@ use crate::attributes::{ use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{ - class_introspection_code, function_introspection_code, introspection_id_const, + class_introspection_code, function_introspection_code, introspection_id_const, PythonIdentifier, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -1900,7 +1900,7 @@ fn descriptors_to_items( &FunctionSignature::from_arguments(vec![]), Some("self"), parse_quote!(-> #return_type), - vec!["property".into()], + vec![PythonIdentifier::builtins("property")], Some(&parse_quote!(#cls)), )); } @@ -1939,7 +1939,7 @@ fn descriptors_to_items( })]), Some("self"), syn::ReturnType::Default, - vec![format!("{name}.setter")], + vec![PythonIdentifier::local(format!("{name}.setter"))], Some(&parse_quote!(#cls)), )); } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 1333e6d2bcd..ec2195a4e3c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -15,6 +15,8 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use std::cmp::PartialEq; use std::ffi::CString; +#[cfg(feature = "experimental-inspect")] +use std::iter::empty; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::LitCStr; @@ -393,7 +395,7 @@ pub fn impl_wrap_pyfunction( &signature, None, func.sig.output.clone(), - [] as [String; 0], + empty(), None, ); #[cfg(not(feature = "experimental-inspect"))] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index ecb11610004..57634e20fc1 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -2,7 +2,9 @@ use std::collections::HashSet; use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] -use crate::introspection::{attribute_introspection_code, function_introspection_code}; +use crate::introspection::{ + attribute_introspection_code, function_introspection_code, PythonIdentifier, +}; #[cfg(feature = "experimental-inspect")] use crate::method::{FnSpec, FnType}; #[cfg(feature = "experimental-inspect")] @@ -400,11 +402,11 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - match &spec.tp { FnType::Getter(_) => { first_argument = Some("self"); - decorators.push("property".into()); + decorators.push(PythonIdentifier::builtins("property")); } FnType::Setter(_) => { first_argument = Some("self"); - decorators.push(format!("{name}.setter")); + decorators.push(PythonIdentifier::local(format!("{name}.setter"))); } FnType::Fn(_) => { first_argument = Some("self"); @@ -415,17 +417,17 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - } FnType::FnClass(_) => { first_argument = Some("cls"); - decorators.push("classmethod".into()); + decorators.push(PythonIdentifier::builtins("classmethod")); } FnType::FnStatic => { - decorators.push("staticmethod".into()); + decorators.push(PythonIdentifier::builtins("staticmethod")); } FnType::FnModule(_) => (), // TODO: not sure this can happen FnType::ClassAttribute => { first_argument = Some("cls"); // TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod - decorators.push("classmethod".into()); - decorators.push("property".into()); + decorators.push(PythonIdentifier::builtins("classmethod")); + decorators.push(PythonIdentifier::builtins("property")); } } function_introspection_code(