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    /// Return doc-gen configuration with output_dir resolved relative to pyproject.toml directory
74    pub fn doc_gen_config_resolved(&self) -> Option<crate::docgen::DocGenConfig> {
75        if let Some(mut config) = self.stub_gen_config().doc_gen {
76            // Resolve output_dir relative to pyproject.toml directory
77            // Only resolve if the path is relative (absolute paths stay unchanged)
78            if config.output_dir.is_relative() {
79                if let Some(base) = self.toml_path.parent() {
80                    config.output_dir = base.join(&config.output_dir);
81                }
82            }
83            Some(config)
84        } else {
85            None
86        }
87    }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Project {
92    pub name: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct Tool {
97    pub maturin: Option<Maturin>,
98    #[serde(rename = "pyo3-stub-gen")]
99    pub pyo3_stub_gen: Option<StubGenConfig>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Maturin {
104    #[serde(rename = "python-source")]
105    pub python_source: Option<String>,
106    #[serde(rename = "module-name")]
107    pub module_name: Option<String>,
108}
109
110/// Configuration options for stub generation from `[tool.pyo3-stub-gen]` in pyproject.toml.
111///
112/// This struct is marked as `#[non_exhaustive]` to allow adding new configuration
113/// options in future versions without breaking backward compatibility.
114#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
115#[non_exhaustive]
116pub struct StubGenConfig {
117    /// Whether to use Python 3.12+ `type` statement syntax for type aliases.
118    /// Default is `false` (use pre-3.12 `TypeAlias` syntax).
119    #[serde(rename = "use-type-statement", default)]
120    pub use_type_statement: bool,
121    /// Documentation generation configuration
122    #[serde(rename = "doc-gen")]
123    pub doc_gen: Option<crate::docgen::DocGenConfig>,
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_stub_gen_config_true() {
132        let toml_str = r#"
133            [project]
134            name = "test"
135
136            [tool.pyo3-stub-gen]
137            use-type-statement = true
138        "#;
139        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
140        assert!(pyproject.stub_gen_config().use_type_statement);
141    }
142
143    #[test]
144    fn test_stub_gen_config_false() {
145        let toml_str = r#"
146            [project]
147            name = "test"
148
149            [tool.pyo3-stub-gen]
150            use-type-statement = false
151        "#;
152        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
153        assert!(!pyproject.stub_gen_config().use_type_statement);
154    }
155
156    #[test]
157    fn test_stub_gen_config_default() {
158        let toml_str = r#"
159            [project]
160            name = "test"
161        "#;
162        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
163        assert!(!pyproject.stub_gen_config().use_type_statement);
164    }
165
166    #[test]
167    fn test_stub_gen_config_empty_section() {
168        let toml_str = r#"
169            [project]
170            name = "test"
171
172            [tool.pyo3-stub-gen]
173        "#;
174        let pyproject: PyProject = toml::from_str(toml_str).unwrap();
175        assert!(!pyproject.stub_gen_config().use_type_statement);
176    }
177}