pyo3_stub_gen/
pyproject.rs

1//! `pyproject.toml` parser for reading `[tool.maturin]` configuration.
2//!
3//! ```
4//! use pyo3_stub_gen::pyproject::PyProject;
5//! use std::path::Path;
6//!
7//! let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
8//! let pyproject = PyProject::parse_toml(
9//!     root.join("examples/mixed/pyproject.toml")
10//! ).unwrap();
11//! ```
12
13use anyhow::{bail, Result};
14use serde::{Deserialize, Serialize};
15use std::{fs, path::*};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct PyProject {
19    pub project: Project,
20    pub tool: Option<Tool>,
21
22    #[serde(skip)]
23    toml_path: PathBuf,
24}
25
26impl PyProject {
27    pub fn parse_toml(path: impl AsRef<Path>) -> Result<Self> {
28        let path = path.as_ref();
29        if path.file_name() != Some("pyproject.toml".as_ref()) {
30            bail!("{} is not a pyproject.toml", path.display())
31        }
32        let mut out: PyProject = toml::de::from_str(&fs::read_to_string(path)?)?;
33        out.toml_path = path.to_path_buf();
34        Ok(out)
35    }
36
37    pub fn module_name(&self) -> &str {
38        if let Some(tool) = &self.tool {
39            if let Some(maturin) = &tool.maturin {
40                if let Some(module_name) = &maturin.module_name {
41                    return module_name;
42                }
43            }
44        }
45        &self.project.name
46    }
47
48    /// Return `tool.maturin.python_source` if it exists, which means the project is a mixed Rust/Python project.
49    pub fn python_source(&self) -> Option<PathBuf> {
50        if let Some(tool) = &self.tool {
51            if let Some(maturin) = &tool.maturin {
52                if let Some(python_source) = &maturin.python_source {
53                    if let Some(base) = self.toml_path.parent() {
54                        return Some(base.join(python_source));
55                    } else {
56                        return Some(PathBuf::from(python_source));
57                    }
58                }
59            }
60        }
61        None
62    }
63
64    /// Return stub generation configuration from `[tool.pyo3-stub-gen]`.
65    /// Returns default configuration if the section is not present.
66    pub fn stub_gen_config(&self) -> StubGenConfig {
67        self.tool
68            .as_ref()
69            .and_then(|t| t.pyo3_stub_gen.clone())
70            .unwrap_or_default()
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct Project {
76    pub name: String,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Tool {
81    pub maturin: Option<Maturin>,
82    #[serde(rename = "pyo3-stub-gen")]
83    pub pyo3_stub_gen: Option<StubGenConfig>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Maturin {
88    #[serde(rename = "python-source")]
89    pub python_source: Option<String>,
90    #[serde(rename = "module-name")]
91    pub module_name: Option<String>,
92}
93
94/// Configuration options for stub generation from `[tool.pyo3-stub-gen]` in pyproject.toml.
95///
96/// This struct is marked as `#[non_exhaustive]` to allow adding new configuration
97/// options in future versions without breaking backward compatibility.
98#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct StubGenConfig {
101    /// Whether to use Python 3.12+ `type` statement syntax for type aliases.
102    /// Default is `false` (use pre-3.12 `TypeAlias` syntax).
103    #[serde(rename = "use-type-statement", default)]
104    pub use_type_statement: bool,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_stub_gen_config_true() {
113        let toml_str = r#"
114            [project]
115            name = "test"
116
117            [tool.pyo3-stub-gen]
118            use-type-statement = true
119        "#;
120        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
121        assert!(pyproject.stub_gen_config().use_type_statement);
122    }
123
124    #[test]
125    fn test_stub_gen_config_false() {
126        let toml_str = r#"
127            [project]
128            name = "test"
129
130            [tool.pyo3-stub-gen]
131            use-type-statement = false
132        "#;
133        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
134        assert!(!pyproject.stub_gen_config().use_type_statement);
135    }
136
137    #[test]
138    fn test_stub_gen_config_default() {
139        let toml_str = r#"
140            [project]
141            name = "test"
142        "#;
143        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
144        assert!(!pyproject.stub_gen_config().use_type_statement);
145    }
146
147    #[test]
148    fn test_stub_gen_config_empty_section() {
149        let toml_str = r#"
150            [project]
151            name = "test"
152
153            [tool.pyo3-stub-gen]
154        "#;
155        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
156        assert!(!pyproject.stub_gen_config().use_type_statement);
157    }
158}