From f3a40facd22b4bd3acbf2991b42544cdec22eda8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 21 Apr 2019 00:05:14 +0200 Subject: [PATCH] documentation/python: support pybind11 enums. --- doc/documentation/python.rst | 18 +++- documentation/.gitignore | 2 + documentation/python.py | 86 ++++++++++++------- .../templates/python/entry-enum.html | 2 +- documentation/test_python/CMakeLists.txt | 33 +++++++ .../test_python/pybind_enums/pybind_enums.cpp | 29 +++++++ .../pybind_enums/pybind_enums.html | 48 +++++++++++ documentation/test_python/test_pybind.py | 35 ++++++++ 8 files changed, 218 insertions(+), 35 deletions(-) create mode 100644 documentation/test_python/CMakeLists.txt create mode 100644 documentation/test_python/pybind_enums/pybind_enums.cpp create mode 100644 documentation/test_python/pybind_enums/pybind_enums.html create mode 100644 documentation/test_python/test_pybind.py diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 2d070e57..67b154e4 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -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]_ diff --git a/documentation/.gitignore b/documentation/.gitignore index aabcaf98..74eddd17 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -6,3 +6,5 @@ node_modules/ package-lock.json test_doxygen/package-lock.json test_python/*/output/ +test_python/build* +test_python/*/*.so diff --git a/documentation/python.py b/documentation/python.py index dfb4cbf8..872367d8 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -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 diff --git a/documentation/templates/python/entry-enum.html b/documentation/templates/python/entry-enum.html index 6b830833..1dc1c712 100644 --- a/documentation/templates/python/entry-enum.html +++ b/documentation/templates/python/entry-enum.html @@ -1,5 +1,5 @@
{% set j = joiner('\n ') %} - class {{ enum.name }}({{ enum.base }}): {% for value in enum.values %}{{ j() }}{{ value.name }}{% if value.value is not none %} = {{ value.value }}{% endif %}{% endfor %} + class {{ enum.name }}{% if enum.base %}({{ enum.base }}){% endif %}: {% for value in enum.values %}{{ j() }}{{ value.name }}{% if value.value is not none %} = {{ value.value }}{% endif %}{% endfor %}
{{ enum.brief }}
diff --git a/documentation/test_python/CMakeLists.txt b/documentation/test_python/CMakeLists.txt new file mode 100644 index 00000000..ac0550bd --- /dev/null +++ b/documentation/test_python/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# 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 index 00000000..1befd5cd --- /dev/null +++ b/documentation/test_python/pybind_enums/pybind_enums.cpp @@ -0,0 +1,29 @@ +#include + +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_(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_(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 index 00000000..abfbad4e --- /dev/null +++ b/documentation/test_python/pybind_enums/pybind_enums.html @@ -0,0 +1,48 @@ + + + + + pybind_enums | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py new file mode 100644 index 00000000..ec47be11 --- /dev/null +++ b/documentation/test_python/test_pybind.py @@ -0,0 +1,35 @@ +# +# This file is part of m.css. +# +# Copyright © 2017, 2018, 2019 Vladimír Vondruš +# +# 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')) -- 2.30.2