Skip to main content

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