11from collections .abc import Callable
2+ import hashlib
23import os
34from pathlib import Path
45from typing import Any , ClassVar , cast
78from docutils .parsers .rst import directives
89from packaging .version import Version
910import sphinx
11+ from sphinx .config import Config as _SphinxConfig
1012from sphinx .util .docutils import SphinxDirective
1113from sphinx_needs .api import add_need # type: ignore[import-untyped]
1214from sphinx_needs .utils import add_doc # type: ignore[import-untyped]
3335logger = logging .getLogger (__name__ )
3436
3537
38+ def _check_id (
39+ config : _SphinxConfig ,
40+ id : str | None ,
41+ src_strings : list [str ],
42+ options : dict [str , str ],
43+ additional_options : dict [str , str ],
44+ ) -> None :
45+ """Check and set the id for the need.
46+
47+ src_strings[0] is always the title.
48+ src_strings[1] is always the project.
49+ """
50+ if config .needs_id_required :
51+ if id :
52+ additional_options ["id" ] = id
53+ else :
54+ if "directory" in options :
55+ src_strings .append (options ["directory" ])
56+ if "file" in options :
57+ src_strings .append (options ["file" ])
58+
59+ additional_options ["id" ] = _make_hashed_id ("SRCTRACE_" , src_strings , config )
60+
61+
62+ def _make_hashed_id (
63+ type_prefix : str , src_strings : list [str ], config : _SphinxConfig
64+ ) -> str :
65+ """Create an ID based on the type and title of the need."""
66+ full_title = src_strings [0 ] # title is always the first element
67+ hashable_content = "_" .join (src_strings )
68+ hashed = hashlib .sha256 (hashable_content .encode ("UTF-8" )).hexdigest ().upper ()
69+ if config .needs_id_from_title :
70+ hashed = full_title .upper ().replace (" " , "_" ) + "_" + hashed
71+ return f"{ type_prefix } { hashed [: config .needs_id_length ]} "
72+
73+
3674def get_rel_path (doc_path : Path , code_path : Path , base_dir : Path ) -> tuple [Path , Path ]:
3775 """Get the relative path from the document to the source code file and vice versa."""
3876 doc_depth = len (doc_path .parents ) - 1
@@ -93,6 +131,7 @@ def run(self) -> list[nodes.Node]:
93131 validate_option (self .options )
94132
95133 project = self .options ["project" ]
134+ id = self .options .get ("id" )
96135 title = self .arguments [0 ]
97136 # get source tracing config
98137 src_trace_sphinx_config = CodeLinksConfig .from_sphinx (self .env .config )
@@ -108,7 +147,12 @@ def run(self) -> list[nodes.Node]:
108147 # the directory where the source files are copied to
109148 target_dir = out_dir / src_dir .name
110149
111- extra_options = {"project" : project }
150+ additional_options = {"project" : project }
151+
152+ _check_id (
153+ self .env .config , id , [title , project ], self .options , additional_options
154+ )
155+
112156 source_files = self .get_src_files (self .options , src_dir , src_discover_config )
113157
114158 # add source files into the dependency
@@ -132,7 +176,7 @@ def run(self) -> list[nodes.Node]:
132176 lineno = self .lineno , # The line number where the directive is used
133177 need_type = "srctrace" , # The type of the need
134178 title = title , # The title of the need
135- ** extra_options ,
179+ ** additional_options ,
136180 )
137181 needs .extend (src_trace_need )
138182
@@ -200,7 +244,7 @@ def run(self) -> list[nodes.Node]:
200244
201245 def get_src_files (
202246 self ,
203- extra_options : dict [str , str ],
247+ additional_options : dict [str , str ],
204248 src_dir : Path ,
205249 src_discover_config : SourceDiscoverConfig ,
206250 ) -> list [Path ]:
@@ -210,14 +254,14 @@ def get_src_files(
210254 file : str = self .options ["file" ]
211255 filepath = src_dir / file
212256 source_files .append (filepath .resolve ())
213- extra_options ["file" ] = file
257+ additional_options ["file" ] = file
214258 else :
215259 directory = self .options .get ("directory" )
216260 if directory is None :
217261 # when neither "file" and "directory" are given, the project root dir is by default
218262 directory = "./"
219263 else :
220- extra_options ["directory" ] = directory
264+ additional_options ["directory" ] = directory
221265 dir_path = src_dir / directory
222266 # create a new config for the specified directory
223267 src_discover = SourceDiscoverConfig (
0 commit comments