Skip to main content

pyo3/inspect/
mod.rs

1//! Runtime inspection of objects exposed to Python.
2//!
3//! Tracking issue: <https://github.com/PyO3/pyo3/issues/2454>.
4
5use crate::impl_::introspection::{escape_json_string, escaped_json_string_len};
6use std::fmt::{self, Display, Write};
7
8pub mod types;
9
10/// Builds a type hint from a module name and a member name in the module
11///
12/// ```
13/// use pyo3::type_hint_identifier;
14/// use pyo3::inspect::PyStaticExpr;
15///
16/// const T: PyStaticExpr = type_hint_identifier!("datetime", "date");
17/// assert_eq!(T.to_string(), "datetime.date");
18///
19/// const T2: PyStaticExpr = type_hint_identifier!("builtins", "int");
20/// assert_eq!(T2.to_string(), "int");
21/// ```
22#[macro_export]
23macro_rules! type_hint_identifier {
24    ("builtins", $name:expr) => {
25        $crate::inspect::PyStaticExpr::Name { id: $name }
26    };
27    ($module:expr, $name:expr) => {
28        $crate::inspect::PyStaticExpr::Attribute {
29            value: &$crate::inspect::PyStaticExpr::Name { id: $module },
30            attr: $name,
31        }
32    };
33}
34pub(crate) use type_hint_identifier;
35
36/// Builds the union of multiple type hints
37///
38/// ```
39/// use pyo3::{type_hint_identifier, type_hint_union};
40/// use pyo3::inspect::PyStaticExpr;
41///
42/// const T: PyStaticExpr = type_hint_union!(type_hint_identifier!("builtins", "int"), type_hint_identifier!("builtins", "float"));
43/// assert_eq!(T.to_string(), "int | float");
44/// ```
45#[macro_export]
46macro_rules! type_hint_union {
47    ($e:expr) => { $e };
48    ($l:expr , $($r:expr),+) => { $crate::inspect::PyStaticExpr::BinOp {
49        left: &$l,
50        op: $crate::inspect::PyStaticOperator::BitOr,
51        right: &type_hint_union!($($r),+),
52    } };
53}
54pub(crate) use type_hint_union;
55
56/// Builds a subscribed type hint
57///
58/// ```
59/// use pyo3::{type_hint_identifier, type_hint_subscript};
60/// use pyo3::inspect::PyStaticExpr;
61///
62/// const T: PyStaticExpr = type_hint_subscript!(type_hint_identifier!("collections.abc", "Sequence"), type_hint_identifier!("builtins", "float"));
63/// assert_eq!(T.to_string(), "collections.abc.Sequence[float]");
64///
65/// const T2: PyStaticExpr = type_hint_subscript!(type_hint_identifier!("builtins", "dict"), type_hint_identifier!("builtins", "str"), type_hint_identifier!("builtins", "float"));
66/// assert_eq!(T2.to_string(), "dict[str, float]");
67/// ```
68#[macro_export]
69macro_rules! type_hint_subscript {
70    ($l:expr, $r:expr) => {
71        $crate::inspect::PyStaticExpr::Subscript {
72            value: &$l,
73            slice: &$r
74        }
75    };
76    ($l:expr, $($r:expr),*) => {
77        $crate::inspect::PyStaticExpr::Subscript {
78            value: &$l,
79            slice: &$crate::inspect::PyStaticExpr::Tuple { elts: &[$($r),*] }
80        }
81    };
82}
83pub(crate) use type_hint_subscript;
84
85/// A Python expression.
86///
87/// This is the `expr` production of the [Python `ast` module grammar](https://docs.python.org/3/library/ast.html#abstract-grammar)
88///
89/// This struct aims at being used in `const` contexts like in [`FromPyObject::INPUT_TYPE`](crate::FromPyObject::INPUT_TYPE) and [`IntoPyObject::OUTPUT_TYPE`](crate::IntoPyObject::OUTPUT_TYPE).
90///
91/// Use macros like [`type_hint_identifier`], [`type_hint_union`] and [`type_hint_subscript`] to construct values.
92#[derive(Clone, Copy)]
93#[non_exhaustive]
94#[allow(missing_docs)]
95pub enum PyStaticExpr {
96    /// A constant like `None` or `123`
97    Constant { value: PyStaticConstant },
98    /// A name
99    Name { id: &'static str },
100    /// An attribute `value.attr`
101    Attribute {
102        value: &'static Self,
103        attr: &'static str,
104    },
105    /// A binary operator
106    BinOp {
107        left: &'static Self,
108        op: PyStaticOperator,
109        right: &'static Self,
110    },
111    /// A tuple
112    Tuple { elts: &'static [Self] },
113    /// A list
114    List { elts: &'static [Self] },
115    /// A subscript `value[slice]`
116    Subscript {
117        value: &'static Self,
118        slice: &'static Self,
119    },
120    /// A `#[pyclass]` type. This is separated type for introspection reasons.
121    PyClass(PyClassNameStaticExpr),
122}
123
124/// Serialize the type for introspection and return the number of written bytes
125#[doc(hidden)]
126pub const fn serialize_for_introspection(expr: &PyStaticExpr, mut output: &mut [u8]) -> usize {
127    let original_len = output.len();
128    match expr {
129        PyStaticExpr::Constant { value } => match value {
130            PyStaticConstant::None => {
131                output = write_slice_and_move_forward(
132                    b"{\"type\":\"constant\",\"kind\":\"none\"}",
133                    output,
134                )
135            }
136            PyStaticConstant::Bool(value) => {
137                output = write_slice_and_move_forward(
138                    if *value {
139                        b"{\"type\":\"constant\",\"kind\":\"bool\",\"value\":true}"
140                    } else {
141                        b"{\"type\":\"constant\",\"kind\":\"bool\",\"value\":false}"
142                    },
143                    output,
144                )
145            }
146            PyStaticConstant::Int(value) => {
147                output = write_slice_and_move_forward(
148                    b"{\"type\":\"constant\",\"kind\":\"int\",\"value\":\"",
149                    output,
150                );
151                output = write_slice_and_move_forward(value.as_bytes(), output);
152                output = write_slice_and_move_forward(b"\"}", output);
153            }
154            PyStaticConstant::Float(value) => {
155                output = write_slice_and_move_forward(
156                    b"{\"type\":\"constant\",\"kind\":\"float\",\"value\":\"",
157                    output,
158                );
159                output = write_slice_and_move_forward(value.as_bytes(), output);
160                output = write_slice_and_move_forward(b"\"}", output);
161            }
162            PyStaticConstant::Str(value) => {
163                output = write_slice_and_move_forward(
164                    b"{\"type\":\"constant\",\"kind\":\"str\",\"value\":",
165                    output,
166                );
167                output = write_json_string_and_move_forward(value, output);
168                output = write_slice_and_move_forward(b"}", output);
169            }
170            PyStaticConstant::Ellipsis => {
171                output = write_slice_and_move_forward(
172                    b"{\"type\":\"constant\",\"kind\":\"ellipsis\"}",
173                    output,
174                )
175            }
176        },
177        PyStaticExpr::Name { id } => {
178            output = write_slice_and_move_forward(b"{\"type\":\"name\",\"id\":\"", output);
179            output = write_slice_and_move_forward(id.as_bytes(), output);
180            output = write_slice_and_move_forward(b"\"}", output);
181        }
182        PyStaticExpr::Attribute { value, attr } => {
183            output = write_slice_and_move_forward(b"{\"type\":\"attribute\",\"value\":", output);
184            output = write_expr_and_move_forward(value, output);
185            output = write_slice_and_move_forward(b",\"attr\":\"", output);
186            output = write_slice_and_move_forward(attr.as_bytes(), output);
187            output = write_slice_and_move_forward(b"\"}", output);
188        }
189        PyStaticExpr::BinOp { left, op, right } => {
190            output = write_slice_and_move_forward(b"{\"type\":\"binop\",\"left\":", output);
191            output = write_expr_and_move_forward(left, output);
192            output = write_slice_and_move_forward(b",\"op\":\"", output);
193            output = write_slice_and_move_forward(
194                match op {
195                    PyStaticOperator::BitOr => b"bitor",
196                },
197                output,
198            );
199            output = write_slice_and_move_forward(b"\",\"right\":", output);
200            output = write_expr_and_move_forward(right, output);
201            output = write_slice_and_move_forward(b"}", output);
202        }
203        PyStaticExpr::Tuple { elts } => {
204            output = write_container_and_move_forward(b"tuple", elts, output);
205        }
206        PyStaticExpr::List { elts } => {
207            output = write_container_and_move_forward(b"list", elts, output);
208        }
209        PyStaticExpr::Subscript { value, slice } => {
210            output = write_slice_and_move_forward(b"{\"type\":\"subscript\",\"value\":", output);
211            output = write_expr_and_move_forward(value, output);
212            output = write_slice_and_move_forward(b",\"slice\":", output);
213            output = write_expr_and_move_forward(slice, output);
214            output = write_slice_and_move_forward(b"}", output);
215        }
216        PyStaticExpr::PyClass(expr) => {
217            output = write_slice_and_move_forward(b"{\"type\":\"id\",\"id\":\"", output);
218            output = write_slice_and_move_forward(expr.introspection_id.as_bytes(), output);
219            output = write_slice_and_move_forward(b"\"}", output);
220        }
221    }
222    original_len - output.len()
223}
224
225/// Length required by [`serialize_for_introspection`]
226#[doc(hidden)]
227pub const fn serialized_len_for_introspection(expr: &PyStaticExpr) -> usize {
228    match expr {
229        PyStaticExpr::Constant { value } => match value {
230            PyStaticConstant::None => 33,
231            PyStaticConstant::Bool(value) => 42 + if *value { 4 } else { 5 },
232            PyStaticConstant::Int(value) => 43 + value.len(),
233            PyStaticConstant::Float(value) => 45 + value.len(),
234            PyStaticConstant::Str(value) => 43 + escaped_json_string_len(value),
235            PyStaticConstant::Ellipsis => 37,
236        },
237        PyStaticExpr::Name { id } => 23 + id.len(),
238        PyStaticExpr::Attribute { value, attr } => {
239            39 + serialized_len_for_introspection(value) + attr.len()
240        }
241        PyStaticExpr::BinOp { left, op, right } => {
242            41 + serialized_len_for_introspection(left)
243                + match op {
244                    PyStaticOperator::BitOr => 5,
245                }
246                + serialized_len_for_introspection(right)
247        }
248        PyStaticExpr::Tuple { elts } => 5 + serialized_container_len_for_introspection(elts),
249        PyStaticExpr::List { elts } => 4 + serialized_container_len_for_introspection(elts),
250        PyStaticExpr::Subscript { value, slice } => {
251            38 + serialized_len_for_introspection(value) + serialized_len_for_introspection(slice)
252        }
253        PyStaticExpr::PyClass(expr) => 21 + expr.introspection_id.len(),
254    }
255}
256
257impl fmt::Display for PyStaticExpr {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self {
260            Self::Constant { value } => match value {
261                PyStaticConstant::None => f.write_str("None"),
262                PyStaticConstant::Bool(value) => f.write_str(if *value { "True" } else { "False" }),
263                PyStaticConstant::Int(value) => f.write_str(value),
264                PyStaticConstant::Float(value) => {
265                    f.write_str(value)?;
266                    if !value.contains(['.', 'e', 'E']) {
267                        // Makes sure it's not parsed as an int
268                        f.write_char('.')?;
269                    }
270                    Ok(())
271                }
272                PyStaticConstant::Str(value) => write!(f, "{value:?}"),
273                PyStaticConstant::Ellipsis => f.write_str("..."),
274            },
275            Self::Name { id, .. } => f.write_str(id),
276            Self::Attribute { value, attr } => {
277                value.fmt(f)?;
278                f.write_str(".")?;
279                f.write_str(attr)
280            }
281            Self::BinOp { left, op, right } => {
282                left.fmt(f)?;
283                f.write_char(' ')?;
284                f.write_char(match op {
285                    PyStaticOperator::BitOr => '|',
286                })?;
287                f.write_char(' ')?;
288                right.fmt(f)
289            }
290            Self::Tuple { elts } => {
291                f.write_char('(')?;
292                fmt_elements(elts, f)?;
293                if elts.len() == 1 {
294                    f.write_char(',')?;
295                }
296                f.write_char(')')
297            }
298            Self::List { elts } => {
299                f.write_char('[')?;
300                fmt_elements(elts, f)?;
301                f.write_char(']')
302            }
303            Self::Subscript { value, slice } => {
304                value.fmt(f)?;
305                f.write_char('[')?;
306                if let PyStaticExpr::Tuple { elts } = slice {
307                    // We don't display the tuple parentheses
308                    fmt_elements(elts, f)?;
309                } else {
310                    slice.fmt(f)?;
311                }
312                f.write_char(']')
313            }
314            Self::PyClass(expr) => expr.expr.fmt(f),
315        }
316    }
317}
318
319/// A PyO3 extension to the Python AST to know more about [`PyStaticExpr::Constant`].
320///
321/// This enables advanced features like escaping.
322#[derive(Clone, Copy)]
323#[non_exhaustive]
324pub enum PyStaticConstant {
325    /// None
326    None,
327    /// The `True` and `False` booleans
328    Bool(bool),
329    /// `int` value written in base 10 (`[+-]?[0-9]+`)
330    Int(&'static str),
331    /// `float` value written in base-10 (`[+-]?[0-9]*(.[0-9]*)*([eE])[0-9]*`), not including Inf and NaN
332    Float(&'static str),
333    /// `str` value unescaped and without quotes
334    Str(&'static str),
335    /// `...` value
336    Ellipsis,
337}
338
339/// An operator used in [`PyStaticExpr::BinOp`].
340#[derive(Clone, Copy)]
341#[non_exhaustive]
342pub enum PyStaticOperator {
343    /// `|` operator
344    BitOr,
345}
346
347const fn write_slice_and_move_forward<'a>(value: &[u8], output: &'a mut [u8]) -> &'a mut [u8] {
348    // TODO: use copy_from_slice with MSRV 1.87+
349    let mut i = 0;
350    while i < value.len() {
351        output[i] = value[i];
352        i += 1;
353    }
354    output.split_at_mut(value.len()).1
355}
356
357const fn write_json_string_and_move_forward<'a>(value: &str, output: &'a mut [u8]) -> &'a mut [u8] {
358    output[0] = b'"';
359    let output = output.split_at_mut(1).1;
360    let written = escape_json_string(value, output);
361    output[written] = b'"';
362    output.split_at_mut(written + 1).1
363}
364
365const fn write_expr_and_move_forward<'a>(
366    value: &PyStaticExpr,
367    output: &'a mut [u8],
368) -> &'a mut [u8] {
369    let written = serialize_for_introspection(value, output);
370    output.split_at_mut(written).1
371}
372
373const fn write_container_and_move_forward<'a>(
374    name: &'static [u8],
375    elts: &[PyStaticExpr],
376    mut output: &'a mut [u8],
377) -> &'a mut [u8] {
378    output = write_slice_and_move_forward(b"{\"type\":\"", output);
379    output = write_slice_and_move_forward(name, output);
380    output = write_slice_and_move_forward(b"\",\"elts\":[", output);
381    let mut i = 0;
382    while i < elts.len() {
383        if i > 0 {
384            output = write_slice_and_move_forward(b",", output);
385        }
386        output = write_expr_and_move_forward(&elts[i], output);
387        i += 1;
388    }
389    write_slice_and_move_forward(b"]}", output)
390}
391
392const fn serialized_container_len_for_introspection(elts: &[PyStaticExpr]) -> usize {
393    let mut len = 21;
394    let mut i = 0;
395    while i < elts.len() {
396        if i > 0 {
397            len += 1;
398        }
399        len += serialized_len_for_introspection(&elts[i]);
400        i += 1;
401    }
402    len
403}
404
405fn fmt_elements(elts: &[PyStaticExpr], f: &mut fmt::Formatter<'_>) -> fmt::Result {
406    for (i, elt) in elts.iter().enumerate() {
407        if i > 0 {
408            f.write_str(", ")?;
409        }
410        elt.fmt(f)?;
411    }
412    Ok(())
413}
414
415/// The full name of a `#[pyclass]` inside a [`PyStaticExpr`].
416///
417/// To get the underlying [`PyStaticExpr`] use [`expr`](PyClassNameStaticExpr::expr).
418#[derive(Clone, Copy)]
419pub struct PyClassNameStaticExpr {
420    expr: &'static PyStaticExpr,
421    introspection_id: &'static str,
422}
423
424impl PyClassNameStaticExpr {
425    #[doc(hidden)]
426    #[inline]
427    pub const fn new(expr: &'static PyStaticExpr, introspection_id: &'static str) -> Self {
428        Self {
429            expr,
430            introspection_id,
431        }
432    }
433
434    /// The pyclass type as an expression like `module.name`
435    ///
436    /// This is based on the `name` and `module` parameter of the `#[pyclass]` macro.
437    /// The `module` part might not be a valid module from which the type can be imported.
438    #[inline]
439    pub const fn expr(&self) -> &'static PyStaticExpr {
440        self.expr
441    }
442}
443
444impl AsRef<PyStaticExpr> for PyClassNameStaticExpr {
445    #[inline]
446    fn as_ref(&self) -> &PyStaticExpr {
447        self.expr
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_to_string() {
457        const T: PyStaticExpr = type_hint_subscript!(
458            type_hint_identifier!("builtins", "dict"),
459            type_hint_union!(
460                type_hint_identifier!("builtins", "int"),
461                type_hint_subscript!(
462                    type_hint_identifier!("typing", "Literal"),
463                    PyStaticExpr::Constant {
464                        value: PyStaticConstant::Str("\0\t\\\"")
465                    }
466                )
467            ),
468            type_hint_identifier!("datetime", "time")
469        );
470        assert_eq!(
471            T.to_string(),
472            "dict[int | typing.Literal[\"\\0\\t\\\\\\\"\"], datetime.time]"
473        )
474    }
475
476    #[test]
477    fn test_serialize_for_introspection() {
478        fn check_serialization(expr: PyStaticExpr, expected: &str) {
479            let mut out = vec![0; serialized_len_for_introspection(&expr)];
480            serialize_for_introspection(&expr, &mut out);
481            assert_eq!(std::str::from_utf8(&out).unwrap(), expected)
482        }
483
484        check_serialization(
485            PyStaticExpr::Constant {
486                value: PyStaticConstant::None,
487            },
488            r#"{"type":"constant","kind":"none"}"#,
489        );
490        check_serialization(
491            type_hint_identifier!("builtins", "int"),
492            r#"{"type":"name","id":"int"}"#,
493        );
494        check_serialization(
495            type_hint_identifier!("datetime", "date"),
496            r#"{"type":"attribute","value":{"type":"name","id":"datetime"},"attr":"date"}"#,
497        );
498        check_serialization(
499            type_hint_union!(
500                type_hint_identifier!("builtins", "int"),
501                type_hint_identifier!("builtins", "float")
502            ),
503            r#"{"type":"binop","left":{"type":"name","id":"int"},"op":"bitor","right":{"type":"name","id":"float"}}"#,
504        );
505        check_serialization(
506            PyStaticExpr::Tuple {
507                elts: &[type_hint_identifier!("builtins", "list")],
508            },
509            r#"{"type":"tuple","elts":[{"type":"name","id":"list"}]}"#,
510        );
511        check_serialization(
512            PyStaticExpr::List {
513                elts: &[type_hint_identifier!("builtins", "list")],
514            },
515            r#"{"type":"list","elts":[{"type":"name","id":"list"}]}"#,
516        );
517        check_serialization(
518            type_hint_subscript!(
519                type_hint_identifier!("builtins", "list"),
520                type_hint_identifier!("builtins", "int")
521            ),
522            r#"{"type":"subscript","value":{"type":"name","id":"list"},"slice":{"type":"name","id":"int"}}"#,
523        );
524        check_serialization(
525            PyStaticExpr::PyClass(PyClassNameStaticExpr::new(
526                &type_hint_identifier!("builtins", "foo"),
527                "foo",
528            )),
529            r#"{"type":"id","id":"foo"}"#,
530        );
531        check_serialization(
532            PyStaticExpr::Constant {
533                value: PyStaticConstant::Bool(true),
534            },
535            r#"{"type":"constant","kind":"bool","value":true}"#,
536        );
537        check_serialization(
538            PyStaticExpr::Constant {
539                value: PyStaticConstant::Bool(false),
540            },
541            r#"{"type":"constant","kind":"bool","value":false}"#,
542        );
543        check_serialization(
544            PyStaticExpr::Constant {
545                value: PyStaticConstant::Int("-123"),
546            },
547            r#"{"type":"constant","kind":"int","value":"-123"}"#,
548        );
549        check_serialization(
550            PyStaticExpr::Constant {
551                value: PyStaticConstant::Float("-2.1"),
552            },
553            r#"{"type":"constant","kind":"float","value":"-2.1"}"#,
554        );
555        check_serialization(
556            PyStaticExpr::Constant {
557                value: PyStaticConstant::Float("+2.1e10"),
558            },
559            r#"{"type":"constant","kind":"float","value":"+2.1e10"}"#,
560        );
561        check_serialization(
562            PyStaticExpr::Constant {
563                value: PyStaticConstant::Str("abc(1)"),
564            },
565            r#"{"type":"constant","kind":"str","value":"abc(1)"}"#,
566        );
567        check_serialization(
568            PyStaticExpr::Constant {
569                value: PyStaticConstant::Str("\"\\/\x08\x0C\n\r\t\0\x19a"),
570            },
571            r#"{"type":"constant","kind":"str","value":"\"\\/\b\f\n\r\t\u0000\u0019a"}"#,
572        );
573        check_serialization(
574            PyStaticExpr::Constant {
575                value: PyStaticConstant::Ellipsis,
576            },
577            r#"{"type":"constant","kind":"ellipsis"}"#,
578        );
579    }
580}