sphinx-examples-as-code 0.5.0__tar.gz → 0.6.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 (43) hide show
  1. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/.github/workflows/ci.yml +6 -6
  2. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/PKG-INFO +20 -5
  3. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/README.md +17 -4
  4. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/pyproject.toml +3 -1
  5. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code/__init__.py +151 -5
  6. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code/_version.py +3 -3
  7. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code.egg-info/PKG-INFO +20 -5
  8. sphinx_examples_as_code-0.6.1/sphinx_examples_as_code.egg-info/requires.txt +4 -0
  9. sphinx_examples_as_code-0.6.1/sphinx_examples_as_code.egg-info/scm_version.json +8 -0
  10. sphinx_examples_as_code-0.6.1/tests/gallery_fixture/index.rst +17 -0
  11. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/test_sphinx_examples_as_code.py +237 -0
  12. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/test_tinypages.py +50 -0
  13. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/tinypages/docstring_cases.py +38 -0
  14. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/tinypages/docstring_cases.rst +4 -0
  15. sphinx_examples_as_code-0.5.0/sphinx_examples_as_code.egg-info/requires.txt +0 -1
  16. sphinx_examples_as_code-0.5.0/sphinx_examples_as_code.egg-info/scm_version.json +0 -8
  17. sphinx_examples_as_code-0.5.0/tests/gallery_fixture/index.rst +0 -7
  18. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/.github/dependabot.yml +0 -0
  19. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/.github/release.yml +0 -0
  20. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/.gitignore +0 -0
  21. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/.pre-commit-config.yaml +0 -0
  22. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/LICENSE +0 -0
  23. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/setup.cfg +0 -0
  24. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code.egg-info/SOURCES.txt +0 -0
  25. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
  26. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code.egg-info/scm_file_list.json +0 -0
  27. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
  28. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/gallery_fixture/conf.py +0 -0
  29. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
  30. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
  31. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/heading_level_fixture/conf.py +0 -0
  32. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +0 -0
  33. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/heading_level_fixture/examples/plot_reused_level.py +0 -0
  34. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/heading_level_fixture/examples/plot_three_levels.py +0 -0
  35. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/heading_level_fixture/index.rst +0 -0
  36. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/single_function_fixture/conf.py +0 -0
  37. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/single_function_fixture/index.rst +0 -0
  38. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/single_function_fixture/mymodule.py +0 -0
  39. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/test_gallery_downloads.py +0 -0
  40. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/test_heading_level_reuse.py +0 -0
  41. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/test_single_function_page.py +0 -0
  42. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/tinypages/conf.py +0 -0
  43. {sphinx_examples_as_code-0.5.0 → sphinx_examples_as_code-0.6.1}/tests/tinypages/index.rst +0 -0
@@ -22,10 +22,10 @@ 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
- - run: uv sync --group dev
28
+ - run: uv sync --group dev --extra gallery
29
29
  - run: uv run pre-commit run --all-files --show-diff-on-failure
30
30
  env:
31
31
  # This hook exists to stop *local* commits straight to main; it
@@ -43,13 +43,13 @@ 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
- - run: uv sync --group dev --python ${{ matrix.python-version }}
52
+ - run: uv sync --group dev --extra gallery --python ${{ matrix.python-version }}
53
53
  - run: uv run pytest tests/ --cov --cov-report=xml
54
54
  - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
55
55
  with:
@@ -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.5.0
3
+ Version: 0.6.1
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
@@ -23,6 +23,8 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pyvista>=0.48
26
+ Provides-Extra: gallery
27
+ Requires-Dist: sphinx-gallery<0.23,>=0.22; extra == "gallery"
26
28
  Dynamic: license-file
27
29
 
28
30
  # sphinx-examples-as-code
@@ -39,6 +41,13 @@ Pages or docstrings without an Examples section are left completely untouched. A
39
41
  pip install sphinx-examples-as-code
40
42
  ```
41
43
 
44
+ Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, which the
45
+ `gallery` extra pins:
46
+
47
+ ```bash
48
+ pip install 'sphinx-examples-as-code[gallery]'
49
+ ```
50
+
42
51
  Add it to your Sphinx `conf.py`:
43
52
 
44
53
  ```python
@@ -123,6 +132,10 @@ What happens to the content of an Examples section:
123
132
  backticks.
124
133
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
125
134
  `#` comment.
135
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
136
+ table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
137
+ line, and a column left entirely empty -- one holding only images, say -- is dropped
138
+ along with its heading.
126
139
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
127
140
  line are dropped entirely.
128
141
  - "See Also" content is always dropped.
@@ -175,16 +188,18 @@ styles:
175
188
 
176
189
  Two things worth knowing before turning this on:
177
190
 
178
- - It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
179
- classes its own theming depends on), not a documented extension API. A future
180
- sphinx-gallery release could shift that structure without warning.
191
+ - It's built against sphinx-gallery's own doctree output the `sphx-glr-*` CSS classes
192
+ its own theming depends on, and the private node type holding a highlighted code
193
+ block — not a documented extension API. A future sphinx-gallery release could shift
194
+ that structure without warning. Only sphinx-gallery 0.22.x is tested, so the `gallery`
195
+ extra caps there rather than letting the next release quietly produce no downloads.
181
196
  - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
182
197
  `_downloads/`, even though nothing on the page links to them anymore.
183
198
 
184
199
  ## Development
185
200
 
186
201
  ```bash
187
- uv sync --group dev
202
+ uv sync --group dev --extra gallery
188
203
  uv run pytest
189
204
  uv run pre-commit run --all-files
190
205
  ```
@@ -12,6 +12,13 @@ Pages or docstrings without an Examples section are left completely untouched. A
12
12
  pip install sphinx-examples-as-code
13
13
  ```
14
14
 
15
+ Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, which the
16
+ `gallery` extra pins:
17
+
18
+ ```bash
19
+ pip install 'sphinx-examples-as-code[gallery]'
20
+ ```
21
+
15
22
  Add it to your Sphinx `conf.py`:
16
23
 
17
24
  ```python
@@ -96,6 +103,10 @@ What happens to the content of an Examples section:
96
103
  backticks.
97
104
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
98
105
  `#` comment.
106
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
107
+ table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
108
+ line, and a column left entirely empty -- one holding only images, say -- is dropped
109
+ along with its heading.
99
110
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
100
111
  line are dropped entirely.
101
112
  - "See Also" content is always dropped.
@@ -148,16 +159,18 @@ styles:
148
159
 
149
160
  Two things worth knowing before turning this on:
150
161
 
151
- - It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
152
- classes its own theming depends on), not a documented extension API. A future
153
- sphinx-gallery release could shift that structure without warning.
162
+ - It's built against sphinx-gallery's own doctree output the `sphx-glr-*` CSS classes
163
+ its own theming depends on, and the private node type holding a highlighted code
164
+ block — not a documented extension API. A future sphinx-gallery release could shift
165
+ that structure without warning. Only sphinx-gallery 0.22.x is tested, so the `gallery`
166
+ extra caps there rather than letting the next release quietly produce no downloads.
154
167
  - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
155
168
  `_downloads/`, even though nothing on the page links to them anymore.
156
169
 
157
170
  ## Development
158
171
 
159
172
  ```bash
160
- uv sync --group dev
173
+ uv sync --group dev --extra gallery
161
174
  uv run pytest
162
175
  uv run pre-commit run --all-files
163
176
  ```
@@ -11,7 +11,6 @@ dev = [
11
11
  'pytest-cov',
12
12
  'sphinx',
13
13
  'sphinx-design',
14
- 'sphinx-gallery>=0.21.0',
15
14
  'trame-pyvista',
16
15
  ]
17
16
 
@@ -41,6 +40,9 @@ name = 'sphinx-examples-as-code'
41
40
  readme = 'README.md'
42
41
  requires-python = '>=3.10'
43
42
 
43
+ [project.optional-dependencies]
44
+ gallery = ['sphinx-gallery>=0.22,<0.23']
45
+
44
46
  [project.urls]
45
47
  Homepage = 'https://github.com/pyvista/sphinx-examples-as-code'
46
48
 
@@ -91,6 +91,22 @@ _ADMONITION_LABELS = {
91
91
 
92
92
  _PYTHON_LANGUAGES = ('python', 'py', 'python3')
93
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
+
94
110
  # A chunk of generated lines tagged with how it should be spaced relative to
95
111
  # its neighbors when segments are joined (see ``_join_segments``):
96
112
  # 'code' real Python source
@@ -319,16 +335,34 @@ def _convert_doctest_block(node: nodes.doctest_block) -> list[Segment]:
319
335
  return [('code', lines)]
320
336
 
321
337
 
322
- 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]:
323
354
  """Convert a ``.. code-block::``. Python blocks stay code, others become comments."""
324
- 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()
325
359
  # Case-insensitive: sphinx-gallery emits ``.. code-block:: Python`` (capitalized).
326
360
  if language.lower() in _PYTHON_LANGUAGES:
327
- lines = [_clean_code_comment(line) for line in node.astext().splitlines()]
361
+ lines = [_clean_code_comment(line) for line in source.splitlines()]
328
362
  while lines and not lines[-1].strip():
329
363
  lines.pop()
330
364
  return [('code', lines)] if lines else []
331
- text = node.astext().strip()
365
+ text = source.strip()
332
366
  if not text:
333
367
  return []
334
368
  comment_lines: list[str] = []
@@ -474,6 +508,116 @@ def _list_segment(
474
508
  return [('directive', lines)] if lines else []
475
509
 
476
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
+
477
621
  def _convert_admonition(
478
622
  node: nodes.Element, label: str, ctx: _RenderContext, *, skip_first_title: bool = False
479
623
  ) -> list[Segment]:
@@ -495,7 +639,7 @@ def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
495
639
  return []
496
640
  if isinstance(node, nodes.doctest_block):
497
641
  return _convert_doctest_block(node)
498
- if isinstance(node, nodes.literal_block):
642
+ if isinstance(node, nodes.literal_block) or _is_gallery_code_block(node):
499
643
  return _convert_literal_block(node)
500
644
  if type(node) in _ADMONITION_LABELS:
501
645
  if _is_see_also_type(node):
@@ -527,6 +671,8 @@ def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
527
671
  return _definition_segment(node, ctx)
528
672
  if isinstance(node, (nodes.bullet_list, nodes.enumerated_list)):
529
673
  return _list_segment(node, ctx)
674
+ if isinstance(node, nodes.table):
675
+ return _table_segment(node, ctx)
530
676
  if isinstance(node, (*_CONTAINER_TYPES, nodes.section)):
531
677
  # nodes.section (gallery mode only): a sphinx-gallery ``# %%`` cell
532
678
  # with its own RST heading is a *sibling* section at the document
@@ -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.5.0'
22
- __version_tuple__ = version_tuple = (0, 5, 0)
21
+ __version__ = version = '0.6.1'
22
+ __version_tuple__ = version_tuple = (0, 6, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g7bd70a33e'
24
+ __commit_id__ = commit_id = 'ge9f0565b9'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-examples-as-code
3
- Version: 0.5.0
3
+ Version: 0.6.1
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
@@ -23,6 +23,8 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pyvista>=0.48
26
+ Provides-Extra: gallery
27
+ Requires-Dist: sphinx-gallery<0.23,>=0.22; extra == "gallery"
26
28
  Dynamic: license-file
27
29
 
28
30
  # sphinx-examples-as-code
@@ -39,6 +41,13 @@ Pages or docstrings without an Examples section are left completely untouched. A
39
41
  pip install sphinx-examples-as-code
40
42
  ```
41
43
 
44
+ Sphinx-Gallery integration is opt-in and needs a compatible sphinx-gallery, which the
45
+ `gallery` extra pins:
46
+
47
+ ```bash
48
+ pip install 'sphinx-examples-as-code[gallery]'
49
+ ```
50
+
42
51
  Add it to your Sphinx `conf.py`:
43
52
 
44
53
  ```python
@@ -123,6 +132,10 @@ What happens to the content of an Examples section:
123
132
  backticks.
124
133
  - Everything else text-bearing (prose, captions, other non-Python code) becomes a plain
125
134
  `#` comment.
135
+ - A table becomes an aligned text table, its caption on the line above: an RST simple
136
+ table in `.py`, a Markdown pipe table in `.ipynb`. Each cell is flattened to a single
137
+ line, and a column left entirely empty -- one holding only images, say -- is dropped
138
+ along with its heading.
126
139
  - Figures/images, raw HTML, sphinx-design dropdowns/tab-sets, and sphinx-tags' `.. tags::`
127
140
  line are dropped entirely.
128
141
  - "See Also" content is always dropped.
@@ -175,16 +188,18 @@ styles:
175
188
 
176
189
  Two things worth knowing before turning this on:
177
190
 
178
- - It's built against sphinx-gallery's own RST/HTML output (the same `sphx-glr-*` CSS
179
- classes its own theming depends on), not a documented extension API. A future
180
- sphinx-gallery release could shift that structure without warning.
191
+ - It's built against sphinx-gallery's own doctree output the `sphx-glr-*` CSS classes
192
+ its own theming depends on, and the private node type holding a highlighted code
193
+ block — not a documented extension API. A future sphinx-gallery release could shift
194
+ that structure without warning. Only sphinx-gallery 0.22.x is tested, so the `gallery`
195
+ extra caps there rather than letting the next release quietly produce no downloads.
181
196
  - sphinx-gallery's own `.py`/`.ipynb`/`.zip` downloads still end up copied into
182
197
  `_downloads/`, even though nothing on the page links to them anymore.
183
198
 
184
199
  ## Development
185
200
 
186
201
  ```bash
187
- uv sync --group dev
202
+ uv sync --group dev --extra gallery
188
203
  uv run pytest
189
204
  uv run pre-commit run --all-files
190
205
  ```
@@ -0,0 +1,4 @@
1
+ pyvista>=0.48
2
+
3
+ [gallery]
4
+ sphinx-gallery<0.23,>=0.22
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.6.1",
3
+ "distance": 0,
4
+ "node": "ge9f0565b9b658b5a3927b2ef0deae82a4108696d",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-22"
8
+ }
@@ -0,0 +1,17 @@
1
+ Gallery Downloads Fixture
2
+ =========================
3
+
4
+ .. toctree::
5
+ :maxdepth: 2
6
+
7
+ auto_examples/index
8
+
9
+ .. Documented purely so that sphinx-gallery has something to link the example's
10
+ code to: it wraps every name it resolves in a reference, and ``x``/``y``/``z``
11
+ are inferred as ``builtins.int``, which resolves to this entry (a builtin is
12
+ looked up under its bare name). Without it the code these tests stitch back
13
+ together never takes its linked shape.
14
+
15
+ .. py:class:: int
16
+
17
+ Stand-in for the builtin, so the example's integers resolve to something.
@@ -16,6 +16,7 @@ from docutils.core import publish_doctree
16
16
  import pytest
17
17
  from sphinx import addnodes
18
18
  from sphinx.errors import ConfigError
19
+ from sphinx_gallery._doctree_links import code_links_block
19
20
 
20
21
  import sphinx_examples_as_code as seac
21
22
 
@@ -389,6 +390,94 @@ def test_convert_literal_block_python_whitespace_only():
389
390
  assert seac._convert_literal_block(node) == []
390
391
 
391
392
 
393
+ # ---------------------------------------------------------------------------
394
+ # _convert_literal_block: sphinx-gallery's own code-block node
395
+ # ---------------------------------------------------------------------------
396
+
397
+
398
+ def _token(text: str, css_class: str | None = None) -> nodes.Node:
399
+ """Build one pygments token, as sphinx-gallery emits it.
400
+
401
+ A classified token becomes an ``inline`` carrying the pygments short
402
+ class; the root token type is left unwrapped, as bare text.
403
+ """
404
+ if css_class is None:
405
+ return nodes.Text(text)
406
+ return nodes.inline(text, text, classes=[css_class])
407
+
408
+
409
+ def _linked_token(text: str, css_class: str) -> nodes.reference:
410
+ """Build a name sphinx-gallery resolved to a documented object, so wrapped in a link."""
411
+ return nodes.reference('', '', _token(text, css_class), refuri='index.html#int')
412
+
413
+
414
+ def _gallery_code_block(*children: nodes.Node, lang: str = 'Python') -> nodes.Element:
415
+ """Build one of sphinx-gallery's tokenized code blocks out of ``children``."""
416
+ node = code_links_block(lang=lang)
417
+ node += list(children)
418
+ return node
419
+
420
+
421
+ def test_gallery_code_block_contract():
422
+ """Pin down what the extension assumes about sphinx-gallery's own node.
423
+
424
+ It is private to sphinx-gallery, and it replaces every Python code block
425
+ on a gallery page before this extension ever sees the doctree -- so a
426
+ change to any of these three leaves each example a block of comments
427
+ with no download link at all. Asserted here, where a failure names the
428
+ assumption that moved, rather than only surfacing further downstream as
429
+ a mangled download.
430
+ """
431
+ node = code_links_block(lang='Python')
432
+ node += [nodes.inline('x', 'x', classes=['n']), nodes.Text(' = 1')]
433
+
434
+ assert node.tagname == seac._GALLERY_CODE_BLOCK_TAGNAME
435
+ assert node['lang'] == 'Python' # not 'language', as a literal_block spells it
436
+ assert node.astext() == 'x = 1' # children concatenate, with nothing between
437
+
438
+
439
+ def test_convert_literal_block_gallery_code_block():
440
+ # The source arrives as a stream of per-token nodes of three different
441
+ # kinds -- bare text, a classified inline, and a reference around a name
442
+ # that resolved -- so it has to be stitched back together rather than
443
+ # just read off the node.
444
+ node = _gallery_code_block(
445
+ _token('x', 'n'),
446
+ _token(' = '),
447
+ _token('1', 'mi'),
448
+ _token('\n', 'w'),
449
+ _token('print', 'nb'),
450
+ _token('(', 'p'),
451
+ _linked_token('x', 'n'),
452
+ _token(')', 'p'),
453
+ _token('\n', 'w'),
454
+ )
455
+ assert seac._convert_literal_block(node) == [('code', ['x = 1', 'print(x)'])]
456
+
457
+
458
+ def test_convert_literal_block_gallery_code_block_reads_lang_not_language():
459
+ # Read from 'language' (which this node type doesn't set) the language
460
+ # comes back empty, and real code quietly degrades into comments.
461
+ node = _gallery_code_block(_token('x = 1'))
462
+ assert 'language' not in node
463
+ assert seac._convert_literal_block(node) == [('code', ['x = 1'])]
464
+
465
+
466
+ def test_convert_literal_block_gallery_code_block_drops_line_numbers():
467
+ # With line numbers on, sphinx-gallery interleaves them as inline nodes
468
+ # of their own -- page furniture that was never part of the source, so
469
+ # it must not be baked into the generated script.
470
+ node = _gallery_code_block(
471
+ _token(' 9', 'linenos'),
472
+ _token('x', 'n'),
473
+ _token(' = 1\n'),
474
+ _token('10', 'linenos'),
475
+ _token('y', 'n'),
476
+ _token(' = 2\n'),
477
+ )
478
+ assert seac._convert_literal_block(node) == [('code', ['x = 1', 'y = 2'])]
479
+
480
+
392
481
  # ---------------------------------------------------------------------------
393
482
  # _clean_stray_rst_markup / _clean_code_comment
394
483
  # ---------------------------------------------------------------------------
@@ -669,6 +758,136 @@ def test_list_segment_ipynb_link_item_renders_as_markdown_link():
669
758
  ]
670
759
 
671
760
 
761
+ # ---------------------------------------------------------------------------
762
+ # _table_segment
763
+ # ---------------------------------------------------------------------------
764
+
765
+ _SIMPLE_TABLE = """
766
+ .. list-table::
767
+ :header-rows: 1
768
+
769
+ * - Style
770
+ - Description
771
+ * - ``"-"``
772
+ - Solid
773
+ """
774
+
775
+ _IMAGE_COLUMN_TABLE = """
776
+ .. list-table::
777
+ :header-rows: 1
778
+
779
+ * - Style
780
+ - Example
781
+ * - Solid
782
+ - .. image:: solid.png
783
+ """
784
+
785
+ _BLANK_BODY_TABLE = """
786
+ .. list-table::
787
+ :header-rows: 1
788
+
789
+ * - Style
790
+ - Example
791
+ * -
792
+ -
793
+ """
794
+
795
+ _SPANNING_TABLE = """
796
+ +------+------+
797
+ | a | b |
798
+ +------+------+
799
+ | spans both |
800
+ +------+------+
801
+ """
802
+
803
+
804
+ def test_table_segment_py_is_an_rst_simple_table():
805
+ doctree = _parse(_SIMPLE_TABLE)
806
+ assert seac._convert_node(doctree[0], _ctx()) == [
807
+ (
808
+ 'directive',
809
+ [
810
+ '# ===== ===========',
811
+ '# Style Description',
812
+ '# ===== ===========',
813
+ '# `"-"` Solid',
814
+ '# ===== ===========',
815
+ ],
816
+ )
817
+ ]
818
+
819
+
820
+ def test_table_segment_ipynb_is_a_markdown_table():
821
+ doctree = _parse(_SIMPLE_TABLE)
822
+ assert seac._convert_node(doctree[0], _ctx(fmt='ipynb')) == [
823
+ (
824
+ 'directive',
825
+ [
826
+ '# | Style | Description |',
827
+ '# | ----- | ----------- |',
828
+ '# | `"-"` | Solid |',
829
+ ],
830
+ )
831
+ ]
832
+
833
+
834
+ def test_table_segment_caption_sits_above_the_table():
835
+ doctree = _parse('.. list-table:: Line styles\n\n * - one\n')
836
+ kind, lines = seac._convert_node(doctree[0], _ctx())[0]
837
+ assert kind == 'directive'
838
+ assert lines[:2] == ['# Line styles', '#']
839
+
840
+
841
+ def test_table_segment_header_less_table_has_no_inner_border():
842
+ doctree = _parse('.. list-table::\n\n * - a\n - b\n * - c\n - d\n')
843
+ assert seac._convert_node(doctree[0], _ctx()) == [
844
+ ('directive', ['# = =', '# a b', '# c d', '# = ='])
845
+ ]
846
+
847
+
848
+ def test_table_segment_header_less_table_gets_a_blank_markdown_header():
849
+ doctree = _parse('.. list-table::\n\n * - a\n - b\n')
850
+ assert seac._convert_node(doctree[0], _ctx(fmt='ipynb')) == [
851
+ ('directive', ['# | | |', '# | --- | --- |', '# | a | b |'])
852
+ ]
853
+
854
+
855
+ def test_table_segment_image_only_column_is_dropped():
856
+ doctree = _parse(_IMAGE_COLUMN_TABLE)
857
+ _kind, lines = seac._convert_node(doctree[0], _ctx())[0]
858
+ assert lines == ['# =====', '# Style', '# =====', '# Solid', '# =====']
859
+
860
+
861
+ def test_table_segment_blank_body_row_is_dropped_with_every_column_kept():
862
+ doctree = _parse(_BLANK_BODY_TABLE)
863
+ _kind, lines = seac._convert_node(doctree[0], _ctx())[0]
864
+ assert lines == ['# ===== =======', '# Style Example', '# ===== =======']
865
+
866
+
867
+ def test_table_segment_column_span_pads_with_empty_cells():
868
+ doctree = _parse(_SPANNING_TABLE)
869
+ _kind, lines = seac._convert_node(doctree[0], _ctx())[0]
870
+ assert lines == ['# ========== =', '# a b', '# spans both', '# ========== =']
871
+
872
+
873
+ def test_table_segment_markdown_escapes_a_pipe_in_a_cell():
874
+ doctree = _parse('.. list-table::\n\n * - ``a | b``\n')
875
+ assert seac._convert_node(doctree[0], _ctx(fmt='ipynb')) == [
876
+ ('directive', ['# | |', '# | -------- |', r'# | `a \| b` |'])
877
+ ]
878
+
879
+
880
+ def test_table_segment_list_in_a_cell_separates_items():
881
+ doctree = _parse('.. list-table::\n\n * - - first\n - second\n')
882
+ assert seac._convert_node(doctree[0], _ctx()) == [
883
+ ('directive', ['# =============', '# first; second', '# ============='])
884
+ ]
885
+
886
+
887
+ def test_table_segment_empty_table_returns_nothing():
888
+ assert seac._convert_node(nodes.table(), _ctx()) == []
889
+
890
+
672
891
  # ---------------------------------------------------------------------------
673
892
  # _convert_admonition / _convert_node dispatch
674
893
  # ---------------------------------------------------------------------------
@@ -802,6 +1021,24 @@ def test_convert_node_literal_block_dispatch():
802
1021
  assert seac._convert_node(node, _ctx()) == [('code', ['x = 1'])]
803
1022
 
804
1023
 
1024
+ def test_convert_node_gallery_code_block_dispatch():
1025
+ # Unrecognized, this node falls through to the prose branch and the whole
1026
+ # example turns into comments -- leaving nothing the extension counts as
1027
+ # real code, and so no download link at all.
1028
+ node = _gallery_code_block(_token('x', 'n'), _token(' = '), _token('1', 'mi'))
1029
+
1030
+ assert seac._convert_node(node, _ctx()) == [('code', ['x = 1'])]
1031
+
1032
+
1033
+ def test_convert_node_gallery_code_block_script_out_still_skipped():
1034
+ # sphinx-gallery copies the original block's classes onto the node it
1035
+ # swaps in, so captured output it happened to rewrite stays excluded.
1036
+ node = _gallery_code_block(_token('3'))
1037
+ node['classes'] = ['sphx-glr-script-out']
1038
+
1039
+ assert seac._convert_node(node, _ctx()) == []
1040
+
1041
+
805
1042
  # ---------------------------------------------------------------------------
806
1043
  # _has_real_code
807
1044
  # ---------------------------------------------------------------------------
@@ -211,6 +211,56 @@ def test_docstring_examples_conversion(built: tuple[Path, list[Path]]):
211
211
  assert '3' not in combined_src # doctest output line dropped entirely, not commented
212
212
 
213
213
 
214
+ def test_table_becomes_an_rst_simple_table_in_py(built: tuple[Path, list[Path]]):
215
+ """A table converts to an aligned RST simple table, captioned above it."""
216
+ lines = _read(built[1], 'case_table.py').splitlines()
217
+ start = lines.index('# Line styles')
218
+ assert lines[start + 1] == '#'
219
+ assert lines[start + 2] == '# ====== ==========='
220
+ assert lines[start + 3] == '# Style Description'
221
+ assert lines[start + 4] == '# ====== ==========='
222
+ assert lines[start + 5] == '# `"-"` Solid'
223
+ assert lines[start + 6] == '# `"--"` Dashed'
224
+ assert lines[start + 7] == '# ====== ==========='
225
+ # the "Example" column holds nothing in any row, so it is dropped
226
+ assert 'Example' not in '\n'.join(lines[start : start + 8])
227
+
228
+
229
+ def test_table_becomes_a_markdown_table_in_ipynb(built_notebooks: list[Path]):
230
+ """The same table is a Markdown pipe table in a notebook's markdown cell."""
231
+ nb_path = next(p for p in built_notebooks if p.stem == 'docstring_cases_case_table')
232
+ notebook = json.loads(nb_path.read_text(encoding='utf-8'))
233
+ lines = [
234
+ line.rstrip()
235
+ for cell in notebook['cells']
236
+ if cell['cell_type'] == 'markdown'
237
+ for line in cell['source']
238
+ ]
239
+ start = lines.index('Line styles')
240
+ assert lines[start + 1] == ''
241
+ assert lines[start + 2] == '| Style | Description |'
242
+ assert lines[start + 3] == '| ------ | ----------- |'
243
+ assert lines[start + 4] == '| `"-"` | Solid |'
244
+ assert lines[start + 5] == '| `"--"` | Dashed |'
245
+
246
+
247
+ def test_header_less_table_gets_a_blank_markdown_header(built_notebooks: list[Path]):
248
+ """A Markdown pipe table needs a header row, so one is left blank."""
249
+ nb_path = next(
250
+ p for p in built_notebooks if p.stem == 'docstring_cases_case_table_without_header'
251
+ )
252
+ notebook = json.loads(nb_path.read_text(encoding='utf-8'))
253
+ lines = [
254
+ line.rstrip()
255
+ for cell in notebook['cells']
256
+ if cell['cell_type'] == 'markdown'
257
+ for line in cell['source']
258
+ ]
259
+ assert '| | |' in lines
260
+ assert '| ---- | ----- |' in lines
261
+ assert '| left | right |' in lines
262
+
263
+
214
264
  def test_no_doctest_output_included(built: tuple[Path, list[Path]]):
215
265
  """Doctest output lines should never appear anywhere -- only the input code."""
216
266
  src = _read(built[1], 'case_doctest_with_output')
@@ -45,6 +45,44 @@ def case_tabset():
45
45
  """
46
46
 
47
47
 
48
+ def case_table():
49
+ """Docstring with a table in its Examples section.
50
+
51
+ Examples
52
+ --------
53
+ >>> import sys
54
+
55
+ .. list-table:: Line styles
56
+ :header-rows: 1
57
+
58
+ * - Style
59
+ - Description
60
+ - Example
61
+ * - ``"-"``
62
+ - Solid
63
+ -
64
+ * - ``"--"``
65
+ - Dashed
66
+ -
67
+
68
+ """
69
+
70
+
71
+ def case_table_without_header():
72
+ """Docstring with a header-less table in its Examples section.
73
+
74
+ Examples
75
+ --------
76
+ >>> import sys
77
+
78
+ .. list-table::
79
+
80
+ * - left
81
+ - right
82
+
83
+ """
84
+
85
+
48
86
  def case_note():
49
87
  """Docstring with a note in its Examples section.
50
88
 
@@ -13,6 +13,10 @@ resolve to.
13
13
 
14
14
  .. autofunction:: docstring_cases.case_tabset
15
15
 
16
+ .. autofunction:: docstring_cases.case_table
17
+
18
+ .. autofunction:: docstring_cases.case_table_without_header
19
+
16
20
  .. autofunction:: docstring_cases.case_note
17
21
 
18
22
  .. autofunction:: docstring_cases.case_warning
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.5.0",
3
- "distance": 0,
4
- "node": "g7bd70a33e84d064efda810292fbddfbbe32209bb",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-20"
8
- }
@@ -1,7 +0,0 @@
1
- Gallery Downloads Fixture
2
- =========================
3
-
4
- .. toctree::
5
- :maxdepth: 2
6
-
7
- auto_examples/index