From 853168039f007e1cbfcbf419eb15b29b2f15d4bc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 8 May 2019 00:03:47 +0200 Subject: [PATCH] documentation/python: implement name mapping for _all__. Basically, if there's this in the code: from ._native import Foo as PublicName from ._native import sub as submodule __all__ = ['PublicName', 'submodule'] then it's clear that the docs should refere library.PublicName and library.submodule instead of library._native.Foo / library._native.sub. This mapping is done for both pybind11 signatures and pure Python code. --- documentation/.gitignore | 2 +- documentation/python.py | 96 +++++++++++++------ documentation/test_python/CMakeLists.txt | 6 ++ .../inspect_name_mapping.Class.html | 51 ++++++++++ .../inspect_name_mapping.html | 66 +++++++++++++ .../inspect_name_mapping.submodule.html | 52 ++++++++++ .../inspect_name_mapping/__init__.py | 11 +++ .../inspect_name_mapping/_sub/__init__.py | 6 ++ .../inspect_name_mapping/_sub/bar.py | 7 ++ .../pybind_name_mapping.Class.html | 51 ++++++++++ .../pybind_name_mapping.html | 66 +++++++++++++ .../pybind_name_mapping.submodule.html | 52 ++++++++++ .../pybind_name_mapping/__init__.py | 11 +++ .../test_python/pybind_name_mapping/sub.cpp | 14 +++ documentation/test_python/test_inspect.py | 10 ++ documentation/test_python/test_pybind.py | 53 ++++++---- 16 files changed, 507 insertions(+), 47 deletions(-) create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping.html create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping/__init__.py create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/__init__.py create mode 100644 documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/bar.py create mode 100644 documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html create mode 100644 documentation/test_python/pybind_name_mapping/pybind_name_mapping.html create mode 100644 documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html create mode 100644 documentation/test_python/pybind_name_mapping/pybind_name_mapping/__init__.py create mode 100644 documentation/test_python/pybind_name_mapping/sub.cpp diff --git a/documentation/.gitignore b/documentation/.gitignore index 74eddd17..ff551325 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -7,4 +7,4 @@ package-lock.json test_doxygen/package-lock.json test_python/*/output/ test_python/build* -test_python/*/*.so +test_python/**/*.so diff --git a/documentation/python.py b/documentation/python.py index ce109b21..c2eb7a6d 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -117,6 +117,7 @@ class State: self.config = config self.class_index: List[IndexEntry] = [] self.page_index: List[IndexEntry] = [] + self.module_mapping: Dict[str, str] = {} def is_internal_function_name(name: str) -> bool: """If the function name is internal. @@ -125,6 +126,14 @@ def is_internal_function_name(name: str) -> bool: """ return name.startswith('_') and not (name.startswith('__') and name.endswith('__')) +def map_name_prefix(state: State, type: str) -> str: + for prefix, replace in state.module_mapping.items(): + if type == prefix or type.startswith(prefix + '.'): + return replace + type[len(prefix):] + + # No mapping found, return the type as-is + return type + def is_internal_or_imported_module_member(state: State, parent, path: str, name: str, object) -> bool: """If the module member is internal or imported.""" @@ -140,7 +149,7 @@ def is_internal_or_imported_module_member(state: State, parent, path: str, name: # Variables don't have the __module__ attribute, so check for its # presence. Right now *any* variable will be present in the output, as # there is no way to check where it comes from. - if hasattr(object, '__module__') and object.__module__ != '.'.join(path): + if hasattr(object, '__module__') and map_name_prefix(state, object.__module__) != '.'.join(path): return True # If this is a module, then things get complicated again and we need to @@ -176,16 +185,16 @@ _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(signature: str) -> str: - type = _pybind_type_rx.match(signature).group(0) - signature = signature[len(type):] +def parse_pybind_type(state: State, signature: str) -> str: + input_type = _pybind_type_rx.match(signature).group(0) + signature = signature[len(input_type):] + type = map_name_prefix(state, input_type) if signature and signature[0] == '[': type += '[' signature = signature[1:] while signature[0] != ']': - inner_type = parse_pybind_type(signature) - type += inner_type - signature = signature[len(inner_type):] + signature, inner_type = parse_pybind_type(state, signature) + type += inner_type if signature[0] == ']': break assert signature.startswith(', ') @@ -193,11 +202,12 @@ def parse_pybind_type(signature: str) -> str: type += ', ' assert signature[0] == ']' + signature = signature[1:] type += ']' - return type + return signature, type -def parse_pybind_signature(signature: str) -> Tuple[str, str, List[Tuple[str, str, str]], str]: +def parse_pybind_signature(state: State, signature: str) -> Tuple[str, str, List[Tuple[str, str, str]], str]: original_signature = signature # For error reporting name = _pybind_name_rx.match(signature).group(0) signature = signature[len(name):] @@ -215,8 +225,7 @@ def parse_pybind_signature(signature: str) -> Tuple[str, str, List[Tuple[str, st # Type (optional) if signature.startswith(': '): signature = signature[2:] - arg_type = parse_pybind_type(signature) - signature = signature[len(arg_type):] + signature, arg_type = parse_pybind_type(state, signature) else: arg_type = None @@ -251,8 +260,7 @@ def parse_pybind_signature(signature: str) -> Tuple[str, str, List[Tuple[str, st # Return type (optional) if signature.startswith(' -> '): signature = signature[4:] - return_type = parse_pybind_type(signature) - signature = signature[len(return_type):] + signature, return_type = parse_pybind_type(state, signature) else: return_type = None @@ -272,7 +280,7 @@ def parse_pybind_signature(signature: str) -> Tuple[str, str, List[Tuple[str, st return (name, summary, args, return_type) -def parse_pybind_docstring(name: str, doc: str) -> List[Tuple[str, str, List[Tuple[str, str, str]], str]]: +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): @@ -285,7 +293,7 @@ def parse_pybind_docstring(name: str, doc: str) -> List[Tuple[str, str, List[Tup next = doc.find('{}. {}('.format(id, name)) # Parse the signature and docs from known slice - overloads += [parse_pybind_signature(doc[3:next])] + overloads += [parse_pybind_signature(state, doc[3:next])] assert overloads[-1][0] == name if next == -1: break @@ -299,7 +307,7 @@ def parse_pybind_docstring(name: str, doc: str) -> List[Tuple[str, str, List[Tup # Normal function, parse and return the first signature else: - return [parse_pybind_signature(doc)] + return [parse_pybind_signature(state, doc)] def extract_summary(doc: str) -> str: if not doc: return '' # some modules (xml.etree) have that :( @@ -312,12 +320,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(annotation) -> str: +def extract_annotation(state: State, 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 annotation + if type(annotation) == str: return map_name_prefix(state, annotation) # To avoid getting for types (and getting foo.bar # instead) but getting the actual type for types annotated with e.g. @@ -325,8 +333,8 @@ def extract_annotation(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 str(annotation) - return extract_type(annotation) + if annotation.__module__ == 'typing': return map_name_prefix(state, str(annotation)) + return map_name_prefix(state, extract_type(annotation)) def render(config, template: str, page, env: jinja2.Environment): template = env.get_template(template) @@ -418,7 +426,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(path[-1], function.__doc__) + funcs = parse_pybind_docstring(state, path[-1], function.__doc__) overloads = [] for name, summary, args, type in funcs: out = Empty() @@ -512,11 +520,11 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis try: signature = inspect.signature(function) - out.type = extract_annotation(signature.return_annotation) + out.type = extract_annotation(state, signature.return_annotation) for i in signature.parameters.values(): param = Empty() param.name = i.name - param.type = extract_annotation(i.annotation) + param.type = extract_annotation(state, i.annotation) if param.type: out.has_complex_params = True if i.default is inspect.Signature.empty: @@ -551,17 +559,17 @@ def extract_property_doc(state: State, path: List[str], property): try: signature = inspect.signature(property.fget) - out.type = extract_annotation(signature.return_annotation) + out.type = extract_annotation(state, signature.return_annotation) except ValueError: # pybind11 properties have the type in the docstring if state.config['PYBIND11_COMPATIBILITY']: - out.type = parse_pybind_signature(property.fget.__doc__)[3] + out.type = parse_pybind_signature(state, property.fget.__doc__)[3] else: out.type = None return out -def extract_data_doc(parent, path: List[str], data): +def extract_data_doc(state: State, parent, path: List[str], data): assert not inspect.ismodule(data) and not inspect.isclass(data) and not inspect.isroutine(data) and not inspect.isframe(data) and not inspect.istraceback(data) and not inspect.iscode(data) out = Empty() @@ -570,7 +578,7 @@ def extract_data_doc(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(parent.__annotations__[out.name]) + out.type = extract_annotation(state, parent.__annotations__[out.name]) else: out.type = None # The autogenerated is useless, so provide the value @@ -614,12 +622,40 @@ def render_module(state: State, path, module, env): # The __all__ is meant to expose the public API, so we don't filter out # underscored things. if hasattr(module, '__all__'): + # Names exposed in __all__ could be also imported from elsewhere, for + # example this is a common pattern with native libraries and we want + # Foo, Bar, submodule and *everything* in submodule to be referred to + # as `library.RealName` (`library.submodule.func()`, etc.) instead of + # `library._native.Foo`, `library._native.sub.func()` etc. + # + # from ._native import Foo as PublicName + # from ._native import sub as submodule + # __all__ = ['PublicName', 'submodule'] + # + # The name references can be cyclic so extract the mapping in a + # separate pass before everything else. for name in module.__all__: # Everything available in __all__ is already imported, so get those # directly object = getattr(module, name) subpath = path + [name] + # Modules have __name__ while other objects have __module__, need + # to check both. + if inspect.ismodule(object) and object.__name__ != '.'.join(subpath): + assert object.__name__ not in state.module_mapping + state.module_mapping[object.__name__] = '.'.join(subpath) + elif hasattr(object, '__module__'): + subname = object.__module__ + '.' + object.__name__ + if subname != '.'.join(subpath): + assert subname not in state.module_mapping + state.module_mapping[subname] = '.'.join(subpath) + + # Now extract the actual docs + for name in module.__all__: + object = getattr(module, name) + subpath = path + [name] + # We allow undocumented submodules (since they're often in the # standard lib), but not undocumented classes etc. Render the # submodules and subclasses recursively. @@ -640,7 +676,7 @@ def render_module(state: State, path, module, env): # https://github.com/python/cpython/blob/d29b3dd9227cfc4a23f77e99d62e20e063272de1/Lib/pydoc.py#L113 # TODO: unify this query elif not inspect.isframe(object) and not inspect.istraceback(object) and not inspect.iscode(object): - page.data += [extract_data_doc(module, subpath, object)] + page.data += [extract_data_doc(state, module, subpath, object)] else: # pragma: no cover logging.warning("unknown symbol %s in %s", name, '.'.join(path)) @@ -691,7 +727,7 @@ def render_module(state: State, path, module, env): for name, object in inspect.getmembers(module, lambda o: not inspect.ismodule(o) and not inspect.isclass(o) and not inspect.isroutine(o) and not inspect.isframe(o) and not inspect.istraceback(o) and not inspect.iscode(o)): if is_internal_or_imported_module_member(state, module, path, name, object): continue - page.data += [extract_data_doc(module, path + [name], object)] + page.data += [extract_data_doc(state, module, path + [name], object)] render(state.config, 'module.html', page, env) return index_entry @@ -839,7 +875,7 @@ def render_class(state: State, path, class_, env): if name.startswith('_'): continue subpath = path + [name] - page.data += [extract_data_doc(class_, subpath, object)] + page.data += [extract_data_doc(state, class_, subpath, object)] render(state.config, 'class.html', page, env) return index_entry diff --git a/documentation/test_python/CMakeLists.txt b/documentation/test_python/CMakeLists.txt index e38cccaa..be599c14 100644 --- a/documentation/test_python/CMakeLists.txt +++ b/documentation/test_python/CMakeLists.txt @@ -31,3 +31,9 @@ foreach(target signatures enums submodules) 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() + +# Need a special name for this one +pybind11_add_module(pybind_name_mapping pybind_name_mapping/sub.cpp) +set_target_properties(pybind_name_mapping PROPERTIES + OUTPUT_NAME _sub + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/pybind_name_mapping/pybind_name_mapping) 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 new file mode 100644 index 00000000..10f6a250 --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html @@ -0,0 +1,51 @@ + + + + + inspect_name_mapping.Class | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html new file mode 100644 index 00000000..1cc90c6e --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.html @@ -0,0 +1,66 @@ + + + + + inspect_name_mapping | My Python Project + + + + + +
+
+
+
+
+

+ inspect_name_mapping module +

+
+

Contents

+ +
+
+

Modules

+
+
module submodule
+
This submodule is renamed from bar to submodule and should have a function member.
+
+
+
+

Classes

+
+
class Class
+
A class
+
+
+
+

Functions

+
+
+ def foo() -> inspect_name_mapping.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 new file mode 100644 index 00000000..658657d8 --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html @@ -0,0 +1,52 @@ + + + + + inspect_name_mapping.submodule | My Python Project + + + + + +
+
+
+
+
+

+ inspect_name_mapping.submodule module +

+

This submodule is renamed from bar to submodule and should have a function member.

+
+

Contents

+ +
+
+

Functions

+
+
+ def foo(a: inspect_name_mapping.Class, + b: int) -> int +
+
A function
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping/__init__.py b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/__init__.py new file mode 100644 index 00000000..304f9c50 --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/__init__.py @@ -0,0 +1,11 @@ +from ._sub import Foo as Class +from ._sub import bar as submodule + +# This test is almost the same as pybind_name_mapping, only pure Python + +"""This module should have a `submodule`, a `Class` and `foo()`""" + +__all__ = ['submodule', 'Class', 'foo'] + +def foo() -> Class: + """This function returns Class, *not* _sub.Foo""" diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/__init__.py b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/__init__.py new file mode 100644 index 00000000..dbb7f59d --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/__init__.py @@ -0,0 +1,6 @@ +class Foo: + """A class""" + # https://stackoverflow.com/a/33533514, have to use a string in Py3 + def a_thing(self) -> 'inspect_name_mapping._sub.Foo': + """A method""" + pass diff --git a/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/bar.py b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/bar.py new file mode 100644 index 00000000..3c24d53b --- /dev/null +++ b/documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/bar.py @@ -0,0 +1,7 @@ +"""This submodule is renamed from bar to submodule and should have a function member.""" + +from . import Foo + +def foo(a: Foo, b: int) -> int: + """A function""" + return b*2 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 new file mode 100644 index 00000000..50e0a0d1 --- /dev/null +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html @@ -0,0 +1,51 @@ + + + + + pybind_name_mapping.Class | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html new file mode 100644 index 00000000..b261231a --- /dev/null +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.html @@ -0,0 +1,66 @@ + + + + + pybind_name_mapping | My Python Project + + + + + +
+
+
+
+
+

+ pybind_name_mapping module +

+
+

Contents

+ +
+
+

Modules

+
+
module submodule
+
This submodule is renamed from bar to submodule and should have a function member.
+
+
+
+

Classes

+
+
class Class
+
A class, renamed from Foo to Class
+
+
+
+

Functions

+
+
+ def foo() -> pybind_name_mapping.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 new file mode 100644 index 00000000..3fc5135c --- /dev/null +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html @@ -0,0 +1,52 @@ + + + + + pybind_name_mapping.submodule | My Python Project + + + + + +
+
+
+
+
+

+ pybind_name_mapping.submodule module +

+

This submodule is renamed from bar to submodule and should have a function member.

+
+

Contents

+ +
+
+

Functions

+
+
+ def foo(arg0: pybind_name_mapping.Class, + arg1: int, /) -> int +
+
A function
+
+
+
+
+
+
+ + diff --git a/documentation/test_python/pybind_name_mapping/pybind_name_mapping/__init__.py b/documentation/test_python/pybind_name_mapping/pybind_name_mapping/__init__.py new file mode 100644 index 00000000..7af6bf14 --- /dev/null +++ b/documentation/test_python/pybind_name_mapping/pybind_name_mapping/__init__.py @@ -0,0 +1,11 @@ +from ._sub import Foo as Class +from ._sub import bar as submodule + +# This test is almost the same as inspect_name_mapping, only natively + +"""This module should have a bar submodule and a Foo class""" + +__all__ = ['submodule', 'Class', 'foo'] + +def foo() -> Class: + """This function returns Class, *not* _sub.Foo""" diff --git a/documentation/test_python/pybind_name_mapping/sub.cpp b/documentation/test_python/pybind_name_mapping/sub.cpp new file mode 100644 index 00000000..7039eede --- /dev/null +++ b/documentation/test_python/pybind_name_mapping/sub.cpp @@ -0,0 +1,14 @@ +#include + +struct Foo { + static Foo aThing() { return {}; } +}; + +PYBIND11_MODULE(_sub, m) { + pybind11::class_{m, "Foo", "A class, renamed from Foo to Class"} + .def("a_thing", &Foo::aThing, "A method"); + + pybind11::module bar = m.def_submodule("bar"); + bar.doc() = "This submodule is renamed from bar to submodule and should have a function member."; + bar.def("foo", [](Foo, int a) { return a*2; }, "A function"); +} diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index aecaa628..6920b9ed 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -126,3 +126,13 @@ class Annotations(BaseInspectTestCase): assert not hasattr(math, '__all__') self.assertEqual(*self.actual_expected_contents('math.html', 'math36.html')) + +class NameMapping(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'name_mapping', *args, **kwargs) + + def test(self): + self.run_python() + self.assertEqual(*self.actual_expected_contents('inspect_name_mapping.html')) + self.assertEqual(*self.actual_expected_contents('inspect_name_mapping.Class.html')) + self.assertEqual(*self.actual_expected_contents('inspect_name_mapping.submodule.html')) diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 2cf73383..b5679d44 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -25,13 +25,13 @@ import sys import unittest -from python import parse_pybind_signature +from python import State, parse_pybind_signature from . import BaseInspectTestCase class Signature(unittest.TestCase): def test(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int, a2: module.Thing) -> module.Thing3'), ('foo', '', [ ('a', 'int', None), @@ -39,7 +39,7 @@ class Signature(unittest.TestCase): ], 'module.Thing3')) def test_newline(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int, a2: module.Thing) -> module.Thing3\n'), ('foo', '', [ ('a', 'int', None), @@ -47,7 +47,7 @@ class Signature(unittest.TestCase): ], 'module.Thing3')) def test_docs(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int, a2: module.Thing) -> module.Thing3\n\nDocs here!!'), ('foo', 'Docs here!!', [ ('a', 'int', None), @@ -55,19 +55,19 @@ class Signature(unittest.TestCase): ], 'module.Thing3')) def test_no_args(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'thingy() -> str'), ('thingy', '', [], 'str')) def test_no_return(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), '__init__(self: module.Thing)'), ('__init__', '', [ ('self', 'module.Thing', None), ], None)) def test_no_arg_types(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'thingy(self, the_other_thing)'), ('thingy', '', [ ('self', None, None), @@ -75,7 +75,7 @@ class Signature(unittest.TestCase): ], None)) def test_square_brackets(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: Tuple[int, str], no_really: str) -> List[str]'), ('foo', '', [ ('a', 'Tuple[int, str]', None), @@ -83,7 +83,7 @@ class Signature(unittest.TestCase): ], 'List[str]')) def test_nested_square_brackets(self): - self.assertEqual(parse_pybind_signature( + 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), @@ -91,7 +91,7 @@ class Signature(unittest.TestCase): ], 'Union[str, Any]')) def test_kwargs(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(*args, **kwargs)'), ('foo', '', [ ('*args', None, None), @@ -99,7 +99,7 @@ class Signature(unittest.TestCase): ], None)) def test_default_values(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: float=1.0, b: str=\'hello\')'), ('foo', '', [ ('a', 'float', '1.0'), @@ -107,30 +107,39 @@ class Signature(unittest.TestCase): ], None)) def test_crazy_stuff(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int, b: Math::Vector<4, UnsignedInt>)'), ('foo', '', [('…', None, None)], None)) def test_crazy_stuff_docs(self): - self.assertEqual(parse_pybind_signature( + 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)) def test_crazy_return(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int) -> Math::Vector<4, UnsignedInt>'), ('foo', '', [('…', None, None)], None)) def test_crazy_return_docs(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), 'foo(a: int) -> Math::Vector<4, UnsignedInt>\n\nThis returns!'), ('foo', 'This returns!', [('…', None, None)], None)) def test_no_name(self): - self.assertEqual(parse_pybind_signature( + self.assertEqual(parse_pybind_signature(State({}), '(arg0: MyClass) -> float'), ('', '', [('arg0', 'MyClass', None)], 'float')) + def test_module_mapping(self): + state = State({}) + state.module_mapping['module._module'] = 'module' + + 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')) + class Signatures(BaseInspectTestCase): def __init__(self, *args, **kwargs): super().__init__(__file__, 'signatures', *args, **kwargs) @@ -192,3 +201,15 @@ class Submodules(BaseInspectTestCase): 'PYBIND11_COMPATIBILITY': True }) self.assertEqual(*self.actual_expected_contents('pybind_submodules.html')) + +class NameMapping(BaseInspectTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'name_mapping', *args, **kwargs) + + def test(self): + self.run_python({ + 'PYBIND11_COMPATIBILITY': True + }) + 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')) -- 2.30.2