From 2a30bb661f575e47be136c4ab0cf91f5f2520300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 15 Jul 2024 06:17:36 +0200 Subject: [PATCH] Fix PEP 695 output for classes in the LaTeX builder (#12561) --- CHANGES.rst | 2 ++ sphinx/writers/latex.py | 28 +++++++++++------ .../index.rst | 13 ++++++++ tests/test_builders/test_build_html.py | 31 +++++++++++++++++++ tests/test_builders/test_build_latex.py | 22 +++++++++++++ tests/test_domains/test_domain_py.py | 4 +-- 6 files changed, 88 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4d999c79c..77138e376 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------- diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index fd750a579..d5c59ef72 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -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) diff --git a/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst index 75e468305..971550007 100644 --- a/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst +++ b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst @@ -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]) + diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 529f6b10d..eb3247445 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -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])')) diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 0ef73f47f..0786702b0 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -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): diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index 3f45842d8..08390b711 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -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, (