From cc916669cb349dbcd4771cf130be8fea65a5543e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 23 Aug 2019 15:45:20 +0200 Subject: [PATCH] documentation/python: revisit None in function return type annotations. Until now it was ignored in pybind return annotations as it seemed to be superfluous. OTOH, while the pybind function is fully annotated, it's often not clear which function comes from pybind and which from pure python code, meaning it's not clear if the return type is None or the return type annotation is just missing. Additionally, the pure python annotations were showing None as NoneType, which is too verbose. According to PEP484 type(None) and None are equivalent, so patching the output to show None instead of NoneType, making it also consistent with pybind. --- documentation/python.py | 13 ++++---- .../inspect_annotations.html | 10 +++++++ .../inspect_annotations.py | 8 +++++ .../pybind_signatures.MyClass.html | 2 +- .../pybind_signatures/pybind_signatures.cpp | 2 ++ .../pybind_signatures/pybind_signatures.html | 30 +++++++++++-------- .../pybind_type_links.Foo.html | 2 +- .../pybind_type_links/pybind_type_links.html | 4 +-- documentation/test_python/test_pybind.py | 15 ++++++++++ 9 files changed, 64 insertions(+), 22 deletions(-) diff --git a/documentation/python.py b/documentation/python.py index 4732b808..c1008f77 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -832,7 +832,7 @@ def get_type_hints_or_nothing(state: State, path: List[str], object) -> Dict: return {} def extract_annotation(state: State, referrer_path: List[str], annotation) -> str: - # TODO: why this is not None directly? + # Empty annotation, as opposed to a None annotation, handled below if annotation is inspect.Signature.empty: return None # If dereferencing with typing.get_type_hints() failed, we might end up @@ -901,6 +901,12 @@ def extract_annotation(state: State, referrer_path: List[str], annotation) -> st logging.warning("invalid annotation %s in %s, ignoring", annotation, '.'.join(referrer_path)) return None + # According to https://www.python.org/dev/peps/pep-0484/#using-none, + # None and type(None) are equivalent. Calling extract_type() on None would + # give us NoneType, which is unnecessarily long. + elif annotation is type(None): + return 'None' + # Otherwise it's a plain type. Turn it into a link. return make_name_link(state, referrer_path, map_name_prefix(state, extract_type(annotation))) @@ -1027,10 +1033,7 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: out.has_complex_params = False out.summary, out.content = extract_docs(state, state.function_docs, entry.path, summary) out.has_details = bool(out.content) - - # Don't show None return type for functions w/o a return - out.type = None if type == 'None' else type - if out.type: out.type = make_name_link(state, entry.path, out.type) + out.type = type # There's no other way to check staticmethods than to check for # self being the name of first parameter :( No support for diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.html b/documentation/test_python/inspect_annotations/inspect_annotations.html index bcdff17e..87d15ffa 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.html +++ b/documentation/test_python/inspect_annotations/inspect_annotations.html @@ -121,6 +121,16 @@ def positional_keyword(positional_kw, *, kw_only)
Function with explicitly delimited keyword args
+
+ def returns_none(a: typing.Callable[[], None]) -> None +
+
In order to disambiguate between a missing return annotation and an +annotated none, the None return annotation is kept, converted from NoneType +to None
+
+ def returns_none_type(a: typing.Callable[[], None]) -> None +
+
And it should behave the same when using None or type(None)
diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.py b/documentation/test_python/inspect_annotations/inspect_annotations.py index 65594737..349d9824 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.py +++ b/documentation/test_python/inspect_annotations/inspect_annotations.py @@ -83,6 +83,14 @@ def annotated_positional_keyword(bar = False, *, foo: str, **kwargs): """Function with explicitly delimited keyword args and type annotations""" pass +def returns_none(a: Callable[[], None]) -> None: + """In order to disambiguate between a missing return annotation and an + annotated none, the None return annotation is kept, converted from NoneType + to None""" + +def returns_none_type(a: Callable[[], type(None)]) -> type(None): + """And it should behave the same when using None or type(None)""" + UNANNOTATED_VAR = 3.45 ANNOTATED_VAR: Tuple[bool, str] = (False, 'No.') diff --git a/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html b/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html index e62b1f2e..258e917a 100644 --- a/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html +++ b/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html @@ -72,7 +72,7 @@

Special methods

- def __init__(self, /) + def __init__(self, /) -> None
Constructor
diff --git a/documentation/test_python/pybind_signatures/pybind_signatures.cpp b/documentation/test_python/pybind_signatures/pybind_signatures.cpp index 62e2aaa8..62a9f655 100644 --- a/documentation/test_python/pybind_signatures/pybind_signatures.cpp +++ b/documentation/test_python/pybind_signatures/pybind_signatures.cpp @@ -24,6 +24,7 @@ bool overloaded(float) { return {}; } // Doesn't work with just a plain function pointer, MEH void takesAFunction(std::function&)>) {} +void takesAFunctionReturningVoid(std::function) {} struct MyClass { static MyClass staticFunction(int, float) { return {}; } @@ -61,6 +62,7 @@ PYBIND11_MODULE(pybind_signatures, m) { .def("overloaded", static_cast(&overloaded), "Overloaded for floats") .def("duck", &duck, "A function taking args/kwargs directly") .def("takes_a_function", &takesAFunction, "A function taking a Callable") + .def("takes_a_function_returning_none", &takesAFunctionReturningVoid, "A function taking a Callable that returns None") .def("tenOverloads", &tenOverloads, "Ten overloads of a function") .def("tenOverloads", &tenOverloads, "Ten overloads of a function") diff --git a/documentation/test_python/pybind_signatures/pybind_signatures.html b/documentation/test_python/pybind_signatures/pybind_signatures.html index c557155d..d7136b39 100644 --- a/documentation/test_python/pybind_signatures/pybind_signatures.html +++ b/documentation/test_python/pybind_signatures/pybind_signatures.html @@ -52,7 +52,7 @@
Function that failed to get parsed
- def duck(*args, **kwargs) + def duck(*args, **kwargs) -> None
A function taking args/kwargs directly
@@ -74,65 +74,69 @@
Scale an integer, kwargs
- def takes_a_function(arg0: Callable[[float, List[float]], int], /) + def takes_a_function(arg0: Callable[[float, List[float]], int], /) -> None
A function taking a Callable
+
+ def takes_a_function_returning_none(arg0: Callable[[], None], /) -> None +
+
A function taking a Callable that returns None
def taking_a_list_returning_a_tuple(arg0: List[float], /) -> Tuple[int, int, int]
Takes a list, returns a tuple
def tenOverloads(arg0: float, - arg1: float, /) + arg1: float, /) -> None
Ten overloads of a function
def tenOverloads(arg0: int, - arg1: float, /) + arg1: float, /) -> None
Ten overloads of a function
def tenOverloads(arg0: bool, - arg1: float, /) + arg1: float, /) -> None
Ten overloads of a function
def tenOverloads(arg0: float, - arg1: int, /) + arg1: int, /) -> None
Ten overloads of a function
def tenOverloads(arg0: int, - arg1: int, /) + arg1: int, /) -> None
Ten overloads of a function
def tenOverloads(arg0: bool, - arg1: int, /) + arg1: int, /) -> None
Ten overloads of a function
def tenOverloads(arg0: float, - arg1: bool, /) + arg1: bool, /) -> None
Ten overloads of a function
def tenOverloads(arg0: int, - arg1: bool, /) + arg1: bool, /) -> None
Ten overloads of a function
def tenOverloads(arg0: bool, - arg1: bool, /) + arg1: bool, /) -> None
Ten overloads of a function
def tenOverloads(arg0: str, - arg1: str, /) + arg1: str, /) -> None
Ten overloads of a function
- def void_function(arg0: int, /) + def void_function(arg0: int, /) -> None
Returns nothing
diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html b/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html index 636ed1eb..a8064058 100644 --- a/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html +++ b/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html @@ -41,7 +41,7 @@
def __init__(self, - arg0: Enum, /) + arg0: Enum, /) -> None
Constructor
diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.html b/documentation/test_python/pybind_type_links/pybind_type_links.html index c3dd014b..49291baa 100644 --- a/documentation/test_python/pybind_type_links/pybind_type_links.html +++ b/documentation/test_python/pybind_type_links/pybind_type_links.html @@ -58,11 +58,11 @@

Functions

- def type_enum(value: Enum = Enum.SECOND) + def type_enum(value: Enum = Enum.SECOND) -> None
A function taking an enum
- def type_nested(arg0: Tuple[Foo, List[Enum]], /) + def type_nested(arg0: Tuple[Foo, List[Enum]], /) -> None
A function with nested type annotation
diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 21b5e44e..451df575 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -71,6 +71,13 @@ class Signature(unittest.TestCase): ('self', 'module.Thing', 'module.Thing', None), ], None)) + def test_none_return(self): + self.assertEqual(parse_pybind_signature(self.state, [], + '__init__(self: module.Thing) -> None'), + ('__init__', '', [ + ('self', 'module.Thing', 'module.Thing', None), + ], 'None')) + def test_no_arg_types(self): self.assertEqual(parse_pybind_signature(self.state, [], 'thingy(self, the_other_thing)'), @@ -79,6 +86,14 @@ class Signature(unittest.TestCase): ('the_other_thing', None, None, None), ], None)) + def test_none_arg_types(self): + self.assertEqual(parse_pybind_signature(self.state, [], + 'thingy(self, the_other_thing: Callable[[], None])'), + ('thingy', '', [ + ('self', None, None, None), + ('the_other_thing', 'Callable[[], None]', 'Callable[[], None]', None), + ], None)) + def test_square_brackets(self): self.assertEqual(parse_pybind_signature(self.state, [], 'foo(a: Tuple[int, str], no_really: str) -> List[str]'), -- 2.30.2