1
1
from collections .abc import Callable
2
+ import hashlib
2
3
import os
3
4
from pathlib import Path
4
5
from typing import Any , ClassVar , cast
7
8
from docutils .parsers .rst import directives
8
9
from packaging .version import Version
9
10
import sphinx
11
+ from sphinx .config import Config as _SphinxConfig
10
12
from sphinx .util .docutils import SphinxDirective
11
13
from sphinx_needs .api import add_need # type: ignore[import-untyped]
12
14
from sphinx_needs .utils import add_doc # type: ignore[import-untyped]
33
35
logger = logging .getLogger (__name__ )
34
36
35
37
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
+
36
74
def get_rel_path (doc_path : Path , code_path : Path , base_dir : Path ) -> tuple [Path , Path ]:
37
75
"""Get the relative path from the document to the source code file and vice versa."""
38
76
doc_depth = len (doc_path .parents ) - 1
@@ -93,6 +131,7 @@ def run(self) -> list[nodes.Node]:
93
131
validate_option (self .options )
94
132
95
133
project = self .options ["project" ]
134
+ id = self .options .get ("id" )
96
135
title = self .arguments [0 ]
97
136
# get source tracing config
98
137
src_trace_sphinx_config = CodeLinksConfig .from_sphinx (self .env .config )
@@ -108,7 +147,12 @@ def run(self) -> list[nodes.Node]:
108
147
# the directory where the source files are copied to
109
148
target_dir = out_dir / src_dir .name
110
149
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
+
112
156
source_files = self .get_src_files (self .options , src_dir , src_discover_config )
113
157
114
158
# add source files into the dependency
@@ -132,7 +176,7 @@ def run(self) -> list[nodes.Node]:
132
176
lineno = self .lineno , # The line number where the directive is used
133
177
need_type = "srctrace" , # The type of the need
134
178
title = title , # The title of the need
135
- ** extra_options ,
179
+ ** additional_options ,
136
180
)
137
181
needs .extend (src_trace_need )
138
182
@@ -200,7 +244,7 @@ def run(self) -> list[nodes.Node]:
200
244
201
245
def get_src_files (
202
246
self ,
203
- extra_options : dict [str , str ],
247
+ additional_options : dict [str , str ],
204
248
src_dir : Path ,
205
249
src_discover_config : SourceDiscoverConfig ,
206
250
) -> list [Path ]:
@@ -210,14 +254,14 @@ def get_src_files(
210
254
file : str = self .options ["file" ]
211
255
filepath = src_dir / file
212
256
source_files .append (filepath .resolve ())
213
- extra_options ["file" ] = file
257
+ additional_options ["file" ] = file
214
258
else :
215
259
directory = self .options .get ("directory" )
216
260
if directory is None :
217
261
# when neither "file" and "directory" are given, the project root dir is by default
218
262
directory = "./"
219
263
else :
220
- extra_options ["directory" ] = directory
264
+ additional_options ["directory" ] = directory
221
265
dir_path = src_dir / directory
222
266
# create a new config for the specified directory
223
267
src_discover = SourceDiscoverConfig (
0 commit comments