Skip to content

Commit 9367241

Browse files
committed
Adds basic input type annotations
1 parent 41ba6f9 commit 9367241

File tree

25 files changed

+362
-29
lines changed

25 files changed

+362
-29
lines changed

guide/src/class.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,9 @@ impl pyo3::types::DerefToPyAny for MyClass {}
13781378
unsafe impl pyo3::type_object::PyTypeInfo for MyClass {
13791379
const NAME: &'static str = "MyClass";
13801380
const MODULE: ::std::option::Option<&'static str> = ::std::option::Option::None;
1381+
#[cfg(feature = "experimental-inspect")]
1382+
const INPUT_TYPE: &'static str = "MyClass";
1383+
13811384
#[inline]
13821385
fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject {
13831386
<Self as pyo3::impl_::pyclass::PyClassImpl>::lazy_type_object()
@@ -1393,6 +1396,8 @@ impl pyo3::PyClass for MyClass {
13931396
impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a MyClass
13941397
{
13951398
type Holder = ::std::option::Option<pyo3::PyRef<'py, MyClass>>;
1399+
#[cfg(feature = "experimental-inspect")]
1400+
const INPUT_TYPE: &'static str = "MyClass";
13961401

13971402
#[inline]
13981403
fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult<Self> {

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ def update_ui_tests(session: nox.Session):
830830
@nox.session(name="test-introspection")
831831
def test_introspection(session: nox.Session):
832832
session.install("maturin")
833+
session.install("ruff")
833834
target = os.environ.get("CARGO_BUILD_TARGET")
834835
for options in ([], ["--release"]):
835836
if target is not None:

pyo3-introspection/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ goblin = "0.9.0"
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
1616

17+
[dev-dependencies]
18+
tempfile = "3.12.0"
19+
1720
[lints]
1821
workspace = true

pyo3-introspection/src/introspection.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ fn convert_argument(arg: &ChunkArgument) -> Argument {
112112
Argument {
113113
name: arg.name.clone(),
114114
default_value: arg.default.clone(),
115+
annotation: arg.annotation.clone(),
115116
}
116117
}
117118

@@ -315,4 +316,6 @@ struct ChunkArgument {
315316
name: String,
316317
#[serde(default)]
317318
default: Option<String>,
319+
#[serde(default)]
320+
annotation: Option<String>,
318321
}

pyo3-introspection/src/model.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub struct Argument {
3636
pub name: String,
3737
/// Default value as a Python expression
3838
pub default_value: Option<String>,
39+
/// Type annotation as a Python expression
40+
pub annotation: Option<String>,
3941
}
4042

4143
/// A variable length argument ie. *vararg or **kwarg

pyo3-introspection/src/stubs.rs

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::model::{Argument, Class, Function, Module, VariableLengthArgument};
2-
use std::collections::HashMap;
2+
use std::collections::{BTreeSet, HashMap};
33
use std::path::{Path, PathBuf};
44

55
/// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module.
@@ -32,51 +32,70 @@ fn add_module_stub_files(
3232

3333
/// Generates the module stubs to a String, not including submodules
3434
fn module_stubs(module: &Module) -> String {
35+
let mut modules_to_import = BTreeSet::new();
3536
let mut elements = Vec::new();
3637
for class in &module.classes {
3738
elements.push(class_stubs(class));
3839
}
3940
for function in &module.functions {
40-
elements.push(function_stubs(function));
41+
elements.push(function_stubs(function, &mut modules_to_import));
4142
}
4243
elements.push(String::new()); // last line jump
43-
elements.join("\n")
44+
45+
let mut final_elements = Vec::new();
46+
for module_to_import in &modules_to_import {
47+
final_elements.push(format!("import {module_to_import}"));
48+
}
49+
final_elements.extend(elements);
50+
final_elements.join("\n")
4451
}
4552

4653
fn class_stubs(class: &Class) -> String {
4754
format!("class {}: ...", class.name)
4855
}
4956

50-
fn function_stubs(function: &Function) -> String {
57+
fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet<String>) -> String {
5158
// Signature
5259
let mut parameters = Vec::new();
5360
for argument in &function.arguments.positional_only_arguments {
54-
parameters.push(argument_stub(argument));
61+
parameters.push(argument_stub(argument, modules_to_import));
5562
}
5663
if !function.arguments.positional_only_arguments.is_empty() {
5764
parameters.push("/".into());
5865
}
5966
for argument in &function.arguments.arguments {
60-
parameters.push(argument_stub(argument));
67+
parameters.push(argument_stub(argument, modules_to_import));
6168
}
6269
if let Some(argument) = &function.arguments.vararg {
6370
parameters.push(format!("*{}", variable_length_argument_stub(argument)));
6471
} else if !function.arguments.keyword_only_arguments.is_empty() {
6572
parameters.push("*".into());
6673
}
6774
for argument in &function.arguments.keyword_only_arguments {
68-
parameters.push(argument_stub(argument));
75+
parameters.push(argument_stub(argument, modules_to_import));
6976
}
7077
if let Some(argument) = &function.arguments.kwarg {
7178
parameters.push(format!("**{}", variable_length_argument_stub(argument)));
7279
}
7380
format!("def {}({}): ...", function.name, parameters.join(", "))
7481
}
7582

76-
fn argument_stub(argument: &Argument) -> String {
83+
fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet<String>) -> String {
7784
let mut output = argument.name.clone();
85+
if let Some(annotation) = &argument.annotation {
86+
output.push_str(": ");
87+
output.push_str(annotation);
88+
if let Some((module, _)) = annotation.rsplit_once('.') {
89+
// TODO: this is very naive
90+
modules_to_import.insert(module.into());
91+
}
92+
}
7893
if let Some(default_value) = &argument.default_value {
79-
output.push('=');
94+
output.push_str(if argument.annotation.is_some() {
95+
" = "
96+
} else {
97+
"="
98+
});
8099
output.push_str(default_value);
81100
}
82101
output
@@ -99,26 +118,29 @@ mod tests {
99118
positional_only_arguments: vec![Argument {
100119
name: "posonly".into(),
101120
default_value: None,
121+
annotation: None,
102122
}],
103123
arguments: vec![Argument {
104124
name: "arg".into(),
105125
default_value: None,
126+
annotation: None,
106127
}],
107128
vararg: Some(VariableLengthArgument {
108129
name: "varargs".into(),
109130
}),
110131
keyword_only_arguments: vec![Argument {
111132
name: "karg".into(),
112133
default_value: None,
134+
annotation: Some("str".into()),
113135
}],
114136
kwarg: Some(VariableLengthArgument {
115137
name: "kwarg".into(),
116138
}),
117139
},
118140
};
119141
assert_eq!(
120-
"def func(posonly, /, arg, *varargs, karg, **kwarg): ...",
121-
function_stubs(&function)
142+
"def func(posonly, /, arg, *varargs, karg: str, **kwarg): ...",
143+
function_stubs(&function, &mut BTreeSet::new())
122144
)
123145
}
124146

@@ -130,22 +152,25 @@ mod tests {
130152
positional_only_arguments: vec![Argument {
131153
name: "posonly".into(),
132154
default_value: Some("1".into()),
155+
annotation: None,
133156
}],
134157
arguments: vec![Argument {
135158
name: "arg".into(),
136159
default_value: Some("True".into()),
160+
annotation: None,
137161
}],
138162
vararg: None,
139163
keyword_only_arguments: vec![Argument {
140164
name: "karg".into(),
141165
default_value: Some("\"foo\"".into()),
166+
annotation: Some("str".into()),
142167
}],
143168
kwarg: None,
144169
},
145170
};
146171
assert_eq!(
147-
"def afunc(posonly=1, /, arg=True, *, karg=\"foo\"): ...",
148-
function_stubs(&function)
172+
"def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...",
173+
function_stubs(&function, &mut BTreeSet::new())
149174
)
150175
}
151176
}

pyo3-introspection/tests/test.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use anyhow::Result;
1+
use anyhow::{ensure, Result};
22
use pyo3_introspection::{introspect_cdylib, module_stub_files};
33
use std::collections::HashMap;
4+
use std::io::{Read, Seek, SeekFrom, Write};
45
use std::path::{Path, PathBuf};
6+
use std::process::Command;
57
use std::{env, fs};
8+
use tempfile::NamedTempFile;
69

710
#[test]
811
fn pytests_stubs() -> Result<()> {
@@ -42,9 +45,12 @@ fn pytests_stubs() -> Result<()> {
4245
file_name.display()
4346
)
4447
});
48+
49+
let actual_file_content = format_with_ruff(actual_file_content)?;
50+
4551
assert_eq!(
46-
&expected_file_content.replace('\r', ""), // Windows compatibility
47-
actual_file_content,
52+
expected_file_content.as_str(),
53+
actual_file_content.as_str(),
4854
"The content of file {} is different",
4955
file_name.display()
5056
)
@@ -75,3 +81,25 @@ fn add_dir_files(
7581
}
7682
Ok(())
7783
}
84+
85+
fn format_with_ruff(code: &str) -> Result<String> {
86+
let temp_file = NamedTempFile::with_suffix(".pyi")?;
87+
// Write to file
88+
{
89+
let mut file = temp_file.as_file();
90+
file.write_all(code.as_bytes())?;
91+
file.flush()?;
92+
file.seek(SeekFrom::Start(0))?;
93+
}
94+
ensure!(
95+
Command::new("ruff")
96+
.arg("format")
97+
.arg(temp_file.path())
98+
.status()?
99+
.success(),
100+
"Failed to run ruff"
101+
);
102+
let mut content = String::new();
103+
temp_file.as_file().read_to_string(&mut content)?;
104+
Ok(content)
105+
}

0 commit comments

Comments
 (0)