From: Vladimír Vondruš Date: Fri, 12 Jul 2019 10:36:09 +0000 (+0200) Subject: documentation/python: implement linking for types. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=3b23510af7c5a2cf1cc6a3aca6486d187d039ec6;p=blog.git documentation/python: implement linking for types. --- diff --git a/documentation/python.py b/documentation/python.py index 2b79f402..f4382583 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -460,34 +460,95 @@ def crawl_module(state: State, path: List[str], module) -> List[Tuple[List[str], return submodules_to_crawl +def make_type_link(state: State, referrer_path: List[str], type) -> str: + if type is None: return None + assert isinstance(type, str) + + # Not found, return as-is + if not type in state.name_map: return type + + entry = state.name_map[type] + + # Strip common prefix from both paths. We always want to keep at least one + # element from the entry path, so strip the last element off. + common_prefix_length = len(os.path.commonprefix([referrer_path, entry.path[:-1]])) + + # Check for ambiguity of the shortened path -- for example, with referrer + # being `module.sub.Foo`, target `module.Foo`, the path will get shortened + # to `Foo`, making it seem like the target is `module.sub.Foo` instead of + # `module.Foo`. To fix that, the shortened path needs to be `sub.Foo` + # instead of `Foo`. + def is_ambiguous(shortened_path): + # Concatenate the shortened path with a prefix of the referrer path, + # going from longest to shortest, until we find a name that exists. If + # the first found name is the actual target, it's not ambiguous -- + # for example, linking from `module.sub` to `module.sub.Foo` can be + # done just with `Foo` even though `module.Foo` exists as well, as it's + # "closer" to the referrer. + # TODO: See test cases in `inspect_type_links.first.Foo` for very + # *very* pathological cases where we're referencing `Foo` from + # `module.Foo` and there's also `module.Foo.Foo`. Not sure which way is + # better. + for i in reversed(range(len(referrer_path))): + potentially_ambiguous = referrer_path[:i] + shortened_path + if '.'.join(potentially_ambiguous) in state.name_map: + if potentially_ambiguous == entry.path: return False + else: return True + # the target *has to be* found + assert False # pragma: no cover + shortened_path = entry.path[common_prefix_length:] + while common_prefix_length and is_ambiguous(shortened_path): + common_prefix_length -= 1 + shortened_path = entry.path[common_prefix_length:] + + # Format the URL + if entry.type == EntryType.CLASS: + url = state.config['URL_FORMATTER'](entry.type, entry.path)[1] + else: + assert entry.type == EntryType.ENUM + parent_entry = state.name_map['.'.join(entry.path[:-1])] + url = '{}#{}'.format( + state.config['URL_FORMATTER'](parent_entry.type, parent_entry.path)[1], + state.config['ID_FORMATTER'](entry.type, entry.path[-1:])) + + return '{}'.format(url, '.'.join(shortened_path)) + _pybind_name_rx = re.compile('[a-zA-Z0-9_]*') _pybind_arg_name_rx = re.compile('[*a-zA-Z0-9_]+') _pybind_type_rx = re.compile('[a-zA-Z0-9_.]+') _pybind_default_value_rx = re.compile('[^,)]+') -def parse_pybind_type(state: State, signature: str) -> str: +def parse_pybind_type(state: State, referrer_path: List[str], signature: str) -> str: input_type = _pybind_type_rx.match(signature).group(0) signature = signature[len(input_type):] type = map_name_prefix(state, input_type) + type_link = make_type_link(state, referrer_path, type) if signature and signature[0] == '[': type += '[' + type_link += '[' signature = signature[1:] while signature[0] != ']': - signature, inner_type = parse_pybind_type(state, signature) + signature, inner_type, inner_type_link = parse_pybind_type(state, referrer_path, signature) type += inner_type + type_link += inner_type_link if signature[0] == ']': break assert signature.startswith(', ') signature = signature[2:] type += ', ' + type_link += ', ' assert signature[0] == ']' signature = signature[1:] type += ']' + type_link += ']' - return signature, type + return signature, type, type_link -def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List[Tuple[str, str, str]], str]: +# Returns function name, summary, list of arguments (name, type, type with HTML +# links, default value) and return type. If argument parsing failed, the +# argument list is a single "ellipsis" item. +def parse_pybind_signature(state: State, referrer_path: List[str], signature: str) -> Tuple[str, str, List[Tuple[str, str, str, str]], str]: original_signature = signature # For error reporting name = _pybind_name_rx.match(signature).group(0) signature = signature[len(name):] @@ -505,9 +566,10 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List # Type (optional) if signature.startswith(': '): signature = signature[2:] - signature, arg_type = parse_pybind_type(state, signature) + signature, arg_type, arg_type_link = parse_pybind_type(state, referrer_path, signature) else: arg_type = None + arg_type_link = None # Default (optional) -- for now take everything until the next comma # TODO: ugh, do properly @@ -520,7 +582,7 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List else: default = None - args += [(arg_name, arg_type, default)] + args += [(arg_name, arg_type, arg_type_link, default)] if signature[0] == ')': break @@ -532,7 +594,7 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List summary = extract_summary(state, {}, [], original_signature[end + 1:]) else: summary = '' - return (name, summary, [('…', None, None)], None) + return (name, summary, [('…', None, None, None)], None) signature = signature[2:] @@ -542,9 +604,9 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List # Return type (optional) if signature.startswith(' -> '): signature = signature[4:] - signature, return_type = parse_pybind_type(state, signature) + signature, _, return_type_link = parse_pybind_type(state, referrer_path, signature) else: - return_type = None + return_type_link = None if signature and signature[0] != '\n': end = original_signature.find('\n') @@ -553,16 +615,18 @@ def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List summary = extract_summary(state, {}, [], original_signature[end + 1:]) else: summary = '' - return (name, summary, [('…', None, None)], None) + return (name, summary, [('…', None, None, None)], None) if len(signature) > 1 and signature[1] == '\n': summary = extract_summary(state, {}, [], signature[2:]) else: summary = '' - return (name, summary, args, return_type) + return (name, summary, args, return_type_link) + +def parse_pybind_docstring(state: State, referrer_path: List[str], doc: str) -> List[Tuple[str, str, List[Tuple[str, str, str]], str]]: + name = referrer_path[-1] -def parse_pybind_docstring(state: State, name: str, doc: str) -> List[Tuple[str, str, List[Tuple[str, str, str]], str]]: # Multiple overloads, parse each separately overload_header = "{}(*args, **kwargs)\nOverloaded function.\n\n".format(name); if doc.startswith(overload_header): @@ -575,7 +639,7 @@ def parse_pybind_docstring(state: State, name: str, doc: str) -> List[Tuple[str, next = doc.find('{}. {}('.format(id, name)) # Parse the signature and docs from known slice - overloads += [parse_pybind_signature(state, doc[len(str(id - 1)) + 2:next])] + overloads += [parse_pybind_signature(state, referrer_path, doc[len(str(id - 1)) + 2:next])] assert overloads[-1][0] == name if next == -1: break @@ -586,7 +650,7 @@ def parse_pybind_docstring(state: State, name: str, doc: str) -> List[Tuple[str, # Normal function, parse and return the first signature else: - return [parse_pybind_signature(state, doc)] + return [parse_pybind_signature(state, referrer_path, doc)] def extract_summary(state: State, external_docs, path: List[str], doc: str) -> str: # Prefer external docs, if available @@ -604,12 +668,12 @@ def extract_type(type) -> str: # builtins (i.e., we want re.Match but not builtins.int). return (type.__module__ + '.' if type.__module__ != 'builtins' else '') + type.__name__ -def extract_annotation(state: State, annotation) -> str: +def extract_annotation(state: State, referrer_path: List[str], annotation) -> str: # TODO: why this is not None directly? if annotation is inspect.Signature.empty: return None # Annotations can be strings, also https://stackoverflow.com/a/33533514 - if type(annotation) == str: return map_name_prefix(state, annotation) + if type(annotation) == str: out = annotation # To avoid getting for types (and getting foo.bar # instead) but getting the actual type for types annotated with e.g. @@ -617,8 +681,11 @@ def extract_annotation(state: State, annotation) -> str: # typing module or it's directly a type. In Python 3.7 this worked with # inspect.isclass(annotation), but on 3.6 that gives True for annotations # as well and then we would get just List instead of List[int]. - if annotation.__module__ == 'typing': return map_name_prefix(state, str(annotation)) - return map_name_prefix(state, extract_type(annotation)) + elif annotation.__module__ == 'typing': out = str(annotation) + else: out = extract_type(annotation) + + # Map name prefix, add links to the result + return make_type_link(state, referrer_path, map_name_prefix(state, out)) def extract_module_doc(state: State, path: List[str], module): assert inspect.ismodule(module) @@ -656,6 +723,7 @@ def extract_enum_doc(state: State, path: List[str], enum_): out.summary = extract_summary(state, {}, [], enum_.__doc__) out.base = extract_type(enum_.__base__) + if out.base: out.base = make_type_link(state, path, out.base) for i in enum_: value = Empty() @@ -705,7 +773,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis # one function in Python may equal more than one function on the C++ side. # To make the docs usable, list all overloads separately. if state.config['PYBIND11_COMPATIBILITY'] and function.__doc__.startswith(path[-1]): - funcs = parse_pybind_docstring(state, path[-1], function.__doc__) + funcs = parse_pybind_docstring(state, path, function.__doc__) overloads = [] for name, summary, args, type in funcs: out = Empty() @@ -718,6 +786,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis # Don't show None return type for void functions out.type = None if type == 'None' else type + if out.type: out.type = make_type_link(state, path, out.type) # There's no other way to check staticmethods than to check for # self being the name of first parameter :( No support for @@ -742,7 +811,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis positional_only = True for i, arg in enumerate(args[1:]): - name, type, default = arg + name, type, type_link, default = arg if name != 'arg{}'.format(i): positional_only = False break @@ -752,18 +821,23 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis else: positional_only = True for i, arg in enumerate(args): - name, type, default = arg + name, type, type_link, default = arg if name != 'arg{}'.format(i): positional_only = False break + arg_types = [] for i, arg in enumerate(args): - name, type, default = arg + name, type, type_link, default = arg param = Empty() param.name = name # Don't include redundant type for the self argument - if name == 'self': param.type = None - else: param.type = type + if name == 'self': + param.type = None + arg_types += [None] + else: + param.type = type_link + arg_types += [type] param.default = html.escape(default or '') if type or default: out.has_complex_params = True @@ -782,7 +856,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis # Format the anchor. Pybind11 functions are sometimes overloaded, # thus name alone is not enough. - out.id = state.config['ID_FORMATTER'](EntryType.OVERLOADED_FUNCTION, path[-1:] + [param.type for param in out.params]) + out.id = state.config['ID_FORMATTER'](EntryType.OVERLOADED_FUNCTION, path[-1:] + arg_types) overloads += [out] @@ -806,11 +880,11 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis try: signature = inspect.signature(function) - out.type = extract_annotation(state, signature.return_annotation) + out.type = extract_annotation(state, path, signature.return_annotation) for i in signature.parameters.values(): param = Empty() param.name = i.name - param.type = extract_annotation(state, i.annotation) + param.type = extract_annotation(state, path, i.annotation) if param.type: out.has_complex_params = True if i.default is inspect.Signature.empty: @@ -847,11 +921,11 @@ def extract_property_doc(state: State, path: List[str], property): try: signature = inspect.signature(property.fget) - out.type = extract_annotation(state, signature.return_annotation) + out.type = extract_annotation(state, path, signature.return_annotation) except ValueError: # pybind11 properties have the type in the docstring if state.config['PYBIND11_COMPATIBILITY']: - out.type = parse_pybind_signature(state, property.fget.__doc__)[3] + out.type = parse_pybind_signature(state, path, property.fget.__doc__)[3] else: out.type = None @@ -867,7 +941,7 @@ def extract_data_doc(state: State, parent, path: List[str], data): out.summary = '' out.has_details = False if hasattr(parent, '__annotations__') and out.name in parent.__annotations__: - out.type = extract_annotation(state, parent.__annotations__[out.name]) + out.type = extract_annotation(state, path, parent.__annotations__[out.name]) else: out.type = None # The autogenerated is useless, so provide the value diff --git a/documentation/test_python/CMakeLists.txt b/documentation/test_python/CMakeLists.txt index 9f41e496..e3966fdb 100644 --- a/documentation/test_python/CMakeLists.txt +++ b/documentation/test_python/CMakeLists.txt @@ -27,7 +27,7 @@ project(McssDocumentationPybindTests) find_package(pybind11 CONFIG REQUIRED) -foreach(target signatures enums submodules) +foreach(target signatures enums submodules type_links) pybind11_add_module(pybind_${target} pybind_${target}/pybind_${target}.cpp) set_target_properties(pybind_${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/pybind_${target}) endforeach() diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html b/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html index a83a1714..7add22f8 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html +++ b/documentation/test_python/inspect_annotations/inspect_annotations.Foo.html @@ -39,7 +39,7 @@

Methods

- def string_annotation(self: inspect_annotations.Foo) + def string_annotation(self: Foo)
String annotations
diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.html b/documentation/test_python/inspect_annotations/inspect_annotations.html index 31af659e..4f62c8ea 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.html +++ b/documentation/test_python/inspect_annotations/inspect_annotations.html @@ -54,7 +54,7 @@
def annotation(param: typing.List[int], another: bool, - third: str = 'hello') -> inspect_annotations.Foo + third: str = 'hello') -> Foo
An annotated function
@@ -75,7 +75,7 @@ def partial_annotation(foo, param: typing.Tuple[int, int], unannotated, - cls: inspect_annotations.Foo) + cls: Foo)
Partially annotated function
diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html index 1772ea86..381b567e 100644 --- a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html @@ -38,7 +38,7 @@

Methods

- def a_thing(self) -> inspect_name_mapping.Class + def a_thing(self) -> Class
A method
diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html index 53ff83d6..dd07e033 100644 --- a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html @@ -53,7 +53,7 @@

Functions

- def foo() -> inspect_name_mapping.Class + def foo() -> Class
This function returns Class, *not* _sub.Foo
diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html index 016ba03e..5c003407 100644 --- a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html @@ -38,7 +38,7 @@

Functions

- def foo(a: inspect_name_mapping.Class, + def foo(a: Class, b: int) -> int
A function
diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.Foo.html b/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.Foo.html new file mode 100644 index 00000000..3c7eb461 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.Foo.html @@ -0,0 +1,57 @@ + + + + + inspect_type_links.first.Foo.Foo | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.html b/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.html new file mode 100644 index 00000000..1fb3539b --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.first.Foo.html @@ -0,0 +1,76 @@ + + + + + inspect_type_links.first.Foo | My Python Project + + + + + +
+
+
+
+
+

+ inspect_type_links.first.Foo class +

+

A class in the first module

+
+

Contents

+ +
+
+

Classes

+
+
class Foo
+
An inner class in the first module
+
+
+
+

Methods

+
+
+ def reference_inner(self, + a: Foo) +
+
A method referencing an inner class. This is quite a pathological case and I'm not sure if Foo or Foo.Foo is better.
+
+ def reference_other(self, + a: second.Foo) +
+
A method referencing a type in another module
+
+ def reference_self(self, + a: first.Foo) +
+
A method referencing its wrapper class. Due to the inner Foo this is quite a pathological case and I'm not sure if first.Foo or Foo is better.
+
+ def reference_sub(self, + a: sub.Foo, + b: sub.Foo) +
+
A method referencing a type in a submodule
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.first.html b/documentation/test_python/inspect_type_links/inspect_type_links.first.html new file mode 100644 index 00000000..535daf39 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.first.html @@ -0,0 +1,77 @@ + + + + + inspect_type_links.first | My Python Project + + + + + +
+
+
+
+
+

+ inspect_type_links.first module +

+

First module

+
+

Contents

+ +
+
+

Modules

+
+
module sub
+
Submodule
+
+
+
+

Classes

+
+
class Foo
+
A class in the first module
+
+
+
+

Functions

+
+
+ def reference_other(a: second.Foo) +
+
A function referencing a type in another module
+
+ def reference_self(a: Foo, + b: Foo) +
+
A function referencing a type in this module
+
+ def reference_sub(a: sub.Foo, + b: sub.Foo) +
+
A function referencing a type in a submodule
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.Foo.html b/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.Foo.html new file mode 100644 index 00000000..b70351aa --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.Foo.html @@ -0,0 +1,56 @@ + + + + + inspect_type_links.first.sub.Foo | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.html b/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.html new file mode 100644 index 00000000..94ca8c67 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.first.sub.html @@ -0,0 +1,65 @@ + + + + + inspect_type_links.first.sub | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.Foo.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.Foo.html new file mode 100644 index 00000000..d57e0541 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.Foo.html @@ -0,0 +1,51 @@ + + + + + inspect_type_links.second.Foo | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.html new file mode 100644 index 00000000..801fed85 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.html @@ -0,0 +1,84 @@ + + + + + inspect_type_links.second | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py b/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py new file mode 100644 index 00000000..35e9047a --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links/__init__.py @@ -0,0 +1 @@ +from . import first, second diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/first/__init__.py b/documentation/test_python/inspect_type_links/inspect_type_links/first/__init__.py new file mode 100644 index 00000000..30b9afdd --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links/first/__init__.py @@ -0,0 +1,41 @@ +"""First module""" + +from inspect_type_links import first +from inspect_type_links import second + +class Foo: + """A class in the first module""" + + def reference_self(self, a: 'inspect_type_links.first.Foo'): + """A method referencing its wrapper class. Due to the inner Foo this is quite a pathological case and I'm not sure if first.Foo or Foo is better.""" + + def reference_inner(self, a: 'inspect_type_links.first.Foo.Foo'): + """A method referencing an inner class. This is quite a pathological case and I'm not sure if Foo or Foo.Foo is better.""" + + def reference_other(self, a: second.Foo): + """A method referencing a type in another module""" + + class Foo: + """An inner class in the first module""" + + def reference_self(self, a: 'inspect_type_links.first.Foo.Foo'): + """A method referencing its wrapper class""" + + def reference_parent(self, a: 'inspect_type_links.first.Foo'): + """A method referencing its parent wrapper class""" + +def reference_self(a: Foo, b: first.Foo): + """A function referencing a type in this module""" + +def reference_other(a: second.Foo): + """A function referencing a type in another module""" + +from . import sub + +def _foo_reference_sub(self, a: sub.Foo, b: first.sub.Foo): + """A method referencing a type in a submodule""" + +setattr(Foo, 'reference_sub', _foo_reference_sub) + +def reference_sub(a: sub.Foo, b: first.sub.Foo): + """A function referencing a type in a submodule""" diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/first/sub.py b/documentation/test_python/inspect_type_links/inspect_type_links/first/sub.py new file mode 100644 index 00000000..77954056 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links/first/sub.py @@ -0,0 +1,18 @@ +"""Submodule""" + +from inspect_type_links import first + +class Foo: + """A class in the submodule""" + + def reference_self(a: 'inspect_type_links.first.sub.Foo'): + """A method referencing a type in this submodule""" + + def reference_parent(a: first.Foo, b: first.Foo): + """A method referencing a type in a parent module""" + +def reference_self(a: Foo, b: 'inspect_type_links.first.sub.Foo'): + """A function referencing a type in this submodule""" + +def reference_parent(a: first.Foo, b: first.Foo): + """A function referencing a type in a parent module""" diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/second.py b/documentation/test_python/inspect_type_links/inspect_type_links/second.py new file mode 100644 index 00000000..3ca7908d --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links/second.py @@ -0,0 +1,24 @@ +"""Second module""" + +import enum + +class Enum(enum.Enum): + """An enum""" + + FIRST = 1 + SECOND = 2 + +def type_enum(a: Enum): + """Function referencing an enum""" + +class Foo: + """A class in the second module""" + + @property + def type_property(self) -> Enum: + """A property""" + +def type_return() -> Foo: + """A function with a return type annotation""" + +TYPE_DATA: Foo = Foo() diff --git a/documentation/test_python/link_formatting/m.link_formatting.pybind.html b/documentation/test_python/link_formatting/m.link_formatting.pybind.html index d0210b14..214996a7 100644 --- a/documentation/test_python/link_formatting/m.link_formatting.pybind.html +++ b/documentation/test_python/link_formatting/m.link_formatting.pybind.html @@ -74,7 +74,7 @@
Each overload should have a different hash
def an_overloaded_function(arg0: int, - arg1: link_formatting.pybind.Foo, /) -> int + arg1: Foo, /) -> int
Each overload should have a different hash
diff --git a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html index db2e1717..b65fa10f 100644 --- a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html @@ -38,7 +38,7 @@

Static methods

- def a_thing() -> pybind_name_mapping.Class + def a_thing() -> Class
A method
diff --git a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html index 0c28175e..8f3df052 100644 --- a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html @@ -53,7 +53,7 @@

Functions

- def foo() -> pybind_name_mapping.Class + def foo() -> Class
This function returns Class, *not* _sub.Foo
diff --git a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html index fcdbee28..ce8dd513 100644 --- a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html @@ -38,7 +38,7 @@

Functions

- def foo(arg0: pybind_name_mapping.Class, + def foo(arg0: Class, arg1: int, /) -> int
A function
diff --git a/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html b/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html index d4ea689e..a15394b2 100644 --- a/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html +++ b/documentation/test_python/pybind_signatures/pybind_signatures.MyClass.html @@ -42,7 +42,7 @@
def static_function(arg0: int, - arg1: float, /) -> pybind_signatures.MyClass + arg1: float, /) -> MyClass
Static method with positional-only args
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 new file mode 100644 index 00000000..c9ca00ae --- /dev/null +++ b/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html @@ -0,0 +1,62 @@ + + + + + pybind_type_links.Foo | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.cpp b/documentation/test_python/pybind_type_links/pybind_type_links.cpp new file mode 100644 index 00000000..fb996185 --- /dev/null +++ b/documentation/test_python/pybind_type_links/pybind_type_links.cpp @@ -0,0 +1,35 @@ +#include + +namespace py = pybind11; + +namespace { + +enum class Enum { + First, Second +}; + +void typeEnum(Enum) {} + +struct Foo { + Enum property; +}; + +Foo typeReturn() { return {}; } + +} + +PYBIND11_MODULE(pybind_type_links, m) { + m.doc() = "pybind11 type linking"; + + py::enum_{m, "Enum", "An enum"} + .value("FIRST", Enum::First) + .value("SECOND", Enum::Second); + + py::class_{m, "Foo", "A class"} + .def(py::init(), "Constructor") + .def_readwrite("property", &Foo::property, "A property"); + + m + .def("type_enum", &typeEnum, "A function taking an enum") + .def("type_return", &typeReturn, "A function returning a type"); +} diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.html b/documentation/test_python/pybind_type_links/pybind_type_links.html new file mode 100644 index 00000000..57cf5886 --- /dev/null +++ b/documentation/test_python/pybind_type_links/pybind_type_links.html @@ -0,0 +1,74 @@ + + + + + pybind_type_links | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index e7ee4612..f28ed0de 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -146,3 +146,18 @@ class Recursive(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('inspect_recursive.html')) self.assertEqual(*self.actual_expected_contents('inspect_recursive.first.html')) self.assertEqual(*self.actual_expected_contents('inspect_recursive.a.html')) + +class TypeLinks(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'type_links', *args, **kwargs) + + def test(self): + self.run_python() + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.sub.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.sub.Foo.html')) + + self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.Foo.html')) diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 76a6d689..098c9de4 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -31,124 +31,124 @@ from . import BaseInspectTestCase class Signature(unittest.TestCase): def test(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int, a2: module.Thing) -> module.Thing3'), ('foo', '', [ - ('a', 'int', None), - ('a2', 'module.Thing', None), + ('a', 'int', 'int', None), + ('a2', 'module.Thing', 'module.Thing', None), ], 'module.Thing3')) def test_newline(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int, a2: module.Thing) -> module.Thing3\n'), ('foo', '', [ - ('a', 'int', None), - ('a2', 'module.Thing', None), + ('a', 'int', 'int', None), + ('a2', 'module.Thing', 'module.Thing', None), ], 'module.Thing3')) def test_docs(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int, a2: module.Thing) -> module.Thing3\n\nDocs here!!'), ('foo', 'Docs here!!', [ - ('a', 'int', None), - ('a2', 'module.Thing', None), + ('a', 'int', 'int', None), + ('a2', 'module.Thing', 'module.Thing', None), ], 'module.Thing3')) def test_no_args(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'thingy() -> str'), ('thingy', '', [], 'str')) def test_no_return(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], '__init__(self: module.Thing)'), ('__init__', '', [ - ('self', 'module.Thing', None), + ('self', 'module.Thing', 'module.Thing', None), ], None)) def test_no_arg_types(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'thingy(self, the_other_thing)'), ('thingy', '', [ - ('self', None, None), - ('the_other_thing', None, None), + ('self', None, None, None), + ('the_other_thing', None, None, None), ], None)) def test_square_brackets(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: Tuple[int, str], no_really: str) -> List[str]'), ('foo', '', [ - ('a', 'Tuple[int, str]', None), - ('no_really', 'str', None), + ('a', 'Tuple[int, str]', 'Tuple[int, str]', None), + ('no_really', 'str', 'str', None), ], 'List[str]')) def test_nested_square_brackets(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: Tuple[int, List[Tuple[int, int]]], another: float) -> Union[str, Any]'), ('foo', '', [ - ('a', 'Tuple[int, List[Tuple[int, int]]]', None), - ('another', 'float', None), + ('a', 'Tuple[int, List[Tuple[int, int]]]', 'Tuple[int, List[Tuple[int, int]]]', None), + ('another', 'float', 'float', None), ], 'Union[str, Any]')) def test_kwargs(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(*args, **kwargs)'), ('foo', '', [ - ('*args', None, None), - ('**kwargs', None, None), + ('*args', None, None, None), + ('**kwargs', None, None, None), ], None)) # https://github.com/pybind/pybind11/commit/0826b3c10607c8d96e1d89dc819c33af3799a7b8, # released in 2.3.0. We want to support both, so test both. def test_default_values_pybind22(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: float=1.0, b: str=\'hello\')'), ('foo', '', [ - ('a', 'float', '1.0'), - ('b', 'str', '\'hello\''), + ('a', 'float', 'float', '1.0'), + ('b', 'str', 'str', '\'hello\''), ], None)) def test_default_values_pybind23(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: float = 1.0, b: str = \'hello\')'), ('foo', '', [ - ('a', 'float', '1.0'), - ('b', 'str', '\'hello\''), + ('a', 'float', 'float', '1.0'), + ('b', 'str', 'str', '\'hello\''), ], None)) def test_crazy_stuff(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int, b: Math::Vector<4, UnsignedInt>)'), - ('foo', '', [('…', None, None)], None)) + ('foo', '', [('…', None, None, None)], None)) def test_crazy_stuff_docs(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int, b: Math::Vector<4, UnsignedInt>)\n\nThis is text!!'), - ('foo', 'This is text!!', [('…', None, None)], None)) + ('foo', 'This is text!!', [('…', None, None, None)], None)) def test_crazy_return(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int) -> Math::Vector<4, UnsignedInt>'), - ('foo', '', [('…', None, None)], None)) + ('foo', '', [('…', None, None, None)], None)) def test_crazy_return_docs(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], 'foo(a: int) -> Math::Vector<4, UnsignedInt>\n\nThis returns!'), - ('foo', 'This returns!', [('…', None, None)], None)) + ('foo', 'This returns!', [('…', None, None, None)], None)) def test_no_name(self): - self.assertEqual(parse_pybind_signature(State({}), + self.assertEqual(parse_pybind_signature(State({}), [], '(arg0: MyClass) -> float'), - ('', '', [('arg0', 'MyClass', None)], 'float')) + ('', '', [('arg0', 'MyClass', 'MyClass', None)], 'float')) def test_module_mapping(self): state = State({}) state.module_mapping['module._module'] = 'module' - self.assertEqual(parse_pybind_signature(state, + self.assertEqual(parse_pybind_signature(state, [], 'foo(a: module._module.Foo, b: Tuple[int, module._module.Bar]) -> module._module.Baz'), - ('foo', '', [('a', 'module.Foo', None), - ('b', 'Tuple[int, module.Bar]', None)], 'module.Baz')) + ('foo', '', [('a', 'module.Foo', 'module.Foo', None), + ('b', 'Tuple[int, module.Bar]', 'Tuple[int, module.Bar]', None)], 'module.Baz')) class Signatures(BaseInspectTestCase): def __init__(self, *args, **kwargs): @@ -233,3 +233,14 @@ class NameMapping(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('pybind_name_mapping.html')) self.assertEqual(*self.actual_expected_contents('pybind_name_mapping.Class.html')) self.assertEqual(*self.actual_expected_contents('pybind_name_mapping.submodule.html')) + +class TypeLinks(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'type_links', *args, **kwargs) + + def test(self): + self.run_python({ + 'PYBIND11_COMPATIBILITY': True + }) + self.assertEqual(*self.actual_expected_contents('pybind_type_links.html')) + self.assertEqual(*self.actual_expected_contents('pybind_type_links.Foo.html'))