pyo3/impl_/pyclass/
doc.rs

1use std::{ffi::CStr, marker::PhantomData};
2
3use crate::{impl_::pyclass::PyClassImpl, PyClass, PyTypeInfo};
4
5/// Trait implemented by classes with a known text signature for instantiation.
6///
7/// This is implemented by the `#[pymethods]` macro when handling expansion for a
8/// `#[new]` method.
9pub trait PyClassNewTextSignature {
10    const TEXT_SIGNATURE: &'static str;
11}
12
13/// Type which uses specialization on impl blocks to facilitate generating the documentation for a
14/// `#[pyclass]` type.
15///
16/// At the moment, this is only used to help lift the `TEXT_SIGNATURE` constant to compile time
17/// providing a base case and a specialized implementation when the signature is known at compile time.
18///
19/// In the future when const eval is more advanced, it will probably be possible to format the whole
20/// class docstring at compile time as part of this type instead of in macro expansion.
21pub struct PyClassDocGenerator<
22    ClassT: PyClass,
23    // switch to determine if a signature for class instantiation is known
24    const HAS_NEW_TEXT_SIGNATURE: bool,
25>(PhantomData<ClassT>);
26
27impl<ClassT: PyClass + PyClassNewTextSignature> PyClassDocGenerator<ClassT, true> {
28    pub const DOC_PIECES: &'static [&'static [u8]] = &[
29        <ClassT as PyTypeInfo>::NAME.as_bytes(),
30        ClassT::TEXT_SIGNATURE.as_bytes(),
31        b"\n--\n\n",
32        <ClassT as PyClassImpl>::RAW_DOC.to_bytes_with_nul(),
33    ];
34}
35
36impl<ClassT: PyClass> PyClassDocGenerator<ClassT, false> {
37    pub const DOC_PIECES: &'static [&'static [u8]] =
38        &[<ClassT as PyClassImpl>::RAW_DOC.to_bytes_with_nul()];
39}
40
41/// Casts bytes to a CStr, ensuring they are valid.
42pub const fn doc_bytes_as_cstr(bytes: &'static [u8]) -> &'static ::std::ffi::CStr {
43    match CStr::from_bytes_with_nul(bytes) {
44        Ok(cstr) => cstr,
45        #[cfg(not(from_bytes_with_nul_error))] // MSRV 1.86
46        Err(_) => panic!("invalid pyclass doc"),
47        #[cfg(from_bytes_with_nul_error)]
48        // This case may happen if the user provides an invalid docstring
49        Err(std::ffi::FromBytesWithNulError::InteriorNul { .. }) => {
50            panic!("pyclass doc contains nul bytes")
51        }
52        // This case shouldn't happen using the macro machinery as long as `PyClassDocGenerator`
53        // uses the RAW_DOC as the final piece, which is nul terminated.
54        #[cfg(from_bytes_with_nul_error)]
55        Err(std::ffi::FromBytesWithNulError::NotNulTerminated) => {
56            panic!("pyclass doc expected to be nul terminated")
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63
64    use crate::ffi;
65
66    use super::*;
67
68    #[test]
69    #[cfg(feature = "macros")]
70    fn test_doc_generator() {
71        use crate::impl_::concat::{combine_to_array, combined_len};
72
73        /// A dummy class with signature.
74        #[crate::pyclass(crate = "crate")]
75        struct MyClass;
76
77        #[crate::pymethods(crate = "crate")]
78        impl MyClass {
79            #[new]
80            fn new(x: i32, y: i32) -> Self {
81                let _ = (x, y); // suppress unused variable warnings
82                MyClass
83            }
84        }
85
86        // simulate what the macro is doing
87        const PIECES: &[&[u8]] = PyClassDocGenerator::<MyClass, true>::DOC_PIECES;
88        assert_eq!(
89            &combine_to_array::<{ combined_len(PIECES) }>(PIECES),
90            b"MyClass(x, y)\n--\n\nA dummy class with signature.\0"
91        );
92
93        // simulate if the macro detected no text signature
94        const PIECES_WITHOUT_SIGNATURE: &[&[u8]] =
95            PyClassDocGenerator::<MyClass, false>::DOC_PIECES;
96        assert_eq!(
97            &combine_to_array::<{ combined_len(PIECES_WITHOUT_SIGNATURE) }>(
98                PIECES_WITHOUT_SIGNATURE
99            ),
100            b"A dummy class with signature.\0"
101        );
102    }
103
104    #[test]
105    fn test_doc_bytes_as_cstr() {
106        let cstr = doc_bytes_as_cstr(b"MyClass\0");
107        assert_eq!(cstr, ffi::c_str!("MyClass"));
108    }
109
110    #[test]
111    #[cfg(from_bytes_with_nul_error)]
112    #[should_panic(expected = "pyclass doc contains nul bytes")]
113    fn test_doc_bytes_as_cstr_central_nul() {
114        doc_bytes_as_cstr(b"MyClass\0Foo");
115    }
116
117    #[test]
118    #[cfg(from_bytes_with_nul_error)]
119    #[should_panic(expected = "pyclass doc expected to be nul terminated")]
120    fn test_doc_bytes_as_cstr_not_nul_terminated() {
121        doc_bytes_as_cstr(b"MyClass");
122    }
123}