Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/usage/restructuredtext/domains.rst
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,14 @@ Multiple types in a type field will be linked automatically if separated by the
word "or"::

:type an_arg: int or None

PEP 604 style unions using the vertical bar are also supported when the pipe is
surrounded by whitespace::

:type another_arg: int | str

Values containing a literal pipe, such as ``Literal['foo| bar']``, are treated
as a single operand and are not split further.
:vartype a_var: str or int
:rtype: float or str

Expand Down
2 changes: 1 addition & 1 deletion sphinx/domains/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def make_xref(self, rolename: str, domain: str, target: str,
def make_xrefs(self, rolename: str, domain: str, target: str,
innernode: Type[TextlikeNode] = nodes.emphasis,
contnode: Node = None, env: BuildEnvironment = None) -> List[Node]:
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?\s*|\s+or\s+|\.\.\.)'
delims = r'(\s*[\[\]\(\),](?:\s*or\s)?(?:\s+(?!\|))?|\s+or\s+|\s+\|\s+|\.\.\.)'
delims_re = re.compile(delims)
sub_targets = re.split(delims, target)

Expand Down
2 changes: 2 additions & 0 deletions tests/roots/test-domain-py-union/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nitpicky = True
exclude_patterns = ['_build']
21 changes: 21 additions & 0 deletions tests/roots/test-domain-py-union/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Python domain docfields with unions
===================================

.. py:function:: sample(text, choice, nested, maybe, literal, literal_spaced)

:param text: textual data
:type text: bytes | str
:param choice: legacy union syntax
:type choice: str or int or None
:param nested: nested generics
:type nested: dict[str | int, list[None | int]]
:param maybe: union with ellipsis
:type maybe: tuple[int, ...] | None
:param literal: literal pipe
:type literal: Literal['|']
:param literal_spaced: literal containing a pipe
:type literal_spaced: Literal['foo| bar']

.. py:attribute:: sample_attribute

:vartype sample_attribute: bytes | str
164 changes: 164 additions & 0 deletions tests/test_domain_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,170 @@ def test_info_field_list_var(app):
refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"})


def test_info_field_list_union_operator(app):
text = (".. py:function:: sample(text, choice, nested, maybe, literal, literal_spaced)\n"
"\n"
" :param text: textual data\n"
" :type text: bytes | str\n"
" :param choice: legacy union syntax\n"
" :type choice: str or int or None\n"
" :param nested: nested generics\n"
" :type nested: dict[str | int, list[None | int]]\n"
" :param maybe: union with ellipsis\n"
" :type maybe: tuple[int, ...] | None\n"
" :param literal: literal pipe\n"
" :type literal: Literal['|']\n"
" :param literal_spaced: literal containing a pipe\n"
" :type literal_spaced: Literal['foo| bar']\n")
doctree = restructuredtext.parse(app, text)

parameters = doctree[1][1][0][0][1][0]

text_para = parameters[0][0]
assert_node(text_para, ([addnodes.literal_strong, "text"],
" (",
[pending_xref, addnodes.literal_emphasis, "bytes"],
[addnodes.literal_emphasis, " | "],
[pending_xref, addnodes.literal_emphasis, "str"],
")",
" -- ",
"textual data"))
assert_node(text_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="bytes")
assert_node(text_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="str")

choice_para = parameters[1][0]
assert_node(choice_para, ([addnodes.literal_strong, "choice"],
" (",
[pending_xref, addnodes.literal_emphasis, "str"],
[addnodes.literal_emphasis, " or "],
[pending_xref, addnodes.literal_emphasis, "int"],
[addnodes.literal_emphasis, " or "],
[pending_xref, addnodes.literal_emphasis, "None"],
")",
" -- ",
"legacy union syntax"))
assert_node(choice_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="str")
assert_node(choice_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="int")
assert_node(choice_para[6], pending_xref,
refdomain="py", reftype="obj", reftarget="None")

nested_para = parameters[2][0]
assert_node(nested_para, ([addnodes.literal_strong, "nested"],
" (",
[pending_xref, addnodes.literal_emphasis, "dict"],
[addnodes.literal_emphasis, "["],
[pending_xref, addnodes.literal_emphasis, "str"],
[addnodes.literal_emphasis, " | "],
[pending_xref, addnodes.literal_emphasis, "int"],
[addnodes.literal_emphasis, ", "],
[pending_xref, addnodes.literal_emphasis, "list"],
[addnodes.literal_emphasis, "["],
[pending_xref, addnodes.literal_emphasis, "None"],
[addnodes.literal_emphasis, " | "],
[pending_xref, addnodes.literal_emphasis, "int"],
[addnodes.literal_emphasis, "]"],
[addnodes.literal_emphasis, "]"],
")",
" -- ",
"nested generics"))
assert_node(nested_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="dict")
assert_node(nested_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="str")
assert_node(nested_para[6], pending_xref,
refdomain="py", reftype="class", reftarget="int")
assert_node(nested_para[8], pending_xref,
refdomain="py", reftype="class", reftarget="list")
assert_node(nested_para[10], pending_xref,
refdomain="py", reftype="obj", reftarget="None")
assert_node(nested_para[12], pending_xref,
refdomain="py", reftype="class", reftarget="int")

maybe_para = parameters[3][0]
assert_node(maybe_para, ([addnodes.literal_strong, "maybe"],
" (",
[pending_xref, addnodes.literal_emphasis, "tuple"],
[addnodes.literal_emphasis, "["],
[pending_xref, addnodes.literal_emphasis, "int"],
[addnodes.literal_emphasis, ", "],
[addnodes.literal_emphasis, "..."],
[addnodes.literal_emphasis, "]"],
[addnodes.literal_emphasis, " | "],
[pending_xref, addnodes.literal_emphasis, "None"],
")",
" -- ",
"union with ellipsis"))
assert_node(maybe_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="tuple")
assert_node(maybe_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="int")
assert_node(maybe_para[9], pending_xref,
refdomain="py", reftype="obj", reftarget="None")

literal_para = parameters[4][0]
assert_node(literal_para, ([addnodes.literal_strong, "literal"],
" (",
[pending_xref, addnodes.literal_emphasis, "Literal"],
[addnodes.literal_emphasis, "["],
[pending_xref, addnodes.literal_emphasis, "'|'"],
[addnodes.literal_emphasis, "]"],
")",
" -- ",
"literal pipe"))
assert_node(literal_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="Literal")
assert_node(literal_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="'|'")
emphasis_texts = [child.astext() for child in literal_para
if isinstance(child, addnodes.literal_emphasis)]
assert " | " not in emphasis_texts

literal_spaced_para = parameters[5][0]
assert_node(literal_spaced_para, ([addnodes.literal_strong, "literal_spaced"],
" (",
[pending_xref, addnodes.literal_emphasis, "Literal"],
[addnodes.literal_emphasis, "["],
[pending_xref, addnodes.literal_emphasis, "'foo| bar'"],
[addnodes.literal_emphasis, "]"],
")",
" -- ",
"literal containing a pipe"))
assert_node(literal_spaced_para[2], pending_xref,
refdomain="py", reftype="class", reftarget="Literal")
assert_node(literal_spaced_para[4], pending_xref,
refdomain="py", reftype="class", reftarget="'foo| bar'")
spaced_emphasis = [child.astext() for child in literal_spaced_para
if isinstance(child, addnodes.literal_emphasis)]
assert " | " not in spaced_emphasis


def test_info_field_list_vartype_union(app):
text = (".. py:class:: Holder\n"
"\n"
" :var sample_attribute: attribute text\n"
" :vartype sample_attribute: bytes | str\n")
doctree = restructuredtext.parse(app, text)

field = doctree[1][1][0][0]
paragraph = field[1][0]
assert_node(paragraph, ([addnodes.literal_strong, "sample_attribute"],
" (",
[pending_xref, addnodes.literal_emphasis, "bytes"],
[addnodes.literal_emphasis, " | "],
[pending_xref, addnodes.literal_emphasis, "str"],
")",
" -- ",
"attribute text"))
assert_node(paragraph[2], pending_xref,
refdomain="py", reftype="class", reftarget="bytes")
assert_node(paragraph[4], pending_xref,
refdomain="py", reftype="class", reftarget="str")


@pytest.mark.sphinx(freshenv=True)
def test_module_index(app):
text = (".. py:module:: docutils\n"
Expand Down