diff --git a/CHANGES b/CHANGES index 5ab2a694d5c..4fb96d00b5e 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,9 @@ Features added Bugs fixed ---------- +* #10031: py domain: Fix spurious whitespace in unparsing various operators (``+``, + ``-``, ``~``, and ``**``). Patch by Adam Turner. + Testing -------- diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 755116475d6..d4646f0b729 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -141,6 +141,9 @@ def visit_Attribute(self, node: ast.Attribute) -> str: return "%s.%s" % (self.visit(node.value), node.attr) def visit_BinOp(self, node: ast.BinOp) -> str: + # Special case ``**`` to not have surrounding spaces. + if isinstance(node.op, ast.Pow): + return "".join(map(self.visit, (node.left, node.op, node.right))) return " ".join(self.visit(e) for e in [node.left, node.op, node.right]) def visit_BoolOp(self, node: ast.BoolOp) -> str: @@ -202,7 +205,11 @@ def is_simple_tuple(value: ast.AST) -> bool: return "%s[%s]" % (self.visit(node.value), self.visit(node.slice)) def visit_UnaryOp(self, node: ast.UnaryOp) -> str: - return "%s %s" % (self.visit(node.op), self.visit(node.operand)) + # UnaryOp is one of {UAdd, USub, Invert, Not}, which refer to ``+x``, + # ``-x``, ``~x``, and ``not x``. Only Not needs a space. + if isinstance(node.op, ast.Not): + return "%s %s" % (self.visit(node.op), self.visit(node.operand)) + return "%s%s" % (self.visit(node.op), self.visit(node.operand)) def visit_Tuple(self, node: ast.Tuple) -> str: if len(node.elts) == 0: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 014067e8459..baad0c2daa2 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -452,6 +452,33 @@ def test_pyfunction_signature_full(app): [desc_sig_name, pending_xref, "str"])])]) +def test_pyfunction_with_unary_operators(app): + text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"], + [desc_sig_operator, "="], + [nodes.inline, "+1"])], + [desc_parameter, ([desc_sig_name, "bacon"], + [desc_sig_operator, "="], + [nodes.inline, "-1"])], + [desc_parameter, ([desc_sig_name, "sausage"], + [desc_sig_operator, "="], + [nodes.inline, "~1"])], + [desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "not spam"])])]) + + +def test_pyfunction_with_binary_operators(app): + text = ".. py:function:: menu(spam=2**64)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "2**64"])])]) + + @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') def test_pyfunction_signature_full_py38(app): # case: separator at head diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index 6143105eb71..31018bacaa3 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -25,7 +25,7 @@ ("...", "..."), # Ellipsis ("a // b", "a // b"), # FloorDiv ("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript - ("~ 1", "~ 1"), # Invert + ("~1", "~1"), # Invert ("lambda x, y: x + y", "lambda x, y: ..."), # Lambda ("[1, 2, 3]", "[1, 2, 3]"), # List @@ -37,14 +37,14 @@ ("1234", "1234"), # Num ("not a", "not a"), # Not ("a or b", "a or b"), # Or - ("a ** b", "a ** b"), # Pow + ("a**b", "a**b"), # Pow ("a >> b", "a >> b"), # RShift ("{1, 2, 3}", "{1, 2, 3}"), # Set ("a - b", "a - b"), # Sub ("'str'", "'str'"), # Str - ("+ a", "+ a"), # UAdd - ("- 1", "- 1"), # UnaryOp - ("- a", "- a"), # USub + ("+a", "+a"), # UAdd + ("-1", "-1"), # UnaryOp + ("-a", "-a"), # USub ("(1, 2, 3)", "(1, 2, 3)"), # Tuple ("()", "()"), # Tuple (empty) ("(1,)", "(1,)"), # Tuple (single item)