chiark / gitweb /
documentation/python: integrate tighter with m.sphinx.
authorVladimír Vondruš <mosra@centrum.cz>
Thu, 22 Aug 2019 22:36:17 +0000 (00:36 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Sun, 25 Aug 2019 10:47:45 +0000 (12:47 +0200)
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
doc/plugins/sphinx.rst
documentation/python.py
documentation/test_python/inspect_type_links/index.html [new file with mode: 0644]
documentation/test_python/inspect_type_links/index.rst [new file with mode: 0644]
documentation/test_python/inspect_type_links/inspect_type_links.second.html
documentation/test_python/test_inspect.py
plugins/m/sphinx.py

index 3295d4e03e2208f93e3587cec5422c7a7586ad0a..9c2b413ea3d414033b0c8d0ecdedce0a80c56d21 100644 (file)
@@ -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:`<a>` tag. Internal entries
+                                        usually have :py:`['m-doc']` while
+                                        exteral have :py:`['m-doc-external']`.
                     =================== =======================================
 =================== ===========================================================
 
index 571771fac87d0090d833a48da8d237f6b3023628..85be2631d55be21c437f9890707920c10bca2e56 100644 (file)
@@ -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
index dc71cca8fe1fb6b52149f28317401da88eecfe93..03e0ca1e884b19a77b592b89ee5d126828930f4a 100755 (executable)
@@ -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 '<a href="{}" class="m-doc">{}</a>'.format(entry.url, relative_name)
+    return '<a href="{}" class="{}">{}</a>'.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 (file)
index 0000000..37ad88e
--- /dev/null
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Type links | My Python Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+documentation.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Python Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          Type links
+        </h1>
+<p>External links with correct class: <a class="m-doc-external" href="https://docs.python.org/3/library/typing.html#typing.Tuple">typing.Tuple</a> or <a class="m-doc-external" href="https://docs.python.org/3/library/stdtypes.html#str">str</a> (tested
+extensively in the <code>m.sphinx</code> tests).</p>
+<p>Internal links with <code>m-doc</code> implicit:</p>
+<ul>
+<li>Module: <a class="m-doc" href="inspect_type_links.html">inspect_type_links</a></li>
+<li>Class: <a class="m-doc" href="inspect_type_links.first.Foo.html">inspect_type_links.first.Foo</a></li>
+<li>Function: <a class="m-doc" href="inspect_type_links.first.Foo.html#reference_inner">inspect_type_links.first.Foo.reference_inner()</a><ul>
+<li>without <code>()</code>: <a class="m-doc" href="inspect_type_links.first.Foo.html#reference_inner">inspect_type_links.first.Foo.reference_inner()</a></li>
+</ul>
+</li>
+<li>Property: <a class="m-doc" href="inspect_type_links.second.Foo.html#type_property">inspect_type_links.second.Foo.type_property</a></li>
+<li>Enum: <a class="m-doc" href="inspect_type_links.second.html#Enum">inspect_type_links.second.Enum</a></li>
+<li>Enum value: <a class="m-doc" href="inspect_type_links.second.html#Enum-SECOND">inspect_type_links.second.Enum.SECOND</a></li>
+<li>Data: <a class="m-doc" href="inspect_type_links.second.html#TYPE_DATA">inspect_type_links.second.TYPE_DATA</a></li>
+</ul>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
diff --git a/documentation/test_python/inspect_type_links/index.rst b/documentation/test_python/inspect_type_links/index.rst
new file mode 100644 (file)
index 0000000..68f9029
--- /dev/null
@@ -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`
index 44eda51ae2d7eb04967cf566221ed0d458eb804f..350f9bde820550c191a75d3323cc979d9b4e326a 100644 (file)
@@ -52,7 +52,7 @@
           <h2><a href="#enums">Enums</a></h2>
           <dl class="m-doc">
             <dt id="Enum">
-              <span class="m-doc-wrap-bumper">class <a href="#Enum" class="m-doc-self">Enum</a>(enum.Enum): </span><span class="m-doc-wrap"><a href="#Enum-FIRST" class="m-doc-self" id="Enum-FIRST">FIRST</a> = 1
+              <span class="m-doc-wrap-bumper">class <a href="#Enum" class="m-doc-self">Enum</a>(<a href="https://docs.python.org/3/library/enum.html#enum.Enum" class="m-doc-external">enum.Enum</a>): </span><span class="m-doc-wrap"><a href="#Enum-FIRST" class="m-doc-self" id="Enum-FIRST">FIRST</a> = 1
               <a href="#Enum-SECOND" class="m-doc-self" id="Enum-SECOND">SECOND</a> = 2</span>
             </dt>
             <dd>An enum</dd>
index 2ee98c8c3e802412b002c10c9a374d4dce7e24ea..501b9de38c5f4cabddb7458dc4dcc78fa33eada4 100644 (file)
@@ -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'))
index 3f3555b60884a6e145055814f0a790273b564be6..74a700332846420e222b30666659e59b92b5b5cf 100644 (file)
@@ -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(),