From 3e85d91b9215aee5f0c43ba8248de0ae42524f56 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 20 Apr 2019 20:29:53 +0200 Subject: [PATCH] documentation/python: module and class index. --- doc/documentation/python.rst | 55 +++++++++++++ documentation/python.py | 81 ++++++++++++++++--- .../templates/python/base-index.html | 20 +++++ documentation/templates/python/classes.html | 21 +++++ documentation/templates/python/modules.html | 21 +++++ documentation/templates/python/pages.html | 21 +++++ .../test_python/inspect_string/classes.html | 72 +++++++++++++++++ .../inspect_string/inspect_string.Foo.html | 12 +++ .../inspect_string.Specials.html | 12 +++ .../inspect_string.another_module.html | 12 +++ .../inspect_string/inspect_string.html | 12 +++ .../test_python/inspect_string/modules.html | 65 +++++++++++++++ documentation/test_python/test_inspect.py | 15 +++- 13 files changed, 408 insertions(+), 11 deletions(-) create mode 100644 documentation/templates/python/base-index.html create mode 100644 documentation/templates/python/classes.html create mode 100644 documentation/templates/python/modules.html create mode 100644 documentation/templates/python/pages.html create mode 100644 documentation/test_python/inspect_string/classes.html create mode 100644 documentation/test_python/inspect_string/modules.html diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 90963796..20b72a0f 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -187,6 +187,13 @@ Variable Description put into the footer. If not set, a default generic text is used. If empty, no footer is rendered at all. +:py:`CLASS_INDEX_EXPAND_LEVELS` How many levels of the class index tree to + expand. :py:`0` means only the top-level + symbols are shown. If not set, :py:`1` is + used. +:py:`CLASS_INDEX_EXPAND_INNER` Whether to expand inner classes in the + class index. If not set, :py:`False` is + used. :py:`SEARCH_DISABLED: bool` Disable search functionality. If this option is set, no search data is compiled and the rendered HTML does not contain @@ -815,6 +822,54 @@ Property Description :py:`False`. [2]_ =================================== =========================================== +`Index page templates`_ +----------------------- + +The following index pages are provided, showing a expandable tree of the +contents: + +.. class:: m-table m-fullwidth + +======================= ======================================================= +Filename Use +======================= ======================================================= +``classes.html`` Class listing +``modules.html`` Module listing +``pages.html`` Page listing +======================= ======================================================= + +Each template is passed all configuration values from the `Configuration`_ +table as-is, together with a :py:`FILENAME`, as above. The navigation tree is +provided in an :py:`index` object, which has the following properties: + +.. class:: m-table m-fullwidth + +=========================== =================================================== +Property Description +=========================== =================================================== +:py:`index.classes` List of all modules + classes +:py:`index.pages` List of all pages +=========================== =================================================== + +The form of each list entry is the same: + +.. class:: m-table m-fullwidth + +=============================== =============================================== +Property Description +=============================== =============================================== +:py:`i.kind` Entry kind (one of :py:`'module'`, + :py:`'class'` or :py:`'page'`) +:py:`i.name` Name +:py:`i.url` URL of the file with detailed documentation +:py:`i.brief` Brief documentation +:py:`i.has_nestable_children` If the list has nestable children (i.e., dirs + or namespaces) +:py:`i.children` Recursive list of child entries +=============================== =============================================== + +Module/class list is ordered in a way that all modules are before all classes. + ------------------------------- .. [1] :py:`i.type` is extracted out of function annotation. If the types diff --git a/documentation/python.py b/documentation/python.py index 59012704..d57addf3 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -69,6 +69,8 @@ default_config = { 'LINKS_NAVBAR2': [], 'PAGE_HEADER': None, 'FINE_PRINT': '[default]', + 'CLASS_INDEX_EXPAND_LEVELS': 1, + 'CLASS_INDEX_EXPAND_INNER': False, 'SEARCH_DISABLED': False, 'SEARCH_DOWNLOAD_BINARY': False, 'SEARCH_HELP': """.. raw:: html @@ -88,6 +90,21 @@ default_config = { 'SEARCH_EXTERNAL_URL': None, } +class IndexEntry: + def __init__(self): + self.kind: str + self.name: str + self.url: str + self.brief: str + self.has_nestaable_children: bool = False + self.children: List[IndexEntry] = [] + +class State: + def __init__(self, config): + self.config = config + self.class_index: List[IndexEntry] = [] + self.page_index: List[IndexEntry] = [] + def is_internal_function_name(name: str) -> bool: """If the function name is internal. @@ -298,7 +315,7 @@ def extract_data_doc(parent, path: List[str], data): return out -def render_module(config, path, module, env): +def render_module(state: State, path, module, env): logging.debug("generating %s.html", '.'.join(path)) url_base = '' @@ -319,6 +336,13 @@ def render_module(config, path, module, env): page.data = [] page.has_enum_details = False + # Index entry for this module, returned together with children at the end + index_entry = IndexEntry() + index_entry.kind = 'module' + index_entry.name = breadcrumb[-1][0] + index_entry.url = page.url + index_entry.brief = page.brief + # 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 # underscored things. @@ -334,10 +358,10 @@ def render_module(config, path, module, env): # submodules and subclasses recursively. if inspect.ismodule(object): page.modules += [extract_module_doc(subpath, object)] - render_module(config, subpath, object, env) + index_entry.children += [render_module(state, subpath, object, env)] elif inspect.isclass(object) and not issubclass(object, enum.Enum): page.classes += [extract_class_doc(subpath, object)] - render_class(config, subpath, object, env) + index_entry.children += [render_class(state, subpath, object, env)] elif inspect.isclass(object) and issubclass(object, enum.Enum): enum_ = extract_enum_doc(subpath, object) page.enums += [enum_] @@ -363,7 +387,7 @@ def render_module(config, path, module, env): subpath = path + [name] page.modules += [extract_module_doc(subpath, object)] - render_module(config, subpath, object, 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)): @@ -373,7 +397,7 @@ def render_module(config, path, module, env): if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) page.classes += [extract_class_doc(subpath, object)] - render_class(config, subpath, object, 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)): @@ -402,7 +426,8 @@ def render_module(config, path, module, env): page.data += [extract_data_doc(module, path + [name], object)] - render(config, 'module.html', page, env) + render(state.config, 'module.html', page, env) + return index_entry # Builtin dunder functions have hardcoded docstrings. This is totally useless # to have in the docs, so filter them out. Uh... kinda ugly. @@ -456,7 +481,7 @@ _filtered_builtin_properties = set([ ('__weakref__', "list of weak references to the object (if defined)") ]) -def render_class(config, path, class_, env): +def render_class(state: State, path, class_, env): logging.debug("generating %s.html", '.'.join(path)) url_base = '' @@ -480,6 +505,13 @@ def render_class(config, path, class_, env): page.data = [] page.has_enum_details = False + # Index entry for this module, returned together with children at the end + index_entry = IndexEntry() + index_entry.kind = 'class' + index_entry.name = breadcrumb[-1][0] + index_entry.url = page.url + 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)): if name in ['__base__', '__class__']: continue # TODO @@ -489,7 +521,7 @@ def render_class(config, path, class_, env): if not object.__doc__: logging.warning("%s is undocumented", '.'.join(subpath)) page.classes += [extract_class_doc(subpath, object)] - render_class(config, subpath, object, 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)): @@ -545,7 +577,8 @@ def render_class(config, path, class_, env): subpath = path + [name] page.data += [extract_data_doc(class_, subpath, object)] - render(config, 'class.html', page, env) + render(state.config, 'class.html', page, env) + return index_entry def run(basedir, config, templates): # Prepare Jinja environment @@ -573,6 +606,8 @@ def run(basedir, config, templates): if config['FAVICON']: config['FAVICON'] = (config['FAVICON'], mimetypes.guess_type(config['FAVICON'])[0]) + state = State(config) + for module in config['INPUT_MODULES']: if isinstance(module, str): module_name = module @@ -580,7 +615,33 @@ def run(basedir, config, templates): else: module_name = module.__name__ - render_module(config, [module_name], module, env) + state.class_index += [render_module(state, [module_name], module, env)] + + # Recurse into the tree and mark every node that has nested modules with + # has_nestaable_children. + def mark_nested_modules(list: List[IndexEntry]): + has_nestaable_children = False + for i in list: + if i.kind != 'module': continue + has_nestaable_children = True + mark_nested_modules(i.children) + return has_nestaable_children + mark_nested_modules(state.class_index) + + # Create module and class index + index = Empty() + index.classes = state.class_index + index.pages = state.page_index + for file in ['modules.html', 'classes.html', 'pages.html']: + template = env.get_template(file) + rendered = template.render(index=index, FILENAME=file, **config) + with open(os.path.join(config['OUTPUT'], file), 'wb') as f: + f.write(rendered.encode('utf-8')) + # Add back a trailing newline so we don't need to bother with + # patching test files to include a trailing newline to make Git + # happy + # TODO could keep_trailing_newline fix this better? + f.write(b'\n') # Create index.html # TODO: use actual reST source and have this just as a fallback diff --git a/documentation/templates/python/base-index.html b/documentation/templates/python/base-index.html new file mode 100644 index 00000000..ce4c343b --- /dev/null +++ b/documentation/templates/python/base-index.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block main %} + +{% endblock %} diff --git a/documentation/templates/python/classes.html b/documentation/templates/python/classes.html new file mode 100644 index 00000000..af1d68bd --- /dev/null +++ b/documentation/templates/python/classes.html @@ -0,0 +1,21 @@ +{% set navbar_current = 'classes' %} +{% extends 'base-index.html' %} + +{% block main %} +

Classes

+ +{{ super() -}} +{% endblock %} diff --git a/documentation/templates/python/modules.html b/documentation/templates/python/modules.html new file mode 100644 index 00000000..2a833471 --- /dev/null +++ b/documentation/templates/python/modules.html @@ -0,0 +1,21 @@ +{% set navbar_current = 'modules' %} +{% extends 'base-index.html' %} + +{% block main %} +

Modules

+ +{{ super() -}} +{% endblock %} diff --git a/documentation/templates/python/pages.html b/documentation/templates/python/pages.html new file mode 100644 index 00000000..e109dfd4 --- /dev/null +++ b/documentation/templates/python/pages.html @@ -0,0 +1,21 @@ +{% set navbar_current = 'pages' %} +{% extends 'base-index.html' %} + +{% block main %} +

Pages

+ +{{ super() -}} +{% endblock %} diff --git a/documentation/test_python/inspect_string/classes.html b/documentation/test_python/inspect_string/classes.html new file mode 100644 index 00000000..e7366474 --- /dev/null +++ b/documentation/test_python/inspect_string/classes.html @@ -0,0 +1,72 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Classes

+ + +
+
+
+
+ + diff --git a/documentation/test_python/inspect_string/inspect_string.Foo.html b/documentation/test_python/inspect_string/inspect_string.Foo.html index 234cc9a4..206fba51 100644 --- a/documentation/test_python/inspect_string/inspect_string.Foo.html +++ b/documentation/test_python/inspect_string/inspect_string.Foo.html @@ -12,6 +12,18 @@
My Python Project +
+ + +
+
diff --git a/documentation/test_python/inspect_string/inspect_string.Specials.html b/documentation/test_python/inspect_string/inspect_string.Specials.html index 23e86741..65e68fe4 100644 --- a/documentation/test_python/inspect_string/inspect_string.Specials.html +++ b/documentation/test_python/inspect_string/inspect_string.Specials.html @@ -12,6 +12,18 @@
My Python Project +
+ + +
+
diff --git a/documentation/test_python/inspect_string/inspect_string.another_module.html b/documentation/test_python/inspect_string/inspect_string.another_module.html index 78053490..30ba8504 100644 --- a/documentation/test_python/inspect_string/inspect_string.another_module.html +++ b/documentation/test_python/inspect_string/inspect_string.another_module.html @@ -12,6 +12,18 @@
My Python Project +
+ + +
+
diff --git a/documentation/test_python/inspect_string/inspect_string.html b/documentation/test_python/inspect_string/inspect_string.html index 1687bf51..72155565 100644 --- a/documentation/test_python/inspect_string/inspect_string.html +++ b/documentation/test_python/inspect_string/inspect_string.html @@ -12,6 +12,18 @@
My Python Project +
+ + +
+
diff --git a/documentation/test_python/inspect_string/modules.html b/documentation/test_python/inspect_string/modules.html new file mode 100644 index 00000000..c7b32cd5 --- /dev/null +++ b/documentation/test_python/inspect_string/modules.html @@ -0,0 +1,65 @@ + + + + + My Python Project + + + + + +
+
+
+
+
+

Modules

+ + +
+
+
+
+ + diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index e483e76f..4457bc4a 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -36,12 +36,19 @@ class String(BaseTestCase): super().__init__(__file__, 'string', *args, **kwargs) def test(self): - self.run_python() + self.run_python({ + 'LINKS_NAVBAR1': [ + ('Modules', 'modules', []), + ('Classes', 'classes', [])], + }) self.assertEqual(*self.actual_expected_contents('inspect_string.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.another_module.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html')) + self.assertEqual(*self.actual_expected_contents('classes.html')) + self.assertEqual(*self.actual_expected_contents('modules.html')) + class Object(BaseTestCase): def __init__(self, *args, **kwargs): super().__init__(__file__, 'object', *args, **kwargs) @@ -52,6 +59,9 @@ class Object(BaseTestCase): sys.path.append(os.path.join(os.path.dirname(self.path), 'inspect_string')) import inspect_string self.run_python({ + 'LINKS_NAVBAR1': [ + ('Modules', 'modules', []), + ('Classes', 'classes', [])], 'INPUT_MODULES': [inspect_string] }) @@ -61,6 +71,9 @@ class Object(BaseTestCase): self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html', '../inspect_string/inspect_string.Foo.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html', '../inspect_string/inspect_string.Specials.html')) + self.assertEqual(*self.actual_expected_contents('classes.html', '../inspect_string/classes.html')) + self.assertEqual(*self.actual_expected_contents('modules.html', '../inspect_string/modules.html')) + class AllProperty(BaseTestCase): def __init__(self, *args, **kwargs): super().__init__(__file__, 'all_property', *args, **kwargs) -- 2.30.2