Fix PEP 695 output for classes in the LaTeX builder (#12561)

This commit is contained in:
Bénédikt Tran 2024-07-15 06:17:36 +02:00 committed by GitHub
parent aeebfabe09
commit 2a30bb661f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 88 additions and 12 deletions

View File

@ -160,6 +160,8 @@ Bugs fixed
Patch by Jakob Lykke Andersen and Adam Turner.
* #11041: linkcheck: Ignore URLs that respond with non-Unicode content.
Patch by James Addison.
* #12543: Fix :pep:`695` formatting for LaTeX output.
Patch by Bénédikt Tran.
Testing
-------

View File

@ -724,19 +724,21 @@ class LaTeXTranslator(SphinxTranslator):
return e.get('multi_line_parameter_list')
self.has_tp_list = False
self.orphan_tp_list = False
for child in node:
if isinstance(child, addnodes.desc_type_parameter_list):
self.has_tp_list = True
# recall that return annotations must follow an argument list,
# so signatures of the form "foo[tp_list] -> retann" will not
# be encountered (if they should, the `domains.python.py_sig_re`
# pattern must be modified accordingly)
arglist = next_sibling(child)
assert isinstance(arglist, addnodes.desc_parameterlist)
# tp_list + arglist: \macro{name}{tp_list}{arglist}{return}
multi_tp_list = has_multi_line(child)
multi_arglist = has_multi_line(arglist)
arglist = next_sibling(child)
if isinstance(arglist, addnodes.desc_parameterlist):
# tp_list + arglist: \macro{name}{tp_list}{arglist}{retann}
multi_arglist = has_multi_line(arglist)
else:
# orphan tp_list: \macro{name}{tp_list}{}{retann}
# see: https://github.com/sphinx-doc/sphinx/issues/12543
self.orphan_tp_list = True
multi_arglist = False
if multi_tp_list:
if multi_arglist:
@ -751,7 +753,7 @@ class LaTeXTranslator(SphinxTranslator):
break
if isinstance(child, addnodes.desc_parameterlist):
# arglist only: \macro{name}{arglist}{return}
# arglist only: \macro{name}{arglist}{retann}
if has_multi_line(child):
self.body.append(CR + r'\pysigwithonelineperarg{')
else:
@ -857,7 +859,13 @@ class LaTeXTranslator(SphinxTranslator):
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
def visit_desc_parameterlist(self, node: Element) -> None:
if not self.has_tp_list:
if self.has_tp_list:
if self.orphan_tp_list:
# close type parameters list (#2)
self.body.append('}{')
# empty parameters list argument (#3)
return
else:
# close name argument (#1), open parameters list argument (#2)
self.body.append('}{')
self._visit_sig_parameter_list(node, addnodes.desc_parameter)

View File

@ -4,3 +4,16 @@ domain-py-maximum_signature_line_length
.. py:function:: hello(name: str) -> str
.. py:function:: foo([a, [b, ]]c, d[, e, f])
.. py:function:: generic_arg[T]
.. py:function:: generic_foo[T]()
.. py:function:: generic_bar[T](x: list[T])
.. py:function:: generic_ret[R]() -> R
.. py:class:: MyGenericClass[X]
.. py:class:: MyList[T](list[T])

View File

@ -376,3 +376,34 @@ def test_html_remove_sources_before_write_gh_issue_10786(app, warning):
file = os.fsdecode(target)
assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1]
@pytest.mark.sphinx('html', testroot='domain-py-python_maximum_signature_line_length',
confoverrides={'python_maximum_signature_line_length': 1})
def test_html_pep_695_one_type_per_line(app, cached_etree_parse):
app.build()
fname = app.outdir / 'index.html'
etree = cached_etree_parse(fname)
class chk:
def __init__(self, expect):
self.expect = expect
def __call__(self, nodes):
assert len(nodes) == 1, nodes
objnode = ''.join(nodes[0].itertext()).replace('\n\n', '')
objnode = objnode.rstrip(chr(182)) # remove '¶' symbol
objnode = objnode.strip('\n') # remove surrounding new lines
assert objnode == self.expect
# each signature has a dangling ',' at the end of its parameters lists
check_xpath(etree, fname, r'.//dt[@id="generic_foo"][1]',
chk('generic_foo[\nT,\n]()'))
check_xpath(etree, fname, r'.//dt[@id="generic_bar"][1]',
chk('generic_bar[\nT,\n](\nx: list[T],\n)'))
check_xpath(etree, fname, r'.//dt[@id="generic_ret"][1]',
chk('generic_ret[\nR,\n]() → R'))
check_xpath(etree, fname, r'.//dt[@id="MyGenericClass"][1]',
chk('class MyGenericClass[\nX,\n]'))
check_xpath(etree, fname, r'.//dt[@id="MyList"][1]',
chk('class MyList[\nT,\n](list[T])'))

View File

@ -1760,6 +1760,28 @@ def test_one_parameter_per_line(app, status, warning):
assert ('\\pysigwithonelineperarg{\\sphinxbfcode{\\sphinxupquote{foo}}}' in result)
# generic_arg[T]
assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_arg}}}'
'{\\sphinxtypeparam{\\DUrole{n}{T}}}{}{}' in result)
# generic_foo[T]()
assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_foo}}}' in result)
# generic_bar[T](x: list[T])
assert ('\\pysigwithonelineperargwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_bar}}}' in result)
# generic_ret[R]() -> R
assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_ret}}}'
'{\\sphinxtypeparam{\\DUrole{n}{R}}}{}{{ $\\rightarrow$ R}}' in result)
# MyGenericClass[X]
assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ '
'}}}\\sphinxbfcode{\\sphinxupquote{MyGenericClass}}}' in result)
# MyList[T](list[T])
assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ '
'}}}\\sphinxbfcode{\\sphinxupquote{MyList}}}' in result)
@pytest.mark.sphinx('latex', testroot='markup-rubric')
def test_latex_rubric(app):

View File

@ -753,7 +753,7 @@ def test_function_pep_695(app):
S,\
T: int,\
U: (int, str),\
R: int | int,\
R: int | str,\
A: int | Annotated[int, ctype("char")],\
*V,\
**P\
@ -795,7 +795,7 @@ def test_function_pep_695(app):
desc_sig_space,
[desc_sig_punctuation, '|'],
desc_sig_space,
[pending_xref, 'int'],
[pending_xref, 'str'],
)],
)],
[desc_type_parameter, (