cpdocs 0.3.0__tar.gz → 0.3.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 (152) hide show
  1. {cpdocs-0.3.0 → cpdocs-0.3.1}/PKG-INFO +1 -1
  2. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/__init__.py +1 -1
  3. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/build.py +82 -6
  4. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/clang_backend.py +73 -3
  5. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/html_renderer.py +16 -2
  6. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/model.py +9 -0
  7. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/render.py +7 -2
  8. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_api_reference.py +14 -3
  9. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_build.py +23 -1
  10. cpdocs-0.3.0/src/cpdocs/html_renderer.py.rej +0 -12
  11. {cpdocs-0.3.0 → cpdocs-0.3.1}/.github/workflows/docs.yml +0 -0
  12. {cpdocs-0.3.0 → cpdocs-0.3.1}/.github/workflows/test.yml +0 -0
  13. {cpdocs-0.3.0 → cpdocs-0.3.1}/.gitignore +0 -0
  14. {cpdocs-0.3.0 → cpdocs-0.3.1}/LICENSES/Apache-2.0.txt +0 -0
  15. {cpdocs-0.3.0 → cpdocs-0.3.1}/REUSE.toml +0 -0
  16. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/api/index.md +0 -0
  17. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/cpdocs.yml +0 -0
  18. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/examples/index.md +0 -0
  19. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/explanation/architecture.md +0 -0
  20. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/explanation/feature-sets.md +0 -0
  21. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/how-to/ci.md +0 -0
  22. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/how-to/feature-sets.md +0 -0
  23. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/how-to/generated-headers.md +0 -0
  24. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/how-to/other-build-systems.md +0 -0
  25. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/how-to/properdocs.md +0 -0
  26. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/index.md +0 -0
  27. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/api.md +0 -0
  28. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/cli.md +0 -0
  29. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/cmake.md +0 -0
  30. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/comments.md +0 -0
  31. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/configuration.md +0 -0
  32. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/contract.md +0 -0
  33. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/extraction.md +0 -0
  34. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/reference/links.md +0 -0
  35. {cpdocs-0.3.0 → cpdocs-0.3.1}/docs/web/tutorials/first-reference.md +0 -0
  36. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/.gitignore +0 -0
  37. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/CMakeLists.txt +0 -0
  38. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/docs/api/index.md +0 -0
  39. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/docs/cpdocs.yml +0 -0
  40. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/docs/web/api.md +0 -0
  41. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/docs/web/index.md +0 -0
  42. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/include/acme/math.hpp +0 -0
  43. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/properdocs.yml +0 -0
  44. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/readme.md +0 -0
  45. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/basic-cpp/src/math.cpp +0 -0
  46. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/.gitignore +0 -0
  47. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/CMakeLists.txt +0 -0
  48. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/docs/api/index.md +0 -0
  49. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/docs/cpdocs.yml +0 -0
  50. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/docs/web/api.md +0 -0
  51. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/docs/web/index.md +0 -0
  52. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/include/ring/ring.h +0 -0
  53. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/properdocs.yml +0 -0
  54. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/readme.md +0 -0
  55. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/c-basic/src/ring.c +0 -0
  56. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/.gitignore +0 -0
  57. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/CMakeLists.txt +0 -0
  58. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/docs/api/index.md +0 -0
  59. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/docs/cpdocs-minibuild.yml +0 -0
  60. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/docs/cpdocs.yml +0 -0
  61. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/docs/web/api.md +0 -0
  62. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/docs/web/index.md +0 -0
  63. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/include/demo/solver.hpp +0 -0
  64. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/properdocs.yml +0 -0
  65. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/readme.md +0 -0
  66. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-features/tools/minibuild.py +0 -0
  67. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/.gitignore +0 -0
  68. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/CMakeLists.txt +0 -0
  69. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/docs/api/index.md +0 -0
  70. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/docs/cpdocs.yml +0 -0
  71. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/docs/web/api.md +0 -0
  72. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/docs/web/index.md +0 -0
  73. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/include/meta/meta.hpp +0 -0
  74. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/properdocs.yml +0 -0
  75. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/cpp-templates/readme.md +0 -0
  76. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/CMakeLists.txt +0 -0
  77. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/docs/cpdocs.yml +0 -0
  78. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/docs/index.md +0 -0
  79. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/lib/normalize.f90 +0 -0
  80. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/mod/geometry.f90 +0 -0
  81. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/properdocs.yml +0 -0
  82. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/fortran-basic/readme.md +0 -0
  83. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/.gitignore +0 -0
  84. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/CMakeLists.txt +0 -0
  85. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/docs/api/index.md +0 -0
  86. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/docs/cpdocs.yml +0 -0
  87. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/docs/web/api.md +0 -0
  88. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/docs/web/index.md +0 -0
  89. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/generated-template/generated.hpp +0 -0
  90. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/include/acme/core.hpp +0 -0
  91. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/properdocs.yml +0 -0
  92. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/readme.md +0 -0
  93. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/generated-cpp/src/generated.cpp +0 -0
  94. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/.gitignore +0 -0
  95. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/CMakeLists.txt +0 -0
  96. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/docs/api/index.md +0 -0
  97. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/docs/cpdocs.yml +0 -0
  98. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/docs/web/api.md +0 -0
  99. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/docs/web/index.md +0 -0
  100. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/include/geom/vector.hpp +0 -0
  101. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/properdocs.yml +0 -0
  102. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/python/pygeom/__init__.py +0 -0
  103. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/readme.md +0 -0
  104. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/mixed/src/geom.cpp +0 -0
  105. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/.gitignore +0 -0
  106. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/docs/api/index.md +0 -0
  107. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/docs/cpdocs.yml +0 -0
  108. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/docs/web/api.md +0 -0
  109. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/docs/web/index.md +0 -0
  110. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/properdocs.yml +0 -0
  111. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/pyproject.toml +0 -0
  112. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/readme.md +0 -0
  113. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/src/dice/__init__.py +0 -0
  114. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/src/dice/_rng.py +0 -0
  115. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/python-basic/src/dice/core.py +0 -0
  116. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/Cargo.lock +0 -0
  117. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/Cargo.toml +0 -0
  118. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/docs/api.md +0 -0
  119. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/docs/cpdocs.yml +0 -0
  120. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/docs/index.md +0 -0
  121. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/properdocs.yml +0 -0
  122. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/readme.md +0 -0
  123. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/rust-toolchain.toml +0 -0
  124. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/src/geometry.rs +0 -0
  125. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/src/internal.rs +0 -0
  126. {cpdocs-0.3.0 → cpdocs-0.3.1}/examples/rust-basic/src/lib.rs +0 -0
  127. {cpdocs-0.3.0 → cpdocs-0.3.1}/properdocs.yml +0 -0
  128. {cpdocs-0.3.0 → cpdocs-0.3.1}/pyproject.toml +0 -0
  129. {cpdocs-0.3.0 → cpdocs-0.3.1}/readme.md +0 -0
  130. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/__main__.py +0 -0
  131. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/cli.py +0 -0
  132. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/config.py +0 -0
  133. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/contract.py +0 -0
  134. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/fortran_backend.py +0 -0
  135. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/properdocs_core.py +0 -0
  136. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/properdocs_plugin.py +0 -0
  137. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/python_backend.py +0 -0
  138. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/python_manifest.py +0 -0
  139. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/rust_backend.py +0 -0
  140. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/rust_manifest.py +0 -0
  141. {cpdocs-0.3.0 → cpdocs-0.3.1}/src/cpdocs/versioning.py +0 -0
  142. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/conftest.py +0 -0
  143. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_api_reference.py.rej +0 -0
  144. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_config.py +0 -0
  145. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_contract.py +0 -0
  146. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_examples.py +0 -0
  147. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_feature_set_signatures.py +0 -0
  148. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_properdocs_core.py +0 -0
  149. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_rust_example.py +0 -0
  150. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_rust_provider.py +0 -0
  151. {cpdocs-0.3.0 → cpdocs-0.3.1}/test/test_versioning.py +0 -0
  152. {cpdocs-0.3.0 → cpdocs-0.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cpdocs
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Semantic, versioned API documentation for C, C++, CUDA, HIP, Fortran, Python and Rust projects
5
5
  Project-URL: Source, https://github.com/ambhora/cpdocs
6
6
  Author-email: Jayesh Badwaik <j.badwaik@fz-juelich.de>
@@ -2,4 +2,4 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """Language-neutral semantic API documentation generator."""
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.1"
@@ -36,7 +36,7 @@ from .contract import (
36
36
  )
37
37
  from .fortran_backend import extract_fortran_graph
38
38
  from .html_renderer import render_html_site
39
- from .model import ApiGraph, merge_graphs
39
+ from .model import ApiGraph, merge_graphs, relative_source_path
40
40
  from .python_backend import build_python_graph
41
41
  from .rust_backend import build_rust_graph
42
42
  from .versioning import SelectedRef, current_branch, select_refs
@@ -199,8 +199,8 @@ def _browse_logical_path(
199
199
  try:
200
200
  return path.resolve().relative_to(project_root.resolve()).as_posix()
201
201
  except (OSError, ValueError):
202
- # Generated files live outside the source checkout. Give them a stable logical home rather
203
- # than leaking profile/build-directory paths into the published file hierarchy.
202
+ # Keep a stable internal key for files outside the checkout. This path is not presented to
203
+ # readers; rendering uses the semantic layout recorded in ``source_display_paths``.
204
204
  base = Path("_generated") / unit.id / category
205
205
  try:
206
206
  relative = (
@@ -211,11 +211,69 @@ def _browse_logical_path(
211
211
  return (base / relative).as_posix()
212
212
 
213
213
 
214
+ def _semantic_display_path(path: Path, *, root: Path, generated: bool) -> str:
215
+ """Return the compiler-facing layout path shown in the file browser.
216
+
217
+ Manifest roots are semantic roots. A file below ``.../src/cuda/include`` is therefore shown
218
+ below ``include`` and a file below ``.../src/cuda/lib`` below ``lib``. Build-generated roots
219
+ use the same flattened layout under one ``_generated`` prefix.
220
+ """
221
+
222
+ try:
223
+ if root.is_dir():
224
+ relative = path.resolve().relative_to(root.resolve())
225
+ root_name = root.name
226
+ else:
227
+ relative = Path(path.name)
228
+ root_name = root.parent.name
229
+ except (OSError, ValueError):
230
+ relative = Path(path.name)
231
+ root_name = root.name if root.name != path.name else root.parent.name
232
+
233
+ base = Path("_generated") if generated else Path()
234
+ if root_name:
235
+ base /= root_name
236
+ return (base / relative).as_posix()
237
+
238
+
239
+ def _api_display_paths(
240
+ unit: UnitManifest, graph: ApiGraph, *, project_root: Path
241
+ ) -> dict[str, str]:
242
+ """Map API source keys to their semantic include/module layout."""
243
+
244
+ values: dict[str, str] = {}
245
+ for api_root in unit.api:
246
+ root = api_root.path
247
+ if not root.is_dir():
248
+ continue
249
+ for path in sorted(root.rglob("*")):
250
+ if not path.is_file():
251
+ continue
252
+ try:
253
+ root_relative = path.resolve().relative_to(root.resolve()).as_posix()
254
+ except (OSError, ValueError):
255
+ root_relative = path.name
256
+ project_relative = relative_source_path(path, project_root)
257
+ logical = next(
258
+ (candidate for candidate in (root_relative, project_relative) if candidate in graph.sources),
259
+ None,
260
+ )
261
+ if logical is not None:
262
+ values.setdefault(
263
+ logical,
264
+ _semantic_display_path(
265
+ path, root=root, generated=unit.generated or api_root.generated
266
+ ),
267
+ )
268
+ return values
269
+
270
+
214
271
  def _collect_browse_files(
215
272
  unit: UnitManifest, *, project_root: Path
216
- ) -> tuple[dict[str, str], list[str]]:
273
+ ) -> tuple[dict[str, str], list[str], dict[str, str]]:
217
274
  files: dict[str, str] = {}
218
275
  generated: list[str] = []
276
+ display_paths: dict[str, str] = {}
219
277
  # Public headers are already present in ``graph.sources`` after API extraction. These roots
220
278
  # add implementation-only material without duplicating public headers in the file hierarchy.
221
279
  groups = (
@@ -242,9 +300,13 @@ def _collect_browse_files(
242
300
  path, project_root=project_root, unit=unit, category=category, root=root
243
301
  )
244
302
  files.setdefault(logical, content)
303
+ display_paths.setdefault(
304
+ logical,
305
+ _semantic_display_path(path, root=root, generated=unit.generated),
306
+ )
245
307
  if unit.generated and logical not in generated:
246
308
  generated.append(logical)
247
- return files, generated
309
+ return files, generated, display_paths
248
310
 
249
311
 
250
312
  def _generated_sources(unit: UnitManifest, graph: ApiGraph) -> list[str]:
@@ -325,6 +387,11 @@ def _merge_metadata(target: dict[str, object], source: dict[str, object]) -> Non
325
387
  existing = target.setdefault(key, [])
326
388
  assert isinstance(existing, list)
327
389
  existing.extend(item for item in value if item not in existing)
390
+ elif isinstance(value, dict):
391
+ existing = target.setdefault(key, {})
392
+ assert isinstance(existing, dict)
393
+ for item_key, item_value in value.items():
394
+ existing.setdefault(item_key, item_value)
328
395
  else:
329
396
  target.setdefault(key, value)
330
397
 
@@ -427,9 +494,18 @@ def build_graph(
427
494
  work=work / "feature-sets" / feature_set.key,
428
495
  config=config,
429
496
  )
430
- browse_files, generated_files = _collect_browse_files(unit, project_root=source)
497
+ display_paths = unit_graph.metadata.setdefault("source_display_paths", {})
498
+ assert isinstance(display_paths, dict)
499
+ for path, display in _api_display_paths(unit, unit_graph, project_root=source).items():
500
+ display_paths.setdefault(path, display)
501
+
502
+ browse_files, generated_files, browse_display_paths = _collect_browse_files(
503
+ unit, project_root=source
504
+ )
431
505
  for path, content in browse_files.items():
432
506
  unit_graph.files.setdefault(path, content)
507
+ for path, display in browse_display_paths.items():
508
+ display_paths.setdefault(path, display)
433
509
  if generated_files:
434
510
  existing = unit_graph.metadata.setdefault("generated_files", [])
435
511
  assert isinstance(existing, list)
@@ -21,6 +21,7 @@ from pathlib import Path
21
21
 
22
22
  from .contract import ParseOptions
23
23
  from .model import (
24
+ ApiAttribute,
24
25
  ApiEntity,
25
26
  ApiGraph,
26
27
  ApiParameter,
@@ -351,10 +352,79 @@ def _qualifiers(node: dict[str, object]) -> tuple[str, ...]:
351
352
  return tuple(values)
352
353
 
353
354
 
354
- def _signature(node: dict[str, object]) -> ApiSignature:
355
+ _ATTRIBUTE_NAMES = {
356
+ "WarnUnusedResultAttr": "nodiscard",
357
+ "DeprecatedAttr": "deprecated",
358
+ "UnusedAttr": "maybe_unused",
359
+ "CXX11NoReturnAttr": "noreturn",
360
+ "NoUniqueAddressAttr": "no_unique_address",
361
+ "CarriesDependencyAttr": "carries_dependency",
362
+ "LikelyAttr": "likely",
363
+ "UnlikelyAttr": "unlikely",
364
+ }
365
+
366
+
367
+ def _attribute_name(node: dict[str, object]) -> str:
368
+ kind = str(node.get("kind", ""))
369
+ if kind in _ATTRIBUTE_NAMES:
370
+ return _ATTRIBUTE_NAMES[kind]
371
+ if not kind.endswith("Attr"):
372
+ return ""
373
+ stem = kind[:-4]
374
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", stem).lower()
375
+
376
+
377
+ def _attribute_spelling(node: dict[str, object], path: Path | None) -> str:
378
+ """Recover the complete ``[[...]]`` specifier containing one Clang Attr node."""
379
+
380
+ if path is None:
381
+ return ""
382
+ offsets = _range_offsets(node)
383
+ if offsets is None:
384
+ return ""
385
+ data = _SOURCE_BYTES.get(path)
386
+ if data is None:
387
+ try:
388
+ data = path.read_bytes()
389
+ except OSError:
390
+ return ""
391
+ _SOURCE_BYTES[path] = data
392
+ start, stop = offsets
393
+ left = data.rfind(b"[[", max(0, start - 2048), start + 1)
394
+ if left < 0:
395
+ return ""
396
+ right = data.find(b"]]", left + 2, min(len(data), stop + 2048))
397
+ if right < 0 or not (left <= start and stop <= right + 2):
398
+ return ""
399
+ return re.sub(r"\s+", " ", data[left : right + 2].decode("utf-8", errors="replace")).strip()
400
+
401
+
402
+ def _attributes(node: dict[str, object], path: Path | None) -> tuple[ApiAttribute, ...]:
403
+ children = node.get("inner")
404
+ if not isinstance(children, list):
405
+ return ()
406
+ values: list[ApiAttribute] = []
407
+ for child in children:
408
+ if not isinstance(child, dict) or not str(child.get("kind", "")).endswith("Attr"):
409
+ continue
410
+ name = _attribute_name(child)
411
+ if not name:
412
+ continue
413
+ attribute = ApiAttribute(name=name, spelling=_attribute_spelling(child, path))
414
+ if attribute not in values:
415
+ values.append(attribute)
416
+ return tuple(values)
417
+
418
+
419
+ def _signature(node: dict[str, object], path: Path | None) -> ApiSignature:
355
420
  parameters = _parameters(node)
356
421
  qualifiers = _qualifiers(node)
357
- return ApiSignature(parameters=parameters, returns=_return_type(node), qualifiers=qualifiers)
422
+ return ApiSignature(
423
+ parameters=parameters,
424
+ returns=_return_type(node),
425
+ qualifiers=qualifiers,
426
+ attributes=_attributes(node, path),
427
+ )
358
428
 
359
429
 
360
430
  # --------------------------------------------------------------------------------------------------
@@ -704,7 +774,7 @@ def _walk_ast(
704
774
  parent=parent_entity.id if parent_entity else None,
705
775
  signatures=[
706
776
  replace(
707
- _signature(node),
777
+ _signature(node, path),
708
778
  template=template_header,
709
779
  documentation=_attached_comment(node),
710
780
  source=_location(node, path, project_root, public_roots),
@@ -137,7 +137,13 @@ def _entity_href(source: Path, entity: ApiEntity, graph: ApiGraph) -> str:
137
137
 
138
138
 
139
139
  def _display_source_path(source_path: str, graph: ApiGraph) -> str:
140
- """Return the source path shown to readers while preserving the real repository path for links."""
140
+ """Return the semantic path shown to readers while preserving internal source keys for links."""
141
+ display_paths = graph.metadata.get("source_display_paths", {})
142
+ if isinstance(display_paths, dict):
143
+ value = display_paths.get(source_path)
144
+ if isinstance(value, str) and value:
145
+ return value
146
+
141
147
  source_root = str(graph.metadata.get("source_root") or "").strip("/")
142
148
  logical = PurePosixPath(source_path)
143
149
  if not source_root:
@@ -610,7 +616,15 @@ def _signature_card(
610
616
  declaration = _highlight_declaration(
611
617
  spelling, entity.language, entity=entity, graph=graph, document_path=document
612
618
  )
613
- properties = list(dict.fromkeys([*entity.properties, *signature.qualifiers]))
619
+ properties = list(
620
+ dict.fromkeys(
621
+ [
622
+ *entity.properties,
623
+ *signature.qualifiers,
624
+ *(attribute.name for attribute in signature.attributes),
625
+ ]
626
+ )
627
+ )
614
628
  rows = []
615
629
  if properties:
616
630
  rows.append(
@@ -24,11 +24,20 @@ class ApiParameter:
24
24
  kind: str = ""
25
25
 
26
26
 
27
+ @dataclass(frozen=True)
28
+ class ApiAttribute:
29
+ """One source-level attribute attached to an API declaration."""
30
+
31
+ name: str
32
+ spelling: str = ""
33
+
34
+
27
35
  @dataclass(frozen=True)
28
36
  class ApiSignature:
29
37
  parameters: tuple[ApiParameter, ...] = ()
30
38
  returns: str = ""
31
39
  qualifiers: tuple[str, ...] = ()
40
+ attributes: tuple[ApiAttribute, ...] = ()
32
41
  spelling: str = ""
33
42
  template: str = ""
34
43
  # Documentation and location of this overload; not part of its identity.
@@ -631,8 +631,12 @@ def _signature_spelling(entity: ApiEntity, signature: ApiSignature) -> str:
631
631
 
632
632
 
633
633
  def _plain_signature_spelling(entity: ApiEntity, signature: ApiSignature) -> str:
634
+ attribute_spellings = list(
635
+ dict.fromkeys(attribute.spelling for attribute in signature.attributes if attribute.spelling)
636
+ )
637
+ attribute_prefix = " ".join(attribute_spellings)
634
638
  if signature.spelling:
635
- return signature.spelling
639
+ return f"{attribute_prefix} {signature.spelling}".strip()
636
640
  parameters = _parameter_spelling(signature, entity.language)
637
641
  if entity.language == "python":
638
642
  prefix = "async def" if "async" in signature.qualifiers else "def"
@@ -656,7 +660,8 @@ def _plain_signature_spelling(entity: ApiEntity, signature: ApiSignature) -> str
656
660
  result = (signature.returns + " ") if signature.returns else ""
657
661
  suffix_values = [value for value in signature.qualifiers if value not in prefix_values]
658
662
  suffix = (" " + " ".join(suffix_values)) if suffix_values else ""
659
- return f"{prefix}{result}{entity.qualified_name}({parameters}){suffix}".strip()
663
+ declaration = f"{prefix}{result}{entity.qualified_name}({parameters}){suffix}".strip()
664
+ return f"{attribute_prefix} {declaration}".strip()
660
665
 
661
666
 
662
667
  def _entity_declaration(entity: ApiEntity) -> str:
@@ -13,6 +13,7 @@ from cpdocs.clang_backend import extract_clang_graph
13
13
  from cpdocs.contract import ParseOptions
14
14
  from cpdocs.html_renderer import render_html_site
15
15
  from cpdocs.model import (
16
+ ApiAttribute,
16
17
  ApiEntity,
17
18
  ApiGraph,
18
19
  ApiParameter,
@@ -37,11 +38,11 @@ def test_cpp_semantic_profiles_merge_conditional_api(tmp_path: Path) -> None:
37
38
  #ifdef USE_CUDA
38
39
  /// Active backend.
39
40
  #define DICE_BACKEND \"cuda\"
40
- namespace dice { struct cuda_view {}; void foo(cuda_view); }
41
+ namespace dice { struct cuda_view {}; void foo(cuda_view); [[nodiscard]] int backend_name(); }
41
42
  #else
42
43
  /// Active backend.
43
44
  #define DICE_BACKEND \"cpu\"
44
- namespace dice { struct cpu_view {}; void foo(cpu_view); }
45
+ namespace dice { struct cpu_view {}; void foo(cpu_view); [[nodiscard]] int backend_name(); }
45
46
  #endif
46
47
  class visible { public: void yes(); private: void no(); };
47
48
  """,
@@ -76,6 +77,10 @@ class visible { public: void yes(); private: void no(); };
76
77
  foo = merged.entities[stable_entity_id("cpp", "function", "dice::foo")]
77
78
  assert {signature.compact for signature in foo.signatures} == {"(cpu_view)", "(cuda_view)"}
78
79
  assert set(foo.feature_sets) == {"cpu", "cuda"}
80
+ backend_name = merged.entities[stable_entity_id("cpp", "function", "dice::backend_name")]
81
+ assert backend_name.signatures[0].attributes == (
82
+ ApiAttribute(name="nodiscard", spelling="[[nodiscard]]"),
83
+ )
79
84
  assert stable_entity_id("cpp", "method", "visible::yes") in merged.entities
80
85
  assert stable_entity_id("cpp", "method", "visible::no") not in merged.entities
81
86
 
@@ -206,7 +211,11 @@ def test_renderer_generates_standalone_code_oriented_html_site(tmp_path: Path) -
206
211
  qualified_name="dice::foo",
207
212
  parent=namespace.id,
208
213
  signatures=[
209
- ApiSignature(parameters=(ApiParameter("value", "int"),), returns="void"),
214
+ ApiSignature(
215
+ parameters=(ApiParameter("value", "int"),),
216
+ returns="void",
217
+ attributes=(ApiAttribute(name="nodiscard", spelling="[[nodiscard]]"),),
218
+ ),
210
219
  ApiSignature(parameters=(ApiParameter("value", "float"),), returns="void"),
211
220
  ],
212
221
  documentation=(
@@ -315,6 +324,8 @@ def test_renderer_generates_standalone_code_oriented_html_site(tmp_path: Path) -
315
324
  page = html_page(entity_document(function)).read_text(encoding="utf-8")
316
325
  assert '<article class="api-signature-card"' in page
317
326
  assert "foo(int)" in page
327
+ assert page.count("nodiscard") >= 2
328
+ assert "<span>nodiscard</span>" in page
318
329
  assert "Available in feature sets" in page
319
330
  assert "<code>cpu</code>" in page
320
331
  assert "<strong>Related:</strong>" in page
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  import pytest
11
11
 
12
12
  from conftest import fake_project, git, init_repository
13
- from cpdocs.build import build_current, build_versions
13
+ from cpdocs.build import _semantic_display_path, build_current, build_versions
14
14
  from cpdocs.cli import main
15
15
  from cpdocs.config import load_config
16
16
  from cpdocs.model import stable_entity_id
@@ -27,6 +27,28 @@ def _no_ci_branch(monkeypatch: pytest.MonkeyPatch) -> None:
27
27
  monkeypatch.delenv(variable, raising=False)
28
28
 
29
29
 
30
+ def test_semantic_file_layout_flattens_manifest_roots(tmp_path: Path) -> None:
31
+ include = tmp_path / "src" / "cuda" / "include"
32
+ library = tmp_path / "src" / "cuda" / "lib"
33
+ header = include / "kprof" / "cuda.hpp"
34
+ source = library / "kprof" / "synchronize.cu"
35
+ header.parent.mkdir(parents=True)
36
+ source.parent.mkdir(parents=True)
37
+ header.write_text("#pragma once\n", encoding="utf-8")
38
+ source.write_text("// source\n", encoding="utf-8")
39
+
40
+ assert _semantic_display_path(header, root=include, generated=False) == "include/kprof/cuda.hpp"
41
+ assert _semantic_display_path(source, root=library, generated=False) == "lib/kprof/synchronize.cu"
42
+ assert (
43
+ _semantic_display_path(header, root=include, generated=True)
44
+ == "_generated/include/kprof/cuda.hpp"
45
+ )
46
+ assert (
47
+ _semantic_display_path(source, root=library, generated=True)
48
+ == "_generated/lib/kprof/synchronize.cu"
49
+ )
50
+
51
+
30
52
  def test_build_runs_feature_sets_once_then_build_once_per_feature_set(tmp_path: Path) -> None:
31
53
  project = fake_project(
32
54
  tmp_path / "p",
@@ -1,12 +0,0 @@
1
- diff a/src/cpdocs/html_renderer.py b/src/cpdocs/html_renderer.py (rejected hunks)
2
- @@ -698,7 +698,9 @@ def _namespace_page(
3
- label = child.member_label
4
- body.append(
5
- '<li>' + _kind_badge(child.kind)
6
- - + f'<a href="{html.escape(_entity_href(document, child, graph))}">{html.escape(label)}</a></li>'
7
- + + f'<a href="{html.escape(_entity_href(document, child, graph))}">{html.escape(label)}</a>'
8
- + + _language_badge(child)
9
- + + '</li>'
10
- )
11
- body.extend(["</ul>", _availability(entity, graph, document), "</section>"])
12
- return "".join(body), [("Members", "members", 1)]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes