Repository: sphinx-doc/sphinx

Issue:
The deco role does not support the tilde modified domain reference
### Describe the bug

When using the `:py:deco:` role the `~` prefix doesn't shorten the title to the last component, and leaves the tilde character in the rendered html output.

ref: 
https://www.sphinx-doc.org/en/master/usage/referencing.html#xref-modifiers 
https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification

### How to Reproduce

`test.rst`:
```
Example
=======

 * deco role with tilde :py:deco:`~aiq.cli.register_workflow.register_embedder_client`
 * deco role without tilde :py:deco:`aiq.cli.register_workflow.register_embedder_client`
 * func role :py:func:`~aiq.cli.register_workflow.register_embedder_client`
```

### Environment Information

```text
Platform:              linux; (Linux-6.1.0-32-amd64-x86_64-with-glibc2.36)
Python version:        3.12.0 (main, Oct  3 2023, 01:27:23) [Clang 17.0.1 ])
Python implementation: CPython
Sphinx version:        8.2.3
Docutils version:      0.21.2
Jinja2 version:        3.1.6
Pygments version:      2.19.1
```

### Sphinx extensions

```python
extensions = [
    'autoapi.extension',
    'IPython.sphinxext.ipython_console_highlighting',
    'IPython.sphinxext.ipython_directive',
    'myst_parser',
    'nbsphinx',
    'sphinx_copybutton',
    'sphinx.ext.doctest',
    'sphinx.ext.graphviz',
    'sphinx.ext.intersphinx',
    "sphinxmermaid"
]
```

### Additional context

_No response_

Discussion:
@AA-Turner: PR welcome!

A

Solution patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 9b86d2df25e..57e8e0efdf9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -31,6 +31,8 @@ Bugs fixed
 
 * #13369: Correctly parse and cross-reference unpacked type annotations.
   Patch by Alicia Garcia-Raboso.
+* #13528: Add tilde ``~`` prefix support for :rst:role:`py:deco`.
+  Patch by Shengyu Zhang and Adam Turner.
 
 Testing
 -------
diff --git a/sphinx/domains/python/__init__.py b/sphinx/domains/python/__init__.py
index af923cae70e..fb030991464 100644
--- a/sphinx/domains/python/__init__.py
+++ b/sphinx/domains/python/__init__.py
@@ -29,7 +29,7 @@
     from collections.abc import Iterable, Iterator, Sequence, Set
     from typing import Any, ClassVar
 
-    from docutils.nodes import Element, Node, TextElement
+    from docutils.nodes import Element, Node
 
     from sphinx.addnodes import desc_signature, pending_xref
     from sphinx.application import Sphinx
@@ -594,23 +594,17 @@ def process_link(
 
 
 class _PyDecoXRefRole(PyXRefRole):
-    def __init__(
+    def process_link(
         self,
-        fix_parens: bool = False,
-        lowercase: bool = False,
-        nodeclass: type[Element] | None = None,
-        innernodeclass: type[TextElement] | None = None,
-        warn_dangling: bool = False,
-    ) -> None:
-        super().__init__(
-            fix_parens=True,
-            lowercase=lowercase,
-            nodeclass=nodeclass,
-            innernodeclass=innernodeclass,
-            warn_dangling=warn_dangling,
+        env: BuildEnvironment,
+        refnode: Element,
+        has_explicit_title: bool,
+        title: str,
+        target: str,
+    ) -> tuple[str, str]:
+        title, target = super().process_link(
+            env, refnode, has_explicit_title, title, target
         )
-
-    def update_title_and_target(self, title: str, target: str) -> tuple[str, str]:
         return f'@{title}', target
 
 


Test patch:
diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py
index 262773af35a..14346b0563a 100644
--- a/tests/test_domains/test_domain_py.py
+++ b/tests/test_domains/test_domain_py.py
@@ -1791,3 +1791,18 @@ def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext):
     text = f'.. py:function:: f{tp_list}() -> Annotated[T, Qux[int]()]'
     doctree = restructuredtext.parse(app, text)
     assert doctree.astext() == f'\n\nf{tptext}() -> Annotated[T, Qux[int]()]\n\n'
+
+
+def test_deco_role(app):
+    text = """\
+.. py:decorator:: foo.bar
+   :no-contents-entry:
+   :no-index-entry:
+   :no-typesetting:
+"""
+
+    doctree = restructuredtext.parse(app, text + '\n:py:deco:`foo.bar`')
+    assert doctree.astext() == '\n\n\n\n@foo.bar'
+
+    doctree = restructuredtext.parse(app, text + '\n:py:deco:`~foo.bar`')
+    assert doctree.astext() == '\n\n\n\n@bar'