From 35f3d45e23d3352156c81247096176d1fd6c77ae Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Thu, 18 Jul 2019 20:42:44 +0200 Subject: [PATCH] documentation/python: support external docs for all stuff. And also detailed docs for enums, functions, properties and data. --- doc/documentation/python.rst | 26 ++- doc/plugins/sphinx.rst | 20 +-- documentation/python.py | 150 +++++++++++------- documentation/templates/python/class.html | 48 ++++++ .../templates/python/details-data.html | 13 ++ .../templates/python/details-function.html | 37 +++++ .../templates/python/details-property.html | 11 ++ documentation/templates/python/module.html | 22 +++ .../test_python/content/classes.html | 3 +- .../test_python/content/content.Class.html | 140 ++++++++++++++++ ...ass.html => content.ClassWithSummary.html} | 4 +- .../content/content.docstring_summary.html | 32 ++++ .../test_python/content/content.html | 129 ++++++++++++++- documentation/test_python/content/content.py | 9 -- .../test_python/content/content/__init__.py | 70 ++++++++ .../content/content/docstring_summary.py | 1 + documentation/test_python/content/docs.rst | 97 ++++++++++- documentation/test_python/test_content.py | 3 +- plugins/m/sphinx.py | 52 +++++- 19 files changed, 774 insertions(+), 93 deletions(-) create mode 100644 documentation/templates/python/details-data.html create mode 100644 documentation/templates/python/details-function.html create mode 100644 documentation/templates/python/details-property.html rename documentation/test_python/content/{content.AnotherClass.html => content.ClassWithSummary.html} (88%) create mode 100644 documentation/test_python/content/content.docstring_summary.html delete mode 100644 documentation/test_python/content/content.py create mode 100644 documentation/test_python/content/content/__init__.py create mode 100644 documentation/test_python/content/content/docstring_summary.py diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 540a1303..3a213c8a 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -663,12 +663,16 @@ Keyword argument Content etc. :py:`module_doc_contents` Module documentation contents :py:`class_doc_contents` Class documentation contents +:py:`enum_doc_contents` Enum documentation contents +:py:`function_doc_contents` Function documentation contents +:py:`property_doc_contents` Property documentation contents :py:`data_doc_contents` Data documentation contents :py:`hooks_pre_page` Hooks to call before each page gets rendered :py:`hooks_post_run` Hooks to call at the very end of the script run =========================== =================================================== -The :py:`module_doc_contents`, :py:`class_doc_contents` and +The :py:`module_doc_contents`, :py:`class_doc_contents`, +:py:`function_doc_contents`, :py:`property_doc_contents` and :py:`data_doc_contents` variables are :py:`Dict[str, Dict[str, str]]`, where the first level is a name and second level are key/value pairs of the actual HTML documentation content. Plugins that parse extra documentation inputs (such @@ -921,6 +925,11 @@ Property Description `Data properties`_ for details. :py:`page.has_enum_details` If there is at least one enum with full description block [2]_ +:py:`page.has_function_details` If there is at least one function (or + method, in case of classes) with full + description block [2]_ +:py:`page.has_data_details` If there is at least one data with full + description block [2]_ ======================================= ======================================= Each class page, rendered with ``class.html``, has the following additional @@ -944,6 +953,8 @@ Property Description `Function properties`_ for details. :py:`page.properties` List of properties. See `Property properties`_ for details. +:py:`page.has_property_details` If there is at least one property with + full description block [2]_ ======================================= ======================================= Explicit documentation pages rendered with ``class.html`` have additional @@ -988,6 +999,7 @@ Property Description :py:`enum.name` Enum name :py:`enum.id` Enum ID [4]_ :py:`enum.summary` Doc summary +:py:`enum.content` Detailed documentation, if any :py:`enum.base` Base class from which the enum is derived. Set to :py:`None` if no base class information is available. @@ -1023,6 +1035,7 @@ Property Description :py:`function.name` Function name :py:`function.id` Function ID [4]_ :py:`function.summary` Doc summary +:py:`function.content` Detailed documentation, if any :py:`function.type` Function return type annotation [1]_ :py:`function.params` List of function parameters. See below for details. @@ -1034,8 +1047,7 @@ Property Description wrapping on multiple lines would only occupy too much vertical space. :py:`function.has_details` If there is enough content for the full - description block. Currently always set to - :py:`False`. [2]_ + description block [2]_ :py:`function.is_classmethod` Set to :py:`True` if the function is annotated with :py:`@classmethod`, :py:`False` otherwise. @@ -1076,12 +1088,12 @@ Property Description :py:`property.id` Property ID [4]_ :py:`property.type` Property getter return type annotation [1]_ :py:`property.summary` Doc summary +:py:`property.content` Detailed documentation, if any :py:`property.is_gettable` If the property is gettable :py:`property.is_settable` If the property is settable :py:`property.is_deletable` If the property is deletable with :py:`del` :py:`property.has_details` If there is enough content for the full - description block. Currently always set to - :py:`False`. [2]_ + description block [2]_ =================================== =========================================== `Data properties`_ @@ -1096,10 +1108,10 @@ Property Description :py:`data.id` Data ID [4]_ :py:`data.type` Data type :py:`data.summary` Doc summary +:py:`data.content` Detailed documentation, if any :py:`data.value` Data value representation :py:`data.has_details` If there is enough content for the full - description block. Currently always set to - :py:`False`. [2]_ + description block [2]_ =================================== =========================================== `Index page templates`_ diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst index 836ccecb..f1f62d1e 100644 --- a/doc/plugins/sphinx.rst +++ b/doc/plugins/sphinx.rst @@ -57,15 +57,17 @@ List the plugin in your :py:`PLUGINS`. PLUGINS += ['m.sphinx'] -`Module, class and data docs`_ -============================== - -The :rst:`.. py:module::`, :rst:`.. py:class::` and :rst:`.. py:data::` -directives provide a way to supply module, class and data documentation -content. Directive option is the name to document, directive contents are -the actual contents; in addition the :py:`:summary:` option can override the -docstring extracted using inspection. No restrictions are made on the contents, -it's possible to make use of any additional plugins in the markup. Example: +`Module, class, enum, function, property and data docs`_ +======================================================== + +The :rst:`.. py:module::`, :rst:`.. py:class::`, :rst:`.. py:enum::`, +:rst:`.. py:function::`, :rst:`.. py:property::` and :rst:`.. py:data::` +directives provide a way to supply module, class, enum, function / method, +property and data documentation content. Directive option is the name to +document, directive contents are the actual contents; in addition the +:py:`:summary:` option can override the docstring extracted using inspection. +No restrictions are made on the contents, it's possible to make use of any +additional plugins in the markup. Example: .. code:: rst diff --git a/documentation/python.py b/documentation/python.py index 6d7987c1..3b8a202d 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -175,6 +175,9 @@ class State: self.module_mapping: Dict[str, str] = {} self.module_docs: Dict[str, Dict[str, str]] = {} self.class_docs: Dict[str, Dict[str, str]] = {} + self.enum_docs: Dict[str, Dict[str, str]] = {} + self.function_docs: Dict[str, Dict[str, str]] = {} + self.property_docs: Dict[str, Dict[str, str]] = {} self.data_docs: Dict[str, Dict[str, str]] = {} self.external_data: Set[str] = set() @@ -706,13 +709,13 @@ def parse_pybind_signature(state: State, referrer_path: List[str], signature: st end = original_signature.find('\n') logging.warning("cannot parse pybind11 function signature %s", original_signature[:end if end != -1 else None]) if end != -1 and len(original_signature) > end + 1 and original_signature[end + 1] == '\n': - summary = extract_summary(state, {}, [], original_signature[end + 1:]) + summary = take_first_paragraph(inspect.cleandoc(original_signature[end + 1:])) else: summary = '' return (name, summary, [('…', None, None, None)], None) if len(signature) > 1 and signature[1] == '\n': - summary = extract_summary(state, {}, [], signature[2:]) + summary = take_first_paragraph(inspect.cleandoc(signature[2:])) else: summary = '' @@ -763,6 +766,10 @@ def format_value(state: State, referrer_path: List[str], value: str) -> Optional else: return None +def take_first_paragraph(doc: str) -> str: + end = doc.find('\n\n') + return doc if end == -1 else doc [:end] + def extract_summary(state: State, external_docs, path: List[str], doc: str) -> str: # Prefer external docs, if available path_str = '.'.join(path) @@ -770,9 +777,35 @@ def extract_summary(state: State, external_docs, path: List[str], doc: str) -> s return render_inline_rst(state, external_docs[path_str]['summary']) if not doc: return '' # some modules (xml.etree) have that :( - doc = inspect.cleandoc(doc) - end = doc.find('\n\n') - return html.escape(doc if end == -1 else doc[:end]) + # TODO: render as rst (config option for that) + return html.escape(take_first_paragraph(inspect.cleandoc(doc))) + +def extract_docs(state: State, external_docs, path: List[str], doc: str) -> Tuple[str, str]: + path_str = '.'.join(path) + if path_str in external_docs: + external_doc_entry = external_docs[path_str] + else: + external_doc_entry = None + + # Summary. Prefer external docs, if available + if external_doc_entry and external_doc_entry['summary']: + summary = render_inline_rst(state, external_doc_entry['summary']) + else: + # some modules (xml.etree) have None as a docstring :( + # TODO: render as rst (config option for that) + summary = html.escape(take_first_paragraph(inspect.cleandoc(doc or ''))) + + # Content + if external_doc_entry and external_doc_entry['content']: + content = render_rst(state, external_doc_entry['content']) + else: + # TODO: extract more than just a summary from the docstring + content = None + + # Mark the docs as used (so it can warn about unused docs at the end) + if external_doc_entry: external_doc_entry['used'] = True + + return summary, content def extract_type(type) -> str: # For types we concatenate the type name with its module unless it's @@ -893,17 +926,17 @@ def extract_enum_doc(state: State, entry: Empty): out.name = entry.path[-1] out.id = state.config['ID_FORMATTER'](EntryType.ENUM, entry.path[-1:]) out.values = [] - out.has_details = False out.has_value_details = False # The happy case if issubclass(entry.object, enum.Enum): # Enum doc is by default set to a generic value. That's useless as well. if entry.object.__doc__ == 'An enumeration.': - out.summary = '' + docstring = '' else: - # TODO: external summary for enums - out.summary = extract_summary(state, {}, [], entry.object.__doc__) + docstring = entry.object.__doc__ + out.summary, out.content = extract_docs(state, state.enum_docs, entry.path, docstring) + out.has_details = bool(out.content) out.base = extract_type(entry.object.__base__) if out.base: out.base = make_name_link(state, entry.path, out.base) @@ -930,8 +963,8 @@ def extract_enum_doc(state: State, entry: Empty): elif state.config['PYBIND11_COMPATIBILITY']: assert hasattr(entry.object, '__members__') - # TODO: external summary for enums - out.summary = extract_summary(state, {}, [], entry.object.__doc__) + out.summary, out.content = extract_docs(state, state.enum_docs, entry.path, entry.object.__doc__) + out.has_details = bool(out.content) out.base = None for name, v in entry.object.__members__.items(): @@ -988,9 +1021,8 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: out.name = entry.path[-1] out.params = [] out.has_complex_params = False - out.has_details = False - # TODO: external summary for functions - out.summary = summary + out.summary, out.content = extract_docs(state, state.function_docs, entry.path, summary) + out.has_details = bool(out.content) # Don't show None return type for functions w/o a return out.type = None if type == 'None' else type @@ -1099,9 +1131,8 @@ def extract_function_doc(state: State, parent, entry: Empty) -> List[Any]: out.id = state.config['ID_FORMATTER'](EntryType.FUNCTION, entry.path[-1:]) out.params = [] out.has_complex_params = False - out.has_details = False - # TODO: external summary for functions - out.summary = extract_summary(state, {}, [], entry.object.__doc__) + out.summary, out.content = extract_docs(state, state.function_docs, entry.path, entry.object.__doc__) + out.has_details = bool(out.content) # Decide if classmethod or staticmethod in case this is a method if inspect.isclass(parent): @@ -1170,16 +1201,15 @@ def extract_property_doc(state: State, parent, entry: Empty): # gettable and settable (couldn't find any way to make them *inspectably* # readonly, all solutions involved throwing from __setattr__()) and # deletable as well (calling del on it seems to simply remove any - # previously set value). Unfortunately we can't get any docstring for these - # either. + # previously set value). # TODO: any better way to detect that those are slots? if entry.object.__class__.__name__ == 'member_descriptor' and entry.object.__class__.__module__ == 'builtins': out.is_gettable = True out.is_settable = True out.is_deletable = True - # TODO: external summary for properties - out.summary = '' - out.has_details = False + # Unfortunately we can't get any docstring for these + out.summary, out.content = extract_docs(state, state.property_docs, entry.path, '') + out.has_details = bool(out.content) # First try to get fully dereferenced type hints (with strings # converted to actual annotations). If that fails (e.g. because a type @@ -1205,21 +1235,22 @@ def extract_property_doc(state: State, parent, entry: Empty): out.is_gettable = True out.is_settable = False out.is_deletable = False - out.summary = '' - out.has_details = False + # Unfortunately we can't get any docstring for these + out.summary, out.content = extract_docs(state, state.property_docs, entry.path, '') + out.has_details = bool(out.content) out.type = None return out - # TODO: external summary for properties out.is_gettable = entry.object.fget is not None if entry.object.fget or (entry.object.fset and entry.object.__doc__): - out.summary = extract_summary(state, {}, [], entry.object.__doc__) + docstring = entry.object.__doc__ else: assert entry.object.fset - out.summary = extract_summary(state, {}, [], entry.object.fset.__doc__) + docstring = entry.object.fset.__doc__ + out.summary, out.content = extract_docs(state, state.property_docs, entry.path, docstring) out.is_settable = entry.object.fset is not None out.is_deletable = entry.object.fdel is not None - out.has_details = False + out.has_details = bool(out.content) # For the type, if the property is gettable, get it from getters's return # type. For write-only properties get it from setter's second argument @@ -1290,8 +1321,8 @@ def extract_data_doc(state: State, parent, entry: Empty): out.name = entry.path[-1] out.id = state.config['ID_FORMATTER'](EntryType.DATA, entry.path[-1:]) # Welp. https://stackoverflow.com/questions/8820276/docstring-for-variable - out.summary = extract_summary(state, state.data_docs, entry.path, '') - out.has_details = False + out.summary, out.content = extract_docs(state, state.data_docs, entry.path, '') + out.has_details = bool(out.content) # First try to get fully dereferenced type hints (with strings converted to # actual annotations). If that fails (e.g. because a type doesn't exist), @@ -1346,7 +1377,7 @@ def render_module(state: State, path, module, env): for hook in state.hooks_pre_page: hook() page = Empty() - page.summary = extract_summary(state, state.module_docs, path, module.__doc__) + page.summary, page.content = extract_docs(state, state.module_docs, path, module.__doc__) page.filename = filename page.url = url page.breadcrumb = breadcrumb @@ -1357,15 +1388,11 @@ def render_module(state: State, path, module, env): page.functions = [] page.data = [] page.has_enum_details = False - - # External page content, if provided - path_str = '.'.join(path) - if path_str in state.module_docs: - page.content = render_rst(state, state.module_docs[path_str]['content']) - state.module_docs[path_str]['used'] = True + page.has_function_details = False + page.has_data_details = False # Find itself in the global map, save the summary back there for index - module_entry = state.name_map[path_str] + module_entry = state.name_map['.'.join(path)] module_entry.summary = page.summary # Extract docs for all members @@ -1386,9 +1413,14 @@ def render_module(state: State, path, module, env): page.enums += [enum_] if enum_.has_details: page.has_enum_details = True elif member_entry.type == EntryType.FUNCTION: - page.functions += extract_function_doc(state, module, member_entry) + functions = extract_function_doc(state, module, member_entry) + page.functions += functions + for function in functions: + if function.has_details: page.has_function_details = True elif member_entry.type == EntryType.DATA: - page.data += [extract_data_doc(state, module, member_entry)] + data = extract_data_doc(state, module, member_entry) + page.data += [data] + if data.has_details: page.has_data_details = True else: # pragma: no cover assert False @@ -1420,7 +1452,7 @@ def render_class(state: State, path, class_, env): for hook in state.hooks_pre_page: hook() page = Empty() - page.summary = extract_summary(state, state.class_docs, path, class_.__doc__) + page.summary, page.content = extract_docs(state, state.class_docs, path, class_.__doc__) page.filename = filename page.url = url page.breadcrumb = breadcrumb @@ -1434,15 +1466,12 @@ def render_class(state: State, path, class_, env): page.properties = [] page.data = [] page.has_enum_details = False - - # External page content, if provided - path_str = '.'.join(path) - if path_str in state.class_docs: - page.content = render_rst(state, state.class_docs[path_str]['content']) - state.class_docs[path_str]['used'] = True + page.has_function_details = False + page.has_property_details = False + page.has_data_details = False # Find itself in the global map, save the summary back there for index - module_entry = state.name_map[path_str] + module_entry = state.name_map['.'.join(path)] module_entry.summary = page.summary # Extract docs for all members @@ -1471,10 +1500,15 @@ def render_class(state: State, path, class_, env): page.staticmethods += [function] else: page.methods += [function] + if function.has_details: page.has_function_details = True elif member_entry.type == EntryType.PROPERTY: - page.properties += [extract_property_doc(state, class_, member_entry)] + property = extract_property_doc(state, class_, member_entry) + page.properties += [property] + if property.has_details: page.has_property_details = True elif member_entry.type == EntryType.DATA: - page.data += [extract_data_doc(state, class_, member_entry)] + data = extract_data_doc(state, class_, member_entry) + page.data += [data] + if data.has_details: page.has_data_details = True else: # pragma: no cover assert False @@ -1754,6 +1788,9 @@ def run(basedir, config, *, templates=default_templates, search_add_lookahead_ba jinja_environment=env, module_doc_contents=state.module_docs, class_doc_contents=state.class_docs, + enum_doc_contents=state.enum_docs, + function_doc_contents=state.function_docs, + property_doc_contents=state.property_docs, data_doc_contents=state.data_docs, hooks_pre_page=state.hooks_pre_page, hooks_post_run=state.hooks_post_run) @@ -1821,15 +1858,10 @@ def run(basedir, config, *, templates=default_templates, search_add_lookahead_ba render_page(state, entry.path, entry.filename, env) # Warn if there are any unused contents left after processing everything - unused_module_docs = [key for key, value in state.module_docs.items() if not 'used' in value] - unused_class_docs = [key for key, value in state.class_docs.items() if not 'used' in value] - unused_data_docs = [key for key, value in state.data_docs.items() if not 'used' in value] - if unused_module_docs: - logging.warning("The following module doc contents were unused: %s", unused_module_docs) - if unused_class_docs: - logging.warning("The following class doc contents were unused: %s", unused_class_docs) - if unused_data_docs: - logging.warning("The following data doc contents were unused: %s", unused_data_docs) + for docs in ['module', 'class', 'enum', 'function', 'property', 'data']: + unused_docs = [key for key, value in getattr(state, f'{docs}_docs').items() if not 'used' in value] + if unused_docs: + logging.warning("The following %s doc contents were unused: %s", docs, unused_docs) # Create module and class index from the toplevel name list. Recursively go # from the top-level index list and gather all class/module children. diff --git a/documentation/templates/python/class.html b/documentation/templates/python/class.html index b26078a3..d6090451 100644 --- a/documentation/templates/python/class.html +++ b/documentation/templates/python/class.html @@ -7,6 +7,9 @@ {% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} {% macro details_enum(enum, prefix) %}{% include 'details-enum.html' %}{% endmacro %} +{% macro details_function(function, prefix) %}{% include 'details-function.html' %}{% endmacro %} +{% macro details_property(property, prefix) %}{% include 'details-property.html' %}{% endmacro %} +{% macro details_data(data, prefix) %}{% include 'details-data.html' %}{% endmacro %} {% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} @@ -146,4 +149,49 @@ {% endfor %} {% endif %} + {% if page.has_function_details %} +
+

Method documentation

+ {% for function in page.classmethods %} + {% if function.has_details %} +{{ details_function(function, page.prefix_wbr) }} + {% endif %} + {% endfor %} + {% for function in page.staticmethods %} + {% if function.has_details %} +{{ details_function(function, page.prefix_wbr) }} + {% endif %} + {% endfor %} + {% for function in page.methods %} + {% if function.has_details %} +{{ details_function(function, page.prefix_wbr) }} + {% endif %} + {% endfor %} + {% for function in page.dunder_methods %} + {% if function.has_details %} +{{ details_function(function, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
+ {% endif %} + {% if page.has_property_details %} +
+

Property documentation

+ {% for property in page.properties %} + {% if property.has_details %} +{{ details_property(property, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
+ {% endif %} + {% if page.has_data_details %} +
+

Data documentation

+ {% for data in page.data %} + {% if data.has_details %} +{{ details_data(data, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
+ {% endif %} {% endblock %} diff --git a/documentation/templates/python/details-data.html b/documentation/templates/python/details-data.html new file mode 100644 index 00000000..b386cdad --- /dev/null +++ b/documentation/templates/python/details-data.html @@ -0,0 +1,13 @@ +
+

+ {{ prefix }}{{ data.name }}{% if data.type %}: {{ data.type }}{% endif %} + {# the empty line needs to be here to prevent the lines from merging #} + +

+ {% if data.summary %} +

{{ data.summary }}

+ {% endif %} + {% if data.content %} +{{ data.content }} + {% endif %} +
diff --git a/documentation/templates/python/details-function.html b/documentation/templates/python/details-function.html new file mode 100644 index 00000000..b78ae71f --- /dev/null +++ b/documentation/templates/python/details-function.html @@ -0,0 +1,37 @@ +
+

+ {% set j = joiner('\n ' if function.has_complex_params else ' ') %} + def {{ prefix }}{{ function.name }}({% for param in function.params %}{% if loop.index0 %}{% if function.params[loop.index0 - 1].kind == 'POSITIONAL_OR_KEYWORD' and param.kind == 'KEYWORD_ONLY' %}, *,{% else %},{% endif %}{% endif %}{{ j() }}{% if param.kind == 'VAR_POSITIONAL' %}*{% elif param.kind == 'VAR_KEYWORD' %}**{% endif %}{{ param.name }}{% if param.type %}: {{ param.type }}{% endif %}{% if param.default %} = {{ param.default }}{% endif %}{% if param.kind == 'POSITIONAL_ONLY' and (loop.last or function.params[loop.index0 + 1].kind != 'POSITIONAL_ONLY') %}, /{% endif %}{% endfor %}){% if function.type %} -> {{ function.type }}{% endif %}{% if function.is_classmethod %} classmethod{% elif function.is_staticmethod %} staticmethod{% endif %} +

+ {% if function.summary %} +

{{ function.summary }}

+ {% endif %} + {% if function.has_param_details or function.return_value %} + + {% if function.has_param_details %} + + + + + {% for param in function.params %} + + {{ param.name }} + + + {% endfor %} + + {% endif %} + {% if function.return_value %} + + + Returns + + + + {% endif %} +
Parameters
{{ param.description }}
{{ function.return_value }}
+ {% endif %} + {% if function.content %} +{{ function.content }} + {% endif %} +
diff --git a/documentation/templates/python/details-property.html b/documentation/templates/python/details-property.html new file mode 100644 index 00000000..5f2159ea --- /dev/null +++ b/documentation/templates/python/details-property.html @@ -0,0 +1,11 @@ +
+

+ {{ prefix }}{{ property.name }}{% if property.type %}: {{ property.type }}{% endif %} {% if property.is_gettable and property.is_settable %}get set{% elif property.is_gettable %}get{% else %}set{% endif %}{% if property.is_deletable %} del{% endif %} +

+ {% if property.summary %} +

{{ property.summary }}

+ {% endif %} + {% if property.content %} +{{ property.content }} + {% endif %} +
diff --git a/documentation/templates/python/module.html b/documentation/templates/python/module.html index 9cf77d45..41acfd56 100644 --- a/documentation/templates/python/module.html +++ b/documentation/templates/python/module.html @@ -7,6 +7,8 @@ {% macro entry_data(data) %}{% include 'entry-data.html' %}{% endmacro %} {% macro details_enum(enum, prefix) %}{% include 'details-enum.html' %}{% endmacro %} +{% macro details_function(function, prefix) %}{% include 'details-function.html' %}{% endmacro %} +{% macro details_data(data, prefix) %}{% include 'details-data.html' %}{% endmacro %} {% block title %}{% set j = joiner('.') %}{% for name, _ in page.breadcrumb %}{{ j() }}{{ name }}{% endfor %} | {{ super() }}{% endblock %} @@ -107,4 +109,24 @@ {% endfor %} {% endif %} + {% if page.has_function_details %} +
+

Function documentation

+ {% for function in page.functions %} + {% if function.has_details %} +{{ details_function(function, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
+ {% endif %} + {% if page.has_data_details %} +
+

Data documentation

+ {% for data in page.data %} + {% if data.has_details %} +{{ details_data(data, page.prefix_wbr) }} + {% endif %} + {% endfor %} +
+ {% endif %} {% endblock %} diff --git a/documentation/test_python/content/classes.html b/documentation/test_python/content/classes.html index ff357b8e..293619c3 100644 --- a/documentation/test_python/content/classes.html +++ b/documentation/test_python/content/classes.html @@ -24,8 +24,9 @@
  • module content This overwrites the docstring for content.
  • diff --git a/documentation/test_python/content/content.Class.html b/documentation/test_python/content/content.Class.html index 6643207a..66eb5822 100644 --- a/documentation/test_python/content/content.Class.html +++ b/documentation/test_python/content/content.Class.html @@ -23,8 +23,148 @@ content.Class class

    This overwrites the docstring for content.Class.

    +
    +

    Contents

    + +

    This is detailed class docs. Here I also hate how it needs to be indented.

    +
    +

    Class methods

    +
    +
    + def class_method(b) +
    +
    This function is a class method
    +
    +
    +
    +

    Static methods

    +
    +
    + def static_method(cls, a) +
    +
    This function is a static method
    +
    +
    +
    +

    Methods

    +
    +
    + def method(self) +
    +
    This overwrites the docstring for content.Class.method, but +doesn't add any detailed block.
    +
    + def method_with_details(self) +
    +
    +
    +
    +
    +

    Special methods

    +
    +
    + def __init__(self) +
    +
    A dunder method
    +
    +
    +
    +

    Properties

    +
    +
    + a_property get +
    +
    This overwrites the docstring for content.Class.a_property, +but doesn't add any detailed block.
    +
    + a_property_with_details get +
    +
    This overwrites the docstring for content.Class.a_property_with_details.
    +
    + annotated_property: float get +
    +
    This is an annotated property
    +
    +
    +
    +

    Data

    +
    +
    + DATA_WITH_DETAILS: str = 'this blows' +
    +
    +
    +
    +
    +

    Method documentation

    +
    +

    + def content.Class.class_method(b) classmethod +

    +

    This function is a class method

    +

    The classmethod should be shown here.

    +
    +
    +

    + def content.Class.static_method(cls, a) staticmethod +

    +

    This function is a static method

    +

    The staticmethod should be shown here.

    +
    +
    +

    + def content.Class.method_with_details(self) +

    +

    This one has a detailed block without any summary.

    +
    +
    +

    + def content.Class.__init__(self) +

    +

    A dunder method

    +

    A dunder method shown in the detailed view.

    +
    +
    +
    +

    Property documentation

    +
    +

    + content.Class.a_property_with_details get +

    +

    This overwrites the docstring for content.Class.a_property_with_details.

    +

    Detailed property docs.

    +
    +
    +

    + content.Class.annotated_property: float get +

    +

    This is an annotated property

    +

    Annotated property, using summary from the docstring.

    +
    +
    +
    +

    Data documentation

    +
    +

    + content.Class.DATA_WITH_DETAILS: str +

    +

    Detailed docs for data in a class to check rendering.

    +
    +
    diff --git a/documentation/test_python/content/content.AnotherClass.html b/documentation/test_python/content/content.ClassWithSummary.html similarity index 88% rename from documentation/test_python/content/content.AnotherClass.html rename to documentation/test_python/content/content.ClassWithSummary.html index 1075f475..23d5231a 100644 --- a/documentation/test_python/content/content.AnotherClass.html +++ b/documentation/test_python/content/content.ClassWithSummary.html @@ -2,7 +2,7 @@ - content.AnotherClass | My Python Project + content.ClassWithSummary | My Python Project @@ -20,7 +20,7 @@

    - content.AnotherClass class + content.ClassWithSummary class

    This class has summary from the docstring

    This class has external details but summary from the docstring.

    diff --git a/documentation/test_python/content/content.docstring_summary.html b/documentation/test_python/content/content.docstring_summary.html new file mode 100644 index 00000000..f9dc838c --- /dev/null +++ b/documentation/test_python/content/content.docstring_summary.html @@ -0,0 +1,32 @@ + + + + + content.docstring_summary | My Python Project + + + + + +
    +
    +
    +
    +
    +

    + content.docstring_summary module +

    +

    This module retains summary from the docstring

    +

    And adds detailed docs.

    +
    +
    +
    +
    + + diff --git a/documentation/test_python/content/content.html b/documentation/test_python/content/content.html index 7797d68e..6fc4bcc8 100644 --- a/documentation/test_python/content/content.html +++ b/documentation/test_python/content/content.html @@ -29,7 +29,10 @@
  • Reference
  • @@ -37,13 +40,59 @@

    This is detailed module docs. I kinda hate how it needs to be indented, tho.

    +
    +

    Modules

    +
    +
    module docstring_summary
    +
    This module retains summary from the docstring
    +
    +

    Classes

    -
    class AnotherClass
    -
    This class has summary from the docstring
    class Class
    This overwrites the docstring for content.Class.
    +
    class ClassWithSummary
    +
    This class has summary from the docstring
    +
    +
    +
    +

    Enums

    +
    +
    + class Enum(enum.Enum): +
    +
    This overwrites the docstring for content.Enum, but +doesn't add any detailed block.
    +
    + class EnumWithSummary(enum.Enum): VALUE = 0 + ANOTHER = 1 +
    +
    This summary is preserved
    +
    +
    +
    +

    Functions

    +
    +
    + def annotations(a: int, + b, + c: float) -> str +
    +
    No annotations shown for this
    +
    + def foo(a, b) +
    +
    This overwrites the docstring for content.foo, but +doesn't add any detailed block.
    +
    + def foo_with_details(a, b) +
    +
    This overwrites the docstring for content.foo_with_details().
    +
    + def function_with_summary() +
    +
    This function has summary from the docstring
    @@ -53,8 +102,84 @@ tho.

    CONSTANT: float = 3.14
    This is finally a docstring for content.CONSTANT
    +
    + DATA_WITH_DETAILS: str = 'heyoo' +
    +
    This is finally a docstring for content.CONSTANT
    +
    + DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE = None +
    +
    +
    +

    Enum documentation

    +
    +

    + class content.EnumWithSummary(enum.Enum) +

    +

    This summary is preserved

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

    A value

    +
    ANOTHER +
    +
    +
    +
    +

    Function documentation

    +
    +

    + def content.annotations(a: int, + b, + c: float) -> str +

    +

    No annotations shown for this

    +

    Type annotations in detailed docs.

    +
    +
    +

    + def content.foo_with_details(a, b) +

    +

    This overwrites the docstring for content.foo_with_details().

    +
    +Detailed docs for this function
    +
    +
    +

    + def content.function_with_summary() +

    +

    This function has summary from the docstring

    +

    This function has external details but summary from the docstring.

    +
    +
    +
    +

    Data documentation

    +
    +

    + content.DATA_WITH_DETAILS: str +

    +

    This is finally a docstring for content.CONSTANT

    +

    Detailed docs for the data. YAY.

    +
    +
    +

    + content.DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE +

    +

    Why it has to be yelling?!

    +
    +
    diff --git a/documentation/test_python/content/content.py b/documentation/test_python/content/content.py deleted file mode 100644 index 185d93a0..00000000 --- a/documentation/test_python/content/content.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Yes so this is module summary, not shown in the output""" - -class Class: - """And this class summary, not shown either""" - -class AnotherClass: - """This class has summary from the docstring""" - -CONSTANT: float = 3.14 diff --git a/documentation/test_python/content/content/__init__.py b/documentation/test_python/content/content/__init__.py new file mode 100644 index 00000000..f48c850a --- /dev/null +++ b/documentation/test_python/content/content/__init__.py @@ -0,0 +1,70 @@ +"""Yes so this is module summary, not shown in the output""" + +import enum +from . import docstring_summary + +class Class: + """And this class summary, not shown either""" + + @classmethod + def class_method(a, b): + """This function is a class method""" + + @staticmethod + def static_method(cls, a): + """This function is a static method""" + + def __init__(self): + """A dunder method""" + + def method(self): + """This summary will get overriden from the docs""" + + def method_with_details(self): + pass + + @property + def a_property(self): + """This summary is not shown either""" + + @property + def a_property_with_details(self): + """This summary is not shown either""" + + @property + def annotated_property(self) -> float: + """This is an annotated property""" + + DATA_WITH_DETAILS: str = 'this blows' + +class ClassWithSummary: + """This class has summary from the docstring""" + +class Enum(enum.Enum): + """This summary gets ignored""" + +class EnumWithSummary(enum.Enum): + """This summary is preserved""" + + VALUE = 0 + ANOTHER = 1 + +EnumWithSummary.VALUE.__doc__ = "A value" + +def foo(a, b): + """This summary is not shown either""" + +def foo_with_details(a, b): + """This summary is not shown either""" + +def function_with_summary(): + """This function has summary from the docstring""" + +def annotations(a: int, b, c: float) -> str: + """No annotations shown for this""" + +CONSTANT: float = 3.14 + +DATA_WITH_DETAILS: str = 'heyoo' + +DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE = None diff --git a/documentation/test_python/content/content/docstring_summary.py b/documentation/test_python/content/content/docstring_summary.py new file mode 100644 index 00000000..98a7cd0c --- /dev/null +++ b/documentation/test_python/content/content/docstring_summary.py @@ -0,0 +1 @@ +"""This module retains summary from the docstring""" diff --git a/documentation/test_python/content/docs.rst b/documentation/test_python/content/docs.rst index 930e6494..bef6a8e8 100644 --- a/documentation/test_python/content/docs.rst +++ b/documentation/test_python/content/docs.rst @@ -1,27 +1,122 @@ +.. role:: label-success + :class: m-label m-success +.. role:: label-info + :class: m-label m-info + .. py:module:: content :summary: This overwrites the docstring for ``content``. This is detailed module docs. I kinda *hate* how it needs to be indented, tho. +.. py:module:: content.docstring_summary + + And adds detailed docs. + .. py:class:: content.Class :summary: This overwrites the docstring for ``content.Class``. This is detailed class docs. Here I *also* hate how it needs to be indented. -.. py:class:: content.AnotherClass +.. py:function:: content.Class.class_method + + The :label-success:`classmethod` should be shown here. + +.. py:function:: content.Class.static_method + + The :label-info:`staticmethod` should be shown here. + +.. py:function:: content.Class.__init__ + + A dunder method shown in the detailed view. + +.. py:function:: content.Class.method + :summary: This overwrites the docstring for ``content.Class.method``, but + doesn't add any detailed block. + +.. py:function:: content.Class.method_with_details + + This one has a detailed block without any summary. + +.. py:property:: content.Class.a_property + :summary: This overwrites the docstring for ``content.Class.a_property``, + but doesn't add any detailed block. + +.. py:property:: content.Class.a_property_with_details + :summary: This overwrites the docstring for ``content.Class.a_property_with_details``. + + Detailed property docs. + +.. py:property:: content.Class.annotated_property + + Annotated property, using summary from the docstring. + +.. py:data:: content.Class.DATA_WITH_DETAILS + + Detailed docs for ``data`` in a class to check rendering. + +.. py:class:: content.ClassWithSummary This class has external details but summary from the docstring. +.. py:enum:: content.Enum + :summary: This overwrites the docstring for ``content.Enum``, but + doesn't add any detailed block. + +.. py:enum:: content.EnumWithSummary + + And this is detailed docs added to the docstring summary. + +.. py:function:: content.foo + :summary: This overwrites the docstring for ``content.foo``, but + doesn't add any detailed block. + +.. py:function:: content.foo_with_details + :summary: This overwrites the docstring for ``content.foo_with_details()``. + + .. container:: m-note m-info + + Detailed docs for this function + +.. py:function:: content.function_with_summary + + This function has external details but summary from the docstring. + +.. py:function:: content.annotations + + Type annotations in detailed docs. + .. py:data:: content.CONSTANT :summary: This is finally a docstring for ``content.CONSTANT`` +.. py:data:: content.DATA_WITH_DETAILS + :summary: This is finally a docstring for ``content.CONSTANT`` + + Detailed docs for the data. **YAY.** + +.. py:data:: content.DATA_WITH_DETAILS_BUT_NO_SUMMARY_NEITHER_TYPE + + Why it has to be yelling?! + +.. py:function: content.foo + + Details for this function + .. py:module:: thismoduledoesnotexist :summary: This docs get unused and produce a warning .. py:class:: content.ThisDoesNotExist :summary: This docs get unused and produce a warning +.. py:enum:: content.ThisEnumDoesNotExist + :summary: This docs get unused and produce a warning + +.. py:function:: content.this_function_does_not_exist + :summary: This docs get unused and produce a warning + +.. py:property:: content.Class.this_property_does_not_exist + :summary: This docs get unused and produce a warning + .. py:data:: content.THIS_DOES_NOT_EXIST :summary: This docs get unused and produce a warning diff --git a/documentation/test_python/test_content.py b/documentation/test_python/test_content.py index 8cafecad..5df26cf6 100644 --- a/documentation/test_python/test_content.py +++ b/documentation/test_python/test_content.py @@ -34,5 +34,6 @@ class Content(BaseInspectTestCase): }) self.assertEqual(*self.actual_expected_contents('classes.html')) self.assertEqual(*self.actual_expected_contents('content.html')) + self.assertEqual(*self.actual_expected_contents('content.docstring_summary.html')) self.assertEqual(*self.actual_expected_contents('content.Class.html')) - self.assertEqual(*self.actual_expected_contents('content.AnotherClass.html')) + self.assertEqual(*self.actual_expected_contents('content.ClassWithSummary.html')) diff --git a/plugins/m/sphinx.py b/plugins/m/sphinx.py index 99aece3d..cc11538e 100644 --- a/plugins/m/sphinx.py +++ b/plugins/m/sphinx.py @@ -27,6 +27,9 @@ from docutils.parsers.rst import directives module_doc_output = None class_doc_output = None +enum_doc_output = None +function_doc_output = None +property_doc_output = None data_doc_output = None class PyModule(rst.Directive): @@ -55,6 +58,45 @@ class PyClass(rst.Directive): } return [] +class PyEnum(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + enum_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + +class PyFunction(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + function_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + +class PyProperty(rst.Directive): + final_argument_whitespace = True + has_content = True + required_arguments = 1 + option_spec = {'summary': directives.unchanged} + + def run(self): + property_doc_output[self.arguments[0]] = { + 'summary': self.options.get('summary', ''), + 'content': '\n'.join(self.content) + } + return [] + class PyData(rst.Directive): final_argument_whitespace = True has_content = True @@ -68,14 +110,20 @@ class PyData(rst.Directive): } return [] -def register_mcss(module_doc_contents, class_doc_contents, data_doc_contents, **kwargs): - global module_doc_output, class_doc_output, data_doc_output +def register_mcss(module_doc_contents, class_doc_contents, enum_doc_contents, function_doc_contents, property_doc_contents, data_doc_contents, **kwargs): + global module_doc_output, class_doc_output, enum_doc_output, function_doc_output, property_doc_output, data_doc_output module_doc_output = module_doc_contents class_doc_output = class_doc_contents + enum_doc_output = enum_doc_contents + function_doc_output = function_doc_contents + property_doc_output = property_doc_contents data_doc_output = data_doc_contents rst.directives.register_directive('py:module', PyModule) rst.directives.register_directive('py:class', PyClass) + rst.directives.register_directive('py:enum', PyEnum) + rst.directives.register_directive('py:function', PyFunction) + rst.directives.register_directive('py:property', PyProperty) rst.directives.register_directive('py:data', PyData) def register(): # for Pelican -- 2.30.2