From 9f34d0a8860078568e369fef2ba0a08abcca3ac7 Mon Sep 17 00:00:00 2001 From: Billy Peake Date: Thu, 13 Dec 2018 11:32:22 -0800 Subject: [PATCH] fixed generic inheritence edge case and added test --- sphinx_autodoc_typehints.py | 43 +++++++++++++++++++++++++- tests/test_sphinx_autodoc_typehints.py | 8 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/sphinx_autodoc_typehints.py b/sphinx_autodoc_typehints.py index 90a8ac88..086b0257 100644 --- a/sphinx_autodoc_typehints.py +++ b/sphinx_autodoc_typehints.py @@ -119,7 +119,48 @@ def format_annotation(annotation): if Generic in annotation_cls.mro(): params = (getattr(annotation, '__parameters__', None) or getattr(annotation, '__args__', None)) - extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) + + try: + # join type annotation together in a formatted string + extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) + except TypeError: + # in some cases, a "Generic" has been inherited from another Generic + # with a concrete type, and therefore cannot be assigned a Type + # annotation. In this case, ``params`` will return None. Consider the + # following: + # + # T = TypeVar('T') + # + # class A(Generic[T]) + # def method() -> T + # pass + # + # + # class B(A[str]) + # pass + # + # + # def some_method_for_a(value: A[int]) -> None: + # + # + # def some_method_for_b(value: B) -> None: + # pass + # + # In the above, B inherits from a Generic type, but supplies a concrete + # type (str). As such, it does not get an additional annotation when + # used in a signature, since it will always be B[str]. This is unlike A, + # which is truly Generic. See the difference between the signatures of + # the two example functions above. + # + # B still passes as a Generic type, since it inherits from one, but has + # no annotations, since its Type Variable is set upon class definition. + # This causes, ``params`` above to return None, and throw a TypeError + # when joined. + # + # Here we need to detect this case, and just set ``extra`` to a blank + # string, so no additional annotation information is returned for B in + # signature annotations. + extra = '' return ':py:class:`~{}.{}`{}'.format(annotation.__module__, annotation_cls.__qualname__, extra) diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 7d28388c..728661d2 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -21,6 +21,11 @@ class B(Generic[T]): pass +# Tests inheriting a concrete generic +class C(B[str]): + pass + + class Slotted: __slots__ = () @@ -71,7 +76,8 @@ class Slotted: (Pattern[str], ':py:class:`~typing.Pattern`\\[:py:class:`str`]'), (A, ':py:class:`~%s.A`' % __name__), (B, ':py:class:`~%s.B`\\[\\~T]' % __name__), - (B[int], ':py:class:`~%s.B`\\[:py:class:`int`]' % __name__) + (B[int], ':py:class:`~%s.B`\\[:py:class:`int`]' % __name__), + (C, ':py:class:`~%s.C`' % __name__) ]) def test_format_annotation(annotation, expected_result): result = format_annotation(annotation)