From c7b169c5a95f9d84583afcfe18773742a67cdef0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 17 Apr 2021 02:06:57 +0900 Subject: [PATCH 1/2] Fix #8127: py domain: Ellipsis in info-field-list causes nit-picky warning On parsing the types, the leading dot of the ellipsis (...) is considered as a reference name. And its first dot is considered as a notation for relative type reference (ex. ".ClassName"). As a result, it was converted double dots unexpectedly. This changes the parsing rule to treat the ellipsis as a symbol, not a name. --- CHANGES | 1 + sphinx/domains/python.py | 2 +- tests/test_domain_py.py | 25 ++++++++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 28e89863f..581be506e 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,7 @@ Deprecated Features added -------------- +* #8127: py domain: Ellipsis in info-field-list causes nit-picky warning * #9023: More CSS classes on domain descriptions, see :ref:`nodes` for details. Bugs fixed diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index d0c5f7118..dbb315e6e 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -304,7 +304,7 @@ class PyXrefMixin: 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+|\.\.\.)' delims_re = re.compile(delims) sub_targets = re.split(delims, target) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index f5df9084b..214cb58a0 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -876,7 +876,9 @@ def test_info_field_list(app): "\n" " :param str name: blah blah\n" " :param age: blah blah\n" - " :type age: int\n") + " :type age: int\n" + " :param items: blah blah\n" + " :type items: Tuple[str, ...]\n") doctree = restructuredtext.parse(app, text) print(doctree) @@ -890,6 +892,7 @@ def test_info_field_list(app): assert_node(doctree[3][1][0][0], ([nodes.field_name, "Parameters"], [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph], + [nodes.list_item, nodes.paragraph], [nodes.list_item, nodes.paragraph])])) # :param str name: @@ -916,6 +919,26 @@ def test_info_field_list(app): refdomain="py", reftype="class", reftarget="int", **{"py:module": "example", "py:class": "Class"}) + # :param items: + :type items: + assert_node(doctree[3][1][0][0][1][0][2][0], + ([addnodes.literal_strong, "items"], + " (", + [pending_xref, addnodes.literal_emphasis, "Tuple"], + [addnodes.literal_emphasis, "["], + [pending_xref, addnodes.literal_emphasis, "str"], + [addnodes.literal_emphasis, ", "], + [addnodes.literal_emphasis, "..."], + [addnodes.literal_emphasis, "]"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][2][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="Tuple", + **{"py:module": "example", "py:class": "Class"}) + assert_node(doctree[3][1][0][0][1][0][2][0][4], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + def test_info_field_list_var(app): text = (".. py:class:: Class\n" From c2c2b81f9156c82df6d7795614736f99187195da Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 17 Apr 2021 23:54:44 +0900 Subject: [PATCH 2/2] Fix #8818: autodoc: Super class having ``Any`` arguments causes nit-picky warning On generating the base class information, unexpected nit-picky warning for ``typing.Any`` was emitted. This fixes it by using `~` prefix on generating a cross-reference to make it valid. --- CHANGES | 1 + sphinx/util/typing.py | 55 +++++++++++++++++--------- tests/test_ext_autodoc.py | 4 +- tests/test_ext_autodoc_autoclass.py | 3 +- tests/test_util_typing.py | 60 ++++++++++++++++++----------- 5 files changed, 78 insertions(+), 45 deletions(-) diff --git a/CHANGES b/CHANGES index 581be506e..654500aaf 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,7 @@ Deprecated Features added -------------- +* #8818: autodoc: Super class having ``Any`` arguments causes nit-picky warning * #8127: py domain: Ellipsis in info-field-list causes nit-picky warning * #9023: More CSS classes on domain descriptions, see :ref:`nodes` for details. diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index fcecb8bb1..957f8a332 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -138,16 +138,16 @@ def _restify_py37(cls: Optional[Type]) -> str: if len(cls.__args__) > 1 and cls.__args__[-1] is NoneType: if len(cls.__args__) > 2: args = ', '.join(restify(a) for a in cls.__args__[:-1]) - return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % args + return ':obj:`~typing.Optional`\\ [:obj:`~typing.Union`\\ [%s]]' % args else: - return ':obj:`Optional`\\ [%s]' % restify(cls.__args__[0]) + return ':obj:`~typing.Optional`\\ [%s]' % restify(cls.__args__[0]) else: args = ', '.join(restify(a) for a in cls.__args__) - return ':obj:`Union`\\ [%s]' % args + return ':obj:`~typing.Union`\\ [%s]' % args elif inspect.isgenericalias(cls): if getattr(cls, '_name', None): if cls.__module__ == 'typing': - text = ':class:`%s`' % cls._name + text = ':class:`~%s.%s`' % (cls.__module__, cls._name) else: text = ':class:`%s.%s`' % (cls.__module__, cls._name) else: @@ -167,20 +167,23 @@ def _restify_py37(cls: Optional[Type]) -> str: return text elif hasattr(cls, '__qualname__'): if cls.__module__ == 'typing': - return ':class:`%s`' % cls.__qualname__ + return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__) else: return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__) elif hasattr(cls, '_name'): # SpecialForm if cls.__module__ == 'typing': - return ':obj:`%s`' % cls._name + return ':obj:`~%s.%s`' % (cls.__module__, cls._name) else: return ':obj:`%s.%s`' % (cls.__module__, cls._name) elif isinstance(cls, ForwardRef): return ':class:`%s`' % cls.__forward_arg__ else: # not a class (ex. TypeVar) - return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) + if cls.__module__ == 'typing': + return ':obj:`~%s.%s`' % (cls.__module__, cls.__name__) + else: + return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) def _restify_py36(cls: Optional[Type]) -> str: @@ -203,13 +206,23 @@ def _restify_py36(cls: Optional[Type]) -> str: if (isinstance(cls, typing.TupleMeta) and # type: ignore not hasattr(cls, '__tuple_params__')): + if module == 'typing': + reftext = ':class:`~typing.%s`' % qualname + else: + reftext = ':class:`%s`' % qualname + params = cls.__args__ if params: param_str = ', '.join(restify(p) for p in params) - return ':class:`%s`\\ [%s]' % (qualname, param_str) + return reftext + '\\ [%s]' % param_str else: - return ':class:`%s`' % qualname + return reftext elif isinstance(cls, typing.GenericMeta): + if module == 'typing': + reftext = ':class:`~typing.%s`' % qualname + else: + reftext = ':class:`%s`' % qualname + if cls.__args__ is None or len(cls.__args__) <= 2: # type: ignore # NOQA params = cls.__args__ # type: ignore elif cls.__origin__ == Generator: # type: ignore @@ -217,13 +230,13 @@ def _restify_py36(cls: Optional[Type]) -> str: else: # typing.Callable args = ', '.join(restify(arg) for arg in cls.__args__[:-1]) # type: ignore result = restify(cls.__args__[-1]) # type: ignore - return ':class:`%s`\\ [[%s], %s]' % (qualname, args, result) + return reftext + '\\ [[%s], %s]' % (args, result) if params: param_str = ', '.join(restify(p) for p in params) - return ':class:`%s`\\ [%s]' % (qualname, param_str) + return reftext + '\\ [%s]' % (param_str) else: - return ':class:`%s`' % qualname + return reftext elif (hasattr(cls, '__origin__') and cls.__origin__ is typing.Union): params = cls.__args__ @@ -231,32 +244,36 @@ def _restify_py36(cls: Optional[Type]) -> str: if len(params) > 1 and params[-1] is NoneType: if len(params) > 2: param_str = ", ".join(restify(p) for p in params[:-1]) - return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % param_str + return (':obj:`~typing.Optional`\\ ' + '[:obj:`~typing.Union`\\ [%s]]' % param_str) else: - return ':obj:`Optional`\\ [%s]' % restify(params[0]) + return ':obj:`~typing.Optional`\\ [%s]' % restify(params[0]) else: param_str = ', '.join(restify(p) for p in params) - return ':obj:`Union`\\ [%s]' % param_str + return ':obj:`~typing.Union`\\ [%s]' % param_str else: return ':obj:`Union`' elif hasattr(cls, '__qualname__'): if cls.__module__ == 'typing': - return ':class:`%s`' % cls.__qualname__ + return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__) else: return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__) elif hasattr(cls, '_name'): # SpecialForm if cls.__module__ == 'typing': - return ':obj:`%s`' % cls._name + return ':obj:`~%s.%s`' % (cls.__module__, cls._name) else: return ':obj:`%s.%s`' % (cls.__module__, cls._name) elif hasattr(cls, '__name__'): # not a class (ex. TypeVar) - return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) + if cls.__module__ == 'typing': + return ':obj:`~%s.%s`' % (cls.__module__, cls.__name__) + else: + return ':obj:`%s.%s`' % (cls.__module__, cls.__name__) else: # others (ex. Any) if cls.__module__ == 'typing': - return ':obj:`%s`' % qualname + return ':obj:`~%s.%s`' % (cls.__module__, qualname) else: return ':obj:`%s.%s`' % (cls.__module__, qualname) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 8ace97813..ccdee6bb2 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1893,12 +1893,12 @@ def test_autodoc_GenericAlias(app): ' .. py:attribute:: Class.T', ' :module: target.genericalias', '', - ' alias of :class:`List`\\ [:class:`int`]', + ' alias of :class:`~typing.List`\\ [:class:`int`]', '', '.. py:attribute:: T', ' :module: target.genericalias', '', - ' alias of :class:`List`\\ [:class:`int`]', + ' alias of :class:`~typing.List`\\ [:class:`int`]', ] else: assert list(actual) == [ diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 940263387..d879f8e14 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -256,7 +256,8 @@ def test_show_inheritance_for_subclass_of_generic_type(app): '.. py:class:: Quux(iterable=(), /)', ' :module: target.classes', '', - ' Bases: :class:`List`\\ [:obj:`Union`\\ [:class:`int`, :class:`float`]]', + ' Bases: :class:`~typing.List`\\ ' + '[:obj:`~typing.Union`\\ [:class:`int`, :class:`float`]]', '', ' A subclass of List[Union[int, float]]', '', diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 5a5808ac5..d85eb0849 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -47,42 +47,56 @@ def test_restify(): assert restify(Integral) == ":class:`numbers.Integral`" assert restify(Struct) == ":class:`struct.Struct`" assert restify(TracebackType) == ":class:`types.TracebackType`" - assert restify(Any) == ":obj:`Any`" + assert restify(Any) == ":obj:`~typing.Any`" def test_restify_type_hints_containers(): - assert restify(List) == ":class:`List`" - assert restify(Dict) == ":class:`Dict`" - assert restify(List[int]) == ":class:`List`\\ [:class:`int`]" - assert restify(List[str]) == ":class:`List`\\ [:class:`str`]" - assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]" - assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]" - assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]" - assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]" - assert restify(MyList[Tuple[int, int]]) == ":class:`tests.test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]" - assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]" + assert restify(List) == ":class:`~typing.List`" + assert restify(Dict) == ":class:`~typing.Dict`" + assert restify(List[int]) == ":class:`~typing.List`\\ [:class:`int`]" + assert restify(List[str]) == ":class:`~typing.List`\\ [:class:`str`]" + assert restify(Dict[str, float]) == (":class:`~typing.Dict`\\ " + "[:class:`str`, :class:`float`]") + assert restify(Tuple[str, str, str]) == (":class:`~typing.Tuple`\\ " + "[:class:`str`, :class:`str`, :class:`str`]") + assert restify(Tuple[str, ...]) == ":class:`~typing.Tuple`\\ [:class:`str`, ...]" + assert restify(List[Dict[str, Tuple]]) == (":class:`~typing.List`\\ " + "[:class:`~typing.Dict`\\ " + "[:class:`str`, :class:`~typing.Tuple`]]") + assert restify(MyList[Tuple[int, int]]) == (":class:`tests.test_util_typing.MyList`\\ " + "[:class:`~typing.Tuple`\\ " + "[:class:`int`, :class:`int`]]") + assert restify(Generator[None, None, None]) == (":class:`~typing.Generator`\\ " + "[:obj:`None`, :obj:`None`, :obj:`None`]") def test_restify_type_hints_Callable(): - assert restify(Callable) == ":class:`Callable`" + assert restify(Callable) == ":class:`~typing.Callable`" if sys.version_info >= (3, 7): - assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]" - assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]" + assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ " + "[[:class:`str`], :class:`int`]") + assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ " + "[[...], :class:`int`]") else: - assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]" - assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]" + assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ " + "[:class:`str`, :class:`int`]") + assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ " + "[..., :class:`int`]") def test_restify_type_hints_Union(): - assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]" - assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]" - assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]" + assert restify(Optional[int]) == ":obj:`~typing.Optional`\\ [:class:`int`]" + assert restify(Union[str, None]) == ":obj:`~typing.Optional`\\ [:class:`str`]" + assert restify(Union[int, str]) == ":obj:`~typing.Union`\\ [:class:`int`, :class:`str`]" if sys.version_info >= (3, 7): - assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]" + assert restify(Union[int, Integral]) == (":obj:`~typing.Union`\\ " + "[:class:`int`, :class:`numbers.Integral`]") assert (restify(Union[MyClass1, MyClass2]) == - ":obj:`Union`\\ [:class:`tests.test_util_typing.MyClass1`, :class:`tests.test_util_typing.`]") + (":obj:`~typing.Union`\\ " + "[:class:`tests.test_util_typing.MyClass1`, " + ":class:`tests.test_util_typing.`]")) else: assert restify(Union[int, Integral]) == ":class:`numbers.Integral`" assert restify(Union[MyClass1, MyClass2]) == ":class:`tests.test_util_typing.MyClass1`" @@ -97,7 +111,7 @@ def test_restify_type_hints_typevars(): assert restify(T) == ":obj:`tests.test_util_typing.T`" assert restify(T_co) == ":obj:`tests.test_util_typing.T_co`" assert restify(T_contra) == ":obj:`tests.test_util_typing.T_contra`" - assert restify(List[T]) == ":class:`List`\\ [:obj:`tests.test_util_typing.T`]" + assert restify(List[T]) == ":class:`~typing.List`\\ [:obj:`tests.test_util_typing.T`]" assert restify(MyInt) == ":class:`MyInt`" @@ -110,7 +124,7 @@ def test_restify_type_hints_alias(): MyStr = str MyTuple = Tuple[str, str] assert restify(MyStr) == ":class:`str`" - assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore + assert restify(MyTuple) == ":class:`~typing.Tuple`\\ [:class:`str`, :class:`str`]" @pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')