Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Contributors
* Justus Magin -- napoleon improvements
* Kazuya Take -- ``sphinx.testing.path`` bug fix
* Kevin Dunn -- MathJax extension
* Kilian Paul -- Docutils compatibility, intl tests, and viewcode fixes
* KINEBUCHI Tomohiko -- typing Sphinx as well as docutils
* Kurt McKee -- documentation updates
* Lars Hupfeldt Nielsen - OpenSSL FIPS mode md5 bug fix
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Release 9.1.1 (in development)
Bugs fixed
----------

* #14680: Support the doctest node and table column-width representations used by
Docutils 1.0 development versions.
Patch by Kilian Paul

* #14465: LaTeX: PDF build crash since LaTeX June 2026 release if tables are
styled with ``'colorrows'`` (which is the default).
Patch by Jean-François B.
Expand Down
4 changes: 2 additions & 2 deletions sphinx/ext/autosummary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,8 @@ def get_table(self, items: list[tuple[str, str | None, str, str]]) -> list[Node]
table.append(real_table)
group = nodes.tgroup('', cols=2)
real_table.append(group)
group.append(nodes.colspec('', colwidth=10))
group.append(nodes.colspec('', colwidth=90))
group.append(nodes.colspec('', colwidth='10'))
group.append(nodes.colspec('', colwidth='90'))
body = nodes.tbody('')
group.append(body)

Expand Down
3 changes: 2 additions & 1 deletion sphinx/ext/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import _is_doctest_block
from sphinx.util.osutil import relpath

if TYPE_CHECKING:
Expand Down Expand Up @@ -661,4 +662,4 @@ def _condition_default(node: Node) -> bool:


def _condition_with_doctest(node: Node) -> bool:
return _condition_default(node) or isinstance(node, nodes.doctest_block)
return _condition_default(node) or _is_doctest_block(node)
15 changes: 12 additions & 3 deletions sphinx/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.i18n import format_date
from sphinx.util.nodes import apply_source_workaround, is_smartquotable
from sphinx.util.nodes import (
_is_doctest_block,
apply_source_workaround,
is_smartquotable,
)

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
Expand Down Expand Up @@ -183,7 +187,7 @@ class HandleCodeBlocks(SphinxTransform):
def apply(self, **kwargs: Any) -> None:
# move doctest blocks out of blockquotes
for node in self.document.findall(nodes.block_quote):
if all(isinstance(child, nodes.doctest_block) for child in node.children):
if all(_is_doctest_block(child) for child in node.children):
node.replace_self(node.children)
# combine successive doctest blocks
# for node in self.document.findall(nodes.doctest_block):
Expand Down Expand Up @@ -278,6 +282,8 @@ def apply(self, **kwargs: Any) -> None:
return

def is_translatable_node(node: Node) -> TypeIs[nodes.Element]:
if isinstance(node, nodes.literal_block) and _is_doctest_block(node):
return 'doctest-block' in targets
return isinstance(node, target_nodes)

for node in self.document.findall(is_translatable_node):
Expand Down Expand Up @@ -325,13 +331,16 @@ def apply(self, **kwargs: Any) -> None:


class DoctestTransform(SphinxTransform):
"""Set "doctest" style to each doctest_block node"""
"""Normalize the attributes of reStructuredText doctest blocks."""

default_priority = 500

def apply(self, **kwargs: Any) -> None:
for node in self.document.findall(nodes.doctest_block):
node['classes'].append('doctest')
for literal_node in self.document.findall(nodes.literal_block):
if _is_doctest_block(literal_node):
literal_node.setdefault('language', 'pycon')


class FilterSystemMessages(SphinxTransform):
Expand Down
29 changes: 29 additions & 0 deletions sphinx/util/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@
caption_ref_re = explicit_title_re # b/w compat alias


def _is_doctest_block(node: Node) -> bool:
"""Return whether *node* represents a reStructuredText doctest block."""
if isinstance(node, nodes.doctest_block):
return True
if not isinstance(node, nodes.literal_block):
return False

classes = cast('list[str]', node.get('classes', []))
return 'doctest' in classes and 'pycon' in classes


def _parse_colwidth(value: object) -> int:
"""Return a positive integral column width from a node attribute."""
# TODO: This integral-only conversion is temporary. Preserve fractional
# proportions once the text, LaTeX, and Texinfo writers support float widths.
msg = f'column width must be a positive integer, got {value!r}'
if isinstance(value, bool) or not isinstance(value, (int, str)):
raise TypeError(msg)

try:
width = int(value)
except ValueError:
raise ValueError(msg) from None

if width <= 0:
raise ValueError(msg)
return width


class NodeMatcher[N: Node]:
"""A helper class for Node.findall().

Expand Down
4 changes: 2 additions & 2 deletions sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from sphinx.util import logging, texescape
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.index_entries import split_index_msg
from sphinx.util.nodes import clean_astext, get_prev_node
from sphinx.util.nodes import _parse_colwidth, clean_astext, get_prev_node
from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_replace_map

Expand Down Expand Up @@ -1249,7 +1249,7 @@ def visit_colspec(self, node: Element) -> None:
assert self.table is not None
self.table.colcount += 1
if 'colwidth' in node:
self.table.colwidths.append(node['colwidth'])
self.table.colwidths.append(_parse_colwidth(node['colwidth']))
if 'stub' in node:
self.table.stubs.append(self.table.colcount - 1)

Expand Down
3 changes: 2 additions & 1 deletion sphinx/writers/texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.i18n import format_date
from sphinx.util.nodes import _parse_colwidth
from sphinx.writers.latex import collected_footnote

if TYPE_CHECKING:
Expand Down Expand Up @@ -1041,7 +1042,7 @@ def depart_tabular_col_spec(self, node: Element) -> None:
pass

def visit_colspec(self, node: Element) -> None:
self.colwidths.append(node['colwidth'])
self.colwidths.append(_parse_colwidth(node['colwidth']))
if len(self.colwidths) != self.n_cols:
return
self.body.append('\n\n@multitable ')
Expand Down
3 changes: 2 additions & 1 deletion sphinx/writers/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sphinx import addnodes
from sphinx.locale import _, admonitionlabels
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.nodes import _parse_colwidth

if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Sequence
Expand Down Expand Up @@ -904,7 +905,7 @@ def visit_tabular_col_spec(self, node: Element) -> None:
raise nodes.SkipNode

def visit_colspec(self, node: Element) -> None:
self.table.colwidth.append(node['colwidth'])
self.table.colwidth.append(_parse_colwidth(node['colwidth']))
raise nodes.SkipNode

def visit_tgroup(self, node: Element) -> None:
Expand Down
3 changes: 3 additions & 0 deletions tests/roots/test-ext-doctest-native/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
extensions = ['sphinx.ext.doctest']

project = 'test project for native doctest blocks'
17 changes: 17 additions & 0 deletions tests/roots/test-ext-doctest-native/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Native doctest blocks
=====================

>>> 1 + 1
2

An indented native doctest block follows.

>>> 2 + 2
4

This ordinary console code block must not be executed as a doctest.

.. code-block:: pycon

>>> 3 + 3
7
1 change: 1 addition & 0 deletions tests/roots/test-table-colwidths/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
project = 'test project for table column widths'
8 changes: 8 additions & 0 deletions tests/roots/test-table-colwidths/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Table column widths
===================

.. list-table::
:widths: 10 90

* - A
- B
10 changes: 10 additions & 0 deletions tests/test_builders/test_build_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@ def test_writer(app: SphinxTestApp) -> None:
) in result


@pytest.mark.sphinx('latex', testroot='table-colwidths')
def test_table_with_explicit_colwidths(app: SphinxTestApp) -> None:
app.build()

output = (app.outdir / 'testprojectfortablecolumnwidths.tex').read_text(
encoding='utf8'
)
assert r'\begin{tabular}[t]{\X{10}{100}\X{90}{100}}' in output


@pytest.mark.sphinx('latex', testroot='basic')
def test_latex_basic(app: SphinxTestApp) -> None:
app.build(force_all=True)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_builders/test_build_texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ def test_texinfo(app: SphinxTestApp) -> None:
raise AssertionError(msg) from exc


@pytest.mark.sphinx('texinfo', testroot='table-colwidths')
def test_table_with_explicit_colwidths(app: SphinxTestApp) -> None:
app.build()

output = (app.outdir / 'testprojectfortablecolumnwidths.texi').read_text(
encoding='utf8'
)
assert f'@multitable {{{"x" * 12}}} {{{"x" * 92}}}' in output


@pytest.mark.sphinx('texinfo', testroot='markup-rubric')
def test_texinfo_rubric(app: SphinxTestApp) -> None:
app.build()
Expand Down
8 changes: 8 additions & 0 deletions tests/test_builders/test_build_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ def test_table_separator() -> None:
assert repr(table).count('<Cell ') == 4


@pytest.mark.sphinx('text', testroot='table-colwidths')
def test_table_with_explicit_colwidths(app: SphinxTestApp) -> None:
app.build()

output = (app.outdir / 'index.txt').read_text(encoding='utf8')
assert f'+{"-" * 12}+{"-" * 92}+' in output


def test_table_cell() -> None:
cell = Cell('Foo bar baz')
cell.wrap(3)
Expand Down
4 changes: 4 additions & 0 deletions tests/test_ext_autosummary/test_ext_autosummary.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ def test_autosummary_generate(app):
],
)
assert_node(extract_node(doctree, 4, 0), addnodes.toctree, caption='An autosummary')
assert [node['colwidth'] for node in doctree[3].findall(nodes.colspec)] == [
'10',
'90',
]

assert len(extract_node(doctree, 3, 0, 0, 2)) == 8
assert extract_node(doctree, 3, 0, 0, 2, 0).astext() == (
Expand Down
32 changes: 32 additions & 0 deletions tests/test_extensions/test_ext_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ def test_highlight_language_python3(app: SphinxTestApp) -> None:
assert node['language'] in {'python', 'pycon', 'none'}


@pytest.mark.sphinx('doctest', testroot='ext-doctest-native')
def test_native_doctest_blocks(app: SphinxTestApp) -> None:
app.build(force_all=True)

assert isinstance(app.builder, DocTestBuilder)
assert app.statuscode == 0, f'failures in doctests:\n{app.status.getvalue()}'
assert app.builder.total_tries == 2
assert app.builder.total_failures == 0


@pytest.mark.sphinx(
'doctest',
testroot='ext-doctest-native',
confoverrides={'doctest_test_doctest_blocks': ''},
)
def test_native_doctest_blocks_disabled(app: SphinxTestApp) -> None:
app.build(force_all=True)

assert isinstance(app.builder, DocTestBuilder)
assert app.statuscode == 0
assert app.builder.total_tries == 0


@pytest.mark.sphinx('html', testroot='ext-doctest-native')
def test_indented_native_doctest_block_html(app: SphinxTestApp) -> None:
app.build(force_all=True)

output = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '<blockquote>' not in output
assert output.count('<span class="gp">&gt;&gt;&gt;') == 3


def test_is_allowed_version() -> None:
assert is_allowed_version('<3.4', '3.3') is True
assert is_allowed_version('<3.4', '3.3') is True
Expand Down
Loading
Loading