From f7fe0322a31c6727abd750bee21e73dd379e9fc5 Mon Sep 17 00:00:00 2001 From: Thomas Pellissier-Tanon Date: Wed, 23 Apr 2025 09:29:22 +0200 Subject: [PATCH] Implements basic method introspection --- pyo3-introspection/src/introspection.rs | 178 +++++++++++++++-------- pyo3-introspection/src/model.rs | 3 + pyo3-introspection/src/stubs.rs | 48 +++++- pyo3-macros-backend/src/introspection.rs | 75 ++++++++-- pyo3-macros-backend/src/pyfunction.rs | 10 +- pyo3-macros-backend/src/pyimpl.rs | 71 ++++++++- pyo3-macros-backend/src/pymethod.rs | 14 +- pytests/src/pyclasses.rs | 42 +++++- pytests/stubs/pyclasses.pyi | 34 ++++- pytests/tests/test_pyclasses.py | 29 ++++ 10 files changed, 404 insertions(+), 100 deletions(-) diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index e4f49d5e0e3..50df8fd6878 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -7,6 +7,7 @@ use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; use serde::Deserialize; +use std::cmp::Ordering; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -21,19 +22,23 @@ pub fn introspect_cdylib(library_path: impl AsRef, main_module_name: &str) /// Parses the introspection chunks found in the binary fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { - let chunks_by_id = chunks - .iter() - .map(|c| { - ( - match c { - Chunk::Module { id, .. } => id, - Chunk::Class { id, .. } => id, - Chunk::Function { id, .. } => id, - }, - c, - ) - }) - .collect::>(); + let mut chunks_by_id = HashMap::<&str, &Chunk>::new(); + let mut chunks_by_parent = HashMap::<&str, Vec<&Chunk>>::new(); + for chunk in chunks { + if let Some(id) = match chunk { + Chunk::Module { id, .. } => Some(id), + Chunk::Class { id, .. } => Some(id), + Chunk::Function { id, .. } => id.as_ref(), + } { + chunks_by_id.insert(id, chunk); + } + if let Some(parent) = match chunk { + Chunk::Module { .. } | Chunk::Class { .. } => None, + Chunk::Function { parent, .. } => parent.as_ref(), + } { + chunks_by_parent.entry(parent).or_default().push(chunk); + } + } // We look for the root chunk for chunk in chunks { if let Chunk::Module { @@ -43,7 +48,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { } = chunk { if name == main_module_name { - return convert_module(name, members, &chunks_by_id); + return convert_module(name, members, &chunks_by_id, &chunks_by_parent); } } } @@ -53,59 +58,111 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { fn convert_module( name: &str, members: &[String], - chunks_by_id: &HashMap<&String, &Chunk>, + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { + let (modules, classes, functions) = convert_members( + &members + .iter() + .filter_map(|id| chunks_by_id.get(id.as_str()).copied()) + .collect::>(), + chunks_by_id, + chunks_by_parent, + )?; + Ok(Module { + name: name.into(), + modules, + classes, + functions, + }) +} + +/// Convert a list of members of a module or a class +fn convert_members( + chunks: &[&Chunk], + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, +) -> Result<(Vec, Vec, Vec)> { let mut modules = Vec::new(); let mut classes = Vec::new(); let mut functions = Vec::new(); - for member in members { - if let Some(chunk) = chunks_by_id.get(member) { - match chunk { - Chunk::Module { + for chunk in chunks { + match chunk { + Chunk::Module { + name, + members, + id: _, + } => { + modules.push(convert_module( name, members, - id: _, - } => { - modules.push(convert_module(name, members, chunks_by_id)?); - } - Chunk::Class { name, id: _ } => classes.push(Class { name: name.into() }), - Chunk::Function { - name, - id: _, - arguments, - } => functions.push(Function { + chunks_by_id, + chunks_by_parent, + )?); + } + Chunk::Class { name, id } => { + let (_, _, mut methods) = convert_members( + chunks_by_parent + .get(&id.as_str()) + .map(Vec::as_slice) + .unwrap_or_default(), + chunks_by_id, + chunks_by_parent, + )?; + // We sort methods to get a stable output + methods.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") { + Ordering::Less + } else if r.decorators.iter().any(|d| d == "property") { + Ordering::Greater + } else { + // We pick an ordering based on decorators + l.decorators.cmp(&r.decorators) + } + } + o => o, + }); + classes.push(Class { name: name.into(), - arguments: Arguments { - positional_only_arguments: arguments - .posonlyargs - .iter() - .map(convert_argument) - .collect(), - arguments: arguments.args.iter().map(convert_argument).collect(), - vararg: arguments - .vararg - .as_ref() - .map(convert_variable_length_argument), - keyword_only_arguments: arguments - .kwonlyargs - .iter() - .map(convert_argument) - .collect(), - kwarg: arguments - .kwarg - .as_ref() - .map(convert_variable_length_argument), - }, - }), + methods, + }) } + Chunk::Function { + name, + id: _, + arguments, + parent: _, + decorators, + } => functions.push(Function { + name: name.into(), + decorators: decorators.clone(), + arguments: Arguments { + positional_only_arguments: arguments + .posonlyargs + .iter() + .map(convert_argument) + .collect(), + arguments: arguments.args.iter().map(convert_argument).collect(), + vararg: arguments + .vararg + .as_ref() + .map(convert_variable_length_argument), + keyword_only_arguments: arguments + .kwonlyargs + .iter() + .map(convert_argument) + .collect(), + kwarg: arguments + .kwarg + .as_ref() + .map(convert_variable_length_argument), + }, + }), } } - Ok(Module { - name: name.into(), - modules, - classes, - functions, - }) + Ok((modules, classes, functions)) } fn convert_argument(arg: &ChunkArgument) -> Argument { @@ -290,9 +347,14 @@ enum Chunk { name: String, }, Function { - id: String, + #[serde(default)] + id: Option, name: String, arguments: ChunkArguments, + #[serde(default)] + parent: Option, + #[serde(default)] + decorators: Vec, }, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 7705a0006a4..021475b9392 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -9,11 +9,14 @@ pub struct Module { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Class { pub name: String, + pub methods: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Function { pub name: String, + /// decorator like 'property' or 'staticmethod' + pub decorators: Vec, pub arguments: Arguments, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 2312d7d37ac..cc0a11ebd38 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -39,12 +39,39 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function)); } - elements.push(String::new()); // last line jump - elements.join("\n") + + // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) + let mut output = String::new(); + for element in elements { + let is_multiline = element.contains('\n'); + if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { + output.push('\n'); + } + output.push_str(&element); + output.push('\n'); + if is_multiline { + output.push('\n'); + } + } + // We remove a line jump at the end if they are two + if output.ends_with("\n\n") { + output.pop(); + } + output } fn class_stubs(class: &Class) -> String { - format!("class {}: ...", class.name) + let mut buffer = format!("class {}:", class.name); + if class.methods.is_empty() { + buffer.push_str(" ..."); + return buffer; + } + for method in &class.methods { + // We do the indentation + buffer.push_str("\n "); + buffer.push_str(&function_stubs(method).replace('\n', "\n ")); + } + buffer } fn function_stubs(function: &Function) -> String { @@ -70,7 +97,18 @@ fn function_stubs(function: &Function) -> String { if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); } - format!("def {}({}): ...", function.name, parameters.join(", ")) + let output = format!("def {}({}): ...", function.name, parameters.join(", ")); + if function.decorators.is_empty() { + return output; + } + let mut buffer = String::new(); + for decorator in &function.decorators { + buffer.push('@'); + buffer.push_str(decorator); + buffer.push('\n'); + } + buffer.push_str(&output); + buffer } fn argument_stub(argument: &Argument) -> String { @@ -95,6 +133,7 @@ mod tests { fn function_stubs_with_variable_length() { let function = Function { name: "func".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), @@ -126,6 +165,7 @@ mod tests { fn function_stubs_without_variable_length() { let function = Function { name: "afunc".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4888417cb08..ba252a0a89c 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::{Attribute, Ident}; +use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -42,7 +42,9 @@ pub fn module_introspection_code<'a>( .zip(members_cfg_attrs) .filter_map(|(member, attributes)| { if attributes.is_empty() { - Some(IntrospectionNode::IntrospectionId(Some(member))) + Some(IntrospectionNode::IntrospectionId(Some(ident_to_type( + member, + )))) } else { None // TODO: properly interpret cfg attributes } @@ -64,7 +66,10 @@ pub fn class_introspection_code( IntrospectionNode::Map( [ ("type", IntrospectionNode::String("class".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), + ( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ), ("name", IntrospectionNode::String(name.into())), ] .into(), @@ -74,23 +79,47 @@ pub fn class_introspection_code( pub fn function_introspection_code( pyo3_crate_path: &PyO3CratePath, - ident: &Ident, + ident: Option<&Ident>, name: &str, signature: &FunctionSignature<'_>, + first_argument: Option<&'static str>, + decorators: impl IntoIterator, + parent: Option<&Type>, ) -> TokenStream { - IntrospectionNode::Map( - [ - ("type", IntrospectionNode::String("function".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), - ("name", IntrospectionNode::String(name.into())), - ("arguments", arguments_introspection_data(signature)), - ] - .into(), - ) - .emit(pyo3_crate_path) + let mut desc = HashMap::from([ + ("type", IntrospectionNode::String("function".into())), + ("name", IntrospectionNode::String(name.into())), + ( + "arguments", + arguments_introspection_data(signature, first_argument), + ), + ]); + if let Some(ident) = ident { + desc.insert( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ); + } + let decorators = decorators + .into_iter() + .map(|d| IntrospectionNode::String(d.into())) + .collect::>(); + if !decorators.is_empty() { + desc.insert("decorators", IntrospectionNode::List(decorators)); + } + if let Some(parent) = parent { + desc.insert( + "parent", + IntrospectionNode::IntrospectionId(Some(parent.clone())), + ); + } + IntrospectionNode::Map(desc).emit(pyo3_crate_path) } -fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> IntrospectionNode<'a> { +fn arguments_introspection_data<'a>( + signature: &'a FunctionSignature<'a>, + first_argument: Option<&'a str>, +) -> IntrospectionNode<'a> { let mut argument_desc = signature.arguments.iter().filter_map(|arg| { if let FnArg::Regular(arg) = arg { Some(arg) @@ -105,6 +134,12 @@ fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> Int let mut kwonlyargs = Vec::new(); let mut kwarg = None; + if let Some(first_argument) = first_argument { + posonlyargs.push(IntrospectionNode::Map( + [("name", IntrospectionNode::String(first_argument.into()))].into(), + )); + } + for (i, param) in signature .python_signature .positional_parameters @@ -184,7 +219,7 @@ fn argument_introspection_data<'a>( enum IntrospectionNode<'a> { String(Cow<'a, str>), - IntrospectionId(Option<&'a Ident>), + IntrospectionId(Option), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -333,3 +368,11 @@ fn unique_element_id() -> u64 { .hash(&mut hasher); // If there are multiple elements in the same call site hasher.finish() } + +fn ident_to_type(ident: &Ident) -> Type { + TypePath { + path: ident.clone().into(), + qself: None, + } + .into() +} diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 301819f42dd..e512ca1cabc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -230,7 +230,15 @@ pub fn impl_wrap_pyfunction( let name = &func.sig.ident; #[cfg(feature = "experimental-inspect")] - let introspection = function_introspection_code(pyo3_path, name, &name.to_string(), &signature); + let introspection = function_introspection_code( + pyo3_path, + Some(name), + &name.to_string(), + &signature, + None, + [] as [String; 0], + None, + ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 72f06721ec4..90b0d961cd8 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,14 +1,19 @@ use std::collections::HashSet; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::function_introspection_code; +#[cfg(feature = "experimental-inspect")] +use crate::method::{FnSpec, FnType}; use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, - pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, + pymethod::{ + self, is_proto_method, GeneratedPyMethod, MethodAndMethodDef, MethodAndSlotDef, PyMethod, + }, }; use proc_macro2::TokenStream; -use pymethod::GeneratedPyMethod; use quote::{format_ident, quote}; use syn::ImplItemFn; use syn::{ @@ -110,7 +115,7 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { - let mut trait_impls = Vec::new(); + let mut extra_fragments = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); let mut associated_methods = Vec::new(); @@ -125,9 +130,10 @@ pub fn impl_methods( fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); check_pyfunction(&ctx.pyo3_path, meth)?; - - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? - { + let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?; + #[cfg(feature = "experimental-inspect")] + extra_fragments.push(method_introspection_code(&method.spec, ty, ctx)); + match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? { GeneratedPyMethod::Method(MethodAndMethodDef { associated_method, method_def, @@ -139,7 +145,7 @@ pub fn impl_methods( GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { implemented_proto_fragments.insert(method_name); let attrs = get_cfg_attributes(&meth.attrs); - trait_impls.push(quote!(#(#attrs)* #token_stream)); + extra_fragments.push(quote!(#(#attrs)* #token_stream)); } GeneratedPyMethod::Proto(MethodAndSlotDef { associated_method, @@ -193,7 +199,7 @@ pub fn impl_methods( }; Ok(quote! { - #(#trait_impls)* + #(#extra_fragments)* #items @@ -336,3 +342,52 @@ pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribut .filter(|attr| attr.path().is_ident("cfg")) .collect() } + +#[cfg(feature = "experimental-inspect")] +fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path, .. } = ctx; + + // We introduce self/cls argument and setup decorators + let name = spec.python_name.to_string(); + let mut first_argument = None; + let mut decorators = Vec::new(); + match &spec.tp { + FnType::Getter(_) => { + first_argument = Some("self"); + decorators.push("property".into()); + } + FnType::Setter(_) => { + first_argument = Some("self"); + decorators.push(format!("{name}.setter")); + } + FnType::Fn(_) => { + first_argument = Some("self"); + } + FnType::FnNew | FnType::FnNewClass(_) => { + first_argument = Some("cls"); + } + FnType::FnClass(_) => { + first_argument = Some("cls"); + decorators.push("classmethod".into()); + } + FnType::FnStatic => { + decorators.push("staticmethod".into()); + } + 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()); + } + } + function_introspection_code( + pyo3_path, + None, + &name, + &spec.signature, + first_argument, + decorators, + Some(parent), + ) +} diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index b4637b48012..1387396f991 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -40,7 +40,7 @@ pub enum GeneratedPyMethod { pub struct PyMethod<'a> { kind: PyMethodKind, method_name: String, - spec: FnSpec<'a>, + pub spec: FnSpec<'a>, } enum PyMethodKind { @@ -160,11 +160,13 @@ enum PyMethodProtoKind { } impl<'a> PyMethod<'a> { - fn parse( + pub fn parse( sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, ) -> Result { + ensure_function_options_valid(&options)?; + check_generic(sig)?; let spec = FnSpec::parse(sig, meth_attrs, options)?; let method_name = spec.python_name.to_string(); @@ -187,14 +189,10 @@ pub fn is_proto_method(name: &str) -> bool { pub fn gen_py_method( cls: &syn::Type, - sig: &mut syn::Signature, - meth_attrs: &mut Vec, - options: PyFunctionOptions, + method: PyMethod<'_>, + meth_attrs: &[syn::Attribute], ctx: &Ctx, ) -> Result { - check_generic(sig)?; - ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options)?; let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 1091e6c16b3..c75690a3375 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -104,6 +104,45 @@ impl ClassWithDict { } } +#[pyclass] +struct ClassWithDecorators { + attr: usize, +} + +#[pymethods] +impl ClassWithDecorators { + #[new] + #[classmethod] + fn new(_cls: Bound<'_, PyType>) -> Self { + Self { attr: 0 } + } + + #[getter] + fn get_attr(&self) -> usize { + self.attr + } + + #[setter] + fn set_attr(&mut self, value: usize) { + self.attr = value; + } + + #[classmethod] + fn cls_method(_cls: &Bound<'_, PyType>) -> usize { + 1 + } + + #[staticmethod] + fn static_method() -> usize { + 2 + } + + #[classattr] + fn cls_attribute() -> usize { + 3 + } +} + #[pymodule(gil_used = false)] pub mod pyclasses { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] @@ -111,6 +150,7 @@ pub mod pyclasses { use super::ClassWithDict; #[pymodule_export] use super::{ - AssertingBaseClass, ClassWithoutConstructor, EmptyClass, PyClassIter, PyClassThreadIter, + AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, PyClassIter, + PyClassThreadIter, }; } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 2a36e0b4540..13b52f1c4e2 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,5 +1,31 @@ -class AssertingBaseClass: ... +class AssertingBaseClass: + def __new__(cls, /, expected_type): ... + +class ClassWithDecorators: + def __new__(cls, /): ... + @property + def attr(self, /): ... + @attr.setter + def attr(self, /, value): ... + @classmethod + @property + def cls_attribute(cls, /): ... + @classmethod + def cls_method(cls, /): ... + @staticmethod + def static_method(): ... + class ClassWithoutConstructor: ... -class EmptyClass: ... -class PyClassIter: ... -class PyClassThreadIter: ... + +class EmptyClass: + def __len__(self, /): ... + def __new__(cls, /): ... + def method(self, /): ... + +class PyClassIter: + def __new__(cls, /): ... + def __next__(self, /): ... + +class PyClassThreadIter: + def __new__(cls, /): ... + def __next__(self, /): ... diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 9f611b634b6..a641c9770a4 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -121,3 +121,32 @@ def test_dict(): d.foo = 42 assert d.__dict__ == {"foo": 42} + + +def test_getter(benchmark): + obj = pyclasses.ClassWithDecorators() + benchmark(lambda: obj.attr) + + +def test_setter(benchmark): + obj = pyclasses.ClassWithDecorators() + + def set_attr(): + obj.attr = 42 + + benchmark(set_attr) + + +def test_class_attribute(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_attribute) + + +def test_class_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_method()) + + +def test_static_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.static_method())