From 87ed31ef6afbb971095a30fe0ae6d8ff918b5a40 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Fri, 23 Aug 2019 00:36:17 +0200 Subject: [PATCH] documentation/python: integrate tighter with m.sphinx. The :ref: from m.sphinx can now link to internal types, while external types provided by m.sphinx are now also linked to from all signatures. Yay, I'm so happy about the design of this. --- doc/documentation/python.rst | 4 + doc/plugins/sphinx.rst | 4 + documentation/python.py | 15 +++- .../test_python/inspect_type_links/index.html | 45 +++++++++++ .../test_python/inspect_type_links/index.rst | 18 +++++ .../inspect_type_links.second.html | 2 +- documentation/test_python/test_inspect.py | 10 ++- plugins/m/sphinx.py | 80 ++++++++++++++++++- 8 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 documentation/test_python/inspect_type_links/index.html create mode 100644 documentation/test_python/inspect_type_links/index.rst diff --git a/doc/documentation/python.rst b/doc/documentation/python.rst index 3295d4e0..9c2b413e 100644 --- a/doc/documentation/python.rst +++ b/doc/documentation/python.rst @@ -725,6 +725,10 @@ Keyword argument Content :py:`path` Path. Equivalent to :py:`key.split('.')`. :py:`url` URL to the entry documentation, formatted with `custom URL formatters`_. + :py:`css_classes` List of CSS classes to add to the + :html:`` tag. Internal entries + usually have :py:`['m-doc']` while + exteral have :py:`['m-doc-external']`. =================== ======================================= =================== =========================================================== diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst index 571771fa..85be2631 100644 --- a/doc/plugins/sphinx.rst +++ b/doc/plugins/sphinx.rst @@ -140,6 +140,10 @@ using plain backticks: - Flat link: :ref-flat:`os.path.join()` - Link using a default role: `str.partition()` +When used with the Python doc theme, the :rst:`:ref` can be used also for +linking to internal types, while external types, classes and enums are also +linked to from all signatures. + .. note-success:: For linking to Doxygen documentation, a similar functionality is provided diff --git a/documentation/python.py b/documentation/python.py index dc71cca8..03e0ca1e 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -275,6 +275,7 @@ def crawl_enum(state: State, path: List[str], enum_, parent_url): enum_entry.object = enum_ enum_entry.path = path enum_entry.url = '{}#{}'.format(parent_url, state.config['ID_FORMATTER'](EntryType.ENUM, path[-1:])) + enum_entry.css_classes = ['m-doc'] enum_entry.values = [] if issubclass(enum_, enum.Enum): @@ -284,6 +285,7 @@ def crawl_enum(state: State, path: List[str], enum_, parent_url): entry.type = EntryType.ENUM_VALUE entry.path = subpath entry.url = '{}#{}'.format(parent_url, state.config['ID_FORMATTER'](EntryType.ENUM_VALUE, subpath[-2:])) + entry.css_classes = ['m-doc'] state.name_map['.'.join(subpath)] = entry elif state.config['PYBIND11_COMPATIBILITY']: @@ -295,6 +297,7 @@ def crawl_enum(state: State, path: List[str], enum_, parent_url): entry.type = EntryType.ENUM_VALUE entry.path = subpath entry.url = '{}#{}'.format(parent_url, state.config['ID_FORMATTER'](EntryType.ENUM_VALUE, subpath[-2:])) + entry.css_classes = ['m-doc'] state.name_map['.'.join(subpath)] = entry # Add itself to the name map @@ -313,6 +316,7 @@ def crawl_class(state: State, path: List[str], class_): class_entry.type = EntryType.CLASS class_entry.object = class_ class_entry.path = path + class_entry.css_classes = ['m-doc'] class_entry.url = state.config['URL_FORMATTER'](EntryType.CLASS, path)[1] class_entry.members = [] @@ -356,6 +360,7 @@ def crawl_class(state: State, path: List[str], class_): entry.object = object entry.path = subpath entry.url = '{}#{}'.format(class_entry.url, state.config['ID_FORMATTER'](type, subpath[-1:])) + entry.css_classes = ['m-doc'] state.name_map['.'.join(subpath)] = entry class_entry.members += [name] @@ -380,6 +385,7 @@ def crawl_module(state: State, path: List[str], module) -> List[Tuple[List[str], module_entry.type = EntryType.MODULE module_entry.object = module module_entry.path = path + module_entry.css_classes = ['m-doc'] module_entry.url = state.config['URL_FORMATTER'](EntryType.MODULE, path)[1] module_entry.members = [] @@ -451,6 +457,7 @@ def crawl_module(state: State, path: List[str], module) -> List[Tuple[List[str], entry.object = object entry.path = subpath entry.url = '{}#{}'.format(module_entry.url, state.config['ID_FORMATTER'](type, subpath[-1:])) + entry.css_classes = ['m-doc'] state.name_map['.'.join(subpath)] = entry module_entry.members += [name] @@ -523,6 +530,7 @@ def crawl_module(state: State, path: List[str], module) -> List[Tuple[List[str], entry.object = object entry.path = subpath entry.url = '{}#{}'.format(module_entry.url, state.config['ID_FORMATTER'](type, subpath[-1:])) + entry.css_classes = ['m-doc'] state.name_map['.'.join(subpath)] = entry module_entry.members += [name] @@ -593,7 +601,7 @@ def make_name_link(state: State, referrer_path: List[str], name) -> str: relative_name = make_relative_name(state, referrer_path, name) entry = state.name_map[name] - return '{}'.format(entry.url, relative_name) + return '{}'.format(entry.url, ' '.join(entry.css_classes), relative_name) _pybind_name_rx = re.compile('[a-zA-Z0-9_]*') _pybind_arg_name_rx = re.compile('[*a-zA-Z0-9_]+') @@ -1951,6 +1959,11 @@ def run(basedir, config, *, templates=default_templates, search_add_lookahead_ba # side effect of the render is entry.summary (and entry.name for pages) # being filled. for entry in state.name_map.values(): + # If there is no object, the entry is an external reference. Skip + # those. Can't do `not entry.object` because that gives ValueError + # for numpy ("use a.any() or a.all()") + if hasattr(entry, 'object') and entry.object is None: continue + if entry.type == EntryType.MODULE: render_module(state, entry.path, entry.object, env) elif entry.type == EntryType.CLASS: diff --git a/documentation/test_python/inspect_type_links/index.html b/documentation/test_python/inspect_type_links/index.html new file mode 100644 index 00000000..37ad88e7 --- /dev/null +++ b/documentation/test_python/inspect_type_links/index.html @@ -0,0 +1,45 @@ + + + + + Type links | My Python Project + + + + + +
+
+
+
+
+

+ Type links +

+

External links with correct class: typing.Tuple or str (tested +extensively in the m.sphinx tests).

+

Internal links with m-doc implicit:

+ +
+
+
+
+ + diff --git a/documentation/test_python/inspect_type_links/index.rst b/documentation/test_python/inspect_type_links/index.rst new file mode 100644 index 00000000..68f9029d --- /dev/null +++ b/documentation/test_python/inspect_type_links/index.rst @@ -0,0 +1,18 @@ +Type links +########## + +External links with correct class: :ref:`typing.Tuple` or :ref:`str` (tested +extensively in the ``m.sphinx`` tests). + +Internal links with ``m-doc`` implicit: + +- Module: :ref:`inspect_type_links` +- Class: :ref:`inspect_type_links.first.Foo` +- Function: :ref:`inspect_type_links.first.Foo.reference_inner()` + + - without ``()``: :ref:`inspect_type_links.first.Foo.reference_inner` + +- Property: :ref:`inspect_type_links.second.Foo.type_property` +- Enum: :ref:`inspect_type_links.second.Enum` +- Enum value: :ref:`inspect_type_links.second.Enum.SECOND` +- Data: :ref:`inspect_type_links.second.TYPE_DATA` diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.html index 44eda51a..350f9bde 100644 --- a/documentation/test_python/inspect_type_links/inspect_type_links.second.html +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.html @@ -52,7 +52,7 @@

Enums

- class Enum(enum.Enum): FIRST = 1 + class Enum(enum.Enum): FIRST = 1 SECOND = 2
An enum
diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index 2ee98c8c..501b9de3 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -157,7 +157,15 @@ class Recursive(BaseInspectTestCase): class TypeLinks(BaseInspectTestCase): def test(self): - self.run_python() + self.run_python({ + 'PLUGINS': ['m.sphinx'], + 'INPUT_PAGES': ['index.rst'], + 'M_SPHINX_INVENTORIES': [ + ('../../../doc/documentation/python.inv', 'https://docs.python.org/3/', [], ['m-doc-external'])] + }) + + self.assertEqual(*self.actual_expected_contents('index.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.first.Foo.Foo.html')) diff --git a/plugins/m/sphinx.py b/plugins/m/sphinx.py index 3f3555b6..74a70033 100644 --- a/plugins/m/sphinx.py +++ b/plugins/m/sphinx.py @@ -25,6 +25,7 @@ import logging import os import re +from types import SimpleNamespace as Empty from typing import Dict from urllib.parse import urljoin import zlib @@ -251,7 +252,12 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]): # and unknown domains such as c++ for now as I'm unsure about potential # name clashes. if not found: - for type in ['py:exception', 'py:attribute', 'py:method', 'py:data', 'py:module', 'py:function', 'py:class', 'py:classmethod', 'py:staticmethod', 'c:var', 'c:type', 'c:function', 'c:member', 'c:macro']: + for type in [ + 'py:exception', 'py:attribute', 'py:method', 'py:data', 'py:module', 'py:function', 'py:class', 'py:classmethod', 'py:staticmethod', + 'c:var', 'c:type', 'c:function', 'c:member', 'c:macro', + # TODO: those apparently don't exist: + 'py:enum', 'py:enumvalue' + ]: if type in intersphinx_inventory and prefixed in intersphinx_inventory[type]: found = type, intersphinx_inventory[type][prefixed] @@ -279,7 +285,75 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]): node = nodes.literal(rawtext, target, **_options) return [node], [] -def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_doc_contents, function_doc_contents, property_doc_contents, data_doc_contents, **kwargs): +def merge_inventories(name_map, **kwargs): + global intersphinx_inventory + + # Create inventory entries from the name_map + internal_inventory = {} + for path_str, entry in name_map.items(): + EntryType = type(entry.type) # so we don't need to import the enum + if entry.type == EntryType.MODULE: + type_string = 'py:module' + elif entry.type == EntryType.CLASS: + type_string = 'py:class' + elif entry.type == EntryType.FUNCTION: + # TODO: properly distinguish between 'py:function', + # 'py:classmethod', 'py:staticmethod', 'py:method' + type_string = 'py:function' + elif entry.type == EntryType.OVERLOADED_FUNCTION: + # TODO: what about the other overloads? + type_string = 'py:function' + elif entry.type == EntryType.PROPERTY: + # datetime.date.year is decorated with @property and listed as a + # py:attribute, so that's probably it + type_string = 'py:attribute' + elif entry.type == EntryType.ENUM: + type_string = 'py:enum' # this desn't exist in Sphinx + elif entry.type == EntryType.ENUM_VALUE: + type_string = 'py:enumvalue' # these don't exist in Sphinx + elif entry.type == EntryType.DATA: + type_string = 'py:data' + elif entry.type == EntryType.PAGE: + type_string = 'std:doc' + else: + # TODO: what to do with these? allow linking to them? disambiguate + # or prefix the names somehow? + assert entry.type == EntryType.SPECIAL, entry.type + continue + + # Mark those with m-doc (as internal) + internal_inventory.setdefault(type_string, {})[path_str] = (entry.url, '-', ['m-doc']) + + # Add class / enum / enum value inventory entries to the name map for type + # cross-linking + for type_, type_string in [ + # TODO: this will blow up if the above loop is never entered (which is + # unlikely) as EntryType is defined there + (EntryType.CLASS, 'py:class'), + (EntryType.DATA, 'py:data'), # typing.Tuple or typing.Any is data + # Those are custom to m.css, not in Sphinx + (EntryType.ENUM, 'py:enum'), + (EntryType.ENUM_VALUE, 'py:enumvalue'), + ]: + if type_string in intersphinx_inventory: + for path, value in intersphinx_inventory[type_string].items(): + url, _, css_classes = value + entry = Empty() + entry.type = type_ + entry.object = None + entry.path = path.split('.') + entry.css_classes = css_classes + entry.url = url + name_map[path] = entry + + # Add stuff from the name map to our inventory + for type_, data_internal in internal_inventory.items(): + data = intersphinx_inventory.setdefault(type_, {}) + for path, value in data_internal.items(): + assert path not in data + data[path] = value + +def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_doc_contents, function_doc_contents, property_doc_contents, data_doc_contents, hooks_post_crawl, **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 @@ -300,6 +374,8 @@ def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_d rst.roles.register_local_role('ref', ref) + hooks_post_crawl += [merge_inventories] + def _pelican_configure(pelicanobj): # For backwards compatibility, the input directory is pelican's CWD parse_intersphinx_inventories(input=os.getcwd(), -- 2.30.2