pyo3_stub_gen/
pyproject.rs1use 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 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 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 pub fn doc_gen_config_resolved(&self) -> Option<crate::docgen::DocGenConfig> {
75 if let Some(mut config) = self.stub_gen_config().doc_gen {
76 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(untagged)]
118pub enum GenerateInitPy {
119 All(bool),
121 Modules(Vec<String>),
123}
124
125impl Default for GenerateInitPy {
126 fn default() -> Self {
127 GenerateInitPy::All(false)
128 }
129}
130
131impl GenerateInitPy {
132 pub fn is_enabled_for(&self, module_name: &str) -> bool {
141 match self {
142 GenerateInitPy::All(enabled) => *enabled,
143 GenerateInitPy::Modules(modules) => {
144 let normalized_name = module_name.replace('-', "_");
145 modules
146 .iter()
147 .any(|m| m.replace('-', "_") == normalized_name)
148 }
149 }
150 }
151
152 pub fn is_enabled(&self) -> bool {
154 match self {
155 GenerateInitPy::All(enabled) => *enabled,
156 GenerateInitPy::Modules(modules) => !modules.is_empty(),
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
166#[non_exhaustive]
167pub struct StubGenConfig {
168 #[serde(rename = "use-type-statement", default)]
171 pub use_type_statement: bool,
172 #[serde(rename = "doc-gen")]
174 pub doc_gen: Option<crate::docgen::DocGenConfig>,
175 #[serde(rename = "generate-init-py", default)]
177 pub generate_init_py: GenerateInitPy,
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_stub_gen_config_true() {
186 let toml_str = r#"
187 [project]
188 name = "test"
189
190 [tool.pyo3-stub-gen]
191 use-type-statement = true
192 "#;
193 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
194 assert!(pyproject.stub_gen_config().use_type_statement);
195 }
196
197 #[test]
198 fn test_stub_gen_config_false() {
199 let toml_str = r#"
200 [project]
201 name = "test"
202
203 [tool.pyo3-stub-gen]
204 use-type-statement = false
205 "#;
206 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
207 assert!(!pyproject.stub_gen_config().use_type_statement);
208 }
209
210 #[test]
211 fn test_stub_gen_config_default() {
212 let toml_str = r#"
213 [project]
214 name = "test"
215 "#;
216 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
217 assert!(!pyproject.stub_gen_config().use_type_statement);
218 }
219
220 #[test]
221 fn test_stub_gen_config_empty_section() {
222 let toml_str = r#"
223 [project]
224 name = "test"
225
226 [tool.pyo3-stub-gen]
227 "#;
228 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
229 assert!(!pyproject.stub_gen_config().use_type_statement);
230 }
231
232 #[test]
233 fn test_generate_init_py_default() {
234 let toml_str = r#"
235 [project]
236 name = "test"
237 "#;
238 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
239 let config = pyproject.stub_gen_config();
240 assert!(!config.generate_init_py.is_enabled());
241 assert!(!config.generate_init_py.is_enabled_for("pkg"));
242 }
243
244 #[test]
245 fn test_generate_init_py_true() {
246 let toml_str = r#"
247 [project]
248 name = "test"
249
250 [tool.pyo3-stub-gen]
251 generate-init-py = true
252 "#;
253 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
254 let config = pyproject.stub_gen_config();
255 assert!(config.generate_init_py.is_enabled());
256 assert!(config.generate_init_py.is_enabled_for("pkg"));
257 assert!(config.generate_init_py.is_enabled_for("any_module"));
258 }
259
260 #[test]
261 fn test_generate_init_py_false() {
262 let toml_str = r#"
263 [project]
264 name = "test"
265
266 [tool.pyo3-stub-gen]
267 generate-init-py = false
268 "#;
269 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
270 let config = pyproject.stub_gen_config();
271 assert!(!config.generate_init_py.is_enabled());
272 assert!(!config.generate_init_py.is_enabled_for("pkg"));
273 }
274
275 #[test]
276 fn test_generate_init_py_modules() {
277 let toml_str = r#"
278 [project]
279 name = "test"
280
281 [tool.pyo3-stub-gen]
282 generate-init-py = ["pkg", "pkg.submod"]
283 "#;
284 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
285 let config = pyproject.stub_gen_config();
286 assert!(config.generate_init_py.is_enabled());
287 assert!(config.generate_init_py.is_enabled_for("pkg"));
288 assert!(config.generate_init_py.is_enabled_for("pkg.submod"));
289 assert!(!config.generate_init_py.is_enabled_for("other"));
290 }
291
292 #[test]
293 fn test_generate_init_py_dash_underscore_normalization() {
294 let toml_str = r#"
295 [project]
296 name = "test"
297
298 [tool.pyo3-stub-gen]
299 generate-init-py = ["my-package", "my_package.sub-mod"]
300 "#;
301 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
302 let config = pyproject.stub_gen_config();
303 assert!(config.generate_init_py.is_enabled_for("my_package"));
305 assert!(config.generate_init_py.is_enabled_for("my-package"));
306 assert!(config.generate_init_py.is_enabled_for("my_package.sub_mod"));
307 assert!(config.generate_init_py.is_enabled_for("my_package.sub-mod"));
308 assert!(!config.generate_init_py.is_enabled_for("other"));
309 }
310
311 #[test]
312 fn test_generate_init_py_empty_modules() {
313 let toml_str = r#"
314 [project]
315 name = "test"
316
317 [tool.pyo3-stub-gen]
318 generate-init-py = []
319 "#;
320 let pyproject: PyProject = toml::from_str(toml_str).unwrap();
321 let config = pyproject.stub_gen_config();
322 assert!(!config.generate_init_py.is_enabled());
323 }
324}