sphinx-codelinks 1.0.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/PKG-INFO +4 -1
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/pyproject.toml +34 -2
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/analyse.py +17 -10
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/oneline_parser.py +3 -19
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/utils.py +146 -9
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/cmd.py +19 -1
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/config.py +44 -11
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/needextend_write.py +4 -4
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/source_discover/config.py +13 -3
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/source_discover/source_discover.py +1 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +32 -28
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +29 -6
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/LICENSE +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/README.md +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/__init__.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/models.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/projects.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/logger.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
- {sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/ub_sct.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-codelinks
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Fast Source Code Traceability for Sphinx-Needs
|
|
5
5
|
Author-email: team useblocks <info@useblocks.com>
|
|
6
6
|
Maintainer-email: Marco Heinemann <marco.heinemann@useblocks.com>, Jui-Wen Chen <jui-wen.chen@useblocks.com>
|
|
@@ -21,6 +21,9 @@ Requires-Dist: tree-sitter-cpp>=0.23.4
|
|
|
21
21
|
Requires-Dist: giturlparse>=0.12.0
|
|
22
22
|
Requires-Dist: tree-sitter-python>=0.23.6
|
|
23
23
|
Requires-Dist: tree-sitter~=0.25.1
|
|
24
|
+
Requires-Dist: tree-sitter-c-sharp>=0.23.1
|
|
25
|
+
Requires-Dist: tree-sitter-yaml>=0.7.1
|
|
26
|
+
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
24
27
|
|
|
25
28
|
# Sphinx CodeLinks
|
|
26
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sphinx-codelinks"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Fast Source Code Traceability for Sphinx-Needs"
|
|
5
5
|
authors = [{ name = "team useblocks", email = "info@useblocks.com" }]
|
|
6
6
|
maintainers = [
|
|
@@ -27,13 +27,45 @@ dependencies = [
|
|
|
27
27
|
# Compiling issue on manylinux aarch64 image, so install from git instead of Pypi
|
|
28
28
|
# https://github.com/tree-sitter/py-tree-sitter/issues/386#issuecomment-3101430799
|
|
29
29
|
"tree-sitter~=0.25.1",
|
|
30
|
-
|
|
30
|
+
"tree-sitter-c-sharp>=0.23.1",
|
|
31
|
+
"tree-sitter-yaml>=0.7.1",
|
|
32
|
+
"tree-sitter-rust>=0.23.0",
|
|
31
33
|
]
|
|
32
34
|
|
|
33
35
|
[build-system]
|
|
34
36
|
requires = ["flit_core >=3.4,<4"]
|
|
35
37
|
build-backend = "flit_core.buildapi"
|
|
36
38
|
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
testing = [
|
|
41
|
+
"pytest>=8.2.2",
|
|
42
|
+
"pytest-cov>=5.0.0",
|
|
43
|
+
"pytest-docker>=3.1.2",
|
|
44
|
+
"syrupy>=4.9.1",
|
|
45
|
+
"psutil>=7.0.0",
|
|
46
|
+
"moto ~= 5.0",
|
|
47
|
+
"toml>=0.10.2",
|
|
48
|
+
"furo>=2024.5.6",
|
|
49
|
+
]
|
|
50
|
+
docs = [
|
|
51
|
+
"furo>=2024.5.6",
|
|
52
|
+
"myst-parser>=4.0.0",
|
|
53
|
+
"sphinx-design>=0.6.1",
|
|
54
|
+
"sphinx-code-tabs>=0.5.5",
|
|
55
|
+
"sphinxcontrib-typer>=0.5.1",
|
|
56
|
+
"sphinxcontrib-video>=0.4.1",
|
|
57
|
+
]
|
|
58
|
+
mypy = [
|
|
59
|
+
"mypy>=1.12.1",
|
|
60
|
+
"pydantic ~= 2.9",
|
|
61
|
+
"types-docutils",
|
|
62
|
+
"types-Pygments",
|
|
63
|
+
"types-psutil>=7.0.0.20250218",
|
|
64
|
+
"types-jsonschema>=4.23.0.20241208",
|
|
65
|
+
]
|
|
66
|
+
ruff = ["ruff>=0.8.0"]
|
|
67
|
+
build = ["simple-build>=0.0.2", "shiv>=1.0.8"]
|
|
68
|
+
|
|
37
69
|
[tool.rye]
|
|
38
70
|
managed = true
|
|
39
71
|
dev-dependencies = [
|
|
@@ -65,7 +65,11 @@ class SourceAnalyse:
|
|
|
65
65
|
self.oneline_needs: list[OneLineNeed] = []
|
|
66
66
|
self.marked_rst: list[MarkedRst] = []
|
|
67
67
|
self.all_marked_content: list[NeedIdRefs | OneLineNeed | MarkedRst] = []
|
|
68
|
-
|
|
68
|
+
# Use explicitly configured git_root if provided, otherwise auto-detect
|
|
69
|
+
if self.analyse_config.git_root is not None:
|
|
70
|
+
self.git_root: Path | None = self.analyse_config.git_root.resolve()
|
|
71
|
+
else:
|
|
72
|
+
self.git_root = utils.locate_git_root(self.analyse_config.src_dir)
|
|
69
73
|
self.git_remote_url: str | None = (
|
|
70
74
|
utils.get_remote_url(self.git_root) if self.git_root else None
|
|
71
75
|
)
|
|
@@ -127,6 +131,7 @@ class SourceAnalyse:
|
|
|
127
131
|
yield marker, need_ids, row_offset, start_column, end_column
|
|
128
132
|
row_offset += 1
|
|
129
133
|
|
|
134
|
+
# @Extract need ID references from code comments, IMPL_LNK_1, impl, [FE_LNK]
|
|
130
135
|
def extract_anchors(
|
|
131
136
|
self,
|
|
132
137
|
text: str,
|
|
@@ -197,20 +202,21 @@ class SourceAnalyse:
|
|
|
197
202
|
if not src_comment.source_file:
|
|
198
203
|
row_offset += 1
|
|
199
204
|
continue
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
205
|
+
lineno = src_comment.node.start_point.row + row_offset + 1
|
|
206
|
+
warning = AnalyseWarning(
|
|
207
|
+
str(src_comment.source_file.filepath),
|
|
208
|
+
lineno,
|
|
209
|
+
resolved.msg,
|
|
210
|
+
MarkedContentType.need,
|
|
211
|
+
resolved.sub_type.value,
|
|
208
212
|
)
|
|
213
|
+
self.oneline_warnings.append(warning)
|
|
209
214
|
row_offset += 1
|
|
210
215
|
continue
|
|
211
216
|
yield resolved, row_offset
|
|
212
217
|
row_offset += 1
|
|
213
218
|
|
|
219
|
+
# @Extract one-line traceability needs from comments, IMPL_ONE_1, impl, [FE_DEF, FE_CMT]
|
|
214
220
|
def extract_oneline_needs(
|
|
215
221
|
self,
|
|
216
222
|
text: str,
|
|
@@ -258,6 +264,7 @@ class SourceAnalyse:
|
|
|
258
264
|
)
|
|
259
265
|
return oneline_needs
|
|
260
266
|
|
|
267
|
+
# @Extract marked reStructuredText blocks from comments, IMPL_MRST_1, impl, [FE_RST_EXTRACTION]
|
|
261
268
|
def extract_marked_rst(
|
|
262
269
|
self,
|
|
263
270
|
text: str,
|
|
@@ -322,7 +329,7 @@ class SourceAnalyse:
|
|
|
322
329
|
if not filepath:
|
|
323
330
|
continue
|
|
324
331
|
tagged_scope: TreeSitterNode | None = utils.find_associated_scope(
|
|
325
|
-
src_comment.node
|
|
332
|
+
src_comment.node, self.analyse_config.comment_type
|
|
326
333
|
)
|
|
327
334
|
if self.analyse_config.get_need_id_refs:
|
|
328
335
|
anchors = self.extract_anchors(
|
{sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/oneline_parser.py
RENAMED
|
@@ -2,12 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
|
-
from sphinx_codelinks.config import
|
|
6
|
-
ESCAPE,
|
|
7
|
-
SUPPORTED_COMMENT_TYPES,
|
|
8
|
-
UNIX_NEWLINE,
|
|
9
|
-
OneLineCommentStyle,
|
|
10
|
-
)
|
|
5
|
+
from sphinx_codelinks.config import ESCAPE, UNIX_NEWLINE, OneLineCommentStyle
|
|
11
6
|
|
|
12
7
|
# initialize logger
|
|
13
8
|
logger = logging.getLogger(__name__)
|
|
@@ -36,6 +31,7 @@ class OnelineParserInvalidWarning:
|
|
|
36
31
|
msg: str
|
|
37
32
|
|
|
38
33
|
|
|
34
|
+
# @One-line comment parser for traceability markers, IMPL_OLP_1, impl, [FE_DEF, FE_CMT]
|
|
39
35
|
def oneline_parser( # noqa: PLR0912, PLR0911 # handel warnings
|
|
40
36
|
oneline: str, oneline_config: OneLineCommentStyle
|
|
41
37
|
) -> dict[str, str | list[str] | int] | OnelineParserInvalidWarning | None:
|
|
@@ -58,7 +54,7 @@ def oneline_parser( # noqa: PLR0912, PLR0911 # handel warnings
|
|
|
58
54
|
|
|
59
55
|
# extract the string wrapped by start and end
|
|
60
56
|
start_idx = start_idx + len(oneline_config.start_sequence)
|
|
61
|
-
string = oneline[start_idx:end_idx]
|
|
57
|
+
string = oneline[start_idx:end_idx].strip()
|
|
62
58
|
|
|
63
59
|
# numbers of needs_fields which are required
|
|
64
60
|
cnt_required_fields = oneline_config.get_cnt_required_fields()
|
|
@@ -204,15 +200,3 @@ def is_newline_in_field(field: str) -> bool:
|
|
|
204
200
|
Check if the field contains a new line character.
|
|
205
201
|
"""
|
|
206
202
|
return UNIX_NEWLINE in field
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def get_file_types(comment_type: str) -> list[str] | None:
|
|
210
|
-
"""
|
|
211
|
-
Get the list of file types to be discovered.
|
|
212
|
-
"""
|
|
213
|
-
file_types = (
|
|
214
|
-
list(SUPPORTED_COMMENT_TYPES)
|
|
215
|
-
if comment_type in SUPPORTED_COMMENT_TYPES
|
|
216
|
-
else None
|
|
217
|
-
)
|
|
218
|
-
return file_types
|
|
@@ -12,6 +12,25 @@ from tree_sitter import Node as TreeSitterNode
|
|
|
12
12
|
from sphinx_codelinks.config import UNIX_NEWLINE, CommentCategory
|
|
13
13
|
from sphinx_codelinks.source_discover.config import CommentType
|
|
14
14
|
|
|
15
|
+
# Language-specific node types for scope detection
|
|
16
|
+
SCOPE_NODE_TYPES = {
|
|
17
|
+
# @Python Scope Node Types, IMPL_PY_2, impl, [FE_PY]
|
|
18
|
+
CommentType.python: {"function_definition", "class_definition"},
|
|
19
|
+
# @C and C++ Scope Node Types, IMPL_C_2, impl, [FE_C_SUPPORT, FE_CPP]
|
|
20
|
+
CommentType.cpp: {"function_definition", "class_definition"},
|
|
21
|
+
CommentType.cs: {"method_declaration", "class_declaration", "property_declaration"},
|
|
22
|
+
CommentType.yaml: {"block_mapping_pair", "block_sequence_item", "document"},
|
|
23
|
+
# @Rust Scope Node Types, IMPL_RUST_2, impl, [FE_RUST];
|
|
24
|
+
CommentType.rust: {
|
|
25
|
+
"function_item",
|
|
26
|
+
"struct_item",
|
|
27
|
+
"enum_item",
|
|
28
|
+
"impl_item",
|
|
29
|
+
"trait_item",
|
|
30
|
+
"mod_item",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
# initialize logger
|
|
16
35
|
logger = logging.getLogger(__name__)
|
|
17
36
|
logger.setLevel(logging.INFO)
|
|
@@ -35,6 +54,12 @@ PYTHON_QUERY = """
|
|
|
35
54
|
(class_definition (block (expression_statement (string)) @comment))
|
|
36
55
|
"""
|
|
37
56
|
CPP_QUERY = """(comment) @comment"""
|
|
57
|
+
C_SHARP_QUERY = """(comment) @comment"""
|
|
58
|
+
YAML_QUERY = """(comment) @comment"""
|
|
59
|
+
RUST_QUERY = """
|
|
60
|
+
(line_comment) @comment
|
|
61
|
+
(block_comment) @comment
|
|
62
|
+
"""
|
|
38
63
|
|
|
39
64
|
|
|
40
65
|
def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
|
|
@@ -52,6 +77,7 @@ def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
|
|
|
52
77
|
return False
|
|
53
78
|
|
|
54
79
|
|
|
80
|
+
# @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST]
|
|
55
81
|
def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
|
|
56
82
|
if comment_type == CommentType.cpp:
|
|
57
83
|
import tree_sitter_cpp # noqa: PLC0415
|
|
@@ -63,6 +89,21 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
|
|
|
63
89
|
|
|
64
90
|
parsed_language = Language(tree_sitter_python.language())
|
|
65
91
|
query = Query(parsed_language, PYTHON_QUERY)
|
|
92
|
+
elif comment_type == CommentType.cs:
|
|
93
|
+
import tree_sitter_c_sharp # noqa: PLC0415
|
|
94
|
+
|
|
95
|
+
parsed_language = Language(tree_sitter_c_sharp.language())
|
|
96
|
+
query = Query(parsed_language, C_SHARP_QUERY)
|
|
97
|
+
elif comment_type == CommentType.yaml:
|
|
98
|
+
import tree_sitter_yaml # noqa: PLC0415
|
|
99
|
+
|
|
100
|
+
parsed_language = Language(tree_sitter_yaml.language())
|
|
101
|
+
query = Query(parsed_language, YAML_QUERY)
|
|
102
|
+
elif comment_type == CommentType.rust:
|
|
103
|
+
import tree_sitter_rust # noqa: PLC0415
|
|
104
|
+
|
|
105
|
+
parsed_language = Language(tree_sitter_rust.language())
|
|
106
|
+
query = Query(parsed_language, RUST_QUERY)
|
|
66
107
|
else:
|
|
67
108
|
raise ValueError(f"Unsupported comment style: {comment_type}")
|
|
68
109
|
parser = Parser(parsed_language)
|
|
@@ -78,6 +119,7 @@ def wrap_read_callable_point(
|
|
|
78
119
|
return read_callable_byte_offset
|
|
79
120
|
|
|
80
121
|
|
|
122
|
+
# @Comment extraction from source code using tree-sitter, IMPL_EXTR_1, impl, [FE_DEF]
|
|
81
123
|
def extract_comments(
|
|
82
124
|
src_string: ByteString, parser: Parser, query: Query
|
|
83
125
|
) -> list[TreeSitterNode] | None:
|
|
@@ -90,39 +132,133 @@ def extract_comments(
|
|
|
90
132
|
return captures.get("comment")
|
|
91
133
|
|
|
92
134
|
|
|
93
|
-
def find_enclosing_scope(
|
|
135
|
+
def find_enclosing_scope(
|
|
136
|
+
node: TreeSitterNode, comment_type: CommentType = CommentType.cpp
|
|
137
|
+
) -> TreeSitterNode | None:
|
|
94
138
|
"""Find the enclosing scope of a comment."""
|
|
139
|
+
scope_types = SCOPE_NODE_TYPES.get(comment_type, SCOPE_NODE_TYPES[CommentType.cpp])
|
|
95
140
|
current: TreeSitterNode = node
|
|
96
141
|
while current:
|
|
97
|
-
if current.type in
|
|
142
|
+
if current.type in scope_types:
|
|
98
143
|
return current
|
|
99
144
|
current: TreeSitterNode | None = current.parent # type: ignore[no-redef] # required for node traversal
|
|
100
145
|
return None
|
|
101
146
|
|
|
102
147
|
|
|
103
|
-
def find_next_scope(
|
|
148
|
+
def find_next_scope(
|
|
149
|
+
node: TreeSitterNode, comment_type: CommentType = CommentType.cpp
|
|
150
|
+
) -> TreeSitterNode | None:
|
|
104
151
|
"""Find the next scope of a comment."""
|
|
152
|
+
scope_types = SCOPE_NODE_TYPES.get(comment_type, SCOPE_NODE_TYPES[CommentType.cpp])
|
|
105
153
|
current: TreeSitterNode = node
|
|
106
154
|
while current:
|
|
107
|
-
if current.type in
|
|
155
|
+
if current.type in scope_types:
|
|
108
156
|
return current
|
|
109
157
|
current: TreeSitterNode | None = current.next_named_sibling # type: ignore[no-redef] # required for node traversal
|
|
110
158
|
if current and current.type == "block":
|
|
111
159
|
for child in current.named_children:
|
|
112
|
-
if child.type in
|
|
160
|
+
if child.type in scope_types:
|
|
113
161
|
return child
|
|
114
162
|
return None
|
|
115
163
|
|
|
116
164
|
|
|
117
|
-
def
|
|
165
|
+
def _find_yaml_structure_in_block_node(
|
|
166
|
+
block_node: TreeSitterNode,
|
|
167
|
+
) -> TreeSitterNode | None:
|
|
168
|
+
"""Find YAML structure elements within a block_node."""
|
|
169
|
+
for grandchild in block_node.named_children:
|
|
170
|
+
if grandchild.type == "block_mapping":
|
|
171
|
+
for ggchild in grandchild.named_children:
|
|
172
|
+
if ggchild.type == "block_mapping_pair":
|
|
173
|
+
return ggchild
|
|
174
|
+
elif grandchild.type == "block_sequence":
|
|
175
|
+
for ggchild in grandchild.named_children:
|
|
176
|
+
if ggchild.type == "block_sequence_item":
|
|
177
|
+
return ggchild
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def find_yaml_next_structure(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
182
|
+
"""Find the next YAML structure element after the comment node."""
|
|
183
|
+
current = node.next_named_sibling
|
|
184
|
+
while current:
|
|
185
|
+
if current.type in {
|
|
186
|
+
"block_mapping_pair",
|
|
187
|
+
"block_sequence_item",
|
|
188
|
+
"flow_mapping",
|
|
189
|
+
"flow_sequence",
|
|
190
|
+
}:
|
|
191
|
+
return current
|
|
192
|
+
if current.type == "document":
|
|
193
|
+
for child in current.named_children:
|
|
194
|
+
if child.type == "block_node":
|
|
195
|
+
result = _find_yaml_structure_in_block_node(child)
|
|
196
|
+
if result:
|
|
197
|
+
return result
|
|
198
|
+
if current.type == "block_node":
|
|
199
|
+
result = _find_yaml_structure_in_block_node(current)
|
|
200
|
+
if result:
|
|
201
|
+
return result
|
|
202
|
+
current = current.next_named_sibling
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def find_yaml_prev_sibling_on_same_row(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
207
|
+
"""Find a previous named sibling that is on the same row as the comment."""
|
|
208
|
+
comment_row = node.start_point.row
|
|
209
|
+
current = node.prev_named_sibling
|
|
210
|
+
|
|
211
|
+
while current:
|
|
212
|
+
# Check if this sibling ends on the same row as the comment starts
|
|
213
|
+
# This indicates it's an inline comment
|
|
214
|
+
if current.end_point.row == comment_row:
|
|
215
|
+
return current
|
|
216
|
+
# If we find a sibling that ends before the comment row, we can stop
|
|
217
|
+
# as we won't find any siblings on the same row going backwards
|
|
218
|
+
if current.end_point.row < comment_row:
|
|
219
|
+
break
|
|
220
|
+
current = current.prev_named_sibling
|
|
221
|
+
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def find_yaml_associated_structure(node: TreeSitterNode) -> TreeSitterNode | None:
|
|
226
|
+
"""Find the YAML structure (key-value pair, list item, etc.) associated with a comment."""
|
|
227
|
+
# First, check if this is an inline comment by looking for a previous sibling on the same row
|
|
228
|
+
prev_sibling_same_row = find_yaml_prev_sibling_on_same_row(node)
|
|
229
|
+
if prev_sibling_same_row:
|
|
230
|
+
return prev_sibling_same_row
|
|
231
|
+
|
|
232
|
+
# If no previous sibling on same row, try to find the next named sibling (structure after the comment)
|
|
233
|
+
structure = find_yaml_next_structure(node)
|
|
234
|
+
if structure:
|
|
235
|
+
return structure
|
|
236
|
+
|
|
237
|
+
# If no next sibling found, traverse up to find parent structure
|
|
238
|
+
parent = node.parent
|
|
239
|
+
while parent:
|
|
240
|
+
if parent.type in {"block_mapping_pair", "block_sequence_item"}:
|
|
241
|
+
return parent
|
|
242
|
+
parent = parent.parent
|
|
243
|
+
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def find_associated_scope(
|
|
248
|
+
node: TreeSitterNode, comment_type: CommentType = CommentType.cpp
|
|
249
|
+
) -> TreeSitterNode | None:
|
|
118
250
|
"""Find the associated scope of a comment."""
|
|
251
|
+
if comment_type == CommentType.yaml:
|
|
252
|
+
# YAML uses different structure association logic
|
|
253
|
+
return find_yaml_associated_structure(node)
|
|
254
|
+
|
|
119
255
|
if node.type == CommentCategory.docstring:
|
|
120
256
|
# Only for python's docstring
|
|
121
|
-
return find_enclosing_scope(node)
|
|
257
|
+
return find_enclosing_scope(node, comment_type)
|
|
122
258
|
# General comments regardless of comment types
|
|
123
|
-
associated_scope = find_next_scope(node)
|
|
259
|
+
associated_scope = find_next_scope(node, comment_type)
|
|
124
260
|
if not associated_scope:
|
|
125
|
-
associated_scope = find_enclosing_scope(node)
|
|
261
|
+
associated_scope = find_enclosing_scope(node, comment_type)
|
|
126
262
|
return associated_scope
|
|
127
263
|
|
|
128
264
|
|
|
@@ -219,6 +355,7 @@ class ExtractedRstType(TypedDict):
|
|
|
219
355
|
end_idx: int
|
|
220
356
|
|
|
221
357
|
|
|
358
|
+
# @Extract reStructuredText blocks embedded in comments, IMPL_RST_1, impl, [FE_RST_EXTRACTION]
|
|
222
359
|
def extract_rst(
|
|
223
360
|
text: str, start_marker: str, end_marker: str
|
|
224
361
|
) -> ExtractedRstType | None:
|
|
@@ -56,7 +56,7 @@ OptQuiet: TypeAlias = Annotated[ # noqa: UP040 # has to be TypeAlias
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@app.command(no_args_is_help=True)
|
|
59
|
-
def analyse(
|
|
59
|
+
def analyse( # noqa: PLR0912 # for CLI, so it needs the branches
|
|
60
60
|
config: Annotated[
|
|
61
61
|
Path,
|
|
62
62
|
typer.Argument(
|
|
@@ -90,6 +90,7 @@ def analyse(
|
|
|
90
90
|
] = None,
|
|
91
91
|
) -> None:
|
|
92
92
|
"""Analyse marked content in source code."""
|
|
93
|
+
# @CLI command to analyse source code and extract traceability markers, IMPL_CLI_ANALYZE, impl, [FE_CLI_ANALYZE]
|
|
93
94
|
|
|
94
95
|
data: CodeLinksConfigType = load_config_from_toml(config)
|
|
95
96
|
|
|
@@ -140,6 +141,12 @@ def analyse(
|
|
|
140
141
|
analyse_config.src_files = src_discover.source_paths
|
|
141
142
|
analyse_config.src_dir = Path(src_discover.src_discover_config.src_dir)
|
|
142
143
|
|
|
144
|
+
# git_root shall be relative to the config file's location (like src_dir)
|
|
145
|
+
if analyse_config.git_root is not None:
|
|
146
|
+
analyse_config.git_root = (
|
|
147
|
+
config.parent / analyse_config.git_root
|
|
148
|
+
).resolve()
|
|
149
|
+
|
|
143
150
|
analyse_errors = analyse_config.check_fields_configuration()
|
|
144
151
|
errors.extend(analyse_errors)
|
|
145
152
|
if errors:
|
|
@@ -150,6 +157,15 @@ def analyse(
|
|
|
150
157
|
codelinks_config.projects = specifed_project_configs
|
|
151
158
|
analyse_projects = AnalyseProjects(codelinks_config)
|
|
152
159
|
analyse_projects.run()
|
|
160
|
+
|
|
161
|
+
# Output warnings to console for CLI users
|
|
162
|
+
for src_analyse in analyse_projects.projects_analyse.values():
|
|
163
|
+
for warning in src_analyse.oneline_warnings:
|
|
164
|
+
logger.warning(
|
|
165
|
+
f"Oneline parser warning in {warning.file_path}:{warning.lineno} "
|
|
166
|
+
f"- {warning.sub_type}: {warning.msg}",
|
|
167
|
+
)
|
|
168
|
+
|
|
153
169
|
analyse_projects.dump_markers()
|
|
154
170
|
|
|
155
171
|
|
|
@@ -183,6 +199,7 @@ def discover(
|
|
|
183
199
|
help="Glob patterns to be included.",
|
|
184
200
|
),
|
|
185
201
|
] = [], # noqa: B006 # to show the default value on CLI
|
|
202
|
+
# @CLI command to discover source files recursively with gitignore support, IMPL_CLI_DISCOVER, impl, [FE_CLI_DISCOVER]
|
|
186
203
|
gitignore: Annotated[
|
|
187
204
|
bool,
|
|
188
205
|
typer.Option(
|
|
@@ -234,6 +251,7 @@ def write_rst( # noqa: PLR0913 # for CLI, so it takes as many as it requires
|
|
|
234
251
|
resolve_path=True,
|
|
235
252
|
),
|
|
236
253
|
],
|
|
254
|
+
# @CLI command to generate needextend RST file from extracted markers, IMPL_CLI_WRITE, impl, [FE_CLI_WRITE]
|
|
237
255
|
outpath: Annotated[
|
|
238
256
|
Path,
|
|
239
257
|
typer.Option(
|
|
@@ -18,9 +18,14 @@ from sphinx_codelinks.source_discover.source_discover import SourceDiscover
|
|
|
18
18
|
UNIX_NEWLINE = "\n"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
COMMENT_MARKERS = {
|
|
21
|
+
COMMENT_MARKERS = {
|
|
22
|
+
# @Support C and C++ style comments, IMPL_C_1, impl, [FE_C_SUPPORT, FE_CPP]
|
|
23
|
+
CommentType.cpp: ["//", "/*"],
|
|
24
|
+
# @Support Python style comments, IMPL_PY_1, impl, [FE_PY]
|
|
25
|
+
CommentType.python: ["#"],
|
|
26
|
+
CommentType.cs: ["//", "/*", "///"],
|
|
27
|
+
}
|
|
22
28
|
ESCAPE = "\\"
|
|
23
|
-
SUPPORTED_COMMENT_TYPES = {"c", "h", "cpp", "hpp", "py"}
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class CommentCategory(str, Enum):
|
|
@@ -47,7 +52,7 @@ class NeedIdRefsConfig:
|
|
|
47
52
|
@classmethod
|
|
48
53
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
49
54
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
50
|
-
if _field.metadata
|
|
55
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
51
56
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
52
57
|
return None
|
|
53
58
|
|
|
@@ -86,7 +91,7 @@ class MarkedRstConfig:
|
|
|
86
91
|
@classmethod
|
|
87
92
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
88
93
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
89
|
-
if _field.metadata
|
|
94
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
90
95
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
91
96
|
return None
|
|
92
97
|
|
|
@@ -219,14 +224,14 @@ class OneLineCommentStyle:
|
|
|
219
224
|
@classmethod
|
|
220
225
|
def get_required_fields(cls, name: str) -> list[str] | None:
|
|
221
226
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
222
|
-
if _field.metadata
|
|
227
|
+
if _field.metadata:
|
|
223
228
|
return cast(list[str], _field.metadata["required_fields"])
|
|
224
229
|
return None
|
|
225
230
|
|
|
226
231
|
@classmethod
|
|
227
232
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
228
233
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
229
|
-
if _field.metadata
|
|
234
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
230
235
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
231
236
|
return None
|
|
232
237
|
|
|
@@ -300,6 +305,7 @@ class AnalyseSectionConfigType(TypedDict, total=False):
|
|
|
300
305
|
get_oneline_needs: bool
|
|
301
306
|
get_rst: bool
|
|
302
307
|
outdir: str
|
|
308
|
+
git_root: str
|
|
303
309
|
need_id_refs: NeedIdRefsConfigType
|
|
304
310
|
marked_rst: MarkedRstConfigType
|
|
305
311
|
oneline_comment_style: OneLineCommentStyleType
|
|
@@ -314,6 +320,7 @@ class SourceAnalyseConfigType(TypedDict, total=False):
|
|
|
314
320
|
get_need_id_refs: bool
|
|
315
321
|
get_oneline_needs: bool
|
|
316
322
|
get_rst: bool
|
|
323
|
+
git_root: Path | None
|
|
317
324
|
need_id_refs_config: NeedIdRefsConfig
|
|
318
325
|
marked_rst_config: MarkedRstConfig
|
|
319
326
|
oneline_comment_style: OneLineCommentStyle
|
|
@@ -356,6 +363,12 @@ class SourceAnalyseConfig:
|
|
|
356
363
|
get_rst: bool = field(default=False, metadata={"schema": {"type": "boolean"}})
|
|
357
364
|
"""Whether to extract rst texts from comments"""
|
|
358
365
|
|
|
366
|
+
git_root: Path | None = field(
|
|
367
|
+
default=None, metadata={"schema": {"type": ["string", "null"]}}
|
|
368
|
+
)
|
|
369
|
+
"""Explicit path to the Git repository root. If not set, it will be auto-detected
|
|
370
|
+
by traversing parent directories. Useful for Bazel builds or deeply nested configs."""
|
|
371
|
+
|
|
359
372
|
need_id_refs_config: NeedIdRefsConfig = field(default_factory=NeedIdRefsConfig)
|
|
360
373
|
"""Configuration for extracting need id references from comments."""
|
|
361
374
|
|
|
@@ -370,7 +383,7 @@ class SourceAnalyseConfig:
|
|
|
370
383
|
@classmethod
|
|
371
384
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
372
385
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
373
|
-
if _field.metadata
|
|
386
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
374
387
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
375
388
|
return None
|
|
376
389
|
|
|
@@ -566,7 +579,7 @@ class CodeLinksConfig:
|
|
|
566
579
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
567
580
|
"""Get the schema for a config item."""
|
|
568
581
|
_field = next(field for field in fields(cls) if field.name is name)
|
|
569
|
-
if _field.metadata
|
|
582
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
570
583
|
return _field.metadata["schema"] # type: ignore[no-any-return]
|
|
571
584
|
return None
|
|
572
585
|
|
|
@@ -760,9 +773,11 @@ def convert_analyse_config(
|
|
|
760
773
|
if config_dict:
|
|
761
774
|
for k, v in config_dict.items():
|
|
762
775
|
if k not in {"online_comment_style", "need_id_refs", "marked_rst"}:
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
776
|
+
# Convert string paths to Path objects
|
|
777
|
+
if k in {"src_dir", "git_root"} and isinstance(v, str):
|
|
778
|
+
analyse_config_dict[k] = Path(v) # type: ignore[literal-required]
|
|
779
|
+
else:
|
|
780
|
+
analyse_config_dict[k] = v # type: ignore[literal-required] # dynamical assignment
|
|
766
781
|
|
|
767
782
|
# Get oneline_comment_style configuration
|
|
768
783
|
oneline_comment_style_dict: OneLineCommentStyleType | None = config_dict.get(
|
|
@@ -791,6 +806,15 @@ def convert_analyse_config(
|
|
|
791
806
|
if src_discover:
|
|
792
807
|
analyse_config_dict["src_files"] = src_discover.source_paths
|
|
793
808
|
analyse_config_dict["src_dir"] = src_discover.src_discover_config.src_dir
|
|
809
|
+
try:
|
|
810
|
+
analyse_config_dict["comment_type"] = CommentType(
|
|
811
|
+
src_discover.src_discover_config.comment_type
|
|
812
|
+
)
|
|
813
|
+
except ValueError:
|
|
814
|
+
# If invalid comment_type, keep the string value
|
|
815
|
+
# Validation will catch this error later
|
|
816
|
+
comment_type_str: str = src_discover.src_discover_config.comment_type
|
|
817
|
+
analyse_config_dict["comment_type"] = comment_type_str # type: ignore[typeddict-item]
|
|
794
818
|
|
|
795
819
|
return SourceAnalyseConfig(**analyse_config_dict)
|
|
796
820
|
|
|
@@ -853,4 +877,13 @@ def generate_project_configs(
|
|
|
853
877
|
)
|
|
854
878
|
analyse_config = convert_analyse_config(analyse_section_config)
|
|
855
879
|
analyse_config.get_oneline_needs = True # force to get oneline_need
|
|
880
|
+
# Copy comment_type from source_discover_config to analyse_config
|
|
881
|
+
try:
|
|
882
|
+
analyse_config.comment_type = CommentType(
|
|
883
|
+
source_discover_config.comment_type
|
|
884
|
+
)
|
|
885
|
+
except ValueError:
|
|
886
|
+
# If invalid comment_type, keep the string value
|
|
887
|
+
# Validation will catch this error later
|
|
888
|
+
analyse_config.comment_type = source_discover_config.comment_type # type: ignore[assignment]
|
|
856
889
|
project_config["analyse_config"] = analyse_config
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Convert the generated JSON file created by CodeLinks analyse to need-extend in RST."""
|
|
2
2
|
|
|
3
3
|
from collections import deque
|
|
4
|
-
from dataclasses import
|
|
4
|
+
from dataclasses import dataclass, field, fields
|
|
5
5
|
from os import linesep
|
|
6
6
|
from string import Template
|
|
7
7
|
from typing import Any, TypedDict, cast
|
|
@@ -105,7 +105,7 @@ class MarkedContentSchema:
|
|
|
105
105
|
@classmethod
|
|
106
106
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
107
107
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
108
|
-
if _field.metadata
|
|
108
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
109
109
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
110
110
|
return None
|
|
111
111
|
|
|
@@ -165,7 +165,7 @@ def convert_marked_content(
|
|
|
165
165
|
remote_url_field: str = "remote-url",
|
|
166
166
|
title: str | None = None,
|
|
167
167
|
) -> tuple[list[str], list[str]]:
|
|
168
|
-
"""Convert marked objects extracted by
|
|
168
|
+
"""Convert marked objects extracted by analyse CLI to needextend in RST"""
|
|
169
169
|
errors = []
|
|
170
170
|
needextend_texts: list[str] = []
|
|
171
171
|
intersted_objs = [
|
{sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/source_discover/config.py
RENAMED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
from dataclasses import
|
|
1
|
+
from dataclasses import dataclass, field, fields
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any, Required, TypedDict, cast
|
|
5
5
|
|
|
6
6
|
from jsonschema import ValidationError, validate
|
|
7
7
|
|
|
8
|
-
COMMENT_FILETYPE = {
|
|
8
|
+
COMMENT_FILETYPE = {
|
|
9
|
+
"cpp": ["c", "ci", "cpp", "cc", "cxx", "h", "hpp", "hxx", "hh", "ihl"],
|
|
10
|
+
"python": ["py"],
|
|
11
|
+
"cs": ["cs"],
|
|
12
|
+
"yaml": ["yml", "yaml"],
|
|
13
|
+
"rust": ["rs"],
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
|
|
11
17
|
class CommentType(str, Enum):
|
|
12
18
|
python = "python"
|
|
13
19
|
cpp = "cpp"
|
|
20
|
+
cs = "cs"
|
|
21
|
+
yaml = "yaml"
|
|
22
|
+
# @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST];
|
|
23
|
+
rust = "rust"
|
|
14
24
|
|
|
15
25
|
|
|
16
26
|
class SourceDiscoverSectionConfigType(TypedDict, total=False):
|
|
@@ -73,7 +83,7 @@ class SourceDiscoverConfig:
|
|
|
73
83
|
@classmethod
|
|
74
84
|
def get_schema(cls, name: str) -> dict[str, Any] | None: # type: ignore[explicit-any]
|
|
75
85
|
_field = next(_field for _field in fields(cls) if _field.name is name)
|
|
76
|
-
if _field.metadata
|
|
86
|
+
if _field.metadata and "schema" in _field.metadata:
|
|
77
87
|
return cast(dict[str, Any], _field.metadata["schema"]) # type: ignore[explicit-any]
|
|
78
88
|
return None
|
|
79
89
|
|
|
@@ -13,6 +13,7 @@ from sphinx_codelinks.source_discover.config import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
# @Source code file discovery with gitignore support, IMPL_DISC_1, impl, [FE_DISCOVERY, FE_CLI_DISCOVER]
|
|
16
17
|
class SourceDiscover:
|
|
17
18
|
def __init__(self, src_discover_config: SourceDiscoverConfig):
|
|
18
19
|
self.src_discover_config = src_discover_config
|
|
@@ -33,6 +33,15 @@ else:
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def get_rel_path(doc_path: Path, code_path: Path, base_dir: Path) -> tuple[Path, Path]:
|
|
37
|
+
"""Get the relative path from the document to the source code file and vice versa."""
|
|
38
|
+
doc_depth = len(doc_path.parents) - 1
|
|
39
|
+
src_rel_path = Path(*[".."] * doc_depth) / code_path.relative_to(base_dir)
|
|
40
|
+
code_depth = len(code_path.relative_to(base_dir).parents) - 1
|
|
41
|
+
doc_rel_path = Path(*[".."] * code_depth) / doc_path
|
|
42
|
+
return src_rel_path, doc_rel_path.with_suffix(".html")
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
def generate_str_link_name(
|
|
37
46
|
oneline_need: OneLineNeed,
|
|
38
47
|
target_filepath: Path,
|
|
@@ -67,13 +76,12 @@ class SourceTracing(nodes.General, nodes.Element):
|
|
|
67
76
|
|
|
68
77
|
|
|
69
78
|
class SourceTracingDirective(SphinxDirective):
|
|
70
|
-
required_arguments =
|
|
79
|
+
required_arguments = 0
|
|
71
80
|
optional_arguments = 0
|
|
72
81
|
final_argument_whitespace = True
|
|
73
82
|
# this enables content in the directive
|
|
74
|
-
has_content =
|
|
83
|
+
has_content = False
|
|
75
84
|
option_spec: ClassVar[dict[str, Callable[[str], str]] | None] = {
|
|
76
|
-
"id": directives.unchanged_required,
|
|
77
85
|
"project": directives.unchanged_required,
|
|
78
86
|
"file": directives.unchanged_required,
|
|
79
87
|
"directory": directives.unchanged_required,
|
|
@@ -84,7 +92,6 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
84
92
|
validate_option(self.options)
|
|
85
93
|
|
|
86
94
|
project = self.options["project"]
|
|
87
|
-
title = self.arguments[0]
|
|
88
95
|
# get source tracing config
|
|
89
96
|
src_trace_sphinx_config = CodeLinksConfig.from_sphinx(self.env.config)
|
|
90
97
|
|
|
@@ -99,7 +106,6 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
99
106
|
# the directory where the source files are copied to
|
|
100
107
|
target_dir = out_dir / src_dir.name
|
|
101
108
|
|
|
102
|
-
extra_options = {"project": project}
|
|
103
109
|
source_files = self.get_src_files(self.options, src_dir, src_discover_config)
|
|
104
110
|
|
|
105
111
|
# add source files into the dependency
|
|
@@ -110,23 +116,16 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
110
116
|
analyse_config = src_trace_conf["analyse_config"]
|
|
111
117
|
analyse_config.src_dir = src_dir
|
|
112
118
|
analyse_config.src_files = source_files
|
|
119
|
+
# git_root shall be relative to the config file's location (if provided)
|
|
120
|
+
if analyse_config.git_root:
|
|
121
|
+
conf_dir = Path(self.env.app.confdir)
|
|
122
|
+
if src_trace_sphinx_config.config_from_toml:
|
|
123
|
+
src_trace_toml_path = Path(src_trace_sphinx_config.config_from_toml)
|
|
124
|
+
conf_dir = conf_dir / src_trace_toml_path.parent
|
|
125
|
+
analyse_config.git_root = (conf_dir / analyse_config.git_root).resolve()
|
|
113
126
|
src_analyse = SourceAnalyse(analyse_config)
|
|
114
127
|
src_analyse.run()
|
|
115
128
|
|
|
116
|
-
needs = []
|
|
117
|
-
|
|
118
|
-
# create the need for src-trace directive
|
|
119
|
-
src_trace_need = add_need(
|
|
120
|
-
app=self.env.app, # The Sphinx application object
|
|
121
|
-
state=self.state, # The docutils state object
|
|
122
|
-
docname=self.env.docname, # The current document name
|
|
123
|
-
lineno=self.lineno, # The line number where the directive is used
|
|
124
|
-
need_type="srctrace", # The type of the need
|
|
125
|
-
title=title, # The title of the need
|
|
126
|
-
**extra_options,
|
|
127
|
-
)
|
|
128
|
-
needs.extend(src_trace_need)
|
|
129
|
-
|
|
130
129
|
dirs = {
|
|
131
130
|
"src_dir": src_dir,
|
|
132
131
|
"out_dir": out_dir,
|
|
@@ -145,7 +144,7 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
145
144
|
to_remove_str = to_remove_str.replace("\\", "\\\\")
|
|
146
145
|
self.env.config.needs_string_links[local_url_field] = {
|
|
147
146
|
"regex": r"^(?P<value>.+?)\.[^\.]+#L(?P<lineno>\d+)",
|
|
148
|
-
"link_url": ("
|
|
147
|
+
"link_url": ("{{value}}.html#L-{{lineno}}"),
|
|
149
148
|
"link_name": f"{{{{value | replace('{to_remove_str}', '')}}}}#L{{{{lineno}}}}",
|
|
150
149
|
"options": [local_url_field],
|
|
151
150
|
}
|
|
@@ -180,18 +179,16 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
180
179
|
remote_url_field,
|
|
181
180
|
dirs,
|
|
182
181
|
)
|
|
183
|
-
if rendered_needs:
|
|
184
|
-
needs.extend(rendered_needs)
|
|
185
182
|
|
|
186
183
|
# for post-processing of need links
|
|
187
184
|
# https://github.com/useblocks/sphinx-needs/issues/1210
|
|
188
185
|
add_doc(self.env, self.env.docname)
|
|
189
186
|
|
|
190
|
-
return
|
|
187
|
+
return rendered_needs
|
|
191
188
|
|
|
192
189
|
def get_src_files(
|
|
193
190
|
self,
|
|
194
|
-
|
|
191
|
+
additional_options: dict[str, str],
|
|
195
192
|
src_dir: Path,
|
|
196
193
|
src_discover_config: SourceDiscoverConfig,
|
|
197
194
|
) -> list[Path]:
|
|
@@ -201,14 +198,14 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
201
198
|
file: str = self.options["file"]
|
|
202
199
|
filepath = src_dir / file
|
|
203
200
|
source_files.append(filepath.resolve())
|
|
204
|
-
|
|
201
|
+
additional_options["file"] = file
|
|
205
202
|
else:
|
|
206
203
|
directory = self.options.get("directory")
|
|
207
204
|
if directory is None:
|
|
208
205
|
# when neither "file" and "directory" are given, the project root dir is by default
|
|
209
206
|
directory = "./"
|
|
210
207
|
else:
|
|
211
|
-
|
|
208
|
+
additional_options["directory"] = directory
|
|
212
209
|
dir_path = src_dir / directory
|
|
213
210
|
# create a new config for the specified directory
|
|
214
211
|
src_discover = SourceDiscoverConfig(
|
|
@@ -259,7 +256,7 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
259
256
|
# mapping between lineno and need link in docs for local url
|
|
260
257
|
|
|
261
258
|
# The link to the documentation page for the source file
|
|
262
|
-
|
|
259
|
+
|
|
263
260
|
if local_url_field:
|
|
264
261
|
# copy files to _build/html
|
|
265
262
|
target_filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -268,8 +265,15 @@ class SourceTracingDirective(SphinxDirective):
|
|
|
268
265
|
remote_link_name = None
|
|
269
266
|
if local_url_field:
|
|
270
267
|
# generate link name
|
|
268
|
+
# calculate the relative path from the current doc to the target file
|
|
269
|
+
local_rel_path, docs_href = get_rel_path(
|
|
270
|
+
Path(self.env.docname), target_filepath, dirs["out_dir"]
|
|
271
|
+
)
|
|
271
272
|
local_link_name = generate_str_link_name(
|
|
272
|
-
oneline_need,
|
|
273
|
+
oneline_need,
|
|
274
|
+
local_rel_path,
|
|
275
|
+
dirs,
|
|
276
|
+
local=True,
|
|
273
277
|
)
|
|
274
278
|
if remote_url_field:
|
|
275
279
|
remote_link_name = generate_str_link_name(
|
|
@@ -35,7 +35,34 @@ from sphinx_codelinks.sphinx_extension.html_wrapper import html_wrapper
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _check_sphinx_needs_dependency(app: Sphinx) -> bool:
|
|
39
|
+
"""Check if sphinx-needs is actually loaded as an extension."""
|
|
40
|
+
# Check if sphinx-needs is in the loaded extensions
|
|
41
|
+
if "sphinx_needs" not in app.extensions:
|
|
42
|
+
error_msg = (
|
|
43
|
+
"sphinx-codelinks requires sphinx-needs to be loaded as an extension.\n"
|
|
44
|
+
"Please ensure 'sphinx_needs' is properly installed and added to your extensions list in conf.py:\n"
|
|
45
|
+
" extensions = ['sphinx_needs', 'sphinx_codelinks', ...]\n"
|
|
46
|
+
f"Currently loaded extensions: {list(app.extensions.keys())}\n"
|
|
47
|
+
f"Configured extensions: {app.config.extensions}"
|
|
48
|
+
)
|
|
49
|
+
logger.error(error_msg)
|
|
50
|
+
return False
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
38
54
|
def setup(app: Sphinx) -> dict[str, Any]: # type: ignore[explicit-any]
|
|
55
|
+
# Check if sphinx-needs is available and properly configured
|
|
56
|
+
if not _check_sphinx_needs_dependency(app):
|
|
57
|
+
logger.error(
|
|
58
|
+
"Failed to initialize sphinx-codelinks due to missing sphinx-needs dependency"
|
|
59
|
+
)
|
|
60
|
+
return {
|
|
61
|
+
"version": "builtin",
|
|
62
|
+
"parallel_read_safe": True,
|
|
63
|
+
"parallel_write_safe": True,
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
app.add_node(SourceTracing)
|
|
40
67
|
app.add_directive("src-trace", SourceTracingDirective)
|
|
41
68
|
CodeLinksConfig.add_config_values(app)
|
|
@@ -69,7 +96,7 @@ def add_custom_css( # type: ignore[explicit-any]
|
|
|
69
96
|
app: Sphinx,
|
|
70
97
|
pagename: str,
|
|
71
98
|
templatename: str,
|
|
72
|
-
|
|
99
|
+
_context: dict[str, Any],
|
|
73
100
|
_doctree: Any,
|
|
74
101
|
) -> None:
|
|
75
102
|
target_htmls = {
|
|
@@ -78,11 +105,7 @@ def add_custom_css( # type: ignore[explicit-any]
|
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
if pagename in target_htmls and templatename == "page.html":
|
|
81
|
-
|
|
82
|
-
context["css_files"] = []
|
|
83
|
-
context["css_files"].append(
|
|
84
|
-
"_static/source_tracing/ub_sct.css"
|
|
85
|
-
) # Add the custom CSS file to the context
|
|
108
|
+
app.add_css_file("_static/source_tracing/ub_sct.css")
|
|
86
109
|
|
|
87
110
|
|
|
88
111
|
def generate_code_page(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/debug.py
RENAMED
|
File without changes
|
|
File without changes
|
{sphinx_codelinks-1.0.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/ub_sct.css
RENAMED
|
File without changes
|