From 742c89f99b0cbcf4e6a54a1b59ad3e424531463d Mon Sep 17 00:00:00 2001 From: Terence Honles Date: Thu, 9 Aug 2018 12:31:16 -0700 Subject: [PATCH 1/9] change format_args to prefer annotations from typing.get_type_hints The current code looks for the pattern:: class Node: def parent(self) -> 'Node': pass This change allows more complex forward references:: class Node: def children(self) -> Set['Node']: pass --- sphinx/util/inspect.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 615b6c976d..c472a1b96a 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -433,8 +433,7 @@ class Signature(object): if PY2 or self.return_annotation is inspect.Parameter.empty: return '(%s)' % ', '.join(args) else: - if isinstance(self.return_annotation, string_types) and \ - 'return' in self.annotations: + if 'return' in self.annotations: annotation = self.format_annotation(self.annotations['return']) else: annotation = self.format_annotation(self.return_annotation) From da45604485477d50e84ccd825f7348e0f560ead5 Mon Sep 17 00:00:00 2001 From: "Terence D. Honles" Date: Thu, 9 Aug 2018 14:47:35 -0700 Subject: [PATCH 2/9] handle NoneType as None --- sphinx/util/inspect.py | 6 ++++++ tests/test_util_inspect.py | 6 +++++- tests/typing_test_data.py | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index c472a1b96a..7dbf742b4f 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -20,6 +20,7 @@ from six import PY2, PY3, StringIO, binary_type, string_types, itervalues from six.moves import builtins from sphinx.util import force_decode +from sphinx.util.pycompat import NoneType if False: # For type annotation @@ -464,6 +465,8 @@ class Signature(object): return annotation.__name__ elif not annotation: return repr(annotation) + elif annotation is NoneType: # type: ignore + return 'None' elif module == 'builtins': return annotation.__qualname__ elif annotation is Ellipsis: @@ -518,6 +521,9 @@ class Signature(object): else: qualified_name = (annotation.__module__ + '.' + annotation.__qualname__) # type: ignore # NOQA + if annotation is NoneType: # type: ignore + return 'None' + if annotation.__module__ == 'builtins': return annotation.__qualname__ # type: ignore elif (hasattr(typing, 'TupleMeta') and diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 9558f7277c..2b6e0abb26 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -231,7 +231,8 @@ def test_Signature_partialmethod(): @pytest.mark.skipif(sys.version_info < (3, 5), reason='type annotation test is available on py35 or above') def test_Signature_annotations(): - from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12 + from typing_test_data import ( + f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, Node) # Class annotations sig = inspect.Signature(f0).format_args() @@ -288,6 +289,9 @@ def test_Signature_annotations(): sig = inspect.Signature(f12).format_args() assert sig == '() -> Tuple[int, str, int]' + sig = inspect.Signature(Node.children).format_args() + assert sig == '(self) -> List[typing_test_data.Node]' + def test_safe_getattr_with_default(): class Foo(object): diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 71acf1f47f..7ad90551d9 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -66,3 +66,8 @@ def f11(x: CustomAnnotation(), y: 123) -> None: def f12() -> Tuple[int, str, int]: pass + + +class Node: + def children(self) -> List['Node']: + pass From caf57bcc89869edba0b81b174d4957087390299c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 13 Aug 2018 21:41:17 +0900 Subject: [PATCH 3/9] Update CHANGES for PR #5280 --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 26d016677f..4881ce3c45 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,7 @@ Bugs fixed 1.5 * LaTeX: fix the :confval:`latex_engine` documentation regarding Latin Modern font with XeLaTeX/LuaLateX (refs: #5251) +* #5280: autodoc: Fix wrong type annotations for complex typing Testing -------- From e26dc81272b1645fe87a70b68153a77cfd06bb2f Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 00:32:14 +0900 Subject: [PATCH 4/9] Fix autodoc: Optional types are wrongly rendered --- CHANGES | 1 + sphinx/util/inspect.py | 85 +++++++++++++++++++++++--------------- tests/test_util_inspect.py | 7 +++- tests/typing_test_data.py | 6 ++- 4 files changed, 63 insertions(+), 36 deletions(-) diff --git a/CHANGES b/CHANGES index 4881ce3c45..2cdb52a6cb 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,7 @@ Bugs fixed * LaTeX: fix the :confval:`latex_engine` documentation regarding Latin Modern font with XeLaTeX/LuaLateX (refs: #5251) * #5280: autodoc: Fix wrong type annotations for complex typing +* autodoc: Optional types are wrongly rendered Testing -------- diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 7dbf742b4f..2c72af7a07 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -486,8 +486,11 @@ class Signature(object): if getattr(annotation, '__args__', None): if qualname == 'Union': - args = ', '.join(self.format_annotation(a) for a in annotation.__args__) - return '%s[%s]' % (qualname, args) + if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: + return 'Optional[%s]' % self.format_annotation(annotation.__args__[0]) + else: + args = ', '.join(self.format_annotation(a) for a in annotation.__args__) + return '%s[%s]' % (qualname, args) elif qualname == 'Callable': args = ', '.join(self.format_annotation(a) for a in annotation.__args__[:-1]) returns = self.format_annotation(annotation.__args__[-1]) @@ -501,38 +504,39 @@ class Signature(object): def format_annotation_old(self, annotation): # type: (Any) -> str """format_annotation() for py36 or below""" + module = getattr(annotation, '__module__', None) if isinstance(annotation, string_types): return annotation # type: ignore - if isinstance(annotation, typing.TypeVar): # type: ignore + elif isinstance(annotation, typing.TypeVar): # type: ignore return annotation.__name__ - if annotation == Ellipsis: - return '...' - if not isinstance(annotation, type): - qualified_name = repr(annotation) - if qualified_name.startswith('typing.'): # for typing.Union - return qualified_name.split('.', 1)[1] - else: - return qualified_name - - if not annotation: - qualified_name = repr(annotation) - elif annotation.__module__ == 'typing': - qualified_name = annotation.__qualname__ # type: ignore - else: - qualified_name = (annotation.__module__ + '.' + annotation.__qualname__) # type: ignore # NOQA - - if annotation is NoneType: # type: ignore + elif not annotation: + return repr(annotation) + elif annotation is NoneType: # type: ignore return 'None' + elif module == 'builtins': + return annotation.__qualname__ + elif annotation is Ellipsis: + return '...' - if annotation.__module__ == 'builtins': - return annotation.__qualname__ # type: ignore - elif (hasattr(typing, 'TupleMeta') and - isinstance(annotation, typing.TupleMeta) and # type: ignore - not hasattr(annotation, '__tuple_params__')): + if module == 'typing': + if getattr(annotation, '_name', None): + qualname = annotation._name + elif getattr(annotation, '__qualname__', None): + qualname = annotation.__qualname__ + else: + qualname = self.format_annotation(annotation.__origin__) # ex. Union + elif hasattr(annotation, '__qualname__'): + qualname = '%s.%s' % (module, annotation.__qualname__) + else: + qualname = repr(annotation) + + if (hasattr(typing, 'TupleMeta') and + isinstance(annotation, typing.TupleMeta) and # type: ignore + not hasattr(annotation, '__tuple_params__')): # This is for Python 3.6+, 3.5 case is handled below params = annotation.__args__ param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualified_name, param_str) + return '%s[%s]' % (qualname, param_str) elif (hasattr(typing, 'GenericMeta') and # for py36 or below isinstance(annotation, typing.GenericMeta)): # In Python 3.5.2+, all arguments are stored in __args__, @@ -548,19 +552,32 @@ class Signature(object): args = ', '.join(self.format_annotation(arg) for arg in annotation.__args__[:-1]) # type: ignore result = self.format_annotation(annotation.__args__[-1]) # type: ignore - return '%s[[%s], %s]' % (qualified_name, args, result) + return '%s[[%s], %s]' % (qualname, args, result) elif hasattr(annotation, '__parameters__'): params = annotation.__parameters__ # type: ignore if params is not None: param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualified_name, param_str) + return '%s[%s]' % (qualname, param_str) elif (hasattr(typing, 'UnionMeta') and # for py35 or below isinstance(annotation, typing.UnionMeta) and # type: ignore hasattr(annotation, '__union_params__')): params = annotation.__union_params__ if params is not None: - param_str = ', '.join(self.format_annotation(p) for p in params) - return '%s[%s]' % (qualified_name, param_str) + if len(params) == 2 and params[1] is NoneType: + return 'Optional[%s]' % self.format_annotation(params[0]) + else: + param_str = ', '.join(self.format_annotation(p) for p in params) + return '%s[%s]' % (qualname, param_str) + elif (hasattr(typing, 'Union') and # for py36 + hasattr(annotation, '__origin__') and + annotation.__origin__ is typing.Union): + params = annotation.__args__ + if params is not None: + if len(params) == 2 and params[1] is NoneType: + return 'Optional[%s]' % self.format_annotation(params[0]) + else: + param_str = ', '.join(self.format_annotation(p) for p in params) + return 'Union[%s]' % param_str elif (hasattr(typing, 'CallableMeta') and # for py36 or below isinstance(annotation, typing.CallableMeta) and # type: ignore getattr(annotation, '__args__', None) is not None and @@ -568,13 +585,13 @@ class Signature(object): # Skipped in the case of plain typing.Callable args = annotation.__args__ if args is None: - return qualified_name + return qualname elif args is Ellipsis: args_str = '...' else: formatted_args = (self.format_annotation(a) for a in args) args_str = '[%s]' % ', '.join(formatted_args) - return '%s[%s, %s]' % (qualified_name, + return '%s[%s, %s]' % (qualname, args_str, self.format_annotation(annotation.__result__)) elif (hasattr(typing, 'TupleMeta') and # for py36 or below @@ -586,10 +603,10 @@ class Signature(object): param_strings = [self.format_annotation(p) for p in params] if annotation.__tuple_use_ellipsis__: param_strings.append('...') - return '%s[%s]' % (qualified_name, + return '%s[%s]' % (qualname, ', '.join(param_strings)) - return qualified_name + return qualname if sys.version_info >= (3, 5): diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 2b6e0abb26..61a8308e54 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -232,7 +232,7 @@ def test_Signature_partialmethod(): reason='type annotation test is available on py35 or above') def test_Signature_annotations(): from typing_test_data import ( - f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, Node) + f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, Node) # Class annotations sig = inspect.Signature(f0).format_args() @@ -289,6 +289,11 @@ def test_Signature_annotations(): sig = inspect.Signature(f12).format_args() assert sig == '() -> Tuple[int, str, int]' + # optional + sig = inspect.Signature(f13).format_args() + assert sig == '() -> Optional[str]' + + # type hints by string sig = inspect.Signature(Node.children).format_args() assert sig == '(self) -> List[typing_test_data.Node]' diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 7ad90551d9..35064b042e 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -1,5 +1,5 @@ from numbers import Integral -from typing import List, TypeVar, Union, Callable, Tuple +from typing import List, TypeVar, Union, Callable, Tuple, Optional def f0(x: int, y: Integral) -> None: @@ -68,6 +68,10 @@ def f12() -> Tuple[int, str, int]: pass +def f13() -> Optional[str]: + pass + + class Node: def children(self) -> List['Node']: pass From 5a0f0bdc61ef72b7635ab2eff125e6dcd82191f3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 00:42:25 +0900 Subject: [PATCH 5/9] refactor: format_annotation() --- sphinx/util/inspect.py | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 2c72af7a07..2de9ca2948 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -450,6 +450,19 @@ class Signature(object): Displaying complex types from ``typing`` relies on its private API. """ + if isinstance(annotation, string_types): + return annotation # type: ignore + elif isinstance(annotation, typing.TypeVar): # type: ignore + return annotation.__name__ + elif not annotation: + return repr(annotation) + elif annotation is NoneType: # type: ignore + return 'None' + elif getattr(annotation, '__module__', None) == 'builtins': + return annotation.__qualname__ + elif annotation is Ellipsis: + return '...' + if sys.version_info >= (3, 7): # py37+ return self.format_annotation_new(annotation) else: @@ -459,19 +472,6 @@ class Signature(object): # type: (Any) -> str """format_annotation() for py37+""" module = getattr(annotation, '__module__', None) - if isinstance(annotation, string_types): - return annotation # type: ignore - elif isinstance(annotation, typing.TypeVar): # type: ignore - return annotation.__name__ - elif not annotation: - return repr(annotation) - elif annotation is NoneType: # type: ignore - return 'None' - elif module == 'builtins': - return annotation.__qualname__ - elif annotation is Ellipsis: - return '...' - if module == 'typing': if getattr(annotation, '_name', None): qualname = annotation._name @@ -505,19 +505,6 @@ class Signature(object): # type: (Any) -> str """format_annotation() for py36 or below""" module = getattr(annotation, '__module__', None) - if isinstance(annotation, string_types): - return annotation # type: ignore - elif isinstance(annotation, typing.TypeVar): # type: ignore - return annotation.__name__ - elif not annotation: - return repr(annotation) - elif annotation is NoneType: # type: ignore - return 'None' - elif module == 'builtins': - return annotation.__qualname__ - elif annotation is Ellipsis: - return '...' - if module == 'typing': if getattr(annotation, '_name', None): qualname = annotation._name From 955c3d388be01600aad6043c609c5fda01d96ebf Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 00:48:28 +0900 Subject: [PATCH 6/9] test Signature annotations in py34 too --- tests/test_util_inspect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 61a8308e54..51c2d06e1f 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -228,8 +228,8 @@ def test_Signature_partialmethod(): assert sig == '()' -@pytest.mark.skipif(sys.version_info < (3, 5), - reason='type annotation test is available on py35 or above') +@pytest.mark.skipif(sys.version_info < (3, 4), + reason='type annotation test is available on py34 or above') def test_Signature_annotations(): from typing_test_data import ( f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, Node) From cc3de81debdecb62cfb8cc182e4dd5e5002d2396 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 11:39:54 +0900 Subject: [PATCH 7/9] Fix mypy annotations --- sphinx/util/inspect.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 2de9ca2948..c3fe0178d4 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -486,7 +486,7 @@ class Signature(object): if getattr(annotation, '__args__', None): if qualname == 'Union': - if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: + if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA return 'Optional[%s]' % self.format_annotation(annotation.__args__[0]) else: args = ', '.join(self.format_annotation(a) for a in annotation.__args__) @@ -550,7 +550,7 @@ class Signature(object): hasattr(annotation, '__union_params__')): params = annotation.__union_params__ if params is not None: - if len(params) == 2 and params[1] is NoneType: + if len(params) == 2 and params[1] is NoneType: # type: ignore return 'Optional[%s]' % self.format_annotation(params[0]) else: param_str = ', '.join(self.format_annotation(p) for p in params) @@ -560,7 +560,7 @@ class Signature(object): annotation.__origin__ is typing.Union): params = annotation.__args__ if params is not None: - if len(params) == 2 and params[1] is NoneType: + if len(params) == 2 and params[1] is NoneType: # type: ignore return 'Optional[%s]' % self.format_annotation(params[0]) else: param_str = ', '.join(self.format_annotation(p) for p in params) From 91650d89c9bb53c78e22eb94a31edc5656faa0b5 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 01:42:07 +0900 Subject: [PATCH 8/9] Fix #5291: autodoc crashed by ForwardRef types --- CHANGES | 2 ++ sphinx/util/inspect.py | 4 ++++ tests/test_util_inspect.py | 3 +++ tests/typing_test_data.py | 3 +++ 4 files changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 2cdb52a6cb..7472232da6 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,8 @@ Bugs fixed font with XeLaTeX/LuaLateX (refs: #5251) * #5280: autodoc: Fix wrong type annotations for complex typing * autodoc: Optional types are wrongly rendered +* #5291: autodoc crashed by ForwardRef types + Testing -------- diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index c3fe0178d4..64e93f2673 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -477,6 +477,8 @@ class Signature(object): qualname = annotation._name elif getattr(annotation, '__qualname__', None): qualname = annotation.__qualname__ + elif getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ else: qualname = self.format_annotation(annotation.__origin__) # ex. Union elif hasattr(annotation, '__qualname__'): @@ -510,6 +512,8 @@ class Signature(object): qualname = annotation._name elif getattr(annotation, '__qualname__', None): qualname = annotation.__qualname__ + elif getattr(annotation, '__forward_arg__', None): + qualname = annotation.__forward_arg__ else: qualname = self.format_annotation(annotation.__origin__) # ex. Union elif hasattr(annotation, '__qualname__'): diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 51c2d06e1f..a48aedcf0f 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -297,6 +297,9 @@ def test_Signature_annotations(): sig = inspect.Signature(Node.children).format_args() assert sig == '(self) -> List[typing_test_data.Node]' + sig = inspect.Signature(Node.__init__).format_args() + assert sig == '(self, parent: Optional[Node]) -> None' + def test_safe_getattr_with_default(): class Foo(object): diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 35064b042e..5b161eac41 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -73,5 +73,8 @@ def f13() -> Optional[str]: class Node: + def __init__(self, parent: Optional['Node']) -> None: + pass + def children(self) -> List['Node']: pass From 1668f007c299871a0699b58e6b3475cdf4c243a3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 15 Aug 2018 22:29:29 +0900 Subject: [PATCH 9/9] Fix #5298: imgmath: math_number_all causes equations to have two numbers in html --- CHANGES | 1 + sphinx/ext/imgmath.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 2cdb52a6cb..66493877d8 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,7 @@ Bugs fixed font with XeLaTeX/LuaLateX (refs: #5251) * #5280: autodoc: Fix wrong type annotations for complex typing * autodoc: Optional types are wrongly rendered +* #5298: imgmath: math_number_all causes equations to have two numbers in html Testing -------- diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index 5f9d7a1e2b..cfec4db28d 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -319,8 +319,7 @@ def html_visit_displaymath(self, node): if node['nowrap']: latex = node['latex'] else: - latex = wrap_displaymath(node['latex'], None, - self.builder.config.math_number_all) + latex = wrap_displaymath(node['latex'], None, False) try: fname, depth = render_math(self, latex) except MathExtError as exc: