sphinx-examples-as-code 0.3.2__tar.gz → 0.3.3__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 (40) hide show
  1. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/PKG-INFO +6 -4
  2. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/README.md +5 -3
  3. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code/__init__.py +34 -2
  4. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code/_version.py +3 -3
  5. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/PKG-INFO +6 -4
  6. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/scm_version.json +2 -2
  7. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/test_sphinx_examples_as_code.py +75 -9
  8. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/test_tinypages.py +7 -7
  9. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/.github/dependabot.yml +0 -0
  10. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/.github/release.yml +0 -0
  11. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/.github/workflows/ci.yml +0 -0
  12. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/.gitignore +0 -0
  13. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/.pre-commit-config.yaml +0 -0
  14. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/LICENSE +0 -0
  15. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/pyproject.toml +0 -0
  16. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/setup.cfg +0 -0
  17. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/SOURCES.txt +0 -0
  18. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/dependency_links.txt +0 -0
  19. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/requires.txt +0 -0
  20. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/scm_file_list.json +0 -0
  21. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/sphinx_examples_as_code.egg-info/top_level.txt +0 -0
  22. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/gallery_fixture/conf.py +0 -0
  23. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/gallery_fixture/examples/GALLERY_HEADER.rst +0 -0
  24. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/gallery_fixture/examples/plot_minimal.py +0 -0
  25. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/gallery_fixture/index.rst +0 -0
  26. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/heading_level_fixture/conf.py +0 -0
  27. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/heading_level_fixture/examples/GALLERY_HEADER.rst +0 -0
  28. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/heading_level_fixture/examples/plot_reused_level.py +0 -0
  29. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/heading_level_fixture/examples/plot_three_levels.py +0 -0
  30. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/heading_level_fixture/index.rst +0 -0
  31. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/single_function_fixture/conf.py +0 -0
  32. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/single_function_fixture/index.rst +0 -0
  33. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/single_function_fixture/mymodule.py +0 -0
  34. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/test_gallery_downloads.py +0 -0
  35. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/test_heading_level_reuse.py +0 -0
  36. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/test_single_function_page.py +0 -0
  37. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/tinypages/conf.py +0 -0
  38. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/tinypages/docstring_cases.py +0 -0
  39. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/tinypages/docstring_cases.rst +0 -0
  40. {sphinx_examples_as_code-0.3.2 → sphinx_examples_as_code-0.3.3}/tests/tinypages/index.rst +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-examples-as-code
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -107,9 +107,11 @@ What happens to the content of an Examples section:
107
107
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
108
108
  comments, set off with blank lines on both sides like any other directive.
109
109
  - Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
110
- comment followed by their content as comments. "See Also" is recognized in any of its
111
- three forms (`.. seealso::`, a bare `.. rubric:: See Also`, or a hand-written `See
112
- Also` heading) and always renders the same way.
110
+ comment followed by their content as comments, indented one level under the label in
111
+ `.py` (not `.ipynb`, where leading whitespace within a paragraph has no visible effect
112
+ once rendered). "See Also" is recognized in any of its three forms (`.. seealso::`, a
113
+ bare `.. rubric:: See Also`, or a hand-written `See Also` heading) and always renders
114
+ the same way.
113
115
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
114
116
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
115
117
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
@@ -80,9 +80,11 @@ What happens to the content of an Examples section:
80
80
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
81
81
  comments, set off with blank lines on both sides like any other directive.
82
82
  - Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
83
- comment followed by their content as comments. "See Also" is recognized in any of its
84
- three forms (`.. seealso::`, a bare `.. rubric:: See Also`, or a hand-written `See
85
- Also` heading) and always renders the same way.
83
+ comment followed by their content as comments, indented one level under the label in
84
+ `.py` (not `.ipynb`, where leading whitespace within a paragraph has no visible effect
85
+ once rendered). "See Also" is recognized in any of its three forms (`.. seealso::`, a
86
+ bare `.. rubric:: See Also`, or a hand-written `See Also` heading) and always renders
87
+ the same way.
86
88
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
87
89
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
88
90
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
@@ -411,6 +411,36 @@ def _heading_level(title: nodes.title) -> int:
411
411
  return min(level, _MAX_HEADING_LEVEL)
412
412
 
413
413
 
414
+ #: Extra indent for an admonition's content, under its ``# LABEL:`` line --
415
+ #: ``.py`` only (see ``_convert_admonition``).
416
+ _ADMONITION_CONTENT_INDENT = ' '
417
+
418
+
419
+ def _indent_comment_line(line: str) -> str:
420
+ """Indent a ``# text`` comment line's content by ``_ADMONITION_CONTENT_INDENT``.
421
+
422
+ A bare ``#`` (blank line within a comment block) or a blank separator
423
+ line (``''``) has no content to indent, so both pass through unchanged
424
+ -- as does a raw code line (no ``#`` prefix at all), left alone so
425
+ indenting an admonition's content never touches real Python source.
426
+ """
427
+ if not line.startswith('# '):
428
+ return line
429
+ return f'# {_ADMONITION_CONTENT_INDENT}{line.removeprefix("# ")}'
430
+
431
+
432
+ def _indent_label_content(lines: list[str], fmt: str) -> list[str]:
433
+ """Indent every line after the first (a ``# LABEL:`` line) under it.
434
+
435
+ ``.py`` only: in ``.ipynb``, once rendered as markdown, leading
436
+ whitespace within a paragraph collapses to nothing (confirmed against a
437
+ real CommonMark parse), so it would be invisible there anyway.
438
+ """
439
+ if fmt != 'py' or not lines:
440
+ return lines
441
+ return [lines[0], *(_indent_comment_line(line) for line in lines[1:])]
442
+
443
+
414
444
  def _convert_admonition(
415
445
  node: nodes.Element, label: str, ctx: _RenderContext, *, skip_first_title: bool = False
416
446
  ) -> list[Segment]:
@@ -421,7 +451,8 @@ def _convert_admonition(
421
451
  if skip_first_title and isinstance(child, nodes.title):
422
452
  continue
423
453
  inner.extend(_convert_node(child, inner_ctx))
424
- return [('directive', _join_segments(inner))]
454
+ lines = _indent_label_content(_join_segments(inner), ctx.fmt)
455
+ return [('directive', lines)]
425
456
 
426
457
 
427
458
  def _convert_node(node: nodes.Node, ctx: _RenderContext) -> list[Segment]:
@@ -782,7 +813,8 @@ def _build_segments(nodes_in_span: list[nodes.Node], ctx: _RenderContext) -> lis
782
813
  inner: list[Segment] = [('text', ['# SEE ALSO:'])]
783
814
  for later_node in nodes_in_span[i + 1 :]:
784
815
  inner.extend(_convert_node(later_node, inner_ctx))
785
- segments.append(('directive', _join_segments(inner)))
816
+ lines = _indent_label_content(_join_segments(inner), ctx.fmt)
817
+ segments.append(('directive', lines))
786
818
  break
787
819
  segments.extend(_convert_node(node, ctx))
788
820
  return segments
@@ -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.3.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.3'
22
+ __version_tuple__ = version_tuple = (0, 3, 3)
23
23
 
24
- __commit_id__ = commit_id = 'gf152e27a3'
24
+ __commit_id__ = commit_id = 'g2a42d27a0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-examples-as-code
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -107,9 +107,11 @@ What happens to the content of an Examples section:
107
107
  - `.. code-block:: python` (or `py`) blocks are kept as-is; other languages become
108
108
  comments, set off with blank lines on both sides like any other directive.
109
109
  - Admonitions (`.. note::`, `.. warning::`, `.. seealso::`, ...) become a `# LABEL:`
110
- comment followed by their content as comments. "See Also" is recognized in any of its
111
- three forms (`.. seealso::`, a bare `.. rubric:: See Also`, or a hand-written `See
112
- Also` heading) and always renders the same way.
110
+ comment followed by their content as comments, indented one level under the label in
111
+ `.py` (not `.ipynb`, where leading whitespace within a paragraph has no visible effect
112
+ once rendered). "See Also" is recognized in any of its three forms (`.. seealso::`, a
113
+ bare `.. rubric:: See Also`, or a hand-written `See Also` heading) and always renders
114
+ the same way.
113
115
  - Cross-references and inline code (`:class:`, `:meth:`, `:func:`, `:attr:`,
114
116
  double-backtick literals, ...) keep their display text, wrapped in backticks (e.g.
115
117
  ``:class:`pyvista.Plotter` `` -> `` `pyvista.Plotter` ``). If `html_baseurl` is set and
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.3.2",
2
+ "tag": "0.3.3",
3
3
  "distance": 0,
4
- "node": "gf152e27a32b46224579810f6028839aeac8c878c",
4
+ "node": "g2a42d27a03ee769351e69a23c3392b167a7cbe45",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-08-07"
@@ -444,6 +444,47 @@ def test_clean_code_comment_replaces_non_ascii_chars(unicode_char: int, ascii_ch
444
444
  assert seac._clean_code_comment(line) == f'# comment {ascii_char} text'
445
445
 
446
446
 
447
+ # ---------------------------------------------------------------------------
448
+ # _indent_comment_line / _indent_label_content
449
+ # ---------------------------------------------------------------------------
450
+
451
+
452
+ def test_indent_comment_line_indents_content():
453
+ assert seac._indent_comment_line('# hello') == '# hello'
454
+
455
+
456
+ def test_indent_comment_line_bare_hash_unchanged():
457
+ assert seac._indent_comment_line('#') == '#'
458
+
459
+
460
+ def test_indent_comment_line_blank_separator_unchanged():
461
+ assert seac._indent_comment_line('') == ''
462
+
463
+
464
+ def test_indent_comment_line_code_line_unchanged():
465
+ # no '#' prefix at all -- indenting it would change real Python source
466
+ assert seac._indent_comment_line('x = 1') == 'x = 1'
467
+
468
+
469
+ def test_indent_label_content_py_indents_everything_after_the_first_line():
470
+ lines = ['# NOTE:', '# hello', '# world']
471
+ assert seac._indent_label_content(lines, 'py') == ['# NOTE:', '# hello', '# world']
472
+
473
+
474
+ def test_indent_label_content_ipynb_unchanged():
475
+ lines = ['# NOTE:', '# hello', '# world']
476
+ assert seac._indent_label_content(lines, 'ipynb') == lines
477
+
478
+
479
+ def test_indent_label_content_empty_list_unchanged():
480
+ assert seac._indent_label_content([], 'py') == []
481
+
482
+
483
+ def test_indent_label_content_skips_only_the_first_line():
484
+ # a lone label with no content: nothing after it to indent
485
+ assert seac._indent_label_content(['# NOTE:'], 'py') == ['# NOTE:']
486
+
487
+
447
488
  # ---------------------------------------------------------------------------
448
489
  # _convert_admonition / _convert_node dispatch
449
490
  # ---------------------------------------------------------------------------
@@ -451,7 +492,19 @@ def test_clean_code_comment_replaces_non_ascii_chars(unicode_char: int, ascii_ch
451
492
 
452
493
  def test_convert_node_note():
453
494
  doctree = _parse('.. note::\n\n hello')
454
- assert seac._convert_node(doctree[0], _ctx()) == [('directive', ['# NOTE:', '# hello'])]
495
+ assert seac._convert_node(doctree[0], _ctx(fmt='py')) == [
496
+ ('directive', ['# NOTE:', '# hello'])
497
+ ]
498
+
499
+
500
+ def test_convert_node_note_ipynb_not_indented():
501
+ # unlike .py, .ipynb content isn't indented under the label -- once
502
+ # rendered as markdown, leading whitespace within a paragraph collapses
503
+ # to nothing anyway
504
+ doctree = _parse('.. note::\n\n hello')
505
+ assert seac._convert_node(doctree[0], _ctx(fmt='ipynb')) == [
506
+ ('directive', ['# NOTE:', '# hello'])
507
+ ]
455
508
 
456
509
 
457
510
  def test_convert_node_seealso():
@@ -462,8 +515,8 @@ def test_convert_node_seealso():
462
515
  p2 += nodes.Text('More info')
463
516
  node += p1
464
517
  node += p2
465
- assert seac._convert_node(node, _ctx()) == [
466
- ('directive', ['# SEE ALSO:', '# See X', '# More info'])
518
+ assert seac._convert_node(node, _ctx(fmt='py')) == [
519
+ ('directive', ['# SEE ALSO:', '# See X', '# More info'])
467
520
  ]
468
521
 
469
522
 
@@ -478,7 +531,10 @@ def test_convert_node_seealso_propagates_in_see_also_to_references():
478
531
  ctx = _ctx(fmt='py', base_url='https://docs.pyvista.org/')
479
532
  segments = seac._convert_node(node, ctx)
480
533
  assert segments == [
481
- ('directive', ['# SEE ALSO:', '# pyvista.Plotter https://docs.pyvista.org/plotter.html'])
534
+ (
535
+ 'directive',
536
+ ['# SEE ALSO:', '# pyvista.Plotter https://docs.pyvista.org/plotter.html'],
537
+ )
482
538
  ]
483
539
 
484
540
 
@@ -490,8 +546,8 @@ def test_convert_node_see_also_section_treated_like_admonition():
490
546
 
491
547
  def test_convert_node_generic_admonition_uses_title():
492
548
  doctree = _parse('.. admonition:: Custom Title\n\n body text')
493
- assert seac._convert_node(doctree[0], _ctx()) == [
494
- ('directive', ['# Custom Title:', '# body text'])
549
+ assert seac._convert_node(doctree[0], _ctx(fmt='py')) == [
550
+ ('directive', ['# Custom Title:', '# body text'])
495
551
  ]
496
552
 
497
553
 
@@ -500,7 +556,7 @@ def test_convert_node_generic_admonition_no_title_defaults_to_note():
500
556
  p = nodes.paragraph()
501
557
  p += nodes.Text('body')
502
558
  node += p
503
- assert seac._convert_node(node, _ctx()) == [('directive', ['# NOTE:', '# body'])]
559
+ assert seac._convert_node(node, _ctx(fmt='py')) == [('directive', ['# NOTE:', '# body'])]
504
560
 
505
561
 
506
562
  def test_convert_node_admonition_with_no_body_keeps_label():
@@ -508,6 +564,16 @@ def test_convert_node_admonition_with_no_body_keeps_label():
508
564
  assert seac._convert_node(node, _ctx()) == [('directive', ['# NOTE:'])]
509
565
 
510
566
 
567
+ def test_convert_node_note_with_code_indents_text_not_code():
568
+ # a doctest example inside a note: the surrounding prose is indented
569
+ # under the label, but the code itself is left flush -- indenting it
570
+ # would be invalid Python (it isn't actually nested inside anything)
571
+ doctree = _parse('.. note::\n\n See below.\n\n >>> x = 1')
572
+ assert seac._convert_node(doctree[0], _ctx(fmt='py')) == [
573
+ ('directive', ['# NOTE:', '# See below.', 'x = 1'])
574
+ ]
575
+
576
+
511
577
  def test_convert_node_skip_subtree_class():
512
578
  node = nodes.container(classes=['sd-dropdown'])
513
579
  p = nodes.paragraph()
@@ -1226,7 +1292,7 @@ def test_build_segments_bare_rubric_see_also_affects_only_what_follows():
1226
1292
  'directive',
1227
1293
  [
1228
1294
  '# SEE ALSO:',
1229
- '# pyvista.Plotter https://docs.pyvista.org/plotter.html',
1295
+ '# pyvista.Plotter https://docs.pyvista.org/plotter.html',
1230
1296
  'x = 1',
1231
1297
  ],
1232
1298
  )
@@ -1352,7 +1418,7 @@ def test_process_span_includes_external_see_also(tmp_path: Path):
1352
1418
  written = next((tmp_path / '_downloads').rglob('*.py'))
1353
1419
  content = written.read_text()
1354
1420
  assert '# SEE ALSO:' in content
1355
- assert '# related info' in content
1421
+ assert '# related info' in content
1356
1422
 
1357
1423
 
1358
1424
  def test_process_span_appends_footer_with_blank_line_before(tmp_path: Path):
@@ -177,14 +177,14 @@ def test_docstring_examples_conversion(built: tuple[Path, list[Path]]):
177
177
 
178
178
  note_src = _read(examples, 'case_note')
179
179
  assert '# NOTE:' in note_src
180
- assert '# This is a note' in note_src
180
+ assert '# This is a note' in note_src # indented under the label
181
181
 
182
182
  warning_src = _read(examples, 'case_warning')
183
183
  assert '# WARNING:' in warning_src
184
184
 
185
185
  multi_note_src = _read(examples, 'case_multi_paragraph_note')
186
- assert '# First paragraph of the note.' in multi_note_src
187
- assert '# Second paragraph of the note.' in multi_note_src
186
+ assert '# First paragraph of the note.' in multi_note_src
187
+ assert '# Second paragraph of the note.' in multi_note_src
188
188
 
189
189
  admonition_src = _read(examples, 'case_generic_admonition')
190
190
  assert '# Custom Title:' in admonition_src
@@ -307,10 +307,10 @@ def test_seealso_admonition(built: tuple[Path, list[Path]]):
307
307
  """
308
308
  src = _read(built[1], 'case_seealso')
309
309
  assert '# SEE ALSO:' in src
310
- assert '# Some Target' in src
311
- assert '# See this in the gallery for more info.' in src
310
+ assert '# Some Target' in src
311
+ assert '# See this in the gallery for more info.' in src
312
312
  # each paragraph must be its own line -- not concatenated together
313
- assert 'Some Target\n# See' in src or 'Some TargetSee' not in src
313
+ assert 'Some Target\n# See' in src or 'Some TargetSee' not in src
314
314
 
315
315
 
316
316
  def test_stray_markup_in_doctest_comment_cleaned(built: tuple[Path, list[Path]]):
@@ -530,7 +530,7 @@ def test_base_url_set_resolves_links(tmp_path: Path):
530
530
  ):
531
531
  py_src = _read(py_files, name)
532
532
  assert '# SEE ALSO:' in py_src
533
- assert '# docstring_cases.Sample https://docs.example.com/' in py_src
533
+ assert '# docstring_cases.Sample https://docs.example.com/' in py_src
534
534
  assert '`docstring_cases.Sample`' not in py_src # no backticks in See Also url lines
535
535
 
536
536
  nb_path = next(p for p in ipynb_files if p.stem == f'docstring_cases_{name}')