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
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 4 additions & 0 deletions docs/source/components/directive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
5 changes: 0 additions & 5 deletions docs/ubproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,3 @@ ignore = ["block.title_line"]

[needs]
id_required = true

[[needs.types]]
directive = "my-req"
title = "My Requirement"
prefix = "M_"
54 changes: 49 additions & 5 deletions src/sphinx_codelinks/sphinx_extension/directives/src_trace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Callable
import hashlib
import os
from pathlib import Path
from typing import Any, ClassVar, cast
Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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]:
Expand All @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions src/sphinx_codelinks/sphinx_extension/source_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<document source="<source>">
<target anonymous="" ids="SRCTRACE_F70E0" refid="SRCTRACE_F70E0">
<Need classes="need need-srctrace" ids="SRCTRACE_F70E0" refid="SRCTRACE_F70E0">
<target anonymous="" ids="IMPL_1" refid="IMPL_1">
<Need classes="need need-impl" ids="IMPL_1" refid="IMPL_1">
30 changes: 30 additions & 0 deletions tests/doc_test/id_required/conf.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tests/doc_test/id_required/dummy_src.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include <iostream>

// @ title here, IMPL_1, impl
void singleLineExample()
{
std::cout << "Single-line comment example" << std::endl;
}
2 changes: 2 additions & 0 deletions tests/doc_test/id_required/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. src-trace:: dummy src
:project: src
2 changes: 2 additions & 0 deletions tests/doc_test/id_required/src_trace.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[codelinks.projects.src]
remote_url_pattern = "https://github.com/useblocks/sphinx-codelinks/blob/{commit}/{path}#L{line}"
4 changes: 4 additions & 0 deletions tests/test_src_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading