Skip to content

Commit d15a14a

Browse files
authored
Introspection: Introduce TypeHint struct (#5438)
* Introduce TypeHint struct inspect::TypeHint is composed of an "annotation" string and a list of "imports" ("from X import Y" kind) The type is expected to be built using the macros `type_hint!(module, name)`, `type_hint_union!(*args)` and `type_hint_subscript(main, *args)` that take care of maintaining the import list Introspection data generation is done using the hidden type_hint_json macro to avoid that the proc macros generate too much code Sadly, outside `type_hint` these macros can't be converted into const functions because they need to do some concatenation. I introduced `type_hint!` for consistency, happy to convert it to a const function. Miscellaneous changes: - Rename PyType{Info,Check}::TYPE_INFO into TYPE_HINT - Drop redundant PyClassImpl::TYPE_NAME * Makes TypeHint an enum * Code review feedbacks * Handle nested modules imports * MSRV is now 1.83 * Code review feedback
1 parent 85e6507 commit d15a14a

32 files changed

+1108
-419
lines changed

newsfragments/5438.changed.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Introspection: introduce `TypeHint` and make use of it to encode type hint annotations.
2+
Rename `PyType{Info,Check}::TYPE_INFO` into `PyType{Info,Check}::TYPE_HINT`

noxfile.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,15 @@ def test_introspection(session: nox.Session):
11091109
profile = os.environ.get("CARGO_BUILD_PROFILE")
11101110
if profile == "release":
11111111
options.append("--release")
1112-
session.run_always("maturin", "develop", "-m", "./pytests/Cargo.toml", *options)
1112+
session.run_always(
1113+
"maturin",
1114+
"develop",
1115+
"-m",
1116+
"./pytests/Cargo.toml",
1117+
"--features",
1118+
"experimental-inspect",
1119+
*options,
1120+
)
11131121
# We look for the built library
11141122
lib_file = None
11151123
for file in Path(session.virtualenv.location).rglob("pyo3_pytests.*"):

pyo3-introspection/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ anyhow = "1"
1313
goblin = ">=0.9, <0.11"
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
16-
unicode-ident = "1"
1716

1817
[dev-dependencies]
1918
tempfile = "3.12.0"

pyo3-introspection/src/introspection.rs

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::model::{
2-
Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument,
2+
Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr,
3+
VariableLengthArgument,
34
};
45
use anyhow::{anyhow, bail, ensure, Context, Result};
56
use goblin::elf::section_header::SHN_XINDEX;
@@ -9,11 +10,13 @@ use goblin::mach::symbols::{NO_SECT, N_SECT};
910
use goblin::mach::{Mach, MachO, SingleArch};
1011
use goblin::pe::PE;
1112
use goblin::Object;
12-
use serde::Deserialize;
13+
use serde::de::value::MapAccessDeserializer;
14+
use serde::de::{Error, MapAccess, Visitor};
15+
use serde::{Deserialize, Deserializer};
1316
use std::cmp::Ordering;
1417
use std::collections::HashMap;
1518
use std::path::Path;
16-
use std::{fs, str};
19+
use std::{fmt, fs, str};
1720

1821
/// Introspect a cdylib built with PyO3 and returns the definition of a Python module.
1922
///
@@ -192,7 +195,7 @@ fn convert_function(
192195
name: &str,
193196
arguments: &ChunkArguments,
194197
decorators: &[String],
195-
returns: &Option<String>,
198+
returns: &Option<ChunkTypeHint>,
196199
) -> Function {
197200
Function {
198201
name: name.into(),
@@ -210,30 +213,58 @@ fn convert_function(
210213
.as_ref()
211214
.map(convert_variable_length_argument),
212215
},
213-
returns: returns.clone(),
216+
returns: returns.as_ref().map(convert_type_hint),
214217
}
215218
}
216219

217220
fn convert_argument(arg: &ChunkArgument) -> Argument {
218221
Argument {
219222
name: arg.name.clone(),
220223
default_value: arg.default.clone(),
221-
annotation: arg.annotation.clone(),
224+
annotation: arg.annotation.as_ref().map(convert_type_hint),
222225
}
223226
}
224227

225228
fn convert_variable_length_argument(arg: &ChunkArgument) -> VariableLengthArgument {
226229
VariableLengthArgument {
227230
name: arg.name.clone(),
228-
annotation: arg.annotation.clone(),
231+
annotation: arg.annotation.as_ref().map(convert_type_hint),
229232
}
230233
}
231234

232-
fn convert_attribute(name: &str, value: &Option<String>, annotation: &Option<String>) -> Attribute {
235+
fn convert_attribute(
236+
name: &str,
237+
value: &Option<String>,
238+
annotation: &Option<ChunkTypeHint>,
239+
) -> Attribute {
233240
Attribute {
234241
name: name.into(),
235242
value: value.clone(),
236-
annotation: annotation.clone(),
243+
annotation: annotation.as_ref().map(convert_type_hint),
244+
}
245+
}
246+
247+
fn convert_type_hint(arg: &ChunkTypeHint) -> TypeHint {
248+
match arg {
249+
ChunkTypeHint::Ast(expr) => TypeHint::Ast(convert_type_hint_expr(expr)),
250+
ChunkTypeHint::Plain(t) => TypeHint::Plain(t.clone()),
251+
}
252+
}
253+
254+
fn convert_type_hint_expr(expr: &ChunkTypeHintExpr) -> TypeHintExpr {
255+
match expr {
256+
ChunkTypeHintExpr::Builtin { id } => TypeHintExpr::Builtin { id: id.clone() },
257+
ChunkTypeHintExpr::Attribute { module, attr } => TypeHintExpr::Attribute {
258+
module: module.clone(),
259+
attr: attr.clone(),
260+
},
261+
ChunkTypeHintExpr::Union { elts } => TypeHintExpr::Union {
262+
elts: elts.iter().map(convert_type_hint_expr).collect(),
263+
},
264+
ChunkTypeHintExpr::Subscript { value, slice } => TypeHintExpr::Subscript {
265+
value: Box::new(convert_type_hint_expr(value)),
266+
slice: slice.iter().map(convert_type_hint_expr).collect(),
267+
},
237268
}
238269
}
239270

@@ -388,8 +419,8 @@ enum Chunk {
388419
parent: Option<String>,
389420
#[serde(default)]
390421
decorators: Vec<String>,
391-
#[serde(default)]
392-
returns: Option<String>,
422+
#[serde(default, deserialize_with = "deserialize_type_hint")]
423+
returns: Option<ChunkTypeHint>,
393424
},
394425
Attribute {
395426
#[serde(default)]
@@ -399,8 +430,8 @@ enum Chunk {
399430
name: String,
400431
#[serde(default)]
401432
value: Option<String>,
402-
#[serde(default)]
403-
annotation: Option<String>,
433+
#[serde(default, deserialize_with = "deserialize_type_hint")]
434+
annotation: Option<ChunkTypeHint>,
404435
},
405436
}
406437

@@ -423,6 +454,69 @@ struct ChunkArgument {
423454
name: String,
424455
#[serde(default)]
425456
default: Option<String>,
426-
#[serde(default)]
427-
annotation: Option<String>,
457+
#[serde(default, deserialize_with = "deserialize_type_hint")]
458+
annotation: Option<ChunkTypeHint>,
459+
}
460+
461+
/// Variant of [`TypeHint`] that implements deserialization.
462+
///
463+
/// We keep separated type to allow them to evolve independently (this type will need to handle backward compatibility).
464+
enum ChunkTypeHint {
465+
Ast(ChunkTypeHintExpr),
466+
Plain(String),
467+
}
468+
469+
#[derive(Deserialize)]
470+
#[serde(tag = "type", rename_all = "lowercase")]
471+
enum ChunkTypeHintExpr {
472+
Builtin {
473+
id: String,
474+
},
475+
Attribute {
476+
module: String,
477+
attr: String,
478+
},
479+
Union {
480+
elts: Vec<ChunkTypeHintExpr>,
481+
},
482+
Subscript {
483+
value: Box<ChunkTypeHintExpr>,
484+
slice: Vec<ChunkTypeHintExpr>,
485+
},
486+
}
487+
488+
fn deserialize_type_hint<'de, D: Deserializer<'de>>(
489+
deserializer: D,
490+
) -> Result<Option<ChunkTypeHint>, D::Error> {
491+
struct AnnotationVisitor;
492+
493+
impl<'de> Visitor<'de> for AnnotationVisitor {
494+
type Value = ChunkTypeHint;
495+
496+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
497+
formatter.write_str("annotation")
498+
}
499+
500+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
501+
where
502+
E: Error,
503+
{
504+
self.visit_string(v.into())
505+
}
506+
507+
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
508+
where
509+
E: Error,
510+
{
511+
Ok(ChunkTypeHint::Plain(v))
512+
}
513+
514+
fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<ChunkTypeHint, M::Error> {
515+
Ok(ChunkTypeHint::Ast(Deserialize::deserialize(
516+
MapAccessDeserializer::new(map),
517+
)?))
518+
}
519+
}
520+
521+
Ok(Some(deserializer.deserialize_any(AnnotationVisitor)?))
428522
}

pyo3-introspection/src/model.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct Function {
2222
pub decorators: Vec<String>,
2323
pub arguments: Arguments,
2424
/// return type
25-
pub returns: Option<String>,
25+
pub returns: Option<TypeHint>,
2626
}
2727

2828
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
@@ -31,7 +31,7 @@ pub struct Attribute {
3131
/// Value as a Python expression if easily expressible
3232
pub value: Option<String>,
3333
/// Type annotation as a Python expression
34-
pub annotation: Option<String>,
34+
pub annotation: Option<TypeHint>,
3535
}
3636

3737
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
@@ -54,13 +54,38 @@ pub struct Argument {
5454
/// Default value as a Python expression
5555
pub default_value: Option<String>,
5656
/// Type annotation as a Python expression
57-
pub annotation: Option<String>,
57+
pub annotation: Option<TypeHint>,
5858
}
5959

6060
/// A variable length argument ie. *vararg or **kwarg
6161
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
6262
pub struct VariableLengthArgument {
6363
pub name: String,
6464
/// Type annotation as a Python expression
65-
pub annotation: Option<String>,
65+
pub annotation: Option<TypeHint>,
66+
}
67+
68+
/// A type hint annotation
69+
///
70+
/// Might be a plain string or an AST fragment
71+
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
72+
pub enum TypeHint {
73+
Ast(TypeHintExpr),
74+
Plain(String),
75+
}
76+
77+
/// A type hint annotation as an AST fragment
78+
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
79+
pub enum TypeHintExpr {
80+
/// A Python builtin like `int`
81+
Builtin { id: String },
82+
/// The attribute of a python object like `{value}.{attr}`
83+
Attribute { module: String, attr: String },
84+
/// A union `{left} | {right}`
85+
Union { elts: Vec<TypeHintExpr> },
86+
/// A subscript `{value}[*slice]`
87+
Subscript {
88+
value: Box<TypeHintExpr>,
89+
slice: Vec<TypeHintExpr>,
90+
},
6691
}

0 commit comments

Comments
 (0)