chiark / gitweb /
documentation/python: support pybind11 enums.
authorVladimír Vondruš <mosra@centrum.cz>
Sat, 20 Apr 2019 22:05:14 +0000 (00:05 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Mon, 22 Apr 2019 15:53:36 +0000 (17:53 +0200)
doc/documentation/python.rst
documentation/.gitignore
documentation/python.py
documentation/templates/python/entry-enum.html
documentation/test_python/CMakeLists.txt [new file with mode: 0644]
documentation/test_python/pybind_enums/pybind_enums.cpp [new file with mode: 0644]
documentation/test_python/pybind_enums/pybind_enums.html [new file with mode: 0644]
documentation/test_python/test_pybind.py [new file with mode: 0644]

index 2d070e577904ce2b60adf4a6fd60e86d313cc3b0..67b154e45b5b35050775eb5ca29e362e6ddb2854 100644 (file)
@@ -528,6 +528,21 @@ has to do a few pybind11-specific workarounds to generate expected output. This
 behavior is not enabled by default as it *might* have unwanted consequences in
 pure Python code, enable it using the :py:`PYBIND11_COMPATIBILITY` option.
 
+`Enums`_
+--------
+
+Enums in pybind11 are not derived from :py:`enum.Enum`, but rather are plain
+classes. The only reliable way to detect a pybind11 enum is by looking for a
+``__members__`` member, which is a dict providing string names and their
+corresponding values. With pybind 2.2, it's only possible to document the
+enum class itself, not the values.
+
+.. note-info::
+
+    pybind 2.3 (not released yet) is scheduled to support docstrings for enum
+    values (see :gh:`pybind/pybind11#1160`). Support for this feature is not
+    done on the script side yet.
+
 `Command-line options`_
 =======================
 
@@ -729,7 +744,8 @@ Property                                Description
 :py:`enum.name`                         Enum name
 :py:`enum.brief`                        Brief docs
 :py:`enum.base`                         Base class from which the enum is
-                                        derived
+                                        derived. Set to :py:`None` if no base
+                                        class information is available.
 :py:`enum.values`                       List of enum values
 :py:`enum.has_details`                  If there is enough content for the full
                                         description block. [2]_
index aabcaf98afaf315745c7a441affb46cef73dac87..74eddd177efbe3a7f955fb3b159f177d1c586471 100644 (file)
@@ -6,3 +6,5 @@ node_modules/
 package-lock.json
 test_doxygen/package-lock.json
 test_python/*/output/
+test_python/build*
+test_python/*/*.so
index dfb4cbf8cf0a1f0a4318c89275867a709d2c5bf0..872367d8377da12748cd8f588ee3dfa98ca375ce 100755 (executable)
@@ -148,6 +148,9 @@ def is_internal_or_imported_module_member(parent, path: str, name: str, object)
     # If nothing of the above matched, then it's a thing we want to document
     return False
 
+def is_enum(state: State, object) -> bool:
+    return (inspect.isclass(object) and issubclass(object, enum.Enum)) or (state.config['PYBIND11_COMPATIBILITY'] and hasattr(object, '__members__'))
+
 def make_url(path: List[str]) -> str:
     return '.'.join(path) + '.html'
 
@@ -204,38 +207,55 @@ def extract_class_doc(path: List[str], class_):
     out.brief = extract_brief(class_.__doc__)
     return out
 
-def extract_enum_doc(path: List[str], enum_):
-    assert issubclass(enum_, enum.Enum)
-
+def extract_enum_doc(state: State, path: List[str], enum_):
     out = Empty()
     out.name = path[-1]
-
-    # Enum doc is by default set to a generic value. That's useless as well.
-    if enum_.__doc__ == 'An enumeration.':
-        out.brief = ''
-    else:
-        out.brief = extract_brief(enum_.__doc__)
-
-    out.base = extract_type(enum_.__base__)
     out.values = []
     out.has_details = False
     out.has_value_details = False
 
-    for i in enum_:
-        value = Empty()
-        value.name = i.name
-        value.value = html.escape(repr(i.value))
-
-        # Value doc gets by default inherited from the enum, that's useless
-        if i.__doc__ == enum_.__doc__:
-            value.brief = ''
+    # The happy case
+    if issubclass(enum_, enum.Enum):
+        # Enum doc is by default set to a generic value. That's useless as well.
+        if enum_.__doc__ == 'An enumeration.':
+            out.brief = ''
         else:
-            value.brief = extract_brief(i.__doc__)
+            out.brief = extract_brief(enum_.__doc__)
 
-        if value.brief:
-            out.has_details = True
-            out.has_value_details = True
-        out.values += [value]
+        out.base = extract_type(enum_.__base__)
+
+        for i in enum_:
+            value = Empty()
+            value.name = i.name
+            value.value = html.escape(repr(i.value))
+
+            # Value doc gets by default inherited from the enum, that's useless
+            if i.__doc__ == enum_.__doc__:
+                value.brief = ''
+            else:
+                value.brief = extract_brief(i.__doc__)
+
+            if value.brief:
+                out.has_details = True
+                out.has_value_details = True
+            out.values += [value]
+
+    # Pybind11 enums are ... different
+    elif state.config['PYBIND11_COMPATIBILITY']:
+        assert hasattr(enum_, '__members__')
+
+        out.brief = extract_brief(enum_.__doc__)
+        out.base = None
+
+        for name, v in enum_.__members__.items():
+            value = Empty()
+            value. name = name
+            value.value = int(v)
+            # TODO: once https://github.com/pybind/pybind11/pull/1160 is
+            #       released, extract from class docs (until then the class
+            #       docstring is duplicated here, which is useless)
+            value.brief = ''
+            out.values += [value]
 
     return out
 
@@ -360,11 +380,11 @@ def render_module(state: State, path, module, env):
             if inspect.ismodule(object):
                 page.modules += [extract_module_doc(subpath, object)]
                 index_entry.children += [render_module(state, subpath, object, env)]
-            elif inspect.isclass(object) and not issubclass(object, enum.Enum):
+            elif inspect.isclass(object) and not is_enum(state, object):
                 page.classes += [extract_class_doc(subpath, object)]
                 index_entry.children += [render_class(state, subpath, object, env)]
-            elif inspect.isclass(object) and issubclass(object, enum.Enum):
-                enum_ = extract_enum_doc(subpath, object)
+            elif inspect.isclass(object) and is_enum(state, object):
+                enum_ = extract_enum_doc(state, subpath, object)
                 page.enums += [enum_]
                 if enum_.has_details: page.has_enum_details = True
             elif inspect.isfunction(object) or inspect.isbuiltin(object):
@@ -391,7 +411,7 @@ def render_module(state: State, path, module, env):
             index_entry.children += [render_module(state, subpath, object, env)]
 
         # Get (and render) inner classes
-        for name, object in inspect.getmembers(module, lambda o: inspect.isclass(o) and not issubclass(o, enum.Enum)):
+        for name, object in inspect.getmembers(module, lambda o: inspect.isclass(o) and not is_enum(state, o)):
             if is_internal_or_imported_module_member(module, path, name, object): continue
 
             subpath = path + [name]
@@ -401,13 +421,13 @@ def render_module(state: State, path, module, env):
             index_entry.children += [render_class(state, subpath, object, env)]
 
         # Get enums
-        for name, object in inspect.getmembers(module, lambda o: inspect.isclass(o) and issubclass(o, enum.Enum)):
+        for name, object in inspect.getmembers(module, lambda o: is_enum(state, o)):
             if is_internal_or_imported_module_member(module, path, name, object): continue
 
             subpath = path + [name]
             if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath))
 
-            enum_ = extract_enum_doc(subpath, object)
+            enum_ = extract_enum_doc(state, subpath, object)
             page.enums += [enum_]
             if enum_.has_details: page.has_enum_details = True
 
@@ -514,7 +534,7 @@ def render_class(state: State, path, class_, env):
     index_entry.brief = page.brief
 
     # Get inner classes
-    for name, object in inspect.getmembers(class_, lambda o: inspect.isclass(o) and not issubclass(o, enum.Enum)):
+    for name, object in inspect.getmembers(class_, lambda o: inspect.isclass(o) and not is_enum(state, o)):
         if name in ['__base__', '__class__']: continue # TODO
         if name.startswith('_'): continue
 
@@ -525,13 +545,13 @@ def render_class(state: State, path, class_, env):
         index_entry.children += [render_class(state, subpath, object, env)]
 
     # Get enums
-    for name, object in inspect.getmembers(class_, lambda o: inspect.isclass(o) and issubclass(o, enum.Enum)):
+    for name, object in inspect.getmembers(class_, lambda o: is_enum(state, o)):
         if name.startswith('_'): continue
 
         subpath = path + [name]
         if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath))
 
-        enum_ = extract_enum_doc(subpath, object)
+        enum_ = extract_enum_doc(state, subpath, object)
         page.enums += [enum_]
         if enum_.has_details: page.has_enum_details = True
 
index 6b830833c0f9bc10b2f34ebaae8bee32d29db9e0..1dc1c712c4212c940d57e63444c65904a8a12a2e 100644 (file)
@@ -1,5 +1,5 @@
             <dt>
               {% set j = joiner('\n              ') %}
-              <span class="m-doc-wrap-bumper">class <a href="" class="m-doc{% if not enum.has_details %}-self{% endif %}">{{ enum.name }}</a>({{ enum.base }}): </span><span class="m-doc-wrap">{% for value in enum.values %}{{ j() }}<a href="" class="m-doc{% if not enum.has_details %}-self{% endif %}">{{ value.name }}</a>{% if value.value is not none %} = {{ value.value }}{% endif %}{% endfor %}</span>
+              <span class="m-doc-wrap-bumper">class <a href="" class="m-doc{% if not enum.has_details %}-self{% endif %}">{{ enum.name }}</a>{% if enum.base %}({{ enum.base }}){% endif %}: </span><span class="m-doc-wrap">{% for value in enum.values %}{{ j() }}<a href="" class="m-doc{% if not enum.has_details %}-self{% endif %}">{{ value.name }}</a>{% if value.value is not none %} = {{ value.value }}{% endif %}{% endfor %}</span>
             </dt>
             <dd>{{ enum.brief }}</dd>
diff --git a/documentation/test_python/CMakeLists.txt b/documentation/test_python/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ac0550b
--- /dev/null
@@ -0,0 +1,33 @@
+#
+#   This file is part of m.css.
+#
+#   Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+#   Permission is hereby granted, free of charge, to any person obtaining a
+#   copy of this software and associated documentation files (the "Software"),
+#   to deal in the Software without restriction, including without limitation
+#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+#   and/or sell copies of the Software, and to permit persons to whom the
+#   Software is furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included
+#   in all copies or substantial portions of the Software.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#   DEALINGS IN THE SOFTWARE.
+#
+
+cmake_minimum_required(VERSION 3.5)
+project(McssDocumentationPybindTests)
+
+find_package(pybind11 CONFIG REQUIRED)
+
+foreach(target pybind_enums)
+    pybind11_add_module(${target} ${target}/${target}.cpp)
+    set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${target})
+endforeach()
diff --git a/documentation/test_python/pybind_enums/pybind_enums.cpp b/documentation/test_python/pybind_enums/pybind_enums.cpp
new file mode 100644 (file)
index 0000000..1befd5c
--- /dev/null
@@ -0,0 +1,29 @@
+#include <pybind11/pybind11.h>
+
+namespace py = pybind11;
+
+enum MyEnum {
+    First = 0,
+    Second = 1,
+    Third = 74,
+    Consistent = -5
+};
+
+enum SixtyfourBitFlag: std::uint64_t {
+    Yes = 1000000000000ull,
+    No = 18446744073709551615ull
+};
+
+PYBIND11_MODULE(pybind_enums, m) {
+    m.doc() = "pybind11 enum parsing";
+
+    py::enum_<MyEnum>(m, "MyEnum", "An enum without value docs :(")
+        .value("First", MyEnum::First)
+        .value("Second", MyEnum::Second)
+        .value("Third", MyEnum::Third)
+        .value("CONSISTANTE", MyEnum::Consistent);
+
+    py::enum_<SixtyfourBitFlag>(m, "SixtyfourBitFlag", "64-bit flags")
+        .value("Yes", SixtyfourBitFlag::Yes)
+        .value("No", SixtyfourBitFlag::No);
+}
diff --git a/documentation/test_python/pybind_enums/pybind_enums.html b/documentation/test_python/pybind_enums/pybind_enums.html
new file mode 100644 (file)
index 0000000..abfbad4
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>pybind_enums | 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_enums <span class="m-thin">module</span>
+        </h1>
+        <p>pybind11 enum parsing</p>
+        <section id="enums">
+          <h2><a href="#enums">Enums</a></h2>
+          <dl class="m-doc">
+            <dt>
+              <span class="m-doc-wrap-bumper">class <a href="" class="m-doc-self">MyEnum</a>: </span><span class="m-doc-wrap"><a href="" class="m-doc-self">First</a> = 0
+              <a href="" class="m-doc-self">Second</a> = 1
+              <a href="" class="m-doc-self">Third</a> = 74
+              <a href="" class="m-doc-self">CONSISTANTE</a> = -5</span>
+            </dt>
+            <dd>An enum without value docs :(</dd>
+            <dt>
+              <span class="m-doc-wrap-bumper">class <a href="" class="m-doc-self">SixtyfourBitFlag</a>: </span><span class="m-doc-wrap"><a href="" class="m-doc-self">Yes</a> = 1000000000000
+              <a href="" class="m-doc-self">No</a> = 18446744073709551615</span>
+            </dt>
+            <dd>64-bit flags</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py
new file mode 100644 (file)
index 0000000..ec47be1
--- /dev/null
@@ -0,0 +1,35 @@
+#
+#   This file is part of m.css.
+#
+#   Copyright © 2017, 2018, 2019 Vladimír Vondruš <mosra@centrum.cz>
+#
+#   Permission is hereby granted, free of charge, to any person obtaining a
+#   copy of this software and associated documentation files (the "Software"),
+#   to deal in the Software without restriction, including without limitation
+#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+#   and/or sell copies of the Software, and to permit persons to whom the
+#   Software is furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included
+#   in all copies or substantial portions of the Software.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#   DEALINGS IN THE SOFTWARE.
+#
+
+from . import BaseTestCase
+
+class Enums(BaseTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'enums', *args, **kwargs)
+
+    def test(self):
+        self.run_python({
+            'PYBIND11_COMPATIBILITY': True
+        })
+        self.assertEqual(*self.actual_expected_contents('pybind_enums.html'))