chiark / gitweb /
documentation/python: further improve type annotation support.
authorVladimír Vondruš <mosra@centrum.cz>
Sun, 14 Jul 2019 12:02:24 +0000 (14:02 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Sun, 14 Jul 2019 17:11:08 +0000 (19:11 +0200)
This is an awful mess, ffs. Like if nobody designed this with
introspection in mind.

documentation/python.py
documentation/test_python/inspect_annotations/inspect_annotations.html
documentation/test_python/inspect_annotations/inspect_annotations.py

index 2b03d11d2905783c2bde7c5d2ed546363deacc79..97a8ac94120593f2086deefbfc47a6c4eabd0596 100755 (executable)
@@ -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.
index 151ed4007dabaeaa5e217a80ef7a262d96eb2535..82891543e8078bb2d616d21d3cc0d8022d60e43f 100644 (file)
               third: str = &#x27;hello&#x27;) -&gt; <a href="inspect_annotations.Foo.html" class="m-doc">Foo</a></span>
             </dt>
             <dd>An annotated function</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_any" class="m-doc-self" id="annotation_any">annotation_any</a>(</span><span class="m-doc-wrap">a: typing.Any)</span>
+            </dt>
+            <dd>Annotation with the Any type</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_callable" class="m-doc-self" id="annotation_callable">annotation_callable</a>(</span><span class="m-doc-wrap">a: typing.Callable[[float, int], str])</span>
+            </dt>
+            <dd>Annotation with the Callable type</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_callable_no_args" class="m-doc-self" id="annotation_callable_no_args">annotation_callable_no_args</a>(</span><span class="m-doc-wrap">a: typing.Callable[[], typing.Dict[int, float]])</span>
+            </dt>
+            <dd>Annotation with the Callable type w/o arguments</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#annotation_func_instead_of_type" class="m-doc-self" id="annotation_func_instead_of_type">annotation_func_instead_of_type</a>(</span><span class="m-doc-wrap">a)</span>
             </dt>
             <dd>Annotation with a function instead of a type, ignored</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_generic" class="m-doc-self" id="annotation_generic">annotation_generic</a>(</span><span class="m-doc-wrap">a: typing.List[Tp]) -&gt; Tp</span>
+            </dt>
+            <dd>Annotation with a generic type</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_list_noparam" class="m-doc-self" id="annotation_list_noparam">annotation_list_noparam</a>(</span><span class="m-doc-wrap">a: typing.List[T])</span>
+            </dt>
+            <dd>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</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_optional" class="m-doc-self" id="annotation_optional">annotation_optional</a>(</span><span class="m-doc-wrap">a: typing.Optional[float])</span>
+            </dt>
+            <dd>Annotation with the Optional type</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#annotation_tuple_instead_of_tuple" class="m-doc-self" id="annotation_tuple_instead_of_tuple">annotation_tuple_instead_of_tuple</a>(</span><span class="m-doc-wrap">a)</span>
             </dt>
             <dd>Annotation with a tuple instead of Tuple, ignored</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="#annotation_union" class="m-doc-self" id="annotation_union">annotation_union</a>(</span><span class="m-doc-wrap">a: typing.Union[float, int])</span>
+            </dt>
+            <dd>Annotation with the Union type</dd>
             <dt>
               <span class="m-doc-wrap-bumper">def <a href="#args_kwargs" class="m-doc-self" id="args_kwargs">args_kwargs</a>(</span><span class="m-doc-wrap">a, b, *args, **kwargs)</span>
             </dt>
index 330628a1bf3b920eb761ca96675d64749f0a5091..5a4ea25d39b29ee0d95bea7c0fdf941516403b49 100644 (file)
@@ -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"""