From b37b80620698d17fe809a5b46083e7a2edd2477d Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Thu, 24 Jul 2025 10:55:27 +0300 Subject: [PATCH 1/8] Port: Ensure build compatibility with Sphinx 8.x.x Tested both with sphinx 7.4.7 and sphinx 8.1.3. Signed-off-by: Nicolae Dicu --- README.md | 107 +++++++++++++++++++++++++ python/sphinx_rust/directives/_core.py | 7 +- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b3cd8b..92ab84a 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,110 @@ See [docs/](docs/) for more information. [pypi-badge]: https://img.shields.io/pypi/v/sphinx-rust.svg [pypi-link]: https://pypi.org/project/sphinx-rust + +## Fork notes + +### Integration, usage and testing + +**CURRENTLY FORK UNDER DEVELOPMENT! - PIP PACKAGE UNAVAILABLE** + +* In order to integrate in score: + +Update score/docs/conf.py : + +```python +extensions = [ + "sphinx_rust", + ... +] + +# point to a rust crate in score(create a dummy one for testing if unavailable) +rust_crates = [ + "../rust-crates/dummy_crate", +] +``` +In index.rst : +``` +Rust API +``` + +* Testing in score repo: + +For fast development it is recomended to convert sphinx-rust module to bazel module by creating: + +- __sphinx-rust__/BUILD.bazel: + +```python +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "sphinx_rust", + srcs = glob(["python/sphinx_rust/**/*.py"]), + data = glob([ + "python/sphinx_rust/sphinx_rust.cpython-312-*.so", + ]), + imports = ["python"], + visibility = ["//visibility:public"], +) +``` + +- __sphinx-rust__/MODULE.bazel + +```python +module(name = "sphinx_rust", version = "0.0.0-dev") +bazel_dep(name = "rules_python", version = "1.4.1") +``` + +Now we can add the __sphinx_rust__ module directly in __score__: + +- __score__/MODULE.bazel: + +```python +bazel_dep(name = "sphinx_rust", version = "0.0.0-dev") +local_path_override(module_name = "sphinx_rust", path = "../sphinx-rust") +``` + +- create the dummy_crate in __score__ with the following structure and content: + +```sh +../score/rust-crates/ +└── dummy_crate + ├── Cargo.toml + └── src + └── lib.rs +``` + +Cargo.toml: + +```yaml +[package] +name = "dummy_crate" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[lib] +name = "dummy_crate" +``` + +lib.rs: + +```rs +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} +``` \ No newline at end of file diff --git a/python/sphinx_rust/directives/_core.py b/python/sphinx_rust/directives/_core.py index c6570c4..884259c 100644 --- a/python/sphinx_rust/directives/_core.py +++ b/python/sphinx_rust/directives/_core.py @@ -146,8 +146,11 @@ def parse_docstring( return [] docstring = item.docstring if docstring is None else docstring - source_path = env.doc2path( # TODO this actually should be the rust file path - env.docname + + source_path = str( + env.doc2path( # TODO this actually should be the rust file path + env.docname + ) ) # TODO how to handle line numbers? document = utils.new_document(source_path, doc.settings) From d46482d62ec34fb62ac531f64b6efb4abc6b4531 Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Fri, 25 Jul 2025 09:58:04 +0300 Subject: [PATCH 2/8] Port: Added requirements.in update Allows integration via pip. Signed-off-by: Nicolae Dicu --- README.md | 27 ++++++++++++++++++++++++++- pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92ab84a..544ceed 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ bazel_dep(name = "sphinx_rust", version = "0.0.0-dev") local_path_override(module_name = "sphinx_rust", path = "../sphinx-rust") ``` +- __docs-as-code__/docs.bzl: + +```python +def _incremental ... + + py_binary( + name = incremental_name, + srcs = ["@score_docs_as_code//src:incremental.py"], + deps = dependencies + ["@sphinx_rust//:sphinx_rust"], +``` + - create the dummy_crate in __score__ with the following structure and content: ```sh @@ -116,4 +127,18 @@ mod tests { assert_eq!(result, 4); } } -``` \ No newline at end of file +``` + +It can be tested using requirements.in from docs-as-code as well but this is not that reliable as bazel will cache sphinx-rust: + +- in __docs-as-code__/src/requirements.in: + +```python +sphinx-rust @ file:///mnt/d/qorix/forks/sphinx-rust +``` + +- run pip packages update: + +```sh +bazel run //src:requirements.update +``` diff --git a/pyproject.toml b/pyproject.toml index f7c81a4..1beaa40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ "Topic :: Text Processing :: Markup", ] dependencies = [ - "sphinx~=7.3" + "sphinx>=7.3" ] [project.urls] From b4494b6130d968309d756f3ce16a41ca30fa5c11 Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Tue, 29 Jul 2025 15:10:37 +0300 Subject: [PATCH 3/8] Fix: Fixed handling of rust path Resolved to folders instead of creating link with "::". Resolves: https://github.com/eclipse-score/score/issues/1487 and https://github.com/eclipse-score/score/issues/1488 Signed-off-by: Nicolae Dicu --- python/sphinx_rust/domain.py | 75 ++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/python/sphinx_rust/domain.py b/python/sphinx_rust/domain.py index 79dbb39..b30402c 100644 --- a/python/sphinx_rust/domain.py +++ b/python/sphinx_rust/domain.py @@ -5,6 +5,7 @@ from pathlib import Path import shutil from typing import TYPE_CHECKING, Literal, TypedDict +import re from sphinx import addnodes from sphinx.domains import Domain @@ -20,6 +21,19 @@ from sphinx_rust.directives.struct import RustStructAutoDirective from sphinx_rust.sphinx_rust import analyze_crate, load_descendant_modules + +INVALID_CHARS = r"[^A-Za-z0-9._-]" + + +def slugify_rust_name(fullname: str) -> Path: + """ + Turn `crate::mod::Type` into a safe relative Path: + crate/mod/Type_T + """ + parts = fullname.split("::") + cleaned = [re.sub(INVALID_CHARS, "_", p) for p in parts] + return Path(*cleaned) + if TYPE_CHECKING: from docutils.nodes import Element from sphinx.addnodes import pending_xref @@ -225,17 +239,31 @@ def create_pages(srcdir: Path, result: AnalysisResult) -> None: def create_object_pages(folder: Path, otype: str, names: list[str]) -> None: - """Create the pages for the objects of a certain type.""" - ofolder = folder.joinpath(otype + "s") + ofolder = folder / f"{otype}s" ofolder.mkdir(exist_ok=True) - index_content = f"{otype.capitalize()}s\n{'=' * (len(otype) + 1)}\n\n.. toctree::\n :maxdepth: 1\n\n" - for name in names: - index_content += f" {name}\n" - title = f"{otype.capitalize()} ``{name}``" - ofolder.joinpath(f"{name}.rst").write_text( - f"{title}\n{'=' * len(title)}\n\n.. rust:{otype}:: {name}\n" + + idx_lines = [ + f"{otype.capitalize()}s", + "=" * (len(otype) + 1), + "", + ".. toctree::", + " :maxdepth: 1", + "" + ] + + for rust_name in names: + rel_path = slugify_rust_name(rust_name) + idx_lines.append(f" {rel_path.as_posix()}") + + dst_file = ofolder / rel_path.with_suffix(".rst") + dst_file.parent.mkdir(parents=True, exist_ok=True) + + title = f"{otype.capitalize()} ``{rust_name}``" + dst_file.write_text( + f"{title}\n{'=' * len(title)}\n\n.. rust:{otype}:: {rust_name}\n" ) - ofolder.joinpath("index.rst").write_text(index_content) + + (ofolder / "index.rst").write_text("\n".join(idx_lines) + "\n") def create_code_pages(crate_name: str, srcdir: Path, cache: Path) -> None: @@ -247,20 +275,17 @@ def create_code_pages(crate_name: str, srcdir: Path, cache: Path) -> None: code_folder = srcdir.joinpath("api", "crates", crate_name, "code") code_folder.mkdir(exist_ok=True, parents=True) for full_name, file_path in modules: - # TODO catch exceptions here, if a relative path cannot be created - rel_path = os.path.relpath(Path(file_path), code_folder) - # note, this is available only in Python 3.12+ - # rel_path = Path(file_path).relative_to(code_folder, walk_up=True) - # TODO only write the file if it doesn't exist or is different - code_folder.joinpath(f"{full_name}.rst").write_text( - "\n".join( - ( - ":orphan:", - "", - f".. literalinclude:: {rel_path}", - f" :name: rust-code:{full_name}", - " :language: rust", - " :linenos:", - ) - ) + rel = slugify_rust_name(full_name) + dst = code_folder / rel.with_suffix(".rst") + dst.parent.mkdir(parents=True, exist_ok=True) + + dst.write_text( + "\n".join(( + ":orphan:", + "", + f".. literalinclude:: {os.path.relpath(file_path, dst.parent)}", + f" :name: rust-code:{full_name}", + " :language: rust", + " :linenos:", + )) ) From 64bfede126b3c3300b7acc26c38942b329a0045b Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Tue, 29 Jul 2025 15:18:13 +0300 Subject: [PATCH 4/8] Fix: Use cargo_metadata for Cargo.toml parsing The module used old implementation of Cargo.toml parsing. Resolves: https://github.com/eclipse-score/score/issues/1522 Signed-off-by: Nicolae Dicu --- README.md | 132 ---------------------- crates/analyzer/Cargo.toml | 1 + crates/analyzer/src/analyze/crate_.rs | 151 +++++++++++--------------- crates/py_binding/Cargo.toml | 2 +- 4 files changed, 63 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index 544ceed..4b3cd8b 100644 --- a/README.md +++ b/README.md @@ -10,135 +10,3 @@ See [docs/](docs/) for more information. [pypi-badge]: https://img.shields.io/pypi/v/sphinx-rust.svg [pypi-link]: https://pypi.org/project/sphinx-rust - -## Fork notes - -### Integration, usage and testing - -**CURRENTLY FORK UNDER DEVELOPMENT! - PIP PACKAGE UNAVAILABLE** - -* In order to integrate in score: - -Update score/docs/conf.py : - -```python -extensions = [ - "sphinx_rust", - ... -] - -# point to a rust crate in score(create a dummy one for testing if unavailable) -rust_crates = [ - "../rust-crates/dummy_crate", -] -``` -In index.rst : -``` -Rust API -``` - -* Testing in score repo: - -For fast development it is recomended to convert sphinx-rust module to bazel module by creating: - -- __sphinx-rust__/BUILD.bazel: - -```python -load("@rules_python//python:defs.bzl", "py_library") - -py_library( - name = "sphinx_rust", - srcs = glob(["python/sphinx_rust/**/*.py"]), - data = glob([ - "python/sphinx_rust/sphinx_rust.cpython-312-*.so", - ]), - imports = ["python"], - visibility = ["//visibility:public"], -) -``` - -- __sphinx-rust__/MODULE.bazel - -```python -module(name = "sphinx_rust", version = "0.0.0-dev") -bazel_dep(name = "rules_python", version = "1.4.1") -``` - -Now we can add the __sphinx_rust__ module directly in __score__: - -- __score__/MODULE.bazel: - -```python -bazel_dep(name = "sphinx_rust", version = "0.0.0-dev") -local_path_override(module_name = "sphinx_rust", path = "../sphinx-rust") -``` - -- __docs-as-code__/docs.bzl: - -```python -def _incremental ... - - py_binary( - name = incremental_name, - srcs = ["@score_docs_as_code//src:incremental.py"], - deps = dependencies + ["@sphinx_rust//:sphinx_rust"], -``` - -- create the dummy_crate in __score__ with the following structure and content: - -```sh -../score/rust-crates/ -└── dummy_crate - ├── Cargo.toml - └── src - └── lib.rs -``` - -Cargo.toml: - -```yaml -[package] -name = "dummy_crate" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -[lib] -name = "dummy_crate" -``` - -lib.rs: - -```rs -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} -``` - -It can be tested using requirements.in from docs-as-code as well but this is not that reliable as bazel will cache sphinx-rust: - -- in __docs-as-code__/src/requirements.in: - -```python -sphinx-rust @ file:///mnt/d/qorix/forks/sphinx-rust -``` - -- run pip packages update: - -```sh -bazel run //src:requirements.update -``` diff --git a/crates/analyzer/Cargo.toml b/crates/analyzer/Cargo.toml index 57fc287..7f13740 100644 --- a/crates/analyzer/Cargo.toml +++ b/crates/analyzer/Cargo.toml @@ -20,6 +20,7 @@ serde.workspace = true serde_json.workspace = true syn.workspace = true toml.workspace = true +cargo_metadata = "0.18" [dev-dependencies] insta.workspace = true diff --git a/crates/analyzer/src/analyze/crate_.rs b/crates/analyzer/src/analyze/crate_.rs index e72aac9..b2a731f 100644 --- a/crates/analyzer/src/analyze/crate_.rs +++ b/crates/analyzer/src/analyze/crate_.rs @@ -1,79 +1,76 @@ //! Analyze the crate -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; +use cargo_metadata::{MetadataCommand, Target}; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; use crate::data_model::{Crate, Enum, Function, Module, Struct}; pub fn analyze_crate(path: &str) -> Result { // make the path absolute // TODO we use dunce to canonicalize the path because otherwise there is issues with python's os.path.relpath on windows, but maybe we should fix this on the Python side - let path = + let crate_dir = dunce::canonicalize(path).context(format!("Error resolving crate path: {}", path))?; + eprintln!("running new analyzer"); // check the path is a directory - if !path.is_dir() { - return Err(anyhow::anyhow!(format!( + if !crate_dir.is_dir() { + return Err(anyhow!( "Crate path is not a directory: {}", - path.to_string_lossy() - ))); + crate_dir.to_string_lossy() + )); } // check if Cargo.toml exists - let cargo_toml_path = path.join("Cargo.toml"); + let cargo_toml_path = crate_dir.join("Cargo.toml"); if !cargo_toml_path.exists() { - return Err(anyhow::anyhow!(format!( + return Err(anyhow!( "Cargo.toml does not exist in: {}", - path.to_string_lossy() - ))); + crate_dir.to_string_lossy() + )); } - // read the Cargo.toml and initialize the Crate struct - let contents = std::fs::read_to_string(&cargo_toml_path)?; - let cargo_toml: CargoToml = toml::from_str(&contents).context(format!( - "Error parsing: {}", - cargo_toml_path.to_string_lossy() - ))?; + // use `cargo_metadata` instead of implementing own TOML parser + let metadata = MetadataCommand::new() + .manifest_path(&cargo_toml_path) + .exec() + .context("Failed to run `cargo metadata`")?; - // check whether the crate is a library or binary - let (crate_name, to_root) = if let Some(lib) = cargo_toml.lib { - if cargo_toml.bin.is_some() { - return Err(anyhow::anyhow!(format!( - "Both lib and bin sections in: {}", - path.to_string_lossy() - ))); - } - ( - lib.name.unwrap_or(cargo_toml.package.name), - lib.path.unwrap_or("src/lib.rs".to_string()), - ) - } else if let Some(bin) = cargo_toml.bin { - ( - bin.name.unwrap_or(cargo_toml.package.name), - bin.path.unwrap_or("src/main.rs".to_string()), - ) - } else { - return Err(anyhow::anyhow!(format!( - "No lib or bin section in: {}", - path.to_string_lossy() - ))); - }; + let root_pkg = metadata + .root_package() + .ok_or_else(|| anyhow!("`cargo metadata` returned no root package"))?; + + // Prefer library target; fall back to the first binary target + let root_target: &Target = root_pkg + .targets + .iter() + .find(|t| t.kind.contains(&"lib".into())) + .or_else(|| root_pkg.targets.iter().find(|t| t.kind.contains(&"bin".into()))) + .ok_or_else(|| anyhow!("No lib or bin target defined in manifest"))?; + + let crate_name = root_target.name.clone(); + let root_module = PathBuf::from(&root_target.src_path); let mut result = AnalysisResult::new(Crate { - name: crate_name, - version: cargo_toml.package.version.clone(), + name: crate_name.clone(), + version: root_pkg.version.to_string(), // workspace-aware }); // check existence of the root module - let root_module = path.join(to_root); if !root_module.exists() { return Ok(result); } // read the top-level module let content = std::fs::read_to_string(&root_module)?; - let (module, structs, enums, functions) = - Module::parse(Some(&root_module), &[&result.crate_.name], &content).context(format!( - "Error parsing module {}", - root_module.to_string_lossy() - ))?; + let (module, structs, enums, functions) = Module::parse( + Some(&root_module), + &[&result.crate_.name], + &content, + ) + .context(format!( + "Error parsing module {}", + root_module.to_string_lossy() + ))?; + let mut modules_to_read = module .declarations .iter() @@ -91,24 +88,23 @@ pub fn analyze_crate(path: &str) -> Result { result.enums.extend(enums); result.functions.extend(functions); - // recursively find/read the public sub-modules + // recursively find/read the public sub‑modules let mut read_modules = vec![]; while let Some((parent_dir, module_name, parent)) = modules_to_read.pop() { - let (module_path, submodule_dir) = - if parent_dir.join(&module_name).with_extension("rs").exists() { - ( - parent_dir.join(&module_name).with_extension("rs"), - parent_dir.join(&module_name), - ) - } else if parent_dir.join(&module_name).join("mod.rs").exists() { - ( - parent_dir.join(&module_name).join("mod.rs"), - parent_dir.to_path_buf(), - ) - } else { - // TODO warn about missing module? - continue; - }; + let (module_path, submodule_dir) = if parent_dir.join(&module_name).with_extension("rs").exists() { + ( + parent_dir.join(&module_name).with_extension("rs"), + parent_dir.join(&module_name), + ) + } else if parent_dir.join(&module_name).join("mod.rs").exists() { + ( + parent_dir.join(&module_name).join("mod.rs"), + parent_dir.to_path_buf(), + ) + } else { + // TODO warn about missing module? + continue; + }; if read_modules.contains(&module_path) { continue; @@ -126,12 +122,12 @@ pub fn analyze_crate(path: &str) -> Result { "Error parsing module {}", module_path.to_string_lossy() ))?; + modules_to_read.extend( module .declarations .iter() - .map(|s| (submodule_dir.clone(), s.to_string(), path.clone())) - .collect::>(), + .map(|s| (submodule_dir.clone(), s.to_string(), path.clone())), ); result.modules.push(module); result.structs.extend(structs); @@ -164,31 +160,6 @@ impl AnalysisResult { } } -#[derive(Debug, Deserialize)] -struct CargoToml { - package: Package, - bin: Option, - lib: Option, -} - -#[derive(Debug, Deserialize)] -struct Package { - name: String, - version: String, -} - -#[derive(Debug, Deserialize)] -struct Lib { - name: Option, - path: Option, -} - -#[derive(Debug, Deserialize)] -struct Bin { - name: Option, - path: Option, -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/py_binding/Cargo.toml b/crates/py_binding/Cargo.toml index a658141..d5dd3ce 100644 --- a/crates/py_binding/Cargo.toml +++ b/crates/py_binding/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sphinx_rust" -version = "0.0.2" +version = "0.0.2-dev1" publish = false edition = "2021" authors.workspace = true From bacff10cd2257310fe1e0d05a2dd49c087f023b1 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 7 Aug 2025 18:07:06 +0200 Subject: [PATCH 5/8] Update .readthedocs.yaml --- .readthedocs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5e6d9fd..aec0c0c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,6 +4,7 @@ version: 2 sphinx: + configuration: docs/conf.py builder: html build: From 443a2d712989ce0faa063f57ea1d21b9dd444817 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 7 Aug 2025 18:17:23 +0200 Subject: [PATCH 6/8] Update .readthedocs.yaml fix: error: package `cargo-platform v0.1.9` cannot be built because it requires rustc 1.78 or newer, while the currently active rustc version is 1.75.0 --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index aec0c0c..ff2442f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,8 +10,8 @@ sphinx: build: os: "ubuntu-22.04" tools: - python: "3.9" - rust: "1.75" + python: "3.10" + rust: "1.78" python: install: From 65922f19e537551db3f78b9f83c5695bf269ec17 Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Fri, 8 Aug 2025 07:41:30 +0300 Subject: [PATCH 7/8] Update after review Signed-off-by: Nicolae Dicu --- crates/analyzer/src/analyze/crate_.rs | 52 +++++++++++++------------- crates/py_binding/src/data_query.rs | 23 ++++-------- crates/py_binding/src/lib.rs | 15 +++----- pyproject.toml | 2 +- python/sphinx_rust/directives/_core.py | 2 +- python/sphinx_rust/domain.py | 24 ++++++------ 6 files changed, 55 insertions(+), 63 deletions(-) diff --git a/crates/analyzer/src/analyze/crate_.rs b/crates/analyzer/src/analyze/crate_.rs index b2a731f..8367809 100644 --- a/crates/analyzer/src/analyze/crate_.rs +++ b/crates/analyzer/src/analyze/crate_.rs @@ -10,7 +10,7 @@ pub fn analyze_crate(path: &str) -> Result { // make the path absolute // TODO we use dunce to canonicalize the path because otherwise there is issues with python's os.path.relpath on windows, but maybe we should fix this on the Python side let crate_dir = - dunce::canonicalize(path).context(format!("Error resolving crate path: {}", path))?; + dunce::canonicalize(path).context(format!("Error resolving crate path: {path}"))?; eprintln!("running new analyzer"); // check the path is a directory if !crate_dir.is_dir() { @@ -43,7 +43,12 @@ pub fn analyze_crate(path: &str) -> Result { .targets .iter() .find(|t| t.kind.contains(&"lib".into())) - .or_else(|| root_pkg.targets.iter().find(|t| t.kind.contains(&"bin".into()))) + .or_else(|| { + root_pkg + .targets + .iter() + .find(|t| t.kind.contains(&"bin".into())) + }) .ok_or_else(|| anyhow!("No lib or bin target defined in manifest"))?; let crate_name = root_target.name.clone(); @@ -61,15 +66,11 @@ pub fn analyze_crate(path: &str) -> Result { // read the top-level module let content = std::fs::read_to_string(&root_module)?; - let (module, structs, enums, functions) = Module::parse( - Some(&root_module), - &[&result.crate_.name], - &content, - ) - .context(format!( - "Error parsing module {}", - root_module.to_string_lossy() - ))?; + let (module, structs, enums, functions) = + Module::parse(Some(&root_module), &[&result.crate_.name], &content).context(format!( + "Error parsing module {}", + root_module.to_string_lossy() + ))?; let mut modules_to_read = module .declarations @@ -91,20 +92,21 @@ pub fn analyze_crate(path: &str) -> Result { // recursively find/read the public sub‑modules let mut read_modules = vec![]; while let Some((parent_dir, module_name, parent)) = modules_to_read.pop() { - let (module_path, submodule_dir) = if parent_dir.join(&module_name).with_extension("rs").exists() { - ( - parent_dir.join(&module_name).with_extension("rs"), - parent_dir.join(&module_name), - ) - } else if parent_dir.join(&module_name).join("mod.rs").exists() { - ( - parent_dir.join(&module_name).join("mod.rs"), - parent_dir.to_path_buf(), - ) - } else { - // TODO warn about missing module? - continue; - }; + let (module_path, submodule_dir) = + if parent_dir.join(&module_name).with_extension("rs").exists() { + ( + parent_dir.join(&module_name).with_extension("rs"), + parent_dir.join(&module_name), + ) + } else if parent_dir.join(&module_name).join("mod.rs").exists() { + ( + parent_dir.join(&module_name).join("mod.rs"), + parent_dir.to_path_buf(), + ) + } else { + // TODO warn about missing module? + continue; + }; if read_modules.contains(&module_path) { continue; diff --git a/crates/py_binding/src/data_query.rs b/crates/py_binding/src/data_query.rs index 86c0245..bc5da6a 100644 --- a/crates/py_binding/src/data_query.rs +++ b/crates/py_binding/src/data_query.rs @@ -26,8 +26,7 @@ where Ok(crate_) => crate_, Err(err) => { return Err(PyIOError::new_err(format!( - "Could not deserialize {}: {}", - name, err + "Could not deserialize {name}: {err}" ))) } }; @@ -39,7 +38,7 @@ where pub fn load_crate(cache_path: &str, name: &str) -> PyResult> { let path = std::path::Path::new(cache_path) .join("crates") - .join(format!("{}.json", name)); + .join(format!("{name}.json")); if !path.exists() { return Ok(None); } @@ -53,7 +52,7 @@ pub fn load_crate(cache_path: &str, name: &str) -> PyResult> { pub fn load_module(cache_path: &str, full_name: &str) -> PyResult> { let path = std::path::Path::new(cache_path) .join("modules") - .join(format!("{}.json", full_name)); + .join(format!("{full_name}.json")); if !path.exists() { return Ok(None); } @@ -67,7 +66,7 @@ pub fn load_module(cache_path: &str, full_name: &str) -> PyResult pub fn load_struct(cache_path: &str, full_name: &str) -> PyResult> { let path = std::path::Path::new(cache_path) .join("structs") - .join(format!("{}.json", full_name)); + .join(format!("{full_name}.json")); if !path.exists() { return Ok(None); } @@ -81,7 +80,7 @@ pub fn load_struct(cache_path: &str, full_name: &str) -> PyResult pub fn load_enum(cache_path: &str, full_name: &str) -> PyResult> { let path = std::path::Path::new(cache_path) .join("enums") - .join(format!("{}.json", full_name)); + .join(format!("{full_name}.json")); if !path.exists() { return Ok(None); } @@ -95,7 +94,7 @@ pub fn load_enum(cache_path: &str, full_name: &str) -> PyResult> { pub fn load_function(cache_path: &str, full_name: &str) -> PyResult> { let path = std::path::Path::new(cache_path) .join("functions") - .join(format!("{}.json", full_name)); + .join(format!("{full_name}.json")); if !path.exists() { return Ok(None); } @@ -106,14 +105,8 @@ pub fn load_function(cache_path: &str, full_name: &str) -> PyResult) -> Option { - let name = match path.file_stem() { - Some(name) => name, - None => return None, - }; - let name = match name.to_str() { - Some(name) => name, - None => return None, - }; + let name = path.file_stem()?; + let name = name.to_str()?; let name_path = name.split("::").collect::>(); if name_path.len() != parent.len() + 1 { return None; diff --git a/crates/py_binding/src/lib.rs b/crates/py_binding/src/lib.rs index cca1d9b..1383d11 100644 --- a/crates/py_binding/src/lib.rs +++ b/crates/py_binding/src/lib.rs @@ -162,25 +162,20 @@ where Ok(value) => value, Err(err) => { return Err(PyIOError::new_err(format!( - "Could not serialize value: {}", - err + "Could not serialize value: {err}" ))) } }; if path.exists() { - match std::fs::read_to_string(path) { - Ok(old_value) => { - if value == old_value { - return Ok(()); - } + if let Ok(old_value) = std::fs::read_to_string(path) { + if value == old_value { + return Ok(()); } - Err(_) => {} }; } match std::fs::write(path, value) { Err(err) => Err(PyIOError::new_err(format!( - "Could not write value to file: {}", - err + "Could not write value to file: {err}" ))), Ok(_) => Ok(()), } diff --git a/pyproject.toml b/pyproject.toml index 1beaa40..d3684be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ "Topic :: Text Processing :: Markup", ] dependencies = [ - "sphinx>=7.3" + "sphinx>=8,<9" ] [project.urls] diff --git a/python/sphinx_rust/directives/_core.py b/python/sphinx_rust/directives/_core.py index 884259c..dafa53d 100644 --- a/python/sphinx_rust/directives/_core.py +++ b/python/sphinx_rust/directives/_core.py @@ -31,7 +31,7 @@ class RustAutoDirective(SphinxDirective): @property def doc(self) -> nodes.document: - return self.state.document # type: ignore[no-any-return] + return self.state.document @property def rust_config(self) -> RustConfig: diff --git a/python/sphinx_rust/domain.py b/python/sphinx_rust/domain.py index b30402c..681f1cf 100644 --- a/python/sphinx_rust/domain.py +++ b/python/sphinx_rust/domain.py @@ -3,9 +3,9 @@ from dataclasses import dataclass import os from pathlib import Path +import re import shutil from typing import TYPE_CHECKING, Literal, TypedDict -import re from sphinx import addnodes from sphinx.domains import Domain @@ -21,7 +21,6 @@ from sphinx_rust.directives.struct import RustStructAutoDirective from sphinx_rust.sphinx_rust import analyze_crate, load_descendant_modules - INVALID_CHARS = r"[^A-Za-z0-9._-]" @@ -34,6 +33,7 @@ def slugify_rust_name(fullname: str) -> Path: cleaned = [re.sub(INVALID_CHARS, "_", p) for p in parts] return Path(*cleaned) + if TYPE_CHECKING: from docutils.nodes import Element from sphinx.addnodes import pending_xref @@ -248,7 +248,7 @@ def create_object_pages(folder: Path, otype: str, names: list[str]) -> None: "", ".. toctree::", " :maxdepth: 1", - "" + "", ] for rust_name in names: @@ -280,12 +280,14 @@ def create_code_pages(crate_name: str, srcdir: Path, cache: Path) -> None: dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text( - "\n".join(( - ":orphan:", - "", - f".. literalinclude:: {os.path.relpath(file_path, dst.parent)}", - f" :name: rust-code:{full_name}", - " :language: rust", - " :linenos:", - )) + "\n".join( + ( + ":orphan:", + "", + f".. literalinclude:: {os.path.relpath(file_path, dst.parent)}", + f" :name: rust-code:{full_name}", + " :language: rust", + " :linenos:", + ) + ) ) From c13dbf9964fae8d31cab3f027fc7357d4a0e9a91 Mon Sep 17 00:00:00 2001 From: Nicolae Dicu Date: Fri, 22 Aug 2025 11:43:23 +0300 Subject: [PATCH 8/8] Fix: Drop python 3.9 support Sphinx 8.0 dropped python 3.9 support. Signed-off-by: Nicolae Dicu --- .github/workflows/tests.yml | 9 ++++----- pyproject.toml | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ad4e84..138dac6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.10" - uses: pre-commit/action@v3.0.1 test-cargo: @@ -49,16 +49,15 @@ jobs: matrix: os: [ubuntu-latest] python-version: - - '3.9' - '3.10' - '3.11' - '3.12' - - 'pypy3.9' + - 'pypy3.10' include: - os: windows-latest - python-version: '3.9' + python-version: '3.10' - os: macos-latest - python-version: '3.9' + python-version: '3.10' runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index d3684be..faec92d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dynamic = ["version"] description = "Sphinx plugin for documentation of Rust projects." authors = [{ name = "Chris Sewell", email = "chrisj_sewell@hotmail.com" }] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = { file = "LICENSE" } keywords = [ "sphinx", @@ -26,7 +26,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -113,7 +112,7 @@ allowlist_externals = bash commands_pre = bash -c "unset CONDA_PREFIX; maturin develop" commands = {posargs:ipython} -[testenv:test-{py39,py310,py311,py312}] +[testenv:test-{py310,py311,py312}] extras = test passenv = TERM ; ensure that the compilation is up-to-date