diff --git a/src/inspect/fields.rs b/src/inspect/fields.rs index 12e0ccb55f2..1ea88036cd6 100644 --- a/src/inspect/fields.rs +++ b/src/inspect/fields.rs @@ -13,8 +13,6 @@ pub struct FieldInfo<'a> { pub enum FieldKind { /// The special 'new' method New, - /// A top-level or instance attribute - Attribute, /// A top-level or instance getter Getter, /// A top-level or instance setter @@ -41,12 +39,14 @@ pub struct ArgumentInfo<'a> { #[derive(Debug)] pub enum ArgumentKind { /// A normal argument, that can be passed positionally or by keyword. - Regular, + PositionOrKeyword, /// A normal argument that can only be passed positionally (not by keyword). - PositionalOnly, + Position, + /// A normal argument that can only be passed by keyword (not positionally). + Keyword, /// An argument that represents all positional arguments that were provided on the call-site /// but do not match any declared regular argument. - Vararg, + VarArg, /// An argument that represents all keyword arguments that were provided on the call-site /// but do not match any declared regular argument. KeywordArg, diff --git a/src/inspect/interface.rs b/src/inspect/interface.rs new file mode 100644 index 00000000000..30c6ea06cac --- /dev/null +++ b/src/inspect/interface.rs @@ -0,0 +1,150 @@ +//! Generates a Python interface file (.pyi) using the inspected elements. + +use std::fmt::{Display, Formatter}; +use libc::write; +use crate::inspect::classes::ClassInfo; +use crate::inspect::fields::{ArgumentInfo, ArgumentKind, FieldInfo, FieldKind}; + +/// Interface generator for a Python class. +/// +/// Instances are created with [`InterfaceGenerator::new`]. +/// The documentation is generated via the [`Display`] implementation. +pub struct InterfaceGenerator<'a> { + info: ClassInfo<'a> +} + +impl<'a> InterfaceGenerator<'a> { + pub fn new(info: ClassInfo<'a>) -> Self { + Self { + info + } + } + + fn class_header(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // class ClassName(BaseClassName): + + write!(f, "class {}", self.info.name())?; + if let Some(base) = self.info.base() { + write!(f, "({})", base)?; + } + write!(f, ":") + } + + fn field(field: &FieldInfo, f: &mut Formatter<'_>) -> std::fmt::Result { + match field.kind { + FieldKind::New => { + write!(f, " def __new__(cls")?; + Self::arguments(field.arguments, true, f)?; + write!(f, ") -> None")?; + }, + FieldKind::Getter => { + writeln!(f, " @property")?; + write!(f, " def {}(self", field.name)?; + Self::signature_end(field, false, f)?; + }, + FieldKind::Setter => { + writeln!(f, " @{}.setter", field.name)?; + write!(f, " def {}(self", field.name)?; + Self::signature_end(field, true, f)?; + }, + FieldKind::Function => { + write!(f, " def {}(self", field.name)?; + Self::signature_end(field, true, f)?; + }, + FieldKind::ClassMethod => { + writeln!(f, " @classmethod")?; + write!(f, " def {}(cls", field.name)?; + Self::signature_end(field, true, f)?; + }, + FieldKind::ClassAttribute => { + write!(f, " {}", field.name)?; + if let Some(output_type) = field.py_type { + write!(f, ": {}", (output_type)())?; + } + return writeln!(f, " = ..."); + }, + FieldKind::StaticMethod => { + writeln!(f, " @staticmethod")?; + write!(f, " def {}(", field.name)?; + Self::signature_end(field, false, f)?; + }, + }; + + writeln!(f, ": ...") + } + + fn signature_end(field: &FieldInfo, start_with_comma: bool, f: &mut Formatter<'_>) -> std::fmt::Result { + // def whatever(self [THIS FUNCTION] + + Self::arguments(field.arguments, start_with_comma, f)?; + if let Some(output_type) = field.py_type { + write!(f, ") -> {}", (output_type)()) + } else { + write!(f, ")") + } + } + + fn arguments(arguments: &[ArgumentInfo], start_with_comma: bool, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut add_comma = start_with_comma; + let mut positional_only = true; + let mut keyword_only = false; + + for argument in arguments { + if add_comma { + write!(f, ", ")?; + } + + if positional_only && !matches!(argument.kind, ArgumentKind::Position) { + // PEP570 + if add_comma { + write!(f, "/, ")?; + } + positional_only = false + } + + if !keyword_only && matches!(argument.kind, ArgumentKind::Keyword) { + // PEP3102 + write!(f, "*, ")?; + keyword_only = true + } + + match argument.kind { + ArgumentKind::VarArg => write!(f, "*")?, + ArgumentKind::KeywordArg => write!(f, "**")?, + _ => {}, + }; + + write!(f, "{}", argument.name)?; + + if let Some(py_type) = argument.py_type { + write!(f, ": {}", (py_type)())?; + } + + if argument.default_value { + write!(f, " = ...")?; + } + + add_comma = true; + } + + Ok(()) + } +} + +impl<'a> Display for InterfaceGenerator<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.class_header(f)?; + + if self.info.fields.is_empty() && self.info.class.fields.is_empty() { + writeln!(f, " ...")?; + } else { + writeln!(f)?; + } + + for field in self.info.fields() { + Self::field(*field, f)?; + } + + Ok(()) + } +} diff --git a/src/inspect/mod.rs b/src/inspect/mod.rs index 48a610b4989..1542ac7fa6d 100644 --- a/src/inspect/mod.rs +++ b/src/inspect/mod.rs @@ -7,3 +7,4 @@ pub mod types; pub mod fields; pub mod classes; pub mod modules; +pub mod interface; diff --git a/tests/test_interface.rs b/tests/test_interface.rs index defe8ff1972..c77e0c8d06b 100644 --- a/tests/test_interface.rs +++ b/tests/test_interface.rs @@ -1,10 +1,96 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::inspect::classes::InspectClass; +use pyo3::inspect::classes::{ClassInfo, ClassStructInfo, InspectClass}; +use pyo3::inspect::fields::{ArgumentInfo, ArgumentKind, FieldInfo, FieldKind}; +use pyo3::inspect::interface::InterfaceGenerator; +use pyo3::inspect::types::TypeInfo; mod common; +#[test] +fn types() { + assert_eq!("bool", format!("{}", ::type_output())); + assert_eq!("bool", format!("{}", >::type_output())); + assert_eq!("bytes", format!("{}", <&[u8]>::type_output())); + assert_eq!("str", format!("{}", ::type_output())); + assert_eq!("str", format!("{}", ::type_output())); + assert_eq!("Optional[str]", format!("{}", >::type_output())); + assert_eq!("Simple", format!("{}", <&PyCell>::type_input())); +} + +//region Empty class + +const EXPECTED_EMPTY: &str = "class Empty: ...\n"; + +#[test] +fn empty_manual() { + let class = ClassInfo { + class: &ClassStructInfo { + name: "Empty", + base: None, + fields: &[], + }, + fields: &[], + }; + + assert_eq!(EXPECTED_EMPTY, format!("{}", InterfaceGenerator::new(class))) +} + +#[pyclass] +struct Empty {} + +#[pymethods] +impl Empty {} + +#[test] +fn empty_derived() { + assert_eq!(EXPECTED_EMPTY, format!("{}", InterfaceGenerator::new(Empty::inspect()))) +} + +//endregion +//region Constructor + +const EXPECTED_SIMPLE: &str = r#"class Simple: + def __new__(cls) -> None: ... + def plus_one(self, /, a: int) -> int: ... +"#; + +#[test] +fn simple_manual() { + let class = ClassInfo { + class: &ClassStructInfo { + name: "Simple", + base: None, + fields: &[], + }, + fields: &[ + &FieldInfo { + name: "__new__", + kind: FieldKind::New, + py_type: None, + arguments: &[], + }, + &FieldInfo { + name: "plus_one", + kind: FieldKind::Function, + py_type: Some(|| TypeInfo::Builtin("int")), + arguments: &[ + ArgumentInfo { + name: "a", + kind: ArgumentKind::PositionOrKeyword, + py_type: Some(|| TypeInfo::Builtin("int")), + default_value: false, + is_modified: false, + } + ], + } + ], + }; + + assert_eq!(EXPECTED_SIMPLE, format!("{}", InterfaceGenerator::new(class))) +} + #[pyclass] #[derive(Clone)] struct Simple {} @@ -22,27 +108,115 @@ impl Simple { } #[test] -fn compiles() { - // Nothing to do: if we reach this point, the compilation was successful :) +fn simple_derived() { + assert_eq!(EXPECTED_SIMPLE, format!("{}", InterfaceGenerator::new(Simple::inspect()))) } -#[test] -fn simple_info() { - let class_info = Simple::inspect(); - println!("Type of usize: {:?}", usize::type_input()); - println!("Type of class: {:?}", Simple::type_output()); - println!("Class: {:?}", class_info); +//endregion +//region Complicated - assert!(false) -} +const EXPECTED_COMPLICATED: &str = r#"class Complicated(Simple): + @property + def value(self) -> int: ... + @value.setter + def value(self, value: int) -> None: ... + def __new__(cls, /, foo: str = ..., **parent: Any) -> None: ... + @staticmethod + def static(input: Complicated) -> Complicated: ... + @classmethod + def classmeth(cls, /, input: Union[Complicated, str, int]) -> Complicated: ... + counter: int = ... +"#; #[test] -fn types() { - assert_eq!("bool", format!("{}", ::type_output())); - assert_eq!("bool", format!("{}", >::type_output())); - assert_eq!("bytes", format!("{}", <&[u8]>::type_output())); - assert_eq!("str", format!("{}", ::type_output())); - assert_eq!("str", format!("{}", ::type_output())); - assert_eq!("Optional[str]", format!("{}", >::type_output())); - assert_eq!("Simple", format!("{}", <&PyCell>::type_input())); +fn complicated_manual() { + let class = ClassInfo { + class: &ClassStructInfo { + name: "Complicated", + base: Some("Simple"), + fields: &[ + &FieldInfo { + name: "value", + kind: FieldKind::Getter, + py_type: Some(|| TypeInfo::Builtin("int")), + arguments: &[], + }, + &FieldInfo { + name: "value", + kind: FieldKind::Setter, + py_type: Some(|| TypeInfo::None), + arguments: &[ + ArgumentInfo { + name: "value", + kind: ArgumentKind::Position, + py_type: Some(|| TypeInfo::Builtin("int")), + default_value: false, + is_modified: false + } + ], + } + ], + }, + fields: &[ + &FieldInfo { + name: "__new__", + kind: FieldKind::New, + py_type: None, + arguments: &[ + ArgumentInfo { + name: "foo", + kind: ArgumentKind::PositionOrKeyword, + py_type: Some(|| TypeInfo::Builtin("str")), + default_value: true, + is_modified: false, + }, + ArgumentInfo { + name: "parent", + kind: ArgumentKind::KeywordArg, + py_type: Some(|| TypeInfo::Any), + default_value: false, + is_modified: false, + } + ], + }, + &FieldInfo { + name: "static", + kind: FieldKind::StaticMethod, + py_type: Some(|| TypeInfo::Class { module: None, name: "Complicated" }), + arguments: &[ + ArgumentInfo { + name: "input", + kind: ArgumentKind::PositionOrKeyword, + py_type: Some(|| TypeInfo::Class { module: None, name: "Complicated" }), + default_value: false, + is_modified: false, + } + ], + }, + &FieldInfo { + name: "classmeth", + kind: FieldKind::ClassMethod, + py_type: Some(|| TypeInfo::Class { module: None, name: "Complicated" }), + arguments: &[ + ArgumentInfo { + name: "input", + kind: ArgumentKind::PositionOrKeyword, + py_type: Some(|| TypeInfo::Union(vec![TypeInfo::Class { module: None, name: "Complicated" }, TypeInfo::Builtin("str"), TypeInfo::Builtin("int")])), + default_value: false, + is_modified: false + } + ] + }, + &FieldInfo { + name: "counter", + kind: FieldKind::ClassAttribute, + py_type: Some(|| TypeInfo::Builtin("int")), + arguments: &[] + } + ], + }; + + assert_eq!(EXPECTED_COMPLICATED, format!("{}", InterfaceGenerator::new(class))) } + +//endregion