chiark / gitweb /
m.sphinx: implement a page-wide :ref-prefix: option.
authorVladimír Vondruš <mosra@centrum.cz>
Fri, 6 Sep 2019 11:33:06 +0000 (13:33 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Tue, 10 Sep 2019 20:10:14 +0000 (22:10 +0200)
NOW I can start writing real docs without everything being a giant PITA.

doc/plugins/sphinx.rst
documentation/test_python/content/page.html [new file with mode: 0644]
documentation/test_python/content/page.rst [new file with mode: 0644]
documentation/test_python/test_content.py
plugins/m/sphinx.py
plugins/m/test/sphinx/page.html
plugins/m/test/sphinx/page.rst

index e5b839db4840865c2133c8dcf7dea7ec2da2e7e1..c7cf28f02b5f69547fa2995cc305531fa2bd90a4 100644 (file)
@@ -26,6 +26,9 @@ Sphinx
 ######
 
 :breadcrumb: {filename}/plugins.rst Plugins
+:ref-prefix:
+    typing
+    unittest.mock
 :footer:
     .. note-dim::
         :class: m-text-center
@@ -86,8 +89,8 @@ Sphinx provides a so-called Intersphinx files to make names from one
 documentation available for linking from elsewhere. The plugin supports the
 (current) version 2 of those inventory files, version 1 is not supported. You
 need to provide a list of tuples containing tag file path, URL prefix, an
-optional list of implicitly prepended paths and an optional list of CSS classes
-for each link in :py:`M_SPHINX_INVENTORIES`. Every Sphinx-generated
+optional list of implicitly prepended name prefixes and an optional list of CSS
+classes for each link in :py:`M_SPHINX_INVENTORIES`. Every Sphinx-generated
 documentation contains an ``objects.inv`` file in its root directory (and the
 root directory is the URL prefix as well), for example for Python 3 it's
 located at https://docs.python.org/3/objects.inv. Download the files and
@@ -119,14 +122,25 @@ concrete target name and a colon --- for example,
 :rst:`:ref:`std:doc:using/cmdline`` will link to the ``using/cmdline`` page of
 standard documentation.
 
+Apart from global implicitly prepended prefixes defined in
+:rst:`M_SPHINX_INVENTORIES`, these can be set also on a per-page basis by
+listing them in :rst:`:ref-prefix:` in page metadata. This is useful especially
+when writing pages in the `Python doc theme`_ to avoid writing the fully
+qualified name every time you need to linking to some documented name.
+
 The :rst:`:ref:` a good candidate for a `default role <http://docutils.sourceforge.net/docs/ref/rst/directives.html#default-role>`_
 --- setting it using :rst:`.. default-role::` will then make it accessible
-using plain backticks:
+using plain backticks.
 
 .. code-figure::
 
     .. code:: rst
 
+        .. among page metadata
+        :ref-prefix:
+            typing
+            unittest.mock
+
         .. default-role:: ref
 
         .. role:: ref-flat(ref)
@@ -137,6 +151,7 @@ using plain backticks:
         -   Page link: :ref:`std:doc:using/cmdline`
         -   :ref:`Custom link title <PyErr_SetString>`
         -   Flat link: :ref-flat:`os.path.join()`
+        -   Omitting :rst:`:ref-prefix:`: :ref:`Tuple`, :ref:`NonCallableMagicMock`
         -   Link using a default role: `str.partition()`
 
     .. default-role:: ref
@@ -149,6 +164,7 @@ using plain backticks:
     -   Page link: :ref:`std:doc:using/cmdline`
     -   :ref:`Custom link title <PyErr_SetString>`
     -   Flat link: :ref-flat:`os.path.join()`
+    -   Omitting :rst:`:ref-prefix:`: :ref:`Tuple`, :ref:`NonCallableMagicMock`
     -   Link using a default role: `str.partition()`
 
 When used with the Python doc theme, the :rst:`:ref` can be used also for
diff --git a/documentation/test_python/content/page.html b/documentation/test_python/content/page.html
new file mode 100644 (file)
index 0000000..b7ea284
--- /dev/null
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>A page | 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>
+          A page
+        </h1>
+        <p>Link to a <a class="m-doc" href="content.Class.html">Class</a> and <a class="m-doc" href="content.html#EnumWithSummary-VALUE">VALUE</a> from a summary.</p>
+<p>Link to an <a class="m-doc" href="content.html#Enum">Enum</a> and <a class="m-doc" href="content.html#EnumWithSummary-THIRD">THIRD</a> from the content.</p>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
diff --git a/documentation/test_python/content/page.rst b/documentation/test_python/content/page.rst
new file mode 100644 (file)
index 0000000..11ef70a
--- /dev/null
@@ -0,0 +1,9 @@
+A page
+######
+
+:ref-prefix:
+    content
+    content.EnumWithSummary
+:summary: Link to a :ref:`Class` and :ref:`VALUE` from a summary.
+
+Link to an :ref:`Enum` and :ref:`THIRD` from the content.
index be6d9fbcf98fbe9336fc2e4d69899e8352233f2b..e743731bafe41eccc202546a4c5d57d9dd09d903 100644 (file)
@@ -30,7 +30,8 @@ class Content(BaseInspectTestCase):
     def test(self):
         self.run_python({
             'PLUGINS': ['m.sphinx'],
-            'INPUT_DOCS': ['docs.rst']
+            'INPUT_DOCS': ['docs.rst'],
+            'INPUT_PAGES': ['page.rst']
         })
         self.assertEqual(*self.actual_expected_contents('classes.html'))
         self.assertEqual(*self.actual_expected_contents('content.html'))
@@ -40,6 +41,8 @@ class Content(BaseInspectTestCase):
         self.assertEqual(*self.actual_expected_contents('content.ClassWithSlots.html'))
         self.assertEqual(*self.actual_expected_contents('content.ClassWithSummary.html'))
 
+        self.assertEqual(*self.actual_expected_contents('page.html'))
+
 class ParseDocstrings(BaseInspectTestCase):
     def test(self):
         self.run_python({
index 1b8aba7526b2105d1a1e92f1469674aed59e8c70..fcddc7ad6f51b7f960e41728ccaa5496745c6b66 100755 (executable)
@@ -46,6 +46,7 @@ import m.htmlsanity
 
 # All those initialized in register() or register_mcss()
 current_referer_path = None
+page_ref_prefixes = None
 current_param_names = None
 module_doc_output = None
 class_doc_output = None
@@ -291,12 +292,35 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]):
     # Avoid assert on adding to undefined member later
     if 'classes' not in _options: _options['classes'] = []
 
+    # If we're in a page and there's no page module scope yet, look if there
+    # are :py:module: page metadata we could use for a prefix
+    global current_referer_path, page_ref_prefixes
+    if current_referer_path[-1][0].name == 'PAGE' and page_ref_prefixes is None:
+        # Since we're in the middle of parse, the nodes.docinfo is not present
+        # yet (it's produced by the frontmatter.DocInfo transform that's run
+        # after the parsing ends), so we look in field lists instead.
+        # TODO: DocInfo picks only the first ever field list happening right
+        #   after a title and we should do the same to avoid picking up options
+        #   later in the page. There the transform depends on DocTitle being
+        #   ran already, so it would need to be more complex here. See
+        #   docutils.transforms.frontmatter.DocInfo.apply() for details.
+        for docinfo in inliner.document.traverse(docutils.nodes.field_list):
+            for element in docinfo.children:
+                if element.tagname != 'field': continue
+                name_elem, body_elem = element.children
+                if name_elem.astext() == 'ref-prefix':
+                    page_ref_prefixes = [line.strip() + '.' for line in body_elem.astext().splitlines() if line.strip()]
+
+        # If we didn't find any, set it to an empty list (not None), so this is
+        # not traversed again next time
+        if not page_ref_prefixes: page_ref_prefixes = []
+
     # Add prefixes of the referer path to the global prefix list, iterate
     # through all of them, with names "closest" to the referer having a
     # priority and try to find the name
-    global current_referer_path, intersphinx_inventory, intersphinx_name_prefixes
-    referer_path = current_referer_path[-1] if current_referer_path else []
-    prefixes = ['.'.join(referer_path[:len(referer_path) - i]) + '.' for i, _ in enumerate(referer_path)] + intersphinx_name_prefixes
+    global intersphinx_inventory, intersphinx_name_prefixes
+    referer_path = current_referer_path[-1][1] if current_referer_path else []
+    prefixes = ['.'.join(referer_path[:len(referer_path) - i]) + '.' for i, _ in enumerate(referer_path)] + (page_ref_prefixes if page_ref_prefixes else []) + intersphinx_name_prefixes
     for prefix in prefixes:
         found = None
 
@@ -363,14 +387,19 @@ def ref(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]):
         node = nodes.literal(rawtext, target, **_options)
     return [node], []
 
-def scope_enter(path, param_names=None, **kwargs):
+def scope_enter(type, path, param_names=None, **kwargs):
     global current_referer_path, current_param_names
-    current_referer_path += [path]
+    current_referer_path += [(type, path)]
     current_param_names = param_names
 
-def scope_exit(path, **kwargs):
+    # If we're in a page, reset page_ref_prefixes so next time :ref: needs it,
+    # it will look it up instead of using a stale version
+    global page_ref_prefixes
+    if type.name == 'PAGE': page_ref_prefixes = None
+
+def scope_exit(type, path, **kwargs):
     global current_referer_path, current_param_names
-    assert current_referer_path[-1] == path, "%s %s" % (current_referer_path, path)
+    assert current_referer_path[-1] == (type, path), "%s %s" % (current_referer_path, path)
     current_referer_path = current_referer_path[:-1]
     current_param_names = None
 
@@ -383,7 +412,7 @@ def p(name, rawtext, text, lineno, inliner: Inliner, options={}, content=[]):
     if not current_param_names:
         logging.warning("can't reference parameter %s outside of a function scope", text)
     elif text not in current_param_names:
-        logging.warning("parameter %s not found in %s(%s) function signature", text, '.'.join(current_referer_path[-1]), ', '.join(current_param_names))
+        logging.warning("parameter %s not found in %s(%s) function signature", text, '.'.join(current_referer_path[-1][1]), ', '.join(current_param_names))
 
     node = nodes.literal(rawtext, text, **options)
     return [node], []
@@ -578,7 +607,17 @@ def register_mcss(mcss_settings, module_doc_contents, class_doc_contents, enum_d
     # Just a sanity check
     hooks_post_run += [check_scope_stack_empty]
 
+def _pelican_new_page(generator):
+    # Set a dummy page referrer path so :ref-prefixes: works in Pelican as well
+    # TODO: any chance this could be made non-crappy?
+    global current_referer_path
+    assert not current_referer_path or len(current_referer_path) == 1 and current_referer_path[0][0].name == 'PAGE'
+    type = Empty() # We don't have the EntryType enum, so fake it
+    type.name = 'PAGE'
+    current_referer_path = [(type, '')]
+
 def _pelican_configure(pelicanobj):
+
     # For backwards compatibility, the input directory is pelican's CWD
     parse_intersphinx_inventories(input=os.getcwd(),
          inventories=pelicanobj.settings.get('M_SPHINX_INVENTORIES', []))
@@ -589,6 +628,8 @@ def register(): # for Pelican
     rst.roles.register_local_role('ref', ref)
 
     pelican.signals.initialized.connect(_pelican_configure)
+    pelican.signals.article_generator_preread.connect(_pelican_new_page)
+    pelican.signals.page_generator_preread.connect(_pelican_new_page)
 
 def pretty_print_intersphinx_inventory(file):
     return ''.join([
index a8134e20ad687dc6f1879bfc0d3d97f13a2ed514..f3077a0cc63164a5f3c0212156bf1f031fa888cc 100644 (file)
@@ -77,6 +77,8 @@
 <a class="m-flat" href="https://docs.python.org/3/library/os.path.html#os.path.join">Function link with a custom title</a></li>
 <li>Custom CSS class: <a class="m-text m-small m-flat" href="https://docs.python.org/3/library/stdtypes.html#str.join">str.join()</a></li>
 <li>Omitting a prefix: <a class="m-flat" href="https://docs.python.org/3/library/xml.etree.elementtree.html#module-xml.etree.ElementTree">etree.ElementTree</a>, <a class="m-flat" href="https://docs.python.org/3/library/xml.etree.elementtree.html#module-xml.etree.ElementTree">ElementTree</a></li>
+<li>Omitting a page-specific prefix defined in <code>:ref-prefix:</code>:
+<a class="m-flat" href="https://docs.python.org/3/library/typing.html#typing.Tuple">Tuple</a>, <a class="m-flat" href="https://docs.python.org/3/library/unittest.mock.html#unittest.mock.NonCallableMagicMock">NonCallableMagicMock</a></li>
 <li>Custom query string: <a class="m-flat" href="https://docs.python.org/3/library/os.path.html#module-os.path?q=the meaning of life">os.path</a></li>
 </ul>
 <p>These should produce warnings:</p>
index 0ef1a04431e2a2cfcb1fee67d044e39a12654c78..74588eb7d7406ad13c99c3e48f021008895586bd 100644 (file)
@@ -1,6 +1,10 @@
 m.sphinx
 ########
 
+:ref-prefix:
+    typing
+    unittest.mock
+
 .. role:: ref-small(ref)
     :class: m-text m-small
 
@@ -57,6 +61,8 @@ m.sphinx
     :ref:`Function link with a custom title <os.path.join()>`
 -   Custom CSS class: :ref-small:`str.join()`
 -   Omitting a prefix: :ref:`etree.ElementTree`, :ref:`ElementTree`
+-   Omitting a page-specific prefix defined in ``:ref-prefix:``:
+    :ref:`Tuple`, :ref:`NonCallableMagicMock`
 -   Custom query string: :ref:`os.path <os.path?q=the meaning of life>`
 
 These should produce warnings: