Skip to content

Commit 8bec1c2

Browse files
committed
Introspection: properly tag modules as incomplete
1 parent 1040c5a commit 8bec1c2

File tree

7 files changed

+50
-3
lines changed

7 files changed

+50
-3
lines changed

newsfragments/5207.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Type stubs: tag modules created using `#[pymodule]` or `#[pymodule_init]` functions as incomplete

pyo3-introspection/src/introspection.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,19 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result<Module> {
4545
name,
4646
members,
4747
consts,
48+
incomplete,
4849
id: _,
4950
} = chunk
5051
{
5152
if name == main_module_name {
52-
return convert_module(name, members, consts, &chunks_by_id, &chunks_by_parent);
53+
return convert_module(
54+
name,
55+
members,
56+
consts,
57+
*incomplete,
58+
&chunks_by_id,
59+
&chunks_by_parent,
60+
);
5361
}
5462
}
5563
}
@@ -60,6 +68,7 @@ fn convert_module(
6068
name: &str,
6169
members: &[String],
6270
consts: &[ConstChunk],
71+
incomplete: bool,
6372
chunks_by_id: &HashMap<&str, &Chunk>,
6473
chunks_by_parent: &HashMap<&str, Vec<&Chunk>>,
6574
) -> Result<Module> {
@@ -84,6 +93,7 @@ fn convert_module(
8493
value: c.value.clone(),
8594
})
8695
.collect(),
96+
incomplete,
8797
})
8898
}
8999

@@ -102,12 +112,14 @@ fn convert_members(
102112
name,
103113
members,
104114
consts,
115+
incomplete,
105116
id: _,
106117
} => {
107118
modules.push(convert_module(
108119
name,
109120
members,
110121
consts,
122+
*incomplete,
111123
chunks_by_id,
112124
chunks_by_parent,
113125
)?);
@@ -375,6 +387,7 @@ enum Chunk {
375387
name: String,
376388
members: Vec<String>,
377389
consts: Vec<ConstChunk>,
390+
incomplete: bool,
378391
},
379392
Class {
380393
id: String,

pyo3-introspection/src/model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub struct Module {
55
pub classes: Vec<Class>,
66
pub functions: Vec<Function>,
77
pub consts: Vec<Const>,
8+
pub incomplete: bool,
89
}
910

1011
#[derive(Debug, Eq, PartialEq, Clone, Hash)]

pyo3-introspection/src/stubs.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::model::{Argument, Class, Const, Function, Module, VariableLengthArgument};
1+
use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument};
22
use std::collections::{BTreeSet, HashMap};
33
use std::path::{Path, PathBuf};
44

@@ -43,6 +43,31 @@ fn module_stubs(module: &Module) -> String {
4343
for function in &module.functions {
4444
elements.push(function_stubs(function, &mut modules_to_import));
4545
}
46+
47+
// We generate a __getattr__ method to tag incomplete stubs
48+
// See https://typing.python.org/en/latest/guides/writing_stubs.html#incomplete-stubs
49+
if module.incomplete && !module.functions.iter().any(|f| f.name == "__getattr__") {
50+
elements.push(function_stubs(
51+
&Function {
52+
name: "__getattr__".into(),
53+
decorators: Vec::new(),
54+
arguments: Arguments {
55+
positional_only_arguments: Vec::new(),
56+
arguments: vec![Argument {
57+
name: "name".to_string(),
58+
default_value: None,
59+
annotation: Some("str".into()),
60+
}],
61+
vararg: None,
62+
keyword_only_arguments: Vec::new(),
63+
kwarg: None,
64+
},
65+
},
66+
&mut modules_to_import,
67+
));
68+
// TODO: return parameter is _typeshed.Incomplete
69+
}
70+
4671
let mut final_elements = Vec::new();
4772
for module_to_import in &modules_to_import {
4873
final_elements.push(format!("import {module_to_import}"));

pyo3-macros-backend/src/introspection.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use syn::{Attribute, Ident, ReturnType, Type, TypePath};
2525

2626
static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0);
2727

28+
#[allow(clippy::too_many_arguments)]
2829
pub fn module_introspection_code<'a>(
2930
pyo3_crate_path: &PyO3CratePath,
3031
name: &str,
@@ -33,6 +34,7 @@ pub fn module_introspection_code<'a>(
3334
consts: impl IntoIterator<Item = &'a Ident>,
3435
consts_values: impl IntoIterator<Item = &'a String>,
3536
consts_cfg_attrs: impl IntoIterator<Item = &'a Vec<Attribute>>,
37+
incomplete: bool,
3638
) -> TokenStream {
3739
IntrospectionNode::Map(
3840
[
@@ -74,6 +76,7 @@ pub fn module_introspection_code<'a>(
7476
.collect(),
7577
),
7678
),
79+
("incomplete", IntrospectionNode::Bool(incomplete)),
7780
]
7881
.into(),
7982
)
@@ -309,6 +312,7 @@ fn argument_introspection_data<'a>(
309312

310313
enum IntrospectionNode<'a> {
311314
String(Cow<'a, str>),
315+
Bool(bool),
312316
IntrospectionId(Option<Cow<'a, Type>>),
313317
InputType { rust_type: Type, nullable: bool },
314318
OutputType { rust_type: Type },
@@ -342,6 +346,7 @@ impl IntrospectionNode<'_> {
342346
Self::String(string) => {
343347
content.push_str_to_escape(&string);
344348
}
349+
Self::Bool(value) => content.push_str(if value { "true" } else { "false" }),
345350
Self::IntrospectionId(ident) => {
346351
content.push_str("\"");
347352
content.push_tokens(if let Some(ident) = ident {

pyo3-macros-backend/src/module.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ pub fn pymodule_module_impl(
354354
&module_consts,
355355
&module_consts_values,
356356
&module_consts_cfg_attrs,
357+
pymodule_init.is_some(),
357358
);
358359
#[cfg(not(feature = "experimental-inspect"))]
359360
let introspection = quote! {};
@@ -438,7 +439,7 @@ pub fn pymodule_function_impl(
438439

439440
#[cfg(feature = "experimental-inspect")]
440441
let introspection =
441-
module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[]);
442+
module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[], true);
442443
#[cfg(not(feature = "experimental-inspect"))]
443444
let introspection = quote! {};
444445
#[cfg(feature = "experimental-inspect")]

pytests/stubs/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
def __getattr__(name: str): ...

0 commit comments

Comments
 (0)