chiark / gitweb /
documentation/python: implement name mapping for _all__.
authorVladimír Vondruš <mosra@centrum.cz>
Tue, 7 May 2019 22:03:47 +0000 (00:03 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Tue, 21 May 2019 14:51:51 +0000 (16:51 +0200)
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.

16 files changed:
documentation/.gitignore
documentation/python.py
documentation/test_python/CMakeLists.txt
documentation/test_python/inspect_name_mapping/inspect_name_mapping.Class.html [new file with mode: 0644]
documentation/test_python/inspect_name_mapping/inspect_name_mapping.html [new file with mode: 0644]
documentation/test_python/inspect_name_mapping/inspect_name_mapping.submodule.html [new file with mode: 0644]
documentation/test_python/inspect_name_mapping/inspect_name_mapping/__init__.py [new file with mode: 0644]
documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/__init__.py [new file with mode: 0644]
documentation/test_python/inspect_name_mapping/inspect_name_mapping/_sub/bar.py [new file with mode: 0644]
documentation/test_python/pybind_name_mapping/pybind_name_mapping.Class.html [new file with mode: 0644]
documentation/test_python/pybind_name_mapping/pybind_name_mapping.html [new file with mode: 0644]
documentation/test_python/pybind_name_mapping/pybind_name_mapping.submodule.html [new file with mode: 0644]
documentation/test_python/pybind_name_mapping/pybind_name_mapping/__init__.py [new file with mode: 0644]
documentation/test_python/pybind_name_mapping/sub.cpp [new file with mode: 0644]
documentation/test_python/test_inspect.py
documentation/test_python/test_pybind.py

index 74eddd177efbe3a7f955fb3b159f177d1c586471..ff551325fc72b40137532969b5de90ca740ceceb 100644 (file)
@@ -7,4 +7,4 @@ package-lock.json
 test_doxygen/package-lock.json
 test_python/*/output/
 test_python/build*
-test_python/*/*.so
+test_python/**/*.so
index ce109b21fd3b96ddc4672a006dbfb47750a7c4c2..c2eb7a6dcedae8f0dd20c96644a4139ecd54db08 100755 (executable)
@@ -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 <class 'foo.bar'> 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 <foo.bar at 0xbadbeef> 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
index e38cccaa3a9ddb966fc74a685dc00e1d358907e8..be599c14eff5fd4167792374cfa56f0f3eb98774 100644 (file)
@@ -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 (file)
index 0000000..10f6a25
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_name_mapping.Class | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          <span class="m-breadcrumb"><a href="inspect_name_mapping.html">inspect_name_mapping</a>.<wbr/></span>Class <span class="m-thin">class</span>
+        </h1>
+        <p>A class</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#methods">Methods</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="methods">
+          <h2><a href="#methods">Methods</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">a_thing</a>(</span><span class="m-doc-wrap">self) -&gt; inspect_name_mapping.Class</span>
+            </dt>
+            <dd>A method</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..1cc90c6
--- /dev/null
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_name_mapping | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          inspect_name_mapping <span class="m-thin">module</span>
+        </h1>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#packages">Modules</a></li>
+                <li><a href="#classes">Classes</a></li>
+                <li><a href="#functions">Functions</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="namespaces">
+          <h2><a href="#namespaces">Modules</a></h2>
+          <dl class="m-doc">
+            <dt>module <a href="inspect_name_mapping.submodule.html" class="m-doc">submodule</a></dt>
+            <dd>This submodule is renamed from bar to submodule and should have a function member.</dd>
+          </dl>
+        </section>
+        <section id="classes">
+          <h2><a href="#classes">Classes</a></h2>
+          <dl class="m-doc">
+            <dt>class <a href="inspect_name_mapping.Class.html" class="m-doc">Class</a></dt>
+            <dd>A class</dd>
+          </dl>
+        </section>
+        <section id="functions">
+          <h2><a href="#functions">Functions</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">) -&gt; inspect_name_mapping.Class</span>
+            </dt>
+            <dd>This function returns Class, *not* _sub.Foo</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..658657d
--- /dev/null
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>inspect_name_mapping.submodule | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          <span class="m-breadcrumb"><a href="inspect_name_mapping.html">inspect_name_mapping</a>.<wbr/></span>submodule <span class="m-thin">module</span>
+        </h1>
+        <p>This submodule is renamed from bar to submodule and should have a function member.</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#functions">Functions</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="functions">
+          <h2><a href="#functions">Functions</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">a: inspect_name_mapping.Class,
+              b: int) -&gt; int</span>
+            </dt>
+            <dd>A function</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..304f9c5
--- /dev/null
@@ -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 (file)
index 0000000..dbb7f59
--- /dev/null
@@ -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 (file)
index 0000000..3c24d53
--- /dev/null
@@ -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 (file)
index 0000000..50e0a0d
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>pybind_name_mapping.Class | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          <span class="m-breadcrumb"><a href="pybind_name_mapping.html">pybind_name_mapping</a>.<wbr/></span>Class <span class="m-thin">class</span>
+        </h1>
+        <p>A class, renamed from Foo to Class</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#staticmethods">Static methods</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="staticmethods">
+          <h2><a href="#staticmethods">Static methods</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">a_thing</a>(</span><span class="m-doc-wrap">) -&gt; pybind_name_mapping.Class</span>
+            </dt>
+            <dd>A method</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..b261231
--- /dev/null
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>pybind_name_mapping | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          pybind_name_mapping <span class="m-thin">module</span>
+        </h1>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#packages">Modules</a></li>
+                <li><a href="#classes">Classes</a></li>
+                <li><a href="#functions">Functions</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="namespaces">
+          <h2><a href="#namespaces">Modules</a></h2>
+          <dl class="m-doc">
+            <dt>module <a href="pybind_name_mapping.submodule.html" class="m-doc">submodule</a></dt>
+            <dd>This submodule is renamed from bar to submodule and should have a function member.</dd>
+          </dl>
+        </section>
+        <section id="classes">
+          <h2><a href="#classes">Classes</a></h2>
+          <dl class="m-doc">
+            <dt>class <a href="pybind_name_mapping.Class.html" class="m-doc">Class</a></dt>
+            <dd>A class, renamed from Foo to Class</dd>
+          </dl>
+        </section>
+        <section id="functions">
+          <h2><a href="#functions">Functions</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">) -&gt; pybind_name_mapping.Class</span>
+            </dt>
+            <dd>This function returns Class, *not* _sub.Foo</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..3fc5135
--- /dev/null
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>pybind_name_mapping.submodule | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          <span class="m-breadcrumb"><a href="pybind_name_mapping.html">pybind_name_mapping</a>.<wbr/></span>submodule <span class="m-thin">module</span>
+        </h1>
+        <p>This submodule is renamed from bar to submodule and should have a function member.</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#functions">Functions</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="functions">
+          <h2><a href="#functions">Functions</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">def <a href="" class="m-doc-self">foo</a>(</span><span class="m-doc-wrap">arg0: pybind_name_mapping.Class,
+              arg1: int<span class="m-text m-dim">, /</span>) -&gt; int</span>
+            </dt>
+            <dd>A function</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
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 (file)
index 0000000..7af6bf1
--- /dev/null
@@ -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 (file)
index 0000000..7039eed
--- /dev/null
@@ -0,0 +1,14 @@
+#include <pybind11/pybind11.h>
+
+struct Foo {
+    static Foo aThing() { return {}; }
+};
+
+PYBIND11_MODULE(_sub, m) {
+    pybind11::class_<Foo>{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");
+}
index aecaa62876d6c960658b87e638320840574fff80..6920b9edbbe8c01b6138e4c63ae97b401c2a4a2d 100644 (file)
@@ -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'))
index 2cf7338321be3d17f424169c180c0b877f109e66..b5679d447ee8d7c07c036999e5576c0273d43aed 100644 (file)
 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'))