Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 29 additions & 26 deletions crates/analyzer/src/analyze/crate_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,45 +55,44 @@ pub fn analyze_crate(path: &str) -> Result<AnalysisResult> {
)));
};

let crate_ = Crate {
name: crate_name,
version: cargo_toml.package.version.clone(),
docstring: "".to_string(),
};
let mut result_ = AnalysisResult {
crate_: crate_.clone(),
let mut result = AnalysisResult {
crate_: Crate {
name: crate_name,
version: cargo_toml.package.version.clone(),
},
modules: vec![],
structs: vec![],
enums: vec![],
};

// read the src/lib directory
let root_file = path.join(to_root);
if !root_file.exists() {
return Ok(result_);
// 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_file)?;
let (module, structs, enums) = Module::parse(Some(&root_file), &[&crate_.name], &content)
.context(format!(
let content = std::fs::read_to_string(&root_module)?;
let (module, structs, enums) =
Module::parse(Some(&root_module), &[&result.crate_.name], &content).context(format!(
"Error parsing module {}",
root_file.to_string_lossy()
root_module.to_string_lossy()
))?;
result_.crate_.docstring = module.docstring.clone();
let mut modules_to_read = module
.declarations
.iter()
.map(|s| {
(
root_file.parent().unwrap().to_path_buf(),
root_module.parent().unwrap().to_path_buf(),
s.to_string(),
vec![crate_.name.clone()],
vec![result.crate_.name.clone()],
)
})
.collect::<Vec<_>>();
result_.structs.extend(structs);
result_.enums.extend(enums);

result.modules.push(module);
result.structs.extend(structs);
result.enums.extend(enums);

// recursively find/read the public sub-modules
let mut read_modules = vec![];
Expand Down Expand Up @@ -137,12 +136,12 @@ pub fn analyze_crate(path: &str) -> Result<AnalysisResult> {
.map(|s| (submodule_dir.clone(), s.to_string(), path.clone()))
.collect::<Vec<_>>(),
);
result_.modules.push(module);
result_.structs.extend(structs);
result_.enums.extend(enums);
result.modules.push(module);
result.structs.extend(structs);
result.enums.extend(enums);
}

Ok(result_)
Ok(result)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -164,7 +163,6 @@ pub struct AnalysisResult {
pub struct Crate {
pub name: String,
pub version: String,
pub docstring: String,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -272,8 +270,13 @@ mod tests {
crate_:
name: my_crate
version: 0.1.0
docstring: The crate docstring
modules:
- file: ~
path:
- my_crate
docstring: The crate docstring
declarations:
- my_module
- file: ~
path:
- my_crate
Expand Down
53 changes: 35 additions & 18 deletions crates/py_binding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,53 +186,62 @@ pub fn load_crate(cache_path: &str, name: &str) -> PyResult<Option<Crate>> {

#[pyfunction]
/// load a module from the cache, if it exists
pub fn load_module(cache_path: &str, name: &str) -> PyResult<Option<Module>> {
pub fn load_module(cache_path: &str, full_name: &str) -> PyResult<Option<Module>> {
let path = std::path::Path::new(cache_path)
.join("modules")
.join(format!("{}.json", name));
.join(format!("{}.json", full_name));
if !path.exists() {
return Ok(None);
}
let contents = read_file(&path)?;
let mod_: analyze::Module = deserialize_object(name, &contents)?;
let mod_: analyze::Module = deserialize_object(full_name, &contents)?;
Ok(Some(mod_.into()))
}

#[pyfunction]
/// load a struct from the cache, if it exists
pub fn load_struct(cache_path: &str, name: &str) -> PyResult<Option<Struct>> {
pub fn load_struct(cache_path: &str, full_name: &str) -> PyResult<Option<Struct>> {
let path = std::path::Path::new(cache_path)
.join("structs")
.join(format!("{}.json", name));
.join(format!("{}.json", full_name));
if !path.exists() {
return Ok(None);
}
let contents = read_file(&path)?;
let struct_: analyze::Struct = deserialize_object(name, &contents)?;
let struct_: analyze::Struct = deserialize_object(full_name, &contents)?;
Ok(Some(struct_.into()))
}

#[pyfunction]
/// load an enum from the cache, if it exists
pub fn load_enum(cache_path: &str, name: &str) -> PyResult<Option<Enum>> {
pub fn load_enum(cache_path: &str, full_name: &str) -> PyResult<Option<Enum>> {
let path = std::path::Path::new(cache_path)
.join("enums")
.join(format!("{}.json", name));
.join(format!("{}.json", full_name));
if !path.exists() {
return Ok(None);
}
let contents = read_file(&path)?;
let enum_: analyze::Enum = deserialize_object(name, &contents)?;
let enum_: analyze::Enum = deserialize_object(full_name, &contents)?;
Ok(Some(enum_.into()))
}

#[pyfunction]
/// load all modules from the cache that begin with the given prefix
pub fn load_modules(cache_path: &str, prefix: &str) -> PyResult<Vec<Module>> {
/// load all modules from the cache that have a common ancestor
pub fn load_modules(
cache_path: &str,
ancestor: Vec<String>,
include_self: bool,
) -> PyResult<Vec<Module>> {
let path = std::path::Path::new(cache_path).join("modules");
if !path.exists() {
return Ok(vec![]);
}
let ancestor_name = ancestor.join("::");
let mut prefix = ancestor.join("::");
if !prefix.is_empty() {
prefix.push_str("::");
}
let mut modules = vec![];
for entry in std::fs::read_dir(path)? {
let entry = entry?;
Expand All @@ -246,7 +255,7 @@ pub fn load_modules(cache_path: &str, prefix: &str) -> PyResult<Vec<Module>> {
Some(name) => name,
None => continue,
};
if !name.starts_with(prefix) {
if !(name.starts_with(&prefix) || (include_self && name == &ancestor_name)) {
continue;
}
let contents = read_file(&path)?;
Expand All @@ -258,12 +267,16 @@ pub fn load_modules(cache_path: &str, prefix: &str) -> PyResult<Vec<Module>> {
}

#[pyfunction]
/// load all structs from the cache that begin with the given prefix
pub fn load_structs(cache_path: &str, prefix: &str) -> PyResult<Vec<Struct>> {
/// load all structs from the cache that have a common ancestor
pub fn load_structs(cache_path: &str, ancestor: Vec<String>) -> PyResult<Vec<Struct>> {
let path = std::path::Path::new(cache_path).join("structs");
if !path.exists() {
return Ok(vec![]);
}
let mut prefix = ancestor.join("::");
if !prefix.is_empty() {
prefix.push_str("::");
}
let mut structs = vec![];
for entry in std::fs::read_dir(path)? {
let entry = entry?;
Expand All @@ -277,7 +290,7 @@ pub fn load_structs(cache_path: &str, prefix: &str) -> PyResult<Vec<Struct>> {
Some(name) => name,
None => continue,
};
if !name.starts_with(prefix) {
if !name.starts_with(&prefix) {
continue;
}
let contents = read_file(&path)?;
Expand All @@ -289,12 +302,16 @@ pub fn load_structs(cache_path: &str, prefix: &str) -> PyResult<Vec<Struct>> {
}

#[pyfunction]
/// load all enums from the cache that begin with the given prefix
pub fn load_enums(cache_path: &str, prefix: &str) -> PyResult<Vec<Enum>> {
/// load all enums from the cache that that have a common ancestor
pub fn load_enums(cache_path: &str, ancestor: Vec<String>) -> PyResult<Vec<Enum>> {
let path = std::path::Path::new(cache_path).join("enums");
if !path.exists() {
return Ok(vec![]);
}
let mut prefix = ancestor.join("::");
if !prefix.is_empty() {
prefix.push_str("::");
}
let mut enums = vec![];
for entry in std::fs::read_dir(path)? {
let entry = entry?;
Expand All @@ -308,7 +325,7 @@ pub fn load_enums(cache_path: &str, prefix: &str) -> PyResult<Vec<Enum>> {
Some(name) => name,
None => continue,
};
if !name.starts_with(prefix) {
if !name.starts_with(&prefix) {
continue;
}
let contents = read_file(&path)?;
Expand Down
3 changes: 0 additions & 3 deletions crates/py_binding/src/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ pub struct Crate {
pub name: String,
#[pyo3(get)]
pub version: String,
#[pyo3(get)]
pub docstring: String,
}

#[pymethods]
Expand All @@ -34,7 +32,6 @@ impl From<analyze::Crate> for Crate {
Crate {
name: crate_.name,
version: crate_.version,
docstring: crate_.docstring,
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
table.need {
background-color: var(--color-code-background);
}
tr.need.content {
padding-left: 0;
}
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"analyzer": "restructuredtext",
"sphinx_rust": "markdown",
}
rust_viewcode = True

intersphinx_mapping = {
"sphinx": ("https://www.sphinx-doc.org", None),
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Add `sphinx_rust` to your `conf.py`, and specifiy the paths to the Rust crates y
"../path/to/crate",
...
]
rust_viewcode = True # Optional: add "View Source" links
...

Now add a `toctree` in your `index.rst`, to point towards the generated documentation for each crate
Expand Down
3 changes: 3 additions & 0 deletions python/sphinx_rust/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ class RustConfig:

rust_crates: list[str]
rust_doc_formats: dict[str, str]
rust_viewcode: bool

@classmethod
def from_app(cls, app: Sphinx) -> RustConfig:
"""Create a new RustConfig from the Sphinx application."""
return cls(
rust_crates=app.config.rust_crates,
rust_doc_formats=app.config.rust_doc_formats,
rust_viewcode=app.config.rust_viewcode,
)

@staticmethod
def add_configs(app: Sphinx) -> None:
"""Add the configuration values for the Rust domain."""
app.add_config_value("rust_crates", [], "env")
app.add_config_value("rust_doc_formats", {}, "env")
app.add_config_value("rust_viewcode", False, "env")
43 changes: 37 additions & 6 deletions python/sphinx_rust/directives/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class RustAutoDirective(SphinxDirective):
def doc(self) -> nodes.document:
return self.state.document # type: ignore[no-any-return]

@property
def rust_config(self) -> RustConfig:
return RustConfig.from_app(self.env.app)

@property
def rust_domain(self) -> RustDomain:
# avoid circular import
Expand Down Expand Up @@ -157,10 +161,10 @@ def parse_docstring(
return document.children


def create_xref(
docname: str, path: str, objtype: ObjType, *, warn_dangling: bool = False
def create_object_xref(
docname: str, full_name: str, objtype: ObjType, *, warn_dangling: bool = False
) -> addnodes.pending_xref:
"""Create a cross-reference node.
"""Create a cross-reference node to a rust object.

:param docname: The document name.
:param path: The fully qualified path to the object, e.g. ``crate::module::Item``.
Expand All @@ -171,15 +175,42 @@ def create_xref(
"reftype": objtype,
"refexplicit": True,
"refwarn": warn_dangling,
"reftarget": path,
"reftarget": full_name,
}
ref = addnodes.pending_xref(path, **options)
name = path.split("::")[-1]
ref = addnodes.pending_xref(full_name, **options)
name = full_name.split("::")[-1]
ref += nodes.literal(name, name)

return ref


def create_source_xref(
docname: str,
full_name: str,
*,
warn_dangling: bool = False,
text: str | None = None,
) -> addnodes.pending_xref:
"""Create a cross-reference node to the source-code of a rust object.

:param docname: The document name.
:param path: The fully qualified path to the object, e.g. ``crate::module::Item``.
"""
options = {
"refdoc": docname,
"refdomain": "std",
"reftype": "ref",
"refexplicit": True,
"refwarn": warn_dangling,
"reftarget": f"rust-code:{full_name}",
}
ref = addnodes.pending_xref(full_name, **options)
text = full_name.split("::")[-1] if text is None else text
ref += nodes.literal(text, text)

return ref


def type_segs_to_nodes(segs: list[TypeSegment]) -> list[nodes.Node]:
"""Convert a list of type segments to nodes."""
nodes_: list[nodes.Node] = []
Expand Down
Loading