sphinx-codelinks 1.1.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.
Files changed (21) hide show
  1. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/PKG-INFO +2 -1
  2. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/pyproject.toml +32 -1
  3. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/analyse.py +16 -9
  4. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/oneline_parser.py +2 -1
  5. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/utils.py +23 -0
  6. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/cmd.py +19 -1
  7. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/config.py +33 -3
  8. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/source_discover/config.py +3 -0
  9. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/source_discover/source_discover.py +1 -0
  10. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +7 -0
  11. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/LICENSE +0 -0
  12. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/README.md +0 -0
  13. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/__init__.py +0 -0
  14. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/models.py +0 -0
  15. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/analyse/projects.py +0 -0
  16. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/logger.py +0 -0
  17. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/needextend_write.py +0 -0
  18. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/debug.py +0 -0
  19. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +0 -0
  20. {sphinx_codelinks-1.1.0 → sphinx_codelinks-1.2.0}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +0 -0
  21. {sphinx_codelinks-1.1.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.1.0
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>
@@ -23,6 +23,7 @@ Requires-Dist: tree-sitter-python>=0.23.6
23
23
  Requires-Dist: tree-sitter~=0.25.1
24
24
  Requires-Dist: tree-sitter-c-sharp>=0.23.1
25
25
  Requires-Dist: tree-sitter-yaml>=0.7.1
26
+ Requires-Dist: tree-sitter-rust>=0.23.0
26
27
 
27
28
  # Sphinx CodeLinks
28
29
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-codelinks"
3
- version = "1.1.0"
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 = [
@@ -29,12 +29,43 @@ dependencies = [
29
29
  "tree-sitter~=0.25.1",
30
30
  "tree-sitter-c-sharp>=0.23.1",
31
31
  "tree-sitter-yaml>=0.7.1",
32
+ "tree-sitter-rust>=0.23.0",
32
33
  ]
33
34
 
34
35
  [build-system]
35
36
  requires = ["flit_core >=3.4,<4"]
36
37
  build-backend = "flit_core.buildapi"
37
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
+
38
69
  [tool.rye]
39
70
  managed = true
40
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
- self.git_root: Path | None = utils.locate_git_root(self.analyse_config.src_dir)
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
- self.oneline_warnings.append(
201
- AnalyseWarning(
202
- str(src_comment.source_file.filepath),
203
- src_comment.node.start_point.row + row_offset + 1,
204
- resolved.msg,
205
- MarkedContentType.need,
206
- resolved.sub_type.value,
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,
@@ -31,6 +31,7 @@ class OnelineParserInvalidWarning:
31
31
  msg: str
32
32
 
33
33
 
34
+ # @One-line comment parser for traceability markers, IMPL_OLP_1, impl, [FE_DEF, FE_CMT]
34
35
  def oneline_parser( # noqa: PLR0912, PLR0911 # handel warnings
35
36
  oneline: str, oneline_config: OneLineCommentStyle
36
37
  ) -> dict[str, str | list[str] | int] | OnelineParserInvalidWarning | None:
@@ -53,7 +54,7 @@ def oneline_parser( # noqa: PLR0912, PLR0911 # handel warnings
53
54
 
54
55
  # extract the string wrapped by start and end
55
56
  start_idx = start_idx + len(oneline_config.start_sequence)
56
- string = oneline[start_idx:end_idx]
57
+ string = oneline[start_idx:end_idx].strip()
57
58
 
58
59
  # numbers of needs_fields which are required
59
60
  cnt_required_fields = oneline_config.get_cnt_required_fields()
@@ -14,10 +14,21 @@ from sphinx_codelinks.source_discover.config import CommentType
14
14
 
15
15
  # Language-specific node types for scope detection
16
16
  SCOPE_NODE_TYPES = {
17
+ # @Python Scope Node Types, IMPL_PY_2, impl, [FE_PY]
17
18
  CommentType.python: {"function_definition", "class_definition"},
19
+ # @C and C++ Scope Node Types, IMPL_C_2, impl, [FE_C_SUPPORT, FE_CPP]
18
20
  CommentType.cpp: {"function_definition", "class_definition"},
19
21
  CommentType.cs: {"method_declaration", "class_declaration", "property_declaration"},
20
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
+ },
21
32
  }
22
33
 
23
34
  # initialize logger
@@ -45,6 +56,10 @@ PYTHON_QUERY = """
45
56
  CPP_QUERY = """(comment) @comment"""
46
57
  C_SHARP_QUERY = """(comment) @comment"""
47
58
  YAML_QUERY = """(comment) @comment"""
59
+ RUST_QUERY = """
60
+ (line_comment) @comment
61
+ (block_comment) @comment
62
+ """
48
63
 
49
64
 
50
65
  def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
@@ -62,6 +77,7 @@ def is_text_file(filepath: Path, sample_size: int = 2048) -> bool:
62
77
  return False
63
78
 
64
79
 
80
+ # @Tree-sitter parser initialization for multiple languages, IMPL_LANG_1, impl, [FE_C_SUPPORT, FE_CPP, FE_PY, FE_YAML, FE_RUST]
65
81
  def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
66
82
  if comment_type == CommentType.cpp:
67
83
  import tree_sitter_cpp # noqa: PLC0415
@@ -83,6 +99,11 @@ def init_tree_sitter(comment_type: CommentType) -> tuple[Parser, Query]:
83
99
 
84
100
  parsed_language = Language(tree_sitter_yaml.language())
85
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)
86
107
  else:
87
108
  raise ValueError(f"Unsupported comment style: {comment_type}")
88
109
  parser = Parser(parsed_language)
@@ -98,6 +119,7 @@ def wrap_read_callable_point(
98
119
  return read_callable_byte_offset
99
120
 
100
121
 
122
+ # @Comment extraction from source code using tree-sitter, IMPL_EXTR_1, impl, [FE_DEF]
101
123
  def extract_comments(
102
124
  src_string: ByteString, parser: Parser, query: Query
103
125
  ) -> list[TreeSitterNode] | None:
@@ -333,6 +355,7 @@ class ExtractedRstType(TypedDict):
333
355
  end_idx: int
334
356
 
335
357
 
358
+ # @Extract reStructuredText blocks embedded in comments, IMPL_RST_1, impl, [FE_RST_EXTRACTION]
336
359
  def extract_rst(
337
360
  text: str, start_marker: str, end_marker: str
338
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(
@@ -19,7 +19,9 @@ UNIX_NEWLINE = "\n"
19
19
 
20
20
 
21
21
  COMMENT_MARKERS = {
22
+ # @Support C and C++ style comments, IMPL_C_1, impl, [FE_C_SUPPORT, FE_CPP]
22
23
  CommentType.cpp: ["//", "/*"],
24
+ # @Support Python style comments, IMPL_PY_1, impl, [FE_PY]
23
25
  CommentType.python: ["#"],
24
26
  CommentType.cs: ["//", "/*", "///"],
25
27
  }
@@ -303,6 +305,7 @@ class AnalyseSectionConfigType(TypedDict, total=False):
303
305
  get_oneline_needs: bool
304
306
  get_rst: bool
305
307
  outdir: str
308
+ git_root: str
306
309
  need_id_refs: NeedIdRefsConfigType
307
310
  marked_rst: MarkedRstConfigType
308
311
  oneline_comment_style: OneLineCommentStyleType
@@ -317,6 +320,7 @@ class SourceAnalyseConfigType(TypedDict, total=False):
317
320
  get_need_id_refs: bool
318
321
  get_oneline_needs: bool
319
322
  get_rst: bool
323
+ git_root: Path | None
320
324
  need_id_refs_config: NeedIdRefsConfig
321
325
  marked_rst_config: MarkedRstConfig
322
326
  oneline_comment_style: OneLineCommentStyle
@@ -359,6 +363,12 @@ class SourceAnalyseConfig:
359
363
  get_rst: bool = field(default=False, metadata={"schema": {"type": "boolean"}})
360
364
  """Whether to extract rst texts from comments"""
361
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
+
362
372
  need_id_refs_config: NeedIdRefsConfig = field(default_factory=NeedIdRefsConfig)
363
373
  """Configuration for extracting need id references from comments."""
364
374
 
@@ -763,9 +773,11 @@ def convert_analyse_config(
763
773
  if config_dict:
764
774
  for k, v in config_dict.items():
765
775
  if k not in {"online_comment_style", "need_id_refs", "marked_rst"}:
766
- analyse_config_dict[k] = ( # type: ignore[literal-required] # dynamical assignment
767
- Path(v) if k == "src_dic" and isinstance(v, str) else v
768
- )
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
769
781
 
770
782
  # Get oneline_comment_style configuration
771
783
  oneline_comment_style_dict: OneLineCommentStyleType | None = config_dict.get(
@@ -794,6 +806,15 @@ def convert_analyse_config(
794
806
  if src_discover:
795
807
  analyse_config_dict["src_files"] = src_discover.source_paths
796
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]
797
818
 
798
819
  return SourceAnalyseConfig(**analyse_config_dict)
799
820
 
@@ -856,4 +877,13 @@ def generate_project_configs(
856
877
  )
857
878
  analyse_config = convert_analyse_config(analyse_section_config)
858
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]
859
889
  project_config["analyse_config"] = analyse_config
@@ -10,6 +10,7 @@ COMMENT_FILETYPE = {
10
10
  "python": ["py"],
11
11
  "cs": ["cs"],
12
12
  "yaml": ["yml", "yaml"],
13
+ "rust": ["rs"],
13
14
  }
14
15
 
15
16
 
@@ -18,6 +19,8 @@ class CommentType(str, Enum):
18
19
  cpp = "cpp"
19
20
  cs = "cs"
20
21
  yaml = "yaml"
22
+ # @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST];
23
+ rust = "rust"
21
24
 
22
25
 
23
26
  class SourceDiscoverSectionConfigType(TypedDict, total=False):
@@ -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
@@ -116,6 +116,13 @@ class SourceTracingDirective(SphinxDirective):
116
116
  analyse_config = src_trace_conf["analyse_config"]
117
117
  analyse_config.src_dir = src_dir
118
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()
119
126
  src_analyse = SourceAnalyse(analyse_config)
120
127
  src_analyse.run()
121
128