From 07aedfa327a3ee63913e5a923f57173f0e41ec2d Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 20 Apr 2019 16:10:19 +0200 Subject: [PATCH] documentation/python: support enums. --- doc/documentation/python.rst | 72 ++++++++++++++++++ documentation/python.py | 74 ++++++++++++++++++- documentation/templates/python/class.html | 26 +++++++ .../templates/python/details-enum.html | 25 +++++++ .../templates/python/entry-enum.html | 5 ++ documentation/templates/python/module.html | 26 +++++++ .../inspect_all_property.html | 47 ++++++++++++ .../inspect_all_property/__init__.py | 19 ++++- .../inspect_string/inspect_string.Foo.html | 48 ++++++++++++ .../inspect_string/inspect_string.html | 52 +++++++++++++ .../inspect_string/inspect_string/__init__.py | 30 ++++++++ 11 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 documentation/templates/python/details-enum.html create mode 100644 documentation/templates/python/entry-enum.html diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index d9b20475..90963796 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -481,6 +481,32 @@ extracted from the property getter. would require parsing Python code directly (which is what Sphinx has to do to support these). +`Documenting enum values`_ +-------------------------- + +Python supplies an implicit docstrings for enums derived from :py:`enum.Enum` +and enum values implicitly inherit the docstring of the enum class. If either +is detected to be the case, docstring of the enum or the value is ignored. +While it's possible to document enum classes the usual way, there's a +non-obvious way to document enum values as well. + +.. code:: py + + import enum + + class MyEnum(enum.Enum): + """My enum""" + + ZERO = 0 + TWO = 3 + CONSISTENCY = -73 + + MyEnum.ZERO.__doc__ = "Zero value" + MyEnum.TWO.__doc__ = "Three, but named TWO for compatibility" + +The documentation output for enums includes enum value values and the class it +was derived from, so it's possible to know whether it's an enum or a flag. + `Command-line options`_ ======================= @@ -595,14 +621,21 @@ Each module page has the following additional properties: ======================================= ======================================= Property Description ======================================= ======================================= +:py:`page.prefix_wbr` Fully-qualified symbol prefix for given + compound with trailing ``.`` with + :html:`` tag after every ``.``. :py:`page.modules` List of inner modules. See `Module properties`_ for details. :py:`page.classes` List of classes. See `Class properties`_ for details. +:py:`page.enums` List of enums. See + `Enum properties`_ for details. :py:`page.functions` List of module-level functions. See `Function properties`_ for details. :py:`page.data` List of module-level data. See `Data properties`_ for details. +:py:`page.has_enum_details` If there is at least one enum with full + description block [2]_ ======================================= ======================================= Each class page has the following additional properties: @@ -612,8 +645,13 @@ Each class page has the following additional properties: ======================================= ======================================= Property Description ======================================= ======================================= +:py:`page.prefix_wbr` Fully-qualified symbol prefix for given + compound with trailing ``.`` with + :html:`` tag after every ``.``. :py:`page.classes` List of classes. See `Class properties`_ for details. +:py:`page.enums` List of enums. See + `Enum properties`_ for details. :py:`page.classmethods` List of class methods (annotated with :py:`@classmethod`). See `Function properties`_ for details. @@ -629,6 +667,8 @@ Property Description `Property properties`_ for details. :py:`page.data` List of data. See `Data properties`_ for details. +:py:`page.has_enum_details` If there is at least one enum with full + description block [2]_ ======================================= ======================================= `Module properties`_ @@ -657,6 +697,38 @@ Property Description :py:`class_.brief` Brief docs ======================================= ======================================= +`Enum properties`_ +``````````````````` + +.. class:: m-table m-fullwidth + +======================================= ======================================= +Property Description +======================================= ======================================= +:py:`enum.name` Enum name +:py:`enum.brief` Brief docs +:py:`enum.base` Base class from which the enum is + derived +:py:`enum.values` List of enum values +:py:`enum.has_details` If there is enough content for the full + description block. [2]_ +:py:`enum.has_value_details` If the enum values have description. + Impies :py:`enum.has_details`. +======================================= ======================================= + +Every item of :py:`enum.values` has the following properties: + +.. class:: m-table m-fullwidth + +=========================== =================================================== +Property Description +=========================== =================================================== +:py:`value.name` Value name +:py:`value.value` Value value. Set to :py:`None` if no value is + available. +:py:`value.brief` Value brief docs +=========================== =================================================== + `Function properties`_ `````````````````````` diff --git a/documentation/python.py b/documentation/python.py index 4d81ae1b..cd38f8ab 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -26,6 +26,7 @@ import argparse import copy +import enum import urllib.parse import html import importlib @@ -181,6 +182,41 @@ 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) + + 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 = '' + else: + value.brief = extract_brief(i.__doc__) + + if value.brief: + out.has_details = True + out.has_value_details = True + out.values += [value] + + return out + def extract_function_doc(path: List[str], function): assert inspect.isfunction(function) or inspect.ismethod(function) or inspect.isroutine(function) @@ -271,10 +307,13 @@ def render_module(config, path, module, env): page.brief = extract_brief(module.__doc__) page.url = breadcrumb[-1][1] page.breadcrumb = breadcrumb + page.prefix_wbr = '.'.join(path + ['']) page.modules = [] page.classes = [] + page.enums = [] page.functions = [] page.data = [] + page.has_enum_details = False # This is actually complicated -- if the module defines __all__, use that. # The __all__ is meant to expose the public API, so we don't filter out @@ -292,9 +331,13 @@ def render_module(config, path, module, env): if inspect.ismodule(object): page.modules += [extract_module_doc(subpath, object)] render_module(config, subpath, object, env) - elif inspect.isclass(object): + elif inspect.isclass(object) and not issubclass(object, enum.Enum): page.classes += [extract_class_doc(subpath, object)] render_class(config, subpath, object, env) + elif inspect.isclass(object) and issubclass(object, enum.Enum): + enum_ = extract_enum_doc(subpath, object) + page.enums += [enum_] + if enum_.has_details: page.has_enum_details = True elif inspect.isfunction(object) or inspect.isbuiltin(object): page.functions += [extract_function_doc(subpath, object)] # Assume everything else is data. The builtin help help() (from @@ -319,7 +362,7 @@ def render_module(config, path, module, env): render_module(config, subpath, object, env) # Get (and render) inner classes - for name, object in inspect.getmembers(module, inspect.isclass): + for name, object in inspect.getmembers(module, lambda o: inspect.isclass(o) and not issubclass(o, enum.Enum)): if is_internal_or_imported_module_member(module, path, name, object): continue subpath = path + [name] @@ -328,6 +371,17 @@ def render_module(config, path, module, env): page.classes += [extract_class_doc(subpath, object)] render_class(config, subpath, object, env) + # Get enums + for name, object in inspect.getmembers(module, lambda o: inspect.isclass(o) and issubclass(o, enum.Enum)): + 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) + page.enums += [enum_] + if enum_.has_details: page.has_enum_details = True + # Get inner functions for name, object in inspect.getmembers(module, lambda o: inspect.isfunction(o) or inspect.isbuiltin(o)): if is_internal_or_imported_module_member(module, path, name, object): continue @@ -398,16 +452,19 @@ def render_class(config, path, class_, env): page.brief = extract_brief(class_.__doc__) page.url = breadcrumb[-1][1] page.breadcrumb = breadcrumb + page.prefix_wbr = '.'.join(path + ['']) page.classes = [] + page.enums = [] page.classmethods = [] page.staticmethods = [] page.dunder_methods = [] page.methods = [] page.properties = [] page.data = [] + page.has_enum_details = False # Get inner classes - for name, object in inspect.getmembers(class_, inspect.isclass): + for name, object in inspect.getmembers(class_, lambda o: inspect.isclass(o) and not issubclass(o, enum.Enum)): if name in ['__base__', '__class__']: continue # TODO if name.startswith('_'): continue @@ -417,6 +474,17 @@ def render_class(config, path, class_, env): page.classes += [extract_class_doc(subpath, object)] render_class(config, subpath, object, env) + # Get enums + for name, object in inspect.getmembers(class_, lambda o: inspect.isclass(o) and issubclass(o, enum.Enum)): + if name.startswith('_'): continue + + subpath = path + [name] + if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) + + enum_ = extract_enum_doc(subpath, object) + page.enums += [enum_] + if enum_.has_details: page.has_enum_details = True + # Get methods for name, object in inspect.getmembers(class_, inspect.isroutine): # Filter out underscored methods (but not dunder methods) diff --git a/documentation/templates/python/class.html b/documentation/templates/python/class.html index 27d15f6b..7bbcf1a7 100644 --- a/documentation/templates/python/class.html +++ b/documentation/templates/python/class.html @@ -1,10 +1,13 @@ {% extends 'base.html' %} {% macro entry_class(class) %}{% include 'entry-class.html' %}{% endmacro %} +{% macro entry_enum(enum) %}{% include 'entry-enum.html' %}{% endmacro %} {% macro entry_function(function) %}{% include 'entry-function.html' %}{% endmacro %} {% macro entry_property(property) %}{% include 'entry-property.html' %}{% endmacro %} {% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} +{% macro details_enum(enum, prefix) %}{% include 'details-enum.html' %}{% endmacro %} + {% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} {% block main %} @@ -24,6 +27,9 @@ {% if page.classes %}
  • Classes
  • {% endif %} + {% if page.enums %} +
  • Enums
  • + {% endif %} {% if page.classmethods %}
  • Class methods
  • {% endif %} @@ -57,6 +63,16 @@ {% endif %} + {% if page.enums %} +
    +

    Enums

    +
    + {% for enum in page.enums %} +{{ entry_enum(enum) }} + {% endfor %} +
    +
    + {% endif %} {% if page.classmethods %}

    Class methods

    @@ -117,4 +133,14 @@
    {% endif %} + {% if page.has_enum_details %} +
    +

    Enum documentation

    + {% for enum in page.enums %} + {% if enum.has_details %} +{{ details_enum(enum, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
    + {% endif %} {% endblock %} diff --git a/documentation/templates/python/details-enum.html b/documentation/templates/python/details-enum.html new file mode 100644 index 00000000..50fb94d7 --- /dev/null +++ b/documentation/templates/python/details-enum.html @@ -0,0 +1,25 @@ +
    +

    + class {{ prefix }}{{ enum.name }}({{ enum.base }}) +

    + {% if enum.brief %} +

    {{ enum.brief }}

    + {% endif %} + {% if enum.has_value_details %} + + + + {% for value in enum.values %} + + + + + {% endfor %} + +
    Enumerators
    {{ value.name }} + {% if value.brief %} +

    {{ value.brief }}

    + {% endif %} +
    + {% endif %} +
    diff --git a/documentation/templates/python/entry-enum.html b/documentation/templates/python/entry-enum.html new file mode 100644 index 00000000..6b830833 --- /dev/null +++ b/documentation/templates/python/entry-enum.html @@ -0,0 +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 %} +
    +
    {{ enum.brief }}
    diff --git a/documentation/templates/python/module.html b/documentation/templates/python/module.html index 09eada78..0721ba7a 100644 --- a/documentation/templates/python/module.html +++ b/documentation/templates/python/module.html @@ -2,9 +2,12 @@ {% macro entry_module(module) %}{% include 'entry-module.html' %}{% endmacro %} {% macro entry_class(class) %}{% include 'entry-class.html' %}{% endmacro %} +{% macro entry_enum(enum) %}{% include 'entry-enum.html' %}{% endmacro %} {% macro entry_function(function) %}{% include 'entry-function.html' %}{% endmacro %} {% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} +{% macro details_enum(enum, prefix) %}{% include 'details-enum.html' %}{% endmacro %} + {% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} {% block main %} @@ -27,6 +30,9 @@ {% if page.classes %}
  • Classes
  • {% endif %} + {% if page.enums %} +
  • Enums
  • + {% endif %} {% if page.functions %}
  • Functions
  • {% endif %} @@ -58,6 +64,16 @@ {% endif %} + {% if page.enums %} +
    +

    Enums

    +
    + {% for enum in page.enums %} +{{ entry_enum(enum) }} + {% endfor %} +
    +
    + {% endif %} {% if page.functions %}

    Functions

    @@ -78,4 +94,14 @@
    {% endif %} + {% if page.has_enum_details %} +
    +

    Enum documentation

    + {% for enum in page.enums %} + {% if enum.has_details %} +{{ details_enum(enum, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
    + {% endif %} {% endblock %} diff --git a/documentation/test_python/inspect_all_property/inspect_all_property.html b/documentation/test_python/inspect_all_property/inspect_all_property.html index 771b037c..8150aadd 100644 --- a/documentation/test_python/inspect_all_property/inspect_all_property.html +++ b/documentation/test_python/inspect_all_property/inspect_all_property.html @@ -30,6 +30,7 @@ @@ -50,6 +51,22 @@
    +
    +

    Enums

    +
    +
    + class _MyPrivateEnum(enum.Enum): VALUE = 1 + ANOTHER = 2 + YAY = 3 +
    +
    +
    + class UndocumentedEnum(enum.IntFlag): FLAG_ONE = 1 + FLAG_SEVENTEEN = 17 +
    +
    +
    +

    Functions

    @@ -68,6 +85,36 @@
    +
    +

    Enum documentation

    +
    +

    + class inspect_all_property._MyPrivateEnum(enum.Enum) +

    + + + + + + + + + + + + + + + + +
    Enumerators
    VALUE +

    A value

    +
    ANOTHER +

    Another value

    +
    YAY +
    +
    +
    diff --git a/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py b/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py index cd670b64..b6a737da 100644 --- a/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py +++ b/documentation/test_python/inspect_all_property/inspect_all_property/__init__.py @@ -1,3 +1,5 @@ +import enum + from . import hidden from . import _private_but_exposed @@ -21,4 +23,19 @@ hidden_data = 34 _private_data = 'Hey!' -__all__ = ['_private_but_exposed', '_PrivateButExposedClass', '_private_but_exposed_func', '_private_data'] +class _MyPrivateEnum(enum.Enum): + VALUE = 1 + ANOTHER = 2 + YAY = 3 + +_MyPrivateEnum.VALUE.__doc__ = "A value" +_MyPrivateEnum.ANOTHER.__doc__ = "Another value" + +class UndocumentedEnum(enum.IntFlag): + FLAG_ONE = 1 + FLAG_SEVENTEEN = 17 + +class HiddenEnum(enum.Flag): + pass + +__all__ = ['_private_but_exposed', '_PrivateButExposedClass', '_private_but_exposed_func', '_private_data', '_MyPrivateEnum', 'UndocumentedEnum'] diff --git a/documentation/test_python/inspect_string/inspect_string.Foo.html b/documentation/test_python/inspect_string/inspect_string.Foo.html index b7cdce3e..234cc9a4 100644 --- a/documentation/test_python/inspect_string/inspect_string.Foo.html +++ b/documentation/test_python/inspect_string/inspect_string.Foo.html @@ -30,6 +30,7 @@ Reference