sphinx-codelinks 2.0.0__tar.gz → 2.0.1__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 (36) hide show
  1. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/PKG-INFO +4 -3
  2. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/pyproject.toml +8 -6
  3. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/__init__.py +1 -1
  4. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/analyse.py +7 -2
  5. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/config.py +12 -7
  6. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/needextend_write.py +4 -3
  7. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/source_discover/config.py +4 -1
  8. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/debug.py +45 -7
  9. sphinx_codelinks-2.0.1/src/sphinx_codelinks/sphinx_extension/directives/__init__.py +0 -0
  10. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/directives/src_trace.py +4 -6
  11. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/html_wrapper.py +10 -2
  12. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/need_id_refs.py +13 -3
  13. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/source_tracing.py +2 -2
  14. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/LICENSE +0 -0
  15. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/README.md +0 -0
  16. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/__init__.py +0 -0
  17. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/models.py +0 -0
  18. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/multiline_parser.py +0 -0
  19. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/oneline_parser.py +0 -0
  20. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/preproc/__init__.py +0 -0
  21. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/preproc/compile_db.py +0 -0
  22. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/preproc/libclang_parser.py +0 -0
  23. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/preproc/loader.py +0 -0
  24. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/projects.py +0 -0
  25. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/references.py +0 -0
  26. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/analyse/utils.py +0 -0
  27. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/cmd.py +0 -0
  28. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/logger.py +0 -0
  29. /sphinx_codelinks-2.0.0/src/sphinx_codelinks/source_discover/__init__.py → /sphinx_codelinks-2.0.1/src/sphinx_codelinks/py.typed +0 -0
  30. {sphinx_codelinks-2.0.0/src/sphinx_codelinks/sphinx_extension → sphinx_codelinks-2.0.1/src/sphinx_codelinks/source_discover}/__init__.py +0 -0
  31. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/source_discover/source_discover.py +0 -0
  32. {sphinx_codelinks-2.0.0/src/sphinx_codelinks/sphinx_extension/directives → sphinx_codelinks-2.0.1/src/sphinx_codelinks/sphinx_extension}/__init__.py +0 -0
  33. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/project_analysis.py +0 -0
  34. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/rediscovery.py +0 -0
  35. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/string_links.py +0 -0
  36. {sphinx_codelinks-2.0.0 → sphinx_codelinks-2.0.1}/src/sphinx_codelinks/sphinx_extension/ub_sct.css +0 -0
@@ -1,18 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-codelinks
3
- Version: 2.0.0
3
+ Version: 2.0.1
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>
7
7
  Requires-Python: >=3.11,<4
8
8
  Description-Content-Type: text/markdown
9
+ Classifier: Typing :: Typed
9
10
  License-File: LICENSE
10
11
  Requires-Dist: comment-parser>=1.2.4
11
12
  Requires-Dist: ignore-python>=0.3.3
12
13
  Requires-Dist: typer>=0.16.0
13
14
  Requires-Dist: jsonschema
14
15
  Requires-Dist: sphinx>=7.4,<10
15
- Requires-Dist: sphinx-needs>=9.0.0,<10
16
+ Requires-Dist: sphinx-needs>=9.1.0,<10
16
17
  Requires-Dist: ub-project>=1.1.0,<2
17
18
  Requires-Dist: jinja2
18
19
  Requires-Dist: pygments
@@ -28,7 +29,7 @@ Requires-Dist: tree-sitter-rust>=0.23.0
28
29
  Requires-Dist: tree-sitter-go>=0.23.0
29
30
  Requires-Dist: tree-sitter-json>=0.24.8
30
31
  Requires-Dist: tree-sitter-bash>=0.25.1
31
- Requires-Dist: furo>=2024.5.6 ; extra == "docs"
32
+ Requires-Dist: furo>=2025.12.19 ; extra == "docs"
32
33
  Requires-Dist: myst-parser>=4.0.0 ; extra == "docs"
33
34
  Requires-Dist: sphinx-ai-index>=1.0.0,<2 ; extra == "docs"
34
35
  Requires-Dist: sphinx-design>=0.6.1 ; extra == "docs"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sphinx-codelinks"
3
- version = "2.0.0"
3
+ version = "2.0.1"
4
4
  description = "Fast Source Code Traceability for Sphinx-Needs"
5
5
  authors = [{ name = "team useblocks", email = "info@useblocks.com" }]
6
6
  maintainers = [
@@ -15,6 +15,7 @@ readme = "README.md"
15
15
  # locks, builds and publishes -- the gap shows up only as an install failure for a user on
16
16
  # an interpreter inside it.
17
17
  requires-python = ">=3.11,<4"
18
+ classifiers = ["Typing :: Typed"]
18
19
  dependencies = [
19
20
  "comment-parser>=1.2.4",
20
21
  "ignore-python>=0.3.3",
@@ -27,7 +28,7 @@ dependencies = [
27
28
  # edge. uv never validates a specifier it resolves through `workspace = true` (uv#9811),
28
29
  # so the floor is the only statement of what was actually tested, and the cap is what
29
30
  # stops a future major being co-installed with a wheel written against this one.
30
- "sphinx-needs>=9.0.0,<10",
31
+ "sphinx-needs>=9.1.0,<10",
31
32
  "ub-project>=1.1.0,<2",
32
33
  "jinja2",
33
34
  "pygments",
@@ -64,7 +65,7 @@ libclang = ["libclang>=16"]
64
65
  # the root there re-exports each member's extra (`docs-mounts = ["sphinx-mounts[docs]"]`),
65
66
  # because `uv sync --extra` resolves against the workspace root rather than the member.
66
67
  docs = [
67
- "furo>=2024.5.6",
68
+ "furo>=2025.12.19",
68
69
  "myst-parser>=4.0.0",
69
70
  "sphinx-ai-index>=1.0.0,<2",
70
71
  "sphinx-design>=0.6.1",
@@ -84,9 +85,10 @@ Changelog = "https://github.com/useblocks/sphinx-needs/blob/master/packages/sphi
84
85
  requires = ["flit_core >=3.4,<4"]
85
86
  build-backend = "flit_core.buildapi"
86
87
 
87
- # Deliberately NO `[tool.flit.sdist]`: this distribution carries no non-Python payload, so
88
- # flit's module-only sdist is byte-for-byte the shape already published on PyPI, and
89
- # nothing downstream builds from it -- the git pins in the wild clone the repository.
88
+ # Deliberately NO `[tool.flit.sdist]`: this distribution carries nothing outside the module
89
+ # (its `py.typed` marker included, which flit ships as part of it), so flit's module-only
90
+ # sdist is the shape already published on PyPI, and nothing downstream builds from it --
91
+ # the git pins in the wild clone the repository.
90
92
  # Everything else -- ruff, ty, pytest and the dependency groups -- is the ROOT's, and
91
93
  # `tools/src/sn_tools/check_workspace.py` check (7) refuses those tables here: a member
92
94
  # `[tool.ruff]` does not extend the root's configuration, it replaces it.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from sphinx_codelinks.sphinx_extension.source_tracing import setup
4
4
 
5
- __version__ = "2.0.0"
5
+ __version__ = "2.0.1"
6
6
 
7
7
  __all__ = [
8
8
  "__version__",
@@ -363,9 +363,14 @@ class SourceAnalyse:
363
363
  # ``c`` is a LibclangComment duck-typing the tree-sitter Node
364
364
  # interface SourceComment reads (``.text`` / ``.start_point.row``);
365
365
  # the Node-only path (find_associated_scope) is guarded by
366
- # ``is_libclang`` so it never runs on these.
366
+ # ``is_libclang`` so it never runs on these. The cast is that duck typing,
367
+ # which is why ty, seeing two unrelated classes, has to be told.
367
368
  src_comments = [
368
- SourceComment(cast("TreeSitterNode", c), c.column) for c in comments
369
+ SourceComment(
370
+ cast("TreeSitterNode", c), # ty: ignore[disjoint-cast]
371
+ c.column,
372
+ )
373
+ for c in comments
369
374
  ]
370
375
  src_file = SourceFile(src_path.absolute())
371
376
  src_file.add_comments(src_comments)
@@ -67,13 +67,14 @@ class NeedIdRefsConfig:
67
67
  errors = []
68
68
  for _field_name in self.field_names():
69
69
  schema = self.get_schema(_field_name)
70
+ if schema is None:
71
+ # no schema to check against; `validate` would raise TypeError
72
+ continue
70
73
  value = getattr(self, _field_name)
71
74
  try:
72
- # jsonschema's stubs type `schema` as `bool | Mapping`, but
73
- # `get_schema` returns an optional dict
74
75
  validate(
75
76
  instance=value,
76
- schema=schema, # ty: ignore[invalid-argument-type]
77
+ schema=schema,
77
78
  )
78
79
  except ValidationError as e:
79
80
  errors.append(
@@ -137,13 +138,14 @@ class MultilineNeedsConfig:
137
138
  errors = []
138
139
  for _field_name in self.field_names():
139
140
  schema = self.get_schema(_field_name)
141
+ if schema is None:
142
+ # no schema to check against; `validate` would raise TypeError
143
+ continue
140
144
  value = getattr(self, _field_name)
141
145
  try:
142
- # jsonschema's stubs type `schema` as `bool | Mapping`, but
143
- # `get_schema` returns an optional dict
144
146
  validate(
145
147
  instance=value,
146
- schema=schema, # ty: ignore[invalid-argument-type]
148
+ schema=schema,
147
149
  )
148
150
  except ValidationError as e:
149
151
  errors.append(
@@ -334,11 +336,14 @@ class OneLineCommentStyle:
334
336
  errors = []
335
337
  for _field_name in self.field_names():
336
338
  schema = self.get_schema(_field_name)
339
+ if schema is None:
340
+ # no schema to check against; `validate` would raise TypeError
341
+ continue
337
342
  value = getattr(self, _field_name)
338
343
  try:
339
344
  validate(
340
345
  instance=value,
341
- schema=schema, # ty: ignore[invalid-argument-type]
346
+ schema=schema,
342
347
  ) # validate has no type specified
343
348
  except ValidationError as e:
344
349
  if _field_name == "needs_fields":
@@ -118,13 +118,14 @@ class MarkedContentSchema:
118
118
  errors = []
119
119
  for _field_name in self.field_names():
120
120
  schema = self.get_schema(_field_name)
121
+ if schema is None:
122
+ # no schema to check against; `validate` would raise TypeError
123
+ continue
121
124
  value = getattr(self, _field_name)
122
125
  try:
123
- # jsonschema's stubs type `schema` as `bool | Mapping`, but
124
- # `get_schema` returns an optional dict
125
126
  validate(
126
127
  instance=value,
127
- schema=schema, # ty: ignore[invalid-argument-type]
128
+ schema=schema,
128
129
  )
129
130
  except ValidationError as e:
130
131
  errors.append(
@@ -186,13 +186,16 @@ class SourceDiscoverConfig:
186
186
  errors = []
187
187
  for _field_name in self.field_names():
188
188
  schema = self.get_schema(_field_name)
189
+ if schema is None:
190
+ # no schema to check against; `validate` would raise TypeError
191
+ continue
189
192
  value = getattr(self, _field_name)
190
193
  if isinstance(value, Path): # adapt to json schema restriction
191
194
  value = str(value)
192
195
  try:
193
196
  validate(
194
197
  instance=value,
195
- schema=schema, # ty: ignore[invalid-argument-type]
198
+ schema=schema,
196
199
  ) # validate has no type specified
197
200
  except ValidationError as e:
198
201
  errors.append(
@@ -9,7 +9,7 @@ import inspect
9
9
  import json
10
10
  from collections.abc import Callable
11
11
  from datetime import datetime
12
- from functools import wraps
12
+ from functools import partial, wraps
13
13
  from pathlib import Path
14
14
  from timeit import default_timer as timer # Used for timing measurements
15
15
  from typing import Any, TypeVar
@@ -19,15 +19,52 @@ from sphinx.application import Sphinx
19
19
 
20
20
  # Stores the timing results
21
21
  TIME_MEASUREMENTS: dict[str, Any] = {}
22
- EXECUTE_TIME_MEASUREMENTS = (
22
+ EXECUTE_TIME_MEASUREMENTS: bool = (
23
23
  False # Will be used to de/activate measurements. Set during a Sphinx Event
24
24
  )
25
25
 
26
- START_TIME = 0.0
26
+ START_TIME: float = 0.0
27
27
 
28
28
  T = TypeVar("T", bound=Callable[..., Any])
29
29
 
30
30
 
31
+ def _callable_name(func: Callable[..., Any]) -> str:
32
+ """A name for a measured callable, which need not have a ``__name__``.
33
+
34
+ A ``functools.partial`` is named after what it wraps, a callable instance after its
35
+ type; never its ``repr``, which would put memory addresses and bound arguments into
36
+ the report.
37
+ """
38
+ name = getattr(func, "__name__", None)
39
+ if isinstance(name, str) and name:
40
+ return name
41
+ if isinstance(func, partial):
42
+ return f"partial({_callable_name(func.func)})"
43
+ return type(func).__name__
44
+
45
+
46
+ # Where a measured callable came from is a debugging aid, so a callable inspect cannot
47
+ # place -- a partial or an instance (TypeError), a file that is gone or a class in
48
+ # ``__main__`` (OSError), a file rewritten since (tokenize.TokenError) -- is recorded
49
+ # without it rather than ending the build.
50
+
51
+
52
+ def _source_file(func: Callable[..., Any]) -> str | None:
53
+ """The file ``func`` was defined in, or ``None`` if inspect cannot place it."""
54
+ try:
55
+ return inspect.getfile(func)
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def _source_line(func: Callable[..., Any]) -> int | None:
61
+ """The first line of ``func``'s source, or ``None`` if it cannot be read."""
62
+ try:
63
+ return inspect.getsourcelines(func)[1]
64
+ except Exception:
65
+ return None
66
+
67
+
31
68
  def measure_time(
32
69
  category: str | None = None, source: str = "internal", name: str | None = None
33
70
  ) -> Callable[[T], T]:
@@ -77,9 +114,10 @@ def measure_time(
77
114
 
78
115
  runtime = end - start
79
116
 
80
- mt_name = func.__name__ if name is None else name
117
+ func_name = _callable_name(func)
118
+ mt_name = func_name if name is None else name
81
119
 
82
- mt_id = f"{category}_{func.__name__}"
120
+ mt_id = f"{category}_{func_name}"
83
121
 
84
122
  if mt_id not in TIME_MEASUREMENTS:
85
123
  TIME_MEASUREMENTS[mt_id] = {
@@ -87,8 +125,8 @@ def measure_time(
87
125
  "category": category,
88
126
  "source": source,
89
127
  "doc": func.__doc__,
90
- "file": inspect.getfile(func),
91
- "line": inspect.getsourcelines(func)[1],
128
+ "file": _source_file(func),
129
+ "line": _source_line(func),
92
130
  "amount": 0,
93
131
  "overall": 0,
94
132
  "avg": None,
@@ -651,12 +651,10 @@ class SourceTracingDirective(SphinxDirective):
651
651
  fields = frozenset(
652
652
  [*schema.iter_extra_field_names(), *schema.iter_link_field_names()]
653
653
  )
654
- # a default or a predicate (which may not match the need: which one matches
655
- # is only known when the need is created, after its markup is chosen)
656
- template = schema.get_core_field("template")
657
- project_template = template is not None and (
658
- template.default is not None or bool(template.predicate_defaults)
659
- )
654
+ # a default or a predicate of any declaration of the template, global or a
655
+ # need type's (which may not apply to the need: which one applies is only
656
+ # known when the need is created, after its markup is chosen)
657
+ project_template = schema.declares_default("template")
660
658
  for marker in markers:
661
659
  filepath = src_analyse.analyse_config.src_dir / marker.filepath
662
660
 
@@ -1,13 +1,21 @@
1
1
  from collections.abc import Generator
2
2
  from pathlib import Path
3
- from typing import Any
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from pygments import highlight
6
6
  from pygments.formatters import HtmlFormatter
7
7
  from pygments.lexers import CLexer
8
8
 
9
+ if TYPE_CHECKING:
10
+ # generic over its output type in the stubs; pygments accepts the subscript at run
11
+ # time only in recent releases (`Formatter.__class_getitem__`), and this package
12
+ # declares no pygments floor
13
+ _HtmlFormatter = HtmlFormatter[str]
14
+ else:
15
+ _HtmlFormatter = HtmlFormatter
9
16
 
10
- class LineFormatter(HtmlFormatter):
17
+
18
+ class LineFormatter(_HtmlFormatter):
11
19
  def __init__(self, lineno_href: dict[int, str], *args: Any, **kwargs: Any) -> None:
12
20
  super().__init__(*args, **kwargs)
13
21
  self.lineno_href = lineno_href
@@ -17,7 +17,7 @@ runs before ``needextend`` is applied, so a user's ``needextend`` of the field w
17
17
  from __future__ import annotations
18
18
 
19
19
  from collections import Counter
20
- from collections.abc import Iterable, Mapping, MutableMapping
20
+ from collections.abc import Collection, Iterable, Mapping, MutableMapping
21
21
  from dataclasses import dataclass, field
22
22
  from pathlib import Path, PurePosixPath
23
23
  from typing import Any
@@ -35,6 +35,7 @@ from sphinx_codelinks.config import (
35
35
  need_id_refs_fields,
36
36
  )
37
37
  from sphinx_codelinks.sphinx_extension.project_analysis import configured_git_root
38
+ from sphinx_needs.data import SphinxNeedsData
38
39
 
39
40
  logger = logging.getLogger(__name__)
40
41
 
@@ -133,6 +134,7 @@ def attach_need_id_refs(
133
134
  *,
134
135
  fields: Mapping[str, str],
135
136
  roots: Mapping[str, str] | None = None,
137
+ derived: Collection[str] = (),
136
138
  ) -> AttachResult:
137
139
  """Give each referenced need the URLs of its references.
138
140
 
@@ -150,7 +152,10 @@ def attach_need_id_refs(
150
152
  each kept. Records are ordered by ``(path, lineno, start_column)``. A
151
153
  need gets one entry per reference: its remote URL, else its local one; projects
152
154
  naming the same field share one list. The need is not marked as modified, and an
153
- unreferenced need is left alone.
155
+ unreferenced need is left alone. Nothing is written into a field in ``derived``
156
+ (the fields the schema derives: a user's declaration of the field with a ``derive``
157
+ rule), which a need refuses to have set: the need keeps what its rule computes, and
158
+ a reference to an unknown id is still reported.
154
159
  """
155
160
  roots = roots or {}
156
161
  unique: dict[tuple[str, str, str, int, str], NeedIdRef] = {}
@@ -174,6 +179,8 @@ def attach_need_id_refs(
174
179
  if need is None:
175
180
  result.unknown.append(ref)
176
181
  continue
182
+ if fields[ref.project] in derived:
183
+ continue
177
184
  value = ref.remote_url or _local_value(ref.local_url, need.get("docname"))
178
185
  if value is None:
179
186
  continue
@@ -196,7 +203,10 @@ def attach_and_report(
196
203
  codelinks_config = CodeLinksConfig.from_sphinx(app.config)
197
204
  fields = need_id_refs_fields(codelinks_config)
198
205
  roots = project_roots(app.confdir, codelinks_config)
199
- result = attach_need_id_refs(refs, needs, fields=fields, roots=roots)
206
+ derived = SphinxNeedsData(app.env).get_schema().derived_names
207
+ result = attach_need_id_refs(
208
+ refs, needs, fields=fields, roots=roots, derived=derived
209
+ )
200
210
 
201
211
  # one warning per located id, even when two fields cover one file
202
212
  warned: set[tuple[str, int, str]] = set()
@@ -447,8 +447,8 @@ def prepare_env(
447
447
 
448
448
  # Set time measurement flag
449
449
  if src_trace_sphinx_config.debug_measurement:
450
- debug.START_TIME = timer() # Store the rough start time of Sphinx build # ty: ignore[invalid-assignment]
451
- debug.EXECUTE_TIME_MEASUREMENTS = True # ty: ignore[invalid-assignment]
450
+ debug.START_TIME = timer() # Store the rough start time of Sphinx build
451
+ debug.EXECUTE_TIME_MEASUREMENTS = True
452
452
 
453
453
  if src_trace_sphinx_config.debug_filters:
454
454
  with contextlib.suppress(FileNotFoundError):