sphinx-examples-as-code 0.4.1__tar.gz → 0.6.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 (42) hide show
  1. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/.github/workflows/ci.yml +4 -4
  2. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/PKG-INFO +14 -20
  3. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/README.md +13 -19
  4. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/pyproject.toml +1 -1
  5. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code/__init__.py +170 -91
  6. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code/_version.py +3 -3
  7. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/PKG-INFO +14 -20
  8. sphinx_examples_as_code-0.6.0/sphinx_examples_as_code.egg-info/scm_version.json +8 -0
  9. sphinx_examples_as_code-0.6.0/tests/gallery_fixture/index.rst +17 -0
  10. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/test_single_function_page.py +0 -34
  11. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/test_sphinx_examples_as_code.py +255 -284
  12. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/test_tinypages.py +73 -108
  13. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/tinypages/docstring_cases.py +42 -6
  14. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/tinypages/docstring_cases.rst +7 -3
  15. sphinx_examples_as_code-0.4.1/sphinx_examples_as_code.egg-info/scm_version.json +0 -8
  16. sphinx_examples_as_code-0.4.1/tests/gallery_fixture/index.rst +0 -7
  17. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/.github/dependabot.yml +0 -0
  18. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/.github/release.yml +0 -0
  19. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/.gitignore +0 -0
  20. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/.pre-commit-config.yaml +0 -0
  21. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/LICENSE +0 -0
  22. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/setup.cfg +0 -0
  23. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/SOURCES.txt +0 -0
  24. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
  25. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/requires.txt +0 -0
  26. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/scm_file_list.json +0 -0
  27. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
  28. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/gallery_fixture/conf.py +0 -0
  29. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
  30. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
  31. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/heading_level_fixture/conf.py +0 -0
  32. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +0 -0
  33. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/heading_level_fixture/examples/plot_reused_level.py +0 -0
  34. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/heading_level_fixture/examples/plot_three_levels.py +0 -0
  35. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/heading_level_fixture/index.rst +0 -0
  36. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/single_function_fixture/conf.py +0 -0
  37. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/single_function_fixture/index.rst +0 -0
  38. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/single_function_fixture/mymodule.py +0 -0
  39. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/test_gallery_downloads.py +0 -0
  40. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/test_heading_level_reuse.py +0 -0
  41. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/tinypages/conf.py +0 -0
  42. {sphinx_examples_as_code-0.4.1 → sphinx_examples_as_code-0.6.0}/tests/tinypages/index.rst +0 -0
@@ -22,7 +22,7 @@ jobs:
22
22
  with:
23
23
  persist-credentials: false
24
24
  fetch-depth: 0
25
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
25
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
26
26
  with:
27
27
  enable-cache: false
28
28
  - run: uv sync --group dev
@@ -43,10 +43,10 @@ jobs:
43
43
  with:
44
44
  persist-credentials: false
45
45
  fetch-depth: 0
46
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
46
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
47
47
  with:
48
48
  enable-cache: false
49
- - uses: pyvista/setup-headless-display-action@5bc8de3bc71fcda7a96439571287a554901541a0 # v4.3
49
+ - uses: pyvista/setup-headless-display-action@c103a2ff45650d38cb71684b5dc6cdfeb9442c79 # v5.1.0
50
50
  with:
51
51
  pyvista: true
52
52
  - run: uv sync --group dev --python ${{ matrix.python-version }}
@@ -65,7 +65,7 @@ jobs:
65
65
  with:
66
66
  persist-credentials: false
67
67
  fetch-depth: 0
68
- - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
68
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
69
69
  with:
70
70
  enable-cache: false
71
71
  - run: uv build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-examples-as-code
3
- Version: 0.4.1
3
+ Version: 0.6.0
4
4
  Summary: Sphinx extension for converting docstring examples into downloadable code.
5
5
  Author-email: The PyVista Developers <info@pyvista.org>
6
6
  License-Expression: MIT
@@ -66,7 +66,6 @@ sphinx_examples_as_code_conf = {
66
66
  'py': 'Download Python source code',
67
67
  'ipynb': 'Download Jupyter notebook',
68
68
  },
69
- 'include_see_also': True,
70
69
  }
71
70
  ```
72
71
 
@@ -76,13 +75,6 @@ sphinx_examples_as_code_conf = {
76
75
  (default). Always offered in that order regardless of how the list is written.
77
76
  - `link_labels`: the text of the download link(s) themselves, per format. Set only the
78
77
  format(s) you want to change; any left unset keep reading their own default shown above.
79
- - `include_see_also`: whether "See Also" content is included in the generated file.
80
- `True` (default) includes it, in any of its forms: a `.. seealso::` admonition, a bare
81
- `.. rubric:: See Also`, a hand-written `See Also` heading, or numpydoc's own "See Also"
82
- field (including when it's been reordered outside the Examples section itself, or the
83
- Examples section has been hoisted to a heading of its own — a setup some projects use to
84
- get "Examples" listed in the page's own navigation). `False` excludes "See Also" content
85
- in every one of those forms.
86
78
  - `gallery_downloads`: opt-in takeover of
87
79
  [sphinx-gallery](https://sphinx-gallery.github.io)'s own per-example downloads.
88
80
  `False` (default) leaves sphinx-gallery pages untouched. See
@@ -114,11 +106,8 @@ What happens to the content of an Examples section:
114
106
  real Python source. Doctest *output* lines are dropped — only the input code matters.
115
107
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
116
108
  comments, set off with blank lines on both sides like any other directive.
117
- - Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
118
- comment followed by their content as comments, indented one level under the label in
119
- `.py` only. "See Also" is recognized in any of its three forms (`.. seealso::`, a bare
120
- `.. rubric:: See Also`, or a hand-written `See Also` heading) and always renders the
121
- same way.
109
+ - Admonitions (`.. note::`, `.. warning::`, ...) become a `# LABEL:` comment followed by
110
+ their content as comments, indented one level under the label in `.py` only.
122
111
  - A bullet/numbered list becomes a `-`/`N.`-marked line per item, set off with a blank
123
112
  line on both sides in either format — a bare `#` instead of a real blank line when
124
113
  that falls inside an admonition or a definition's own body in `.py`, so the whole
@@ -128,15 +117,19 @@ What happens to the content of an Examples section:
128
117
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
129
118
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
130
119
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
131
- the reference resolves: `.ipynb` turns it into a clickable link everywhere; `.py` only
132
- writes the link inside a "See Also" part (as `name url` on its own line) — everywhere
133
- else in `.py` the link is simply omitted.
120
+ the reference resolves, `.ipynb` turns it into a clickable link; `.py` never writes the
121
+ link, only the display text.
134
122
  - Plain prose-style references (`:ref:`, `:doc:`) are treated the same way, minus the
135
123
  backticks.
136
124
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
137
125
  `#` comment.
126
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
127
+ table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
128
+ line, and a column left entirely empty -- one holding only images, say -- is dropped
129
+ along with its heading.
138
130
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
139
131
  line are dropped entirely.
132
+ - "See Also" content is always dropped.
140
133
 
141
134
  Generated `.py` files start with a `# Examples from <qualified name>` title header
142
135
  (gallery mode uses the page's own title instead -- see below), with a few whitespace
@@ -186,9 +179,10 @@ styles:
186
179
 
187
180
  Two things worth knowing before turning this on:
188
181
 
189
- - It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
190
- classes its own theming depends on), not a documented extension API. A future
191
- sphinx-gallery release could shift that structure without warning.
182
+ - It's built against sphinx-gallery's own doctree output the `sphx-glr-*` CSS classes
183
+ its own theming depends on, and the private node type holding a highlighted code
184
+ block — not a documented extension API. A future sphinx-gallery release could shift
185
+ that structure without warning. Tested against sphinx-gallery 0.22 and later.
192
186
  - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
193
187
  `_downloads/`, even though nothing on the page links to them anymore.
194
188
 
@@ -39,7 +39,6 @@ sphinx_examples_as_code_conf = {
39
39
  'py': 'Download Python source code',
40
40
  'ipynb': 'Download Jupyter notebook',
41
41
  },
42
- 'include_see_also': True,
43
42
  }
44
43
  ```
45
44
 
@@ -49,13 +48,6 @@ sphinx_examples_as_code_conf = {
49
48
  (default). Always offered in that order regardless of how the list is written.
50
49
  - `link_labels`: the text of the download link(s) themselves, per format. Set only the
51
50
  format(s) you want to change; any left unset keep reading their own default shown above.
52
- - `include_see_also`: whether "See Also" content is included in the generated file.
53
- `True` (default) includes it, in any of its forms: a `.. seealso::` admonition, a bare
54
- `.. rubric:: See Also`, a hand-written `See Also` heading, or numpydoc's own "See Also"
55
- field (including when it's been reordered outside the Examples section itself, or the
56
- Examples section has been hoisted to a heading of its own — a setup some projects use to
57
- get "Examples" listed in the page's own navigation). `False` excludes "See Also" content
58
- in every one of those forms.
59
51
  - `gallery_downloads`: opt-in takeover of
60
52
  [sphinx-gallery](https://sphinx-gallery.github.io)'s own per-example downloads.
61
53
  `False` (default) leaves sphinx-gallery pages untouched. See
@@ -87,11 +79,8 @@ What happens to the content of an Examples section:
87
79
  real Python source. Doctest *output* lines are dropped — only the input code matters.
88
80
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
89
81
  comments, set off with blank lines on both sides like any other directive.
90
- - Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
91
- comment followed by their content as comments, indented one level under the label in
92
- `.py` only. "See Also" is recognized in any of its three forms (`.. seealso::`, a bare
93
- `.. rubric:: See Also`, or a hand-written `See Also` heading) and always renders the
94
- same way.
82
+ - Admonitions (`.. note::`, `.. warning::`, ...) become a `# LABEL:` comment followed by
83
+ their content as comments, indented one level under the label in `.py` only.
95
84
  - A bullet/numbered list becomes a `-`/`N.`-marked line per item, set off with a blank
96
85
  line on both sides in either format — a bare `#` instead of a real blank line when
97
86
  that falls inside an admonition or a definition's own body in `.py`, so the whole
@@ -101,15 +90,19 @@ What happens to the content of an Examples section:
101
90
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
102
91
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
103
92
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
104
- the reference resolves: `.ipynb` turns it into a clickable link everywhere; `.py` only
105
- writes the link inside a "See Also" part (as `name url` on its own line) — everywhere
106
- else in `.py` the link is simply omitted.
93
+ the reference resolves, `.ipynb` turns it into a clickable link; `.py` never writes the
94
+ link, only the display text.
107
95
  - Plain prose-style references (`:ref:`, `:doc:`) are treated the same way, minus the
108
96
  backticks.
109
97
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
110
98
  `#` comment.
99
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
100
+ table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
101
+ line, and a column left entirely empty -- one holding only images, say -- is dropped
102
+ along with its heading.
111
103
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
112
104
  line are dropped entirely.
105
+ - "See Also" content is always dropped.
113
106
 
114
107
  Generated `.py` files start with a `# Examples from <qualified name>` title header
115
108
  (gallery mode uses the page's own title instead -- see below), with a few whitespace
@@ -159,9 +152,10 @@ styles:
159
152
 
160
153
  Two things worth knowing before turning this on:
161
154
 
162
- - It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
163
- classes its own theming depends on), not a documented extension API. A future
164
- sphinx-gallery release could shift that structure without warning.
155
+ - It's built against sphinx-gallery's own doctree output the `sphx-glr-*` CSS classes
156
+ its own theming depends on, and the private node type holding a highlighted code
157
+ block — not a documented extension API. A future sphinx-gallery release could shift
158
+ that structure without warning. Tested against sphinx-gallery 0.22 and later.
165
159
  - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
166
160
  `_downloads/`, even though nothing on the page links to them anymore.
167
161
 
@@ -11,7 +11,7 @@ dev = [
11
11
  'pytest-cov',
12
12
  'sphinx',
13
13
  'sphinx-design',
14
- 'sphinx-gallery>=0.21.0',
14
+ 'sphinx-gallery>=0.22',
15
15
  'trame-pyvista',
16
16
  ]
17
17
 
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
 
8
8
  import ast
9
9
  from dataclasses import dataclass
10
- from dataclasses import replace
11
10
  import hashlib
12
11
  import json
13
12
  from pathlib import Path
@@ -92,6 +91,22 @@ _ADMONITION_LABELS = {
92
91
 
93
92
  _PYTHON_LANGUAGES = ('python', 'py', 'python3')
94
93
 
94
+ # sphinx-gallery's own node type for a gallery example's Python code, in
95
+ # place of a ``literal_block``: one child per pygments token, so that names
96
+ # in the code can carry links to their documentation. Its language is spelled
97
+ # ``lang``, and its source has to be read back off its children. Code in any
98
+ # other language stays an ordinary ``literal_block``.
99
+ #
100
+ # Matched by tag name rather than by ``isinstance``: sphinx-gallery is not a
101
+ # dependency of this extension (gallery support is opt-in via
102
+ # ``gallery_downloads``), so the class itself can't be imported here.
103
+ _GALLERY_CODE_BLOCK_TAGNAME = 'code_links_block'
104
+
105
+ # Class on the ``inline`` nodes sphinx-gallery interleaves into the above when
106
+ # the block has line numbers turned on. Rendering furniture, not source, so
107
+ # they're dropped rather than baked into the generated file.
108
+ _GALLERY_LINENOS_CLASS = 'linenos'
109
+
95
110
  # A chunk of generated lines tagged with how it should be spaced relative to
96
111
  # its neighbors when segments are joined (see ``_join_segments``):
97
112
  # 'code' real Python source
@@ -154,9 +169,7 @@ class _RenderContext:
154
169
  app: Sphinx
155
170
  docname: str
156
171
  fmt: str # 'py' or 'ipynb'
157
- in_see_also: bool = False
158
172
  in_footer: bool = False
159
- show_see_also: bool = True
160
173
 
161
174
 
162
175
  def _normalized_html_baseurl(app: Sphinx) -> str | None:
@@ -233,9 +246,8 @@ def _render_reference(node: nodes.reference, ctx: _RenderContext) -> str:
233
246
  """Render a resolved or unresolved cross-reference/hyperlink.
234
247
 
235
248
  The target URL, if resolved, is used as a clickable markdown link in
236
- notebooks (always), and as literal URL text in ``.py`` (only within a
237
- "See Also" part or the footer -- elsewhere in ``.py`` the link is simply
238
- omitted).
249
+ notebooks (always), and as literal URL text in ``.py`` (only within the
250
+ footer -- elsewhere in ``.py`` the link is simply omitted).
239
251
  """
240
252
  display = node.astext()
241
253
  is_code = any(isinstance(child, nodes.literal) for child in node.children)
@@ -247,11 +259,6 @@ def _render_reference(node: nodes.reference, ctx: _RenderContext) -> str:
247
259
  if ctx.fmt == 'ipynb':
248
260
  return f'[`{display}`]({url})' if is_code else f'[{display}]({url})'
249
261
 
250
- if ctx.in_see_also:
251
- # Surrounding newlines set this off on its own line; _add_comment
252
- # splits on them like any other multi-line text.
253
- return f'\n{display} {url}\n'
254
-
255
262
  if ctx.in_footer:
256
263
  # Inline, not surrounded by newlines -- "text url" in place, not a
257
264
  # markdown link.
@@ -328,16 +335,34 @@ def _convert_doctest_block(node: nodes.doctest_block) -> list[Segment]:
328
335
  return [('code', lines)]
329
336
 
330
337
 
331
- def _convert_literal_block(node: nodes.literal_block) -> list[Segment]:
338
+ def _is_gallery_code_block(node: nodes.Node) -> bool:
339
+ """Check whether ``node`` is the node type sphinx-gallery puts example code in."""
340
+ return getattr(node, 'tagname', None) == _GALLERY_CODE_BLOCK_TAGNAME
341
+
342
+
343
+ def _gallery_code_block_source(node: nodes.Element) -> str:
344
+ """Reassemble the source text sphinx-gallery split across per-token nodes."""
345
+ # One child per pygments token (some wrapped in a reference node), which
346
+ # ``astext()`` joins back together with no separator -- apart from the
347
+ # line-number nodes, which were never part of the source.
348
+ return ''.join(
349
+ child.astext() for child in node.children if not _has_class(child, _GALLERY_LINENOS_CLASS)
350
+ )
351
+
352
+
353
+ def _convert_literal_block(node: nodes.Element) -> list[Segment]:
332
354
  """Convert a ``.. code-block::``. Python blocks stay code, others become comments."""
333
- language = node.get('language', '')
355
+ if _is_gallery_code_block(node):
356
+ language, source = node.get('lang', ''), _gallery_code_block_source(node)
357
+ else:
358
+ language, source = node.get('language', ''), node.astext()
334
359
  # Case-insensitive: sphinx-gallery emits ``.. code-block:: Python`` (capitalized).
335
360
  if language.lower() in _PYTHON_LANGUAGES:
336
- lines = [_clean_code_comment(line) for line in node.astext().splitlines()]
361
+ lines = [_clean_code_comment(line) for line in source.splitlines()]
337
362
  while lines and not lines[-1].strip():
338
363
  lines.pop()
339
364
  return [('code', lines)] if lines else []
340
- text = node.astext().strip()
365
+ text = source.strip()
341
366
  if not text:
342
367
  return []
343
368
  comment_lines: list[str] = []
@@ -483,16 +508,125 @@ def _list_segment(
483
508
  return [('directive', lines)] if lines else []
484
509
 
485
510
 
511
+ #: Narrowest a generated table column may be, per format.
512
+ _TABLE_MIN_WIDTH = {'py': 1, 'ipynb': 3}
513
+
514
+
515
+ def _table_cell_text(entry: nodes.entry, ctx: _RenderContext) -> str:
516
+ """Render one cell's content as a single line, list items separated by ``;``."""
517
+ parts: list[str] = []
518
+ for child in entry.children:
519
+ if isinstance(child, (nodes.bullet_list, nodes.enumerated_list)):
520
+ items = (_render_inline(item, ctx).strip() for item in child.children)
521
+ parts.append('; '.join(item for item in items if item))
522
+ else:
523
+ parts.append(_render_inline(child, ctx).strip())
524
+ return ' '.join(part for part in parts if part)
525
+
526
+
527
+ def _table_rows(group: nodes.Element, ctx: _RenderContext) -> list[list[str]]:
528
+ """Render a ``thead``/``tbody``'s rows, padding a column-spanning cell with empties."""
529
+ rows: list[list[str]] = []
530
+ for row in group.children:
531
+ cells: list[str] = []
532
+ for entry in row.children:
533
+ cells.append(_table_cell_text(entry, ctx))
534
+ cells.extend([''] * entry.get('morecols', 0))
535
+ if any(cells):
536
+ rows.append(cells)
537
+ return rows
538
+
539
+
540
+ def _table_grid(node: nodes.table, ctx: _RenderContext) -> tuple[list[list[str]], list[list[str]]]:
541
+ """Split a table into header rows and body rows, dropping any column left empty."""
542
+ head: list[list[str]] = []
543
+ body: list[list[str]] = []
544
+ for tgroup in node.children:
545
+ if not isinstance(tgroup, nodes.tgroup):
546
+ continue
547
+ for group in tgroup.children:
548
+ if isinstance(group, nodes.thead):
549
+ head.extend(_table_rows(group, ctx))
550
+ elif isinstance(group, nodes.tbody):
551
+ body.extend(_table_rows(group, ctx))
552
+ columns = max((len(row) for row in [*head, *body]), default=0)
553
+ for row in [*head, *body]:
554
+ row.extend([''] * (columns - len(row)))
555
+ filled = [index for index in range(columns) if any(row[index] for row in body)]
556
+ if not filled or len(filled) == columns:
557
+ return head, body
558
+ return (
559
+ [[row[index] for index in filled] for row in head],
560
+ [[row[index] for index in filled] for row in body],
561
+ )
562
+
563
+
564
+ def _table_widths(rows: list[list[str]], minimum: int) -> list[int]:
565
+ """Measure each column, never narrower than ``minimum``."""
566
+ return [max(minimum, *map(len, column)) for column in zip(*rows, strict=True)]
567
+
568
+
569
+ def _markdown_table_lines(head: list[list[str]], body: list[list[str]]) -> list[str]:
570
+ """Render rows as a Markdown pipe table, with a blank header row if there is none."""
571
+ head = [[cell.replace('|', r'\|') for cell in row] for row in head]
572
+ body = [[cell.replace('|', r'\|') for cell in row] for row in body]
573
+ widths = _table_widths([*head, *body], _TABLE_MIN_WIDTH['ipynb'])
574
+
575
+ def render(cells: list[str]) -> str:
576
+ padded = (cell.ljust(width) for cell, width in zip(cells, widths, strict=True))
577
+ return f'| {" | ".join(padded)} |'
578
+
579
+ header, *extra = head or [[''] * len(widths)]
580
+ divider = f'| {" | ".join("-" * width for width in widths)} |'
581
+ return [render(header), divider, *(render(cells) for cells in [*extra, *body])]
582
+
583
+
584
+ def _simple_table_lines(head: list[list[str]], body: list[list[str]]) -> list[str]:
585
+ """Render rows as an RST simple table, bordered above, below and under its header."""
586
+ widths = _table_widths([*head, *body], _TABLE_MIN_WIDTH['py'])
587
+ border = ' '.join('=' * width for width in widths)
588
+
589
+ def render(cells: list[str]) -> str:
590
+ return ' '.join(
591
+ cell.ljust(width) for cell, width in zip(cells, widths, strict=True)
592
+ ).rstrip()
593
+
594
+ lines = [border]
595
+ for rows in (head, body):
596
+ if rows:
597
+ lines.extend(render(cells) for cells in rows)
598
+ lines.append(border)
599
+ return lines
600
+
601
+
602
+ def _table_segment(node: nodes.table, ctx: _RenderContext) -> list[Segment]:
603
+ """Convert a table to an RST simple table in ``.py``, a Markdown one in ``.ipynb``.
604
+
605
+ Each cell is flattened to a single line, the caption sits above.
606
+ """
607
+ head, body = _table_grid(node, ctx)
608
+ if not head and not body:
609
+ return []
610
+ lines: list[str] = []
611
+ caption = next((child for child in node.children if isinstance(child, nodes.title)), None)
612
+ if caption is not None and (text := _render_inline(caption, ctx).strip()):
613
+ _add_comment(lines, text)
614
+ lines.append('#')
615
+ render = _markdown_table_lines if ctx.fmt == 'ipynb' else _simple_table_lines
616
+ for line in render(head, body):
617
+ _add_comment(lines, line)
618
+ return [('directive', lines)]
619
+
620
+
486
621
  def _convert_admonition(
487
622
  node: nodes.Element, label: str, ctx: _RenderContext, *, skip_first_title: bool = False
488
623
  ) -> list[Segment]:
489
624
  """Convert an admonition-like container to a ``# LABEL:`` directive segment."""
490
- inner_ctx = replace(ctx, in_see_also=True) if label.upper() == 'SEE ALSO' else ctx
491
625
  inner: list[Segment] = [('text', [f'# {label}:'])]
492
626
  for child in node.children:
493
627
  if skip_first_title and isinstance(child, nodes.title):
494
628
  continue
495
- inner.extend(_convert_node(child, inner_ctx))
629
+ inner.extend(_convert_node(child, ctx))
496
630
  lines = _indent_label_content(_join_segments(inner), ctx.fmt)
497
631
  return [('directive', lines)]
498
632
 
@@ -505,19 +639,20 @@ def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
505
639
  return []
506
640
  if isinstance(node, nodes.doctest_block):
507
641
  return _convert_doctest_block(node)
508
- if isinstance(node, nodes.literal_block):
642
+ if isinstance(node, nodes.literal_block) or _is_gallery_code_block(node):
509
643
  return _convert_literal_block(node)
510
644
  if type(node) in _ADMONITION_LABELS:
511
- if _is_see_also_type(node) and not ctx.show_see_also:
645
+ if _is_see_also_type(node):
646
+ # "See Also" content is always dropped -- see _build_segments
647
+ # for the bare-rubric form and _is_see_also_section below for
648
+ # the hand-written-heading form.
512
649
  return []
513
650
  return _convert_admonition(node, _ADMONITION_LABELS[type(node)], ctx)
514
651
  if _is_see_also_section(node):
515
652
  # a hand-written "See Also\n--------" heading nests as a full
516
- # section rather than a flat sibling - treat it like the
517
- # ``.. seealso::`` directive it's standing in for.
518
- if not ctx.show_see_also:
519
- return []
520
- return _convert_admonition(node, 'SEE ALSO', ctx, skip_first_title=True)
653
+ # section rather than a flat sibling - dropped the same as any
654
+ # other "See Also" form.
655
+ return []
521
656
  if isinstance(node, nodes.admonition):
522
657
  # generic ``.. admonition:: Custom Title`` - use its own title as the label
523
658
  title_node = node.next_node(nodes.title)
@@ -536,6 +671,8 @@ def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
536
671
  return _definition_segment(node, ctx)
537
672
  if isinstance(node, (nodes.bullet_list, nodes.enumerated_list)):
538
673
  return _list_segment(node, ctx)
674
+ if isinstance(node, nodes.table):
675
+ return _table_segment(node, ctx)
539
676
  if isinstance(node, (*_CONTAINER_TYPES, nodes.section)):
540
677
  # nodes.section (gallery mode only): a sphinx-gallery ``# %%`` cell
541
678
  # with its own RST heading is a *sibling* section at the document
@@ -594,42 +731,6 @@ def _is_see_also_type(node: nodes.Node) -> bool:
594
731
  return type(node) in _ADMONITION_LABELS and _ADMONITION_LABELS[type(node)] == 'SEE ALSO'
595
732
 
596
733
 
597
- def _see_also_in_desc_content(desc: addnodes.desc) -> nodes.Node | None:
598
- """Find a "See Also" admonition-type node among a ``desc`` node's own ``desc_content``."""
599
- content = next(
600
- (child for child in desc.children if isinstance(child, addnodes.desc_content)), None
601
- )
602
- if content is None:
603
- return None
604
- return next((child for child in content.children if _is_see_also_type(child)), None)
605
-
606
-
607
- def _find_external_see_also(
608
- parent: nodes.Element, start: int, end: int, heading: nodes.Node
609
- ) -> nodes.Node | None:
610
- """Find a "See Also" part sited outside the normal ``[start, end)`` span.
611
-
612
- numpydoc's own "See Also" field is canonically reordered to sit before
613
- "Examples", as a sibling under ``parent``. If "Examples" was written as
614
- a real RST heading and hoisted out of its own ``desc_content`` to page
615
- level (see ``_enclosing_descs``), "See Also" -- not a ``nodes.section``,
616
- so untouched by that hoisting -- stays behind there instead, in the
617
- ``desc_content`` of the one ``desc`` that used to contain this heading.
618
- Only that nearest enclosing ``desc`` is checked -- not every one
619
- ``_enclosing_descs`` yields -- or a page listing several documented
620
- objects in a row would leak one object's "See Also" onto another's.
621
- """
622
- for i, child in enumerate(parent.children):
623
- if start <= i < end:
624
- continue
625
- if _is_see_also_type(child):
626
- return child
627
- desc = next(_enclosing_descs(heading), None)
628
- if desc is not None:
629
- return _see_also_in_desc_content(desc)
630
- return None
631
-
632
-
633
734
  def _examples_spans(doctree: nodes.document) -> list[tuple[nodes.Element, int, int, nodes.Node]]:
634
735
  """Find every "Examples" heading's content span.
635
736
 
@@ -868,20 +969,14 @@ def _build_segments(nodes_in_span: list[nodes.Node], ctx: _RenderContext) -> lis
868
969
  """Convert a span's nodes into segments.
869
970
 
870
971
  A bare ``.. rubric:: See Also`` heading isn't wrapped in a container the
871
- way ``.. seealso::`` or a nested section are: everything after it gets
872
- gathered into one merged directive segment instead.
972
+ way ``.. seealso::`` or a nested section are, so everything after it is
973
+ just as much "See Also" content as the heading itself, with no node of
974
+ its own marking where it ends -- dropping the heading and stopping
975
+ there drops all of it.
873
976
  """
874
977
  segments: list[Segment] = []
875
- for i, node in enumerate(nodes_in_span):
978
+ for node in nodes_in_span:
876
979
  if isinstance(node, nodes.rubric) and _is_see_also_heading(node):
877
- if not ctx.show_see_also:
878
- break
879
- inner_ctx = replace(ctx, in_see_also=True)
880
- inner: list[Segment] = [('text', ['# SEE ALSO:'])]
881
- for later_node in nodes_in_span[i + 1 :]:
882
- inner.extend(_convert_node(later_node, inner_ctx))
883
- lines = _indent_label_content(_join_segments(inner), ctx.fmt)
884
- segments.append(('directive', lines))
885
980
  break
886
981
  segments.extend(_convert_node(node, ctx))
887
982
  return segments
@@ -896,7 +991,6 @@ def _build_download_entries(
896
991
  formats: list[str],
897
992
  footer: str | None,
898
993
  link_labels: dict[str, str],
899
- include_see_also: bool,
900
994
  ) -> list[tuple[str, str]]:
901
995
  """Convert a span of nodes into written ``.py``/``.ipynb`` files, per ``formats``.
902
996
 
@@ -909,7 +1003,7 @@ def _build_download_entries(
909
1003
  file's own header is the one heading every generated file has exactly
910
1004
  one of (see ``_title_underline_segment``).
911
1005
  """
912
- py_ctx = _RenderContext(app=app, docname=docname, fmt='py', show_see_also=include_see_also)
1006
+ py_ctx = _RenderContext(app=app, docname=docname, fmt='py')
913
1007
  py_segments = _build_segments(nodes_in_span, py_ctx)
914
1008
 
915
1009
  if not any(kind == 'code' for kind, _lines in py_segments):
@@ -930,9 +1024,7 @@ def _build_download_entries(
930
1024
  if fmt == 'py':
931
1025
  rel_path = _write_source(app, name, source)
932
1026
  else:
933
- ipynb_ctx = _RenderContext(
934
- app=app, docname=docname, fmt='ipynb', show_see_also=include_see_also
935
- )
1027
+ ipynb_ctx = _RenderContext(app=app, docname=docname, fmt='ipynb')
936
1028
  ipynb_segments = _build_segments(nodes_in_span, ipynb_ctx)
937
1029
  ipynb_header = _title_underline_segment(header_title, 1, 'ipynb')
938
1030
  # Converted separately from the footer and concatenated, rather
@@ -958,14 +1050,9 @@ def _process_span(
958
1050
  formats: list[str],
959
1051
  footer: str | None,
960
1052
  link_labels: dict[str, str],
961
- include_see_also: bool,
962
1053
  ) -> None:
963
1054
  """Convert one Examples span and insert download link(s) if it has real code."""
964
1055
  nodes_in_span = list(parent.children[start:end])
965
- if include_see_also:
966
- external_see_also = _find_external_see_also(parent, start, end, heading)
967
- if external_see_also is not None:
968
- nodes_in_span.append(external_see_also)
969
1056
 
970
1057
  name = _qualified_name_for(heading, docname, counter)
971
1058
  entries = _build_download_entries(
@@ -977,7 +1064,6 @@ def _process_span(
977
1064
  formats,
978
1065
  footer,
979
1066
  link_labels,
980
- include_see_also,
981
1067
  )
982
1068
  if not entries:
983
1069
  return
@@ -1058,7 +1144,6 @@ def _process_gallery_page(
1058
1144
  formats: list[str],
1059
1145
  footer: str | None,
1060
1146
  link_labels: dict[str, str],
1061
- include_see_also: bool,
1062
1147
  ) -> None:
1063
1148
  """Replace a sphinx-gallery page's own download footer with converted downloads.
1064
1149
 
@@ -1101,7 +1186,6 @@ def _process_gallery_page(
1101
1186
  formats,
1102
1187
  footer,
1103
1188
  link_labels,
1104
- include_see_also,
1105
1189
  )
1106
1190
  if not entries:
1107
1191
  return
@@ -1127,12 +1211,9 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
1127
1211
  formats = conf['formats']
1128
1212
  footer = conf['footer']
1129
1213
  link_labels = conf['link_labels']
1130
- include_see_also = conf['include_see_also']
1131
1214
 
1132
1215
  if conf['gallery_downloads']:
1133
- _process_gallery_page(
1134
- app, docname, doctree, position, formats, footer, link_labels, include_see_also
1135
- )
1216
+ _process_gallery_page(app, docname, doctree, position, formats, footer, link_labels)
1136
1217
 
1137
1218
  # Process spans per shared parent, last to first.
1138
1219
  spans = _examples_spans(doctree)
@@ -1152,7 +1233,6 @@ def _process_doctree(app: Sphinx, doctree: nodes.document, docname: str) -> None
1152
1233
  formats,
1153
1234
  footer,
1154
1235
  link_labels,
1155
- include_see_also,
1156
1236
  )
1157
1237
 
1158
1238
 
@@ -1170,7 +1250,6 @@ _CONF_DEFAULTS: dict[str, object] = {
1170
1250
  'gallery_downloads': False,
1171
1251
  'footer': _DEFAULT_FOOTER,
1172
1252
  'link_labels': _DEFAULT_LINK_LABELS,
1173
- 'include_see_also': True,
1174
1253
  }
1175
1254
 
1176
1255
 
@@ -1188,7 +1267,7 @@ def _coerce_conf_value(key: str, value: object) -> object:
1188
1267
  return value
1189
1268
  if key == 'formats':
1190
1269
  return value.split(',')
1191
- if key in ('gallery_downloads', 'include_see_also'):
1270
+ if key == 'gallery_downloads':
1192
1271
  if value == '0':
1193
1272
  return False
1194
1273
  if value == '1':
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.4.1'
22
- __version_tuple__ = version_tuple = (0, 4, 1)
21
+ __version__ = version = '0.6.0'
22
+ __version_tuple__ = version_tuple = (0, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gfbf3dad3c'
24
+ __commit_id__ = commit_id = 'g7280fbdd2'