From 4d3b09fc129690196aba7b46cf43995d8e59aec5 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 14 Jul 2019 14:02:24 +0200 Subject: [PATCH] documentation/python: further improve type annotation support. This is an awful mess, ffs. Like if nobody designed this with introspection in mind. --- documentation/python.py | 60 +++++++++++++++---- .../inspect_annotations.html | 28 +++++++++ .../inspect_annotations.py | 29 ++++++++- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/documentation/python.py b/documentation/python.py index 2b03d11d..97a8ac94 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -770,20 +770,54 @@ def extract_annotation(state: State, referrer_path: List[str], annotation) -> st elif isinstance(annotation, typing.ForwardRef if sys.version_info >= (3, 7) else typing._ForwardRef): return annotation.__forward_arg__ - # If the annotation is from the typing module, it could be a "bracketed" - # type, in which case we want to recurse to its types as well. Otherwise - # just get its name. - elif hasattr(annotation, '__module__') and annotation.__module__ == 'typing': - if sys.version_info >= (3, 7): - name = annotation._name - elif annotation is typing.Any: - name = 'Any' # Any doesn't have __name__ in 3.6 - else: - name = annotation.__name__ - if hasattr(annotation, '__args__'): - return 'typing.{}[{}]'.format(name, ', '.join([extract_annotation(state, referrer_path, i) for i in annotation.__args__])) + # Generic type names -- use their name directly + elif isinstance(annotation, typing.TypeVar): + return annotation.__name__ + + # If the annotation is from the typing module, it ... gets complicated. It + # could be a "bracketed" type, in which case we want to recurse to its + # types as well. + elif (hasattr(annotation, '__module__') and annotation.__module__ == 'typing'): + # Optional or Union, handle those first + if hasattr(annotation, '__origin__') and annotation.__origin__ is typing.Union: + # FOR SOME REASON `annotation.__args__[1] is None` is always False + if len(annotation.__args__) == 2 and isinstance(None, annotation.__args__[1]): + name = 'typing.Optional' + args = annotation.__args__[:1] + else: + name = 'typing.Union' + args = annotation.__args__ + elif sys.version_info >= (3, 7) and hasattr(annotation, '_name') and annotation._name: + name = 'typing.' + annotation._name + # Any doesn't have __args__ + args = annotation.__args__ if hasattr(annotation, '__args__') else None + # Python 3.6 has __name__ instead of _name + elif sys.version_info < (3, 7) and hasattr(annotation, '__name__'): + name = 'typing.' + annotation.__name__ + args = annotation.__args__ + # Any doesn't have __name__ in 3.6 + elif sys.version_info < (3, 7) and annotation is typing.Any: + name = 'typing.Any' + args = None + # Whoops, something we don't know yet. Warn and return a string + # representation at least. + else: # pragma: no cover + logging.warning("can't inspect annotation %s for %s, falling back to a string representation", annotation, '.'.join(referrer_path)) + return str(annotation) + + # Arguments of generic types, recurse inside + if args: + # For Callable, put the arguments into a nested list to separate + # them from the return type + if name == 'typing.Callable': + assert len(args) >= 1 + return '{}[[{}], {}]'.format(name, + ', '.join([extract_annotation(state, referrer_path, i) for i in args[:-1]]), + extract_annotation(state, referrer_path, args[-1])) + else: + return '{}[{}]'.format(name, ', '.join([extract_annotation(state, referrer_path, i) for i in args])) else: - return 'typing.' + name + return name # Things like (float, int) instead of Tuple[float, int] or using np.array # instead of np.ndarray. Ignore with a warning. diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.html b/documentation/test_python/inspect_annotations/inspect_annotations.html index 151ed400..82891543 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.html +++ b/documentation/test_python/inspect_annotations/inspect_annotations.html @@ -57,14 +57,42 @@ third: str = 'hello') -> Foo
An annotated function
+
+ def annotation_any(a: typing.Any) +
+
Annotation with the Any type
+
+ def annotation_callable(a: typing.Callable[[float, int], str]) +
+
Annotation with the Callable type
+
+ def annotation_callable_no_args(a: typing.Callable[[], typing.Dict[int, float]]) +
+
Annotation with the Callable type w/o arguments
def annotation_func_instead_of_type(a)
Annotation with a function instead of a type, ignored
+
+ def annotation_generic(a: typing.List[Tp]) -> Tp +
+
Annotation with a generic type
+
+ def annotation_list_noparam(a: typing.List[T]) +
+
Annotation with the unparametrized List type. 3.7 adds an implicit TypeVar to it, 3.6 not, emulate that to make the test pass on older versions
+
+ def annotation_optional(a: typing.Optional[float]) +
+
Annotation with the Optional type
def annotation_tuple_instead_of_tuple(a)
Annotation with a tuple instead of Tuple, ignored
+
+ def annotation_union(a: typing.Union[float, int]) +
+
Annotation with the Union type
def args_kwargs(a, b, *args, **kwargs)
diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.py b/documentation/test_python/inspect_annotations/inspect_annotations.py index 330628a1..5a4ea25d 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.py +++ b/documentation/test_python/inspect_annotations/inspect_annotations.py @@ -1,4 +1,6 @@ -from typing import List, Tuple +import sys + +from typing import List, Tuple, Dict, Any, Union, Optional, Callable, TypeVar class Foo: """A class with properties""" @@ -36,6 +38,31 @@ def annotation_tuple_instead_of_tuple(a: (float, int)): def annotation_func_instead_of_type(a: open): """Annotation with a function instead of a type, ignored""" +def annotation_any(a: Any): + """Annotation with the Any type""" + +def annotation_union(a: Union[float, int]): + """Annotation with the Union type""" + +def annotation_list_noparam(a: List): + """Annotation with the unparametrized List type. 3.7 adds an implicit TypeVar to it, 3.6 not, emulate that to make the test pass on older versions""" +if sys.version_info < (3, 7): + annotation_list_noparam.__annotations__['a'] = List[TypeVar('T')] + +_T = TypeVar('Tp') + +def annotation_generic(a: List[_T]) -> _T: + """Annotation with a generic type""" + +def annotation_optional(a: Optional[float]): + """Annotation with the Optional type""" + +def annotation_callable(a: Callable[[float, int], str]): + """Annotation with the Callable type""" + +def annotation_callable_no_args(a: Callable[[], Dict[int, float]]): + """Annotation with the Callable type w/o arguments""" + # Only possible with native code now, https://www.python.org/dev/peps/pep-0570/ #def positionals_only(positional_only, /, positional_kw): #"""Function with explicitly delimited positional args""" -- 2.30.2