From: Vladimír Vondruš Date: Fri, 6 Sep 2019 11:33:06 +0000 (+0200) Subject: m.sphinx: implement a page-wide :ref-prefix: option. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=56f9f7cf8202c737e61af442c5f011f4365e269d;p=blog.git m.sphinx: implement a page-wide :ref-prefix: option. NOW I can start writing real docs without everything being a giant PITA. --- diff --git a/doc/plugins/sphinx.rst b/doc/plugins/sphinx.rst index e5b839db..c7cf28f0 100644 --- a/doc/plugins/sphinx.rst +++ b/doc/plugins/sphinx.rst @@ -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 `_ --- 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 ` - 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 ` - 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 index 00000000..b7ea2843 --- /dev/null +++ b/documentation/test_python/content/page.html @@ -0,0 +1,32 @@ + + + + + A page | My Python Project + + + + + +
+
+
+
+
+

+ A page +

+

Link to a Class and VALUE from a summary.

+

Link to an Enum and THIRD from the content.

+
+
+
+
+ + diff --git a/documentation/test_python/content/page.rst b/documentation/test_python/content/page.rst new file mode 100644 index 00000000..11ef70a4 --- /dev/null +++ b/documentation/test_python/content/page.rst @@ -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. diff --git a/documentation/test_python/test_content.py b/documentation/test_python/test_content.py index be6d9fbc..e743731b 100644 --- a/documentation/test_python/test_content.py +++ b/documentation/test_python/test_content.py @@ -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({ diff --git a/plugins/m/sphinx.py b/plugins/m/sphinx.py index 1b8aba75..fcddc7ad 100755 --- a/plugins/m/sphinx.py +++ b/plugins/m/sphinx.py @@ -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([ diff --git a/plugins/m/test/sphinx/page.html b/plugins/m/test/sphinx/page.html index a8134e20..f3077a0c 100644 --- a/plugins/m/test/sphinx/page.html +++ b/plugins/m/test/sphinx/page.html @@ -77,6 +77,8 @@ Function link with a custom title
  • Custom CSS class: str.join()
  • Omitting a prefix: etree.ElementTree, ElementTree
  • +
  • Omitting a page-specific prefix defined in :ref-prefix:: +Tuple, NonCallableMagicMock
  • Custom query string: os.path
  • These should produce warnings:

    diff --git a/plugins/m/test/sphinx/page.rst b/plugins/m/test/sphinx/page.rst index 0ef1a044..74588eb7 100644 --- a/plugins/m/test/sphinx/page.rst +++ b/plugins/m/test/sphinx/page.rst @@ -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 ` - 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 ` These should produce warnings: