Repository: sphinx-doc/sphinx

Issue:
Proposal: Invite AA-Turner as a new collaborator
# Proposal

Hello maintainers,
Today, I'd like to invite @AA-Turner  as a new collaborator on our project

He had already contributed many works until now:

* https://github.com/sphinx-doc/sphinx/pulls?q=is%3Apr+author%3AAA-Turner+
* migrate PEPs website to Sphinx: https://github.com/python/peps/issues/2

I believe his experience would help us.
What do you think? Please 👍 for this if you agreed.

I know there is no special procedure to welcome a new committer/collaborator. So I will invite him if no objection.

Discussion:

Patch:
diff --git a/AUTHORS b/AUTHORS
index 52d0ee8e5..c870e863e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -15,10 +15,10 @@ Other co-maintainers:
 * Jean-François Burnol <@jfbu>
 * Yoshiki Shibukawa <@shibu_jp>
 * Timotheus Kampik - <@TimKam>
+* Adam Turner - <@AA-Turner>
 
 Other contributors, listed alphabetically, are:
 
-* Adam Turner -- JavaScript improvements
 * Alastair Houghton -- Apple Help builder
 * Alexander Todorov -- inheritance_diagram tests and improvements
 * Andi Albrecht -- agogo theme
diff --git a/CHANGES b/CHANGES
index 5ab2a694d..4fb96d00b 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 755116475..d4646f0b7 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:


Test patch:
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index 014067e84..baad0c2da 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 6143105eb..31018baca 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)