diff --git a/docs/conf.py b/docs/conf.py index a70fc08..a003cce 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,4 +79,8 @@ html_context = {"repository": "useblocks/sphinx-codelinks"} html_css_files = ["furo.css"] +# Sphinx-Needs configuration +needs_from_toml = "ubproject.toml" + +# Src-trace configuration src_trace_config_from_toml = "./src_trace.toml" diff --git a/docs/source/components/directive.rst b/docs/source/components/directive.rst index 182af8a..320393c 100644 --- a/docs/source/components/directive.rst +++ b/docs/source/components/directive.rst @@ -52,12 +52,14 @@ The ``src-trace`` directive can be used with the **file** option: .. code-block:: rst .. src-trace:: dcdc demo_1 + :id: SRC_001 :project: dcdc :file: ./charge/demo_1.cpp The needs defined in source code are extracted and rendered to: .. src-trace:: dcdc demo_1 + :id: SRC_001 :project: dcdc :file: ./charge/demo_1.cpp @@ -66,12 +68,14 @@ The ``src-trace`` directive can be used with the **directory** option: .. code-block:: rst .. src-trace:: dcdc charge + :id: SRC_001 :project: dcdc :directory: ./discharge The needs defined in source code are extracted and rendered to: .. src-trace:: dcdc charge + :id: SRC_002 :project: dcdc :directory: ./discharge diff --git a/docs/ubproject.toml b/docs/ubproject.toml index 5429bcf..841fb98 100644 --- a/docs/ubproject.toml +++ b/docs/ubproject.toml @@ -5,8 +5,3 @@ ignore = ["block.title_line"] [needs] id_required = true - -[[needs.types]] -directive = "my-req" -title = "My Requirement" -prefix = "M_" diff --git a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py index 57db7f0..0e0b941 100644 --- a/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +++ b/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py @@ -1,4 +1,5 @@ from collections.abc import Callable +import hashlib import os from pathlib import Path from typing import Any, ClassVar, cast @@ -7,6 +8,7 @@ from docutils.parsers.rst import directives from packaging.version import Version import sphinx +from sphinx.config import Config as _SphinxConfig from sphinx.util.docutils import SphinxDirective from sphinx_needs.api import add_need # type: ignore[import-untyped] from sphinx_needs.utils import add_doc # type: ignore[import-untyped] @@ -33,6 +35,42 @@ logger = logging.getLogger(__name__) +def _check_id( + config: _SphinxConfig, + id: str | None, + src_strings: list[str], + options: dict[str, str], + additional_options: dict[str, str], +) -> None: + """Check and set the id for the need. + + src_strings[0] is always the title. + src_strings[1] is always the project. + """ + if config.needs_id_required: + if id: + additional_options["id"] = id + else: + if "directory" in options: + src_strings.append(options["directory"]) + if "file" in options: + src_strings.append(options["file"]) + + additional_options["id"] = _make_hashed_id("SRCTRACE_", src_strings, config) + + +def _make_hashed_id( + type_prefix: str, src_strings: list[str], config: _SphinxConfig +) -> str: + """Create an ID based on the type and title of the need.""" + full_title = src_strings[0] # title is always the first element + hashable_content = "_".join(src_strings) + hashed = hashlib.sha256(hashable_content.encode("UTF-8")).hexdigest().upper() + if config.needs_id_from_title: + hashed = full_title.upper().replace(" ", "_") + "_" + hashed + return f"{type_prefix}{hashed[: config.needs_id_length]}" + + def get_rel_path(doc_path: Path, code_path: Path, base_dir: Path) -> tuple[Path, Path]: """Get the relative path from the document to the source code file and vice versa.""" doc_depth = len(doc_path.parents) - 1 @@ -93,6 +131,7 @@ def run(self) -> list[nodes.Node]: validate_option(self.options) project = self.options["project"] + id = self.options.get("id") title = self.arguments[0] # get source tracing config src_trace_sphinx_config = CodeLinksConfig.from_sphinx(self.env.config) @@ -108,7 +147,12 @@ def run(self) -> list[nodes.Node]: # the directory where the source files are copied to target_dir = out_dir / src_dir.name - extra_options = {"project": project} + additional_options = {"project": project} + + _check_id( + self.env.config, id, [title, project], self.options, additional_options + ) + source_files = self.get_src_files(self.options, src_dir, src_discover_config) # add source files into the dependency @@ -132,7 +176,7 @@ def run(self) -> list[nodes.Node]: lineno=self.lineno, # The line number where the directive is used need_type="srctrace", # The type of the need title=title, # The title of the need - **extra_options, + **additional_options, ) needs.extend(src_trace_need) @@ -200,7 +244,7 @@ def run(self) -> list[nodes.Node]: def get_src_files( self, - extra_options: dict[str, str], + additional_options: dict[str, str], src_dir: Path, src_discover_config: SourceDiscoverConfig, ) -> list[Path]: @@ -210,14 +254,14 @@ def get_src_files( file: str = self.options["file"] filepath = src_dir / file source_files.append(filepath.resolve()) - extra_options["file"] = file + additional_options["file"] = file else: directory = self.options.get("directory") if directory is None: # when neither "file" and "directory" are given, the project root dir is by default directory = "./" else: - extra_options["directory"] = directory + additional_options["directory"] = directory dir_path = src_dir / directory # create a new config for the specified directory src_discover = SourceDiscoverConfig( diff --git a/src/sphinx_codelinks/sphinx_extension/source_tracing.py b/src/sphinx_codelinks/sphinx_extension/source_tracing.py index 1daeb01..37504da 100644 --- a/src/sphinx_codelinks/sphinx_extension/source_tracing.py +++ b/src/sphinx_codelinks/sphinx_extension/source_tracing.py @@ -35,7 +35,34 @@ logger = logging.getLogger(__name__) +def _check_sphinx_needs_dependency(app: Sphinx) -> bool: + """Check if sphinx-needs is actually loaded as an extension.""" + # Check if sphinx-needs is in the loaded extensions + if "sphinx_needs" not in app.extensions: + error_msg = ( + "sphinx-codelinks requires sphinx-needs to be loaded as an extension.\n" + "Please ensure 'sphinx_needs' is properly installed and added to your extensions list in conf.py:\n" + " extensions = ['sphinx_needs', 'sphinx_codelinks', ...]\n" + f"Currently loaded extensions: {list(app.extensions.keys())}\n" + f"Configured extensions: {app.config.extensions}" + ) + logger.error(error_msg) + return False + return True + + def setup(app: Sphinx) -> dict[str, Any]: # type: ignore[explicit-any] + # Check if sphinx-needs is available and properly configured + if not _check_sphinx_needs_dependency(app): + logger.error( + "Failed to initialize sphinx-codelinks due to missing sphinx-needs dependency" + ) + return { + "version": "builtin", + "parallel_read_safe": True, + "parallel_write_safe": True, + } + app.add_node(SourceTracing) app.add_directive("src-trace", SourceTracingDirective) CodeLinksConfig.add_config_values(app) diff --git a/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project3-source_code3].doctree.xml b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project3-source_code3].doctree.xml new file mode 100644 index 0000000..4f4c85e --- /dev/null +++ b/tests/__snapshots__/test_src_trace/test_build_html[sphinx_project3-source_code3].doctree.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/doc_test/id_required/conf.py b/tests/doc_test/id_required/conf.py new file mode 100644 index 0000000..6d85ccf --- /dev/null +++ b/tests/doc_test/id_required/conf.py @@ -0,0 +1,30 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "source-tracing-demo" +copyright = "2025, useblocks" +author = "useblocks" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx_needs", "sphinx_codelinks"] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +src_trace_config_from_toml = "src_trace.toml" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] + +# Sphinx-Needs configuration +needs_id_required = True diff --git a/tests/doc_test/id_required/dummy_src.cpp b/tests/doc_test/id_required/dummy_src.cpp new file mode 100644 index 0000000..af1e002 --- /dev/null +++ b/tests/doc_test/id_required/dummy_src.cpp @@ -0,0 +1,7 @@ +#include + +// @ title here, IMPL_1, impl +void singleLineExample() +{ + std::cout << "Single-line comment example" << std::endl; +} diff --git a/tests/doc_test/id_required/index.rst b/tests/doc_test/id_required/index.rst new file mode 100644 index 0000000..1e26a6b --- /dev/null +++ b/tests/doc_test/id_required/index.rst @@ -0,0 +1,2 @@ +.. src-trace:: dummy src + :project: src diff --git a/tests/doc_test/id_required/src_trace.toml b/tests/doc_test/id_required/src_trace.toml new file mode 100644 index 0000000..0561780 --- /dev/null +++ b/tests/doc_test/id_required/src_trace.toml @@ -0,0 +1,2 @@ +[codelinks.projects.src] +remote_url_pattern = "https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}" diff --git a/tests/test_src_trace.py b/tests/test_src_trace.py index ea1cd8d..519eb80 100644 --- a/tests/test_src_trace.py +++ b/tests/test_src_trace.py @@ -187,6 +187,10 @@ def test_src_tracing_config_positive(make_app: Callable[..., SphinxTestApp], tmp Path("doc_test") / "minimum_config", Path("doc_test") / "minimum_config", ), + ( + Path("doc_test") / "id_required", + Path("doc_test") / "id_required", + ), ], ) def test_build_html(